最近遇到一次生产环境 redis sentinel 故障切换失败的问题,以此记录给自己敲响警钟。

背景

生产环境有一组当作数据源使用的 redis ,考虑到数据不可用将影响业务,即 P1 级应用, 需要保障较严格的高可用。 鉴于原先业务使用单台 redis 时使用我们自己封装的客户端。使用 sentinel 方式进行故障 的检测与转移,而客户端改从 sentinel 获取可用节点,都使用 master 节点。

我们的生产环境是部署在阿里云的VPC中,

        AZ-A        |   AZ-B
+------+  +------+  | +------+
|  R1  |  |  R2  |  | |  R3  |
+------+  +------+  | +------+
                    |
                    |
+------+  +------+  | +------+
|  S1  |  |  S2  |  | |  S3  |
+------+  +------+  | +------+
                    |
  • R 为 redis 实例, S 为 sentinel 实例

  • R3 为 master ,R1 与 R2 为 slave

  • 网络带宽 1Gbps

  • 数据量限制在 4 GiB

  • R1与R2所在的系统上安装了 k8s (且 redis 没有部署在 k8s 中)

  • 业务应用在可用区A,与大部分主机在一起

问题描述

出现问题是在 2019-10-17 15:00:04 由 Python 与 Java 应用同时 报出 Connection Refused ,检查 R3:6379 端口无法访问,检查没有进程监听, 没有 redis 进程,首先想到的是是否因内存小号太多被 OOM Killer 杀掉了,翻日志 果不其然

[Thu Oct 17 14:18:36 2019] lowmem_reserve[]: 0 2976 16031 16031 16031
[Thu Oct 17 14:18:36 2019] Node 0 DMA32 free:66352kB min:12412kB low:15512kB high:18616kB active_anon:2933712kB inactive_anon:432kB active_file:2000kB inactive_file:2916kB unevictable:0kB isolated(anon):0kB isolated(file):0kB present:3129216kB managed:3048440kB mlocked:0kB dirty:0kB writeback:8kB mapped:3124kB shmem:572kB slab_reclaimable:6088kB slab_unreclaimable:9308kB kernel_stack:1680kB pagetables:14452kB unstable:0kB bounce:0kB free_pcp:1136kB local_pcp:0kB free_cma:0kB writeback_tmp:0kB pages_scanned:31252 all_unreclaimable? yes
[Thu Oct 17 14:18:36 2019] lowmem_reserve[]: 0 0 13054 13054 13054
[Thu Oct 17 14:18:36 2019] Node 0 Normal free:70440kB min:55104kB low:68880kB high:82656kB active_anon:13064876kB inactive_anon:2476kB active_file:116kB inactive_file:0kB unevictable:0kB isolated(anon):0kB isolated(file):128kB present:13631488kB managed:13367460kB mlocked:0kB dirty:0kB writeback:4kB mapped:688kB shmem:3456kB slab_reclaimable:32776kB slab_unreclaimable:49288kB kernel_stack:9168kB pagetables:68696kB unstable:0kB bounce:0kB free_pcp:1636kB local_pcp:48kB free_cma:0kB writeback_tmp:0kB pages_scanned:1076 all_unreclaimable? yes
[Thu Oct 17 14:18:36 2019] lowmem_reserve[]: 0 0 0 0 0
[Thu Oct 17 14:18:36 2019] Node 0 DMA: 0*4kB 0*8kB 0*16kB 1*32kB (U) 2*64kB (U) 1*128kB (U) 1*256kB (U) 0*512kB 1*1024kB (U) 1*2048kB (M) 3*4096kB (M) = 15904kB
[Thu Oct 17 14:18:36 2019] Node 0 DMA32: 514*4kB (UME) 422*8kB (UME) 431*16kB (UME) 324*32kB (UME) 153*64kB (E) 91*128kB (UME) 33*256kB (UME) 10*512kB (UM) 6*1024kB (UME) 1*2048kB (H) 0*4096kB = 65896kB
[Thu Oct 17 14:18:36 2019] Node 0 Normal: 2452*4kB (UMEH) 1082*8kB (UMEH) 1536*16kB (UMEH) 444*32kB (UME) 168*64kB (UMEH) 17*128kB (UME) 0*256kB 0*512kB 0*1024kB 0*2048kB 0*4096kB = 70176kB
[Thu Oct 17 14:18:36 2019] Node 0 hugepages_total=0 hugepages_free=0 hugepages_surp=0 hugepages_size=1048576kB
[Thu Oct 17 14:18:36 2019] Node 0 hugepages_total=0 hugepages_free=0 hugepages_surp=0 hugepages_size=2048kB
[Thu Oct 17 14:18:36 2019] 2644 total pagecache pages
[Thu Oct 17 14:18:36 2019] 0 pages in swap cache
[Thu Oct 17 14:18:36 2019] Swap cache stats: add 0, delete 0, find 0/0
[Thu Oct 17 14:18:36 2019] Free swap  = 0kB
[Thu Oct 17 14:18:36 2019] Total swap = 0kB
[Thu Oct 17 14:18:36 2019] 4194174 pages RAM
[Thu Oct 17 14:18:36 2019] 0 pages HighMem/MovableOnly
[Thu Oct 17 14:18:36 2019] 86223 pages reserved
[Thu Oct 17 14:18:36 2019] 0 pages cma reserved
[Thu Oct 17 14:18:36 2019] 0 pages hwpoisoned
[Thu Oct 17 14:18:36 2019] [ pid ]   uid  tgid total_vm      rss nr_ptes nr_pmds swapents oom_score_adj name
[Thu Oct 17 14:18:36 2019] [17611]     0 17611    12555      106      29       3        0             0 cron
[Thu Oct 17 14:18:36 2019] [17612]   999 17612     1126       18       8       3        0             0 sh

# ... 中间省略进程表较多

[Thu Oct 17 14:18:36 2019] [17621]   999 17621    48661    10416      98       3        0             0 python
[Thu Oct 17 14:18:36 2019] [17622]   999 17622    48661    10358     101       3        0             0 python
[Thu Oct 17 14:18:36 2019] Out of memory: Kill process 21370 (redis-server) score 461 or sacrifice child
[Thu Oct 17 14:18:36 2019] Killed process 21370 (redis-server) total-vm:7624292kB, anon-rss:7571104kB, file-rss:0kB

不过这个时间点 14:18:36 就比较神奇,有超过40分钟没杀成功?并且检查主机的内存监控的确 是在15:00才释放出来的。

然后是 sentinel 检测出 master 宕机 ,并且很快就满足 quorum 要求

22719:X 17 Oct 15:00:38.423 # +sdown master idfa_check 10.111.5.143 6379
22719:X 17 Oct 15:00:38.516 # +odown master idfa_check 10.111.5.143 6379 #quorum 3/2

开始 sentinel 的 leader 选举,可以说很真实,3台 sentinel 都选自己, 所以第一次没有选出 master ;并且故障转移的延迟要等6分钟(!)。

22719:X 17 Oct 15:00:38.516 # +new-epoch 9
22719:X 17 Oct 15:00:38.516 # +try-failover master idfa_check 10.111.5.143 6379
22719:X 17 Oct 15:00:38.532 # +vote-for-leader bee639a324755c111c238557b244bf3628418629 9
22719:X 17 Oct 15:00:38.533 # 27eb1f1e055b94c91b988d0191640f21946edb9e voted for 27eb1f1e055b94c91b988d0191640f21946edb9e 9
22719:X 17 Oct 15:00:38.555 # 485cb1b8e708517b929aa4036639a9b5be5503ec voted for 485cb1b8e708517b929aa4036639a9b5be5503ec 9
22719:X 17 Oct 15:00:49.008 # -failover-abort-not-elected master idfa_check 10.111.5.143 6379
22719:X 17 Oct 15:00:49.086 # Next failover delay: I will not start a failover before Thu Oct 17 15:06:38 2019

6分钟后,可以看到顺利选举出 sentinel leader 后,看到进行故障转移的过程有异常

22719:X 17 Oct 15:06:38.943 # +new-epoch 10
22719:X 17 Oct 15:06:38.943 # +try-failover master idfa_check 10.111.5.143 6379
22719:X 17 Oct 15:06:38.947 # +vote-for-leader bee639a324755c111c238557b244bf3628418629 10
22719:X 17 Oct 15:06:38.953 # 485cb1b8e708517b929aa4036639a9b5be5503ec voted for bee639a324755c111c238557b244bf3628418629 10
22719:X 17 Oct 15:06:38.954 # 27eb1f1e055b94c91b988d0191640f21946edb9e voted for bee639a324755c111c238557b244bf3628418629 10
22719:X 17 Oct 15:06:39.013 # +elected-leader master idfa_check 10.111.5.143 6379
22719:X 17 Oct 15:06:39.013 # +failover-state-select-slave master idfa_check 10.111.5.143 6379
22719:X 17 Oct 15:06:39.104 # -failover-abort-no-good-slave master idfa_check 10.111.5.143 6379
22719:X 17 Oct 15:06:39.175 # Next failover delay: I will not start a failover before Thu Oct 17 15:12:39 2019

-failover-abort-no-good-slave 状态显示这组主从中没有可用的 slave 实例?

尝试在 sentinel 命令行手工故障转移得到的是一样的结果

(error) NOGOODSLAVE No suitable slave to promote

从两台 slave 实例的日子可以看到,实际上远在这之前,主从复制的连接就有问题了

3847:S 17 Oct 12:43:04.846 * Connecting to MASTER 10.111.5.143:6379
3847:S 17 Oct 12:43:04.846 * MASTER <-> SLAVE sync started
3847:S 17 Oct 12:44:05.940 # Timeout connecting to the MASTER...

最后采取了,手工将其中一台 slave (R1) 切换成 master ,才顺利解决

$ redis-cli slaveof no one

看到故障转移顺利执行

22719:X 17 Oct 15:10:43.524 # +new-epoch 11
22719:X 17 Oct 15:10:43.528 # +vote-for-leader 485cb1b8e708517b929aa4036639a9b5be5503ec 11
22719:X 17 Oct 15:10:43.531 # Next failover delay: I will not start a failover before Thu Oct 17 15:16:43 2019
22719:X 17 Oct 15:10:43.618 # +config-update-from sentinel 485cb1b8e708517b929aa4036639a9b5be5503ec 10.111.5.153 26379 @ idfa_check 10.111.5.143 6379

最后整个故障过程约10分钟后业务恢复, 于是这个月4个9的 SLA 是不要想了,已经完蛋了 XDDD

分析

追溯日志,实际上远在3周前,这个主从复制就已经出现故障,而这个时间点又与该主机上安装 k8s 符合。业务都使用 master 节点故不受影响。

进一步研究主从连接为何会超时,发现

redis-ser 3704 root   13u     IPv4 241658807      0t0       TCP 127.0.0.1:46189->10.111.5.110:6379 (SYN_SENT)

建立 TCP 连接的源地址是 127.0.0.1 这个理解起来有点不符合一般情况,我们知道 如果 connect(2) 调用的发起 socket fd 没有指定地址或者网卡的话,内核会帮我们 选择,一般是符合路由的那张网卡地址。同时,在没有特殊设置的情况下,即使使用 127.0.0.1 源地址发起连接也是可以连接上的。于是这里就有两个可疑的地方

  • 为何源地址是 127.0.0.1

  • 为何发起连接连不上?

解答第一个问题比较简单,我们的 redis 实例配置文件中有这么一行

bind 127.0.0.1 <internal_ip>

这是我们早期在阿里云使用经典网络的教训,不配置内网地址的话,甚至可能被其他 阿里云租户访问到,问题就在这里 127.0.0.1 写在第一个,那么 redis 是如何发起主从 同步连接的呢?

可以看到1还真就是拿配置中第一个地址作为源地址 ,好吧。 但是这应该不影响连接。

于是怀疑到是否因为 netfilter 的若干规则影响到呢?注意到前面介绍背景时, R1 与 R2 上都安装了 k8s 环境,我们知道 k8s 的网络是通过 kube-proxy 设置 netfilter 规则来实现 集群内网络走向控制的。

这个调试原理与过程想以后详细再讲,这里先丢一张图2, 理解一下 iptables 的处理流程。

然后,要知道 k8s 还在跑应用,并不方便将某台主机的 kubelet kube-proxy 以及网络插件停掉,进而清理 iptables 规则来验证。所以这里采取将 redis 的配置 修改,将内网地址放到第一个,重开后 顺利开启 R2 从 R1 同步的复制

可以吸取的教训

  • 重要的数据类服务与业务应用隔离分开部署, 减少因不可控造成资源的抢占导致服务不可用。 事后分析,R3 (原 master) 所在主机因业务增加部署进程导致内存开销吃紧, 而 OOM Killer 计算分数3的时候,粗略认为占用比例越大分数越高, 意味着更有可能被杀死。

  • 缺乏最基础的监控,实例、端口存活情况应当是非常需要重视的基础。

  • 网络因素尤其是 netfilter 相关的细节是许多开发人员并不会注意的细节,在做出调整前 应该仔细评估做测试。

此外特别值得一提的是 sentinel 有一项设置 failover-timeout4 同时在故障切换过程中被多次使用且含义不同,默认 3 分钟

# - The time needed to re-start a failover after a previous failover was
#   already tried against the same master by a given Sentinel, is two
#   times the failover timeout.

故障转移失败后需要 2倍 这个超时,所以上面情景是 6 分钟, 即这个值不宜过大

并且

# - The time needed for a slave replicating to a wrong master according
#   to a Sentinel current configuration, to be forced to replicate
#   with the right master, is exactly the failover timeout (counting since
#   the moment a Sentinel detected the misconfiguration).
#
# - The time needed to cancel a failover that is already in progress but
#   did not produced any configuration change (SLAVEOF NO ONE yet not
#   acknowledged by the promoted slave).
#
# - The maximum time a failover in progress waits for all the slaves to be
#   reconfigured as slaves of the new master. However even after this time
#   the slaves will be reconfigured by the Sentinels anyway, but not with
#   the exact parallel-syncs progression as specified.
  • 旧 master (新 slave) 强制改正同步对象的时间

  • 取消目前已经在同步,但尚未生成配置 slave 的时间

  • 整个故障转移过程中等待所有 slave 都完成到新 master 配置的时间

根据上面日志查看选举成功、修改配置等实际消耗的时间, 这个值可能并不需要 3 分钟那么大

目前已经采取的改进措施有:

  • 增加 redis_exporter 5部署,对所有 redis 实例的存活 ( redis_up ) 进行监控

  • 增加 redis_master_link_up 的监控, master 实例没有这一项,所以检测出来 0 的 即是有问题的 slave

顺便,有找到好几个 sentinel_expoter 的实现,例如这个6 , 不过目前尚未想好需要监控哪些内容,主从同步的延迟等等似乎也应该考虑加入。

可以进一步深入研究的细节

  • k8s 哪条 netfilter 规则导致了从 127.0.0.1 发起向外的连接无法正常建立

  • iptables 处理顺序,以及如何让调试规则更便利

  • sentinel 做故障切换时,还有哪些值得注意的 异常 情况

结语

现在回顾对 redis 的主从复制、故障转移的原理没有吃透;同时作为 DevOps 人员, 应对生产环境的一切保有敬畏心,警钟长鸣

__END__