哨兵机制

一、场景

场景:如果主库挂了,或着说不能提供写服务了,怎么办?需要把一个从库切换为主库?这里就涉及3个问题:

  1. 主库真的挂了吗?
  2. 该选择哪个从库作为主库?
  3. 怎么把新主库的相关信息通知给从库和客户端呢?

兜底方案:在主库挂了,且哨兵没有选出新的主库的这个时间段,如果不想让业务感知到异常,客户端只能把写失败的请求先缓存起来或者写入消息队列中间件中,等哨兵切换主从后,再把这些写请求发送给新的主库,但这种场景只适合对写入请求返回值不敏感的业务,而且还需要业务层做适配,另外主从切换时间过长,也会导致客户端或消息中间件缓存写请求过多,切换完成之后重放这些请求的时间变长。

二、哨兵的工作

哨兵主要负责:监控、选择主库、通知

1. 监控

哨兵进程在运行时,周期性的给所有的主从库发送 PING 命令,检测他们是否仍然在线运行。如果从库在规定时间内没有响应哨兵的 PING 命令,哨兵就会把它标记为“主观下线状态”;但主库特殊,哨兵需要判断主库是否真的处于下线状态,不能由一个哨兵说了算,存在哨兵误判的情况(主从切换,后续的选择主库和通知操作会带来额外的计算和通信开销),误判一般发生在集群网络压力较大、网络阻塞,或者主库本身压力比较大的情况。

如何减少误判呢?通常采用多实例组成的集群模式进行部署(哨兵集群)。

“客观下线”的标准:当有 N 个哨兵实例时,最好要有 N/2+1 个实例判断主库为“主观下线”,才能最终判断主库为“客观下线”

2. 选择主库

哨兵如何决定选择哪个从库实例作为主库?通过筛选和打分机制。

筛选条件

  1. 检查从库当前的在线状态

  2. 判断从库之前的网络连接状态,配置项 down-after-milliseconds 是主从库断连的最大连接超时时间。如果在 down-afetr-milliseconds 毫秒内,主从节点都没有通过网络连接,则认为断连。如果发生断连次数超过10次,就说明这个从库网络状态不好,不适合作为新主库。

    注意:配置的时间越短,哨兵越敏感,哨兵集群认为主库在短时间内连不上就会发起主从切换,这种配置很可能因为网络拥塞但主库正常而发生不必要的切换,当然,当主库真正故障时,因为切换得及时,对业务的影响最小。如果配置的时间比较长,哨兵越保守,这种情况可以减少哨兵误判的概率,但是主库故障发生时,业务写失败的时间也会比较久,缓存写请求数据量越多。

    保证所有哨兵实例的配置是一致的,尤其是主观下线的 down-after-milliseconds 值。如果这个值不一致,导致哨兵集群一直没有对有故障的主库形成共识,也就没有及时切换主库,最终的结果就是集群服务不稳定。

打分机制:三个规则(从库优先级、从库复制进度、从库ID号)

  1. 优先级最高的从库得分高。 配置项 slave-priority 可以用户设置。比如可以手动给内存大的实例设置一个高优先级
  2. 和旧主库同步程度最接近的从库得分高。标记 repl_backlog_buffer 环形缓冲区的 slave_repl_offset 是从库的复制进度,哪个进度越快,则分数高。通过比较不同从库的 slave_repl_offset ,找出最大的 slave_repl_offset 的从库。
  3. 从库ID号小的从库得分高。在优先级和复制进度都相同的情况下,ID号最小的从库得分最高

总结:哨兵会按照在线状态、网络状态,筛选过滤掉一部分不符合要求的从库,然后,依次按照优先级、复制进度、ID号大小在对剩余从库进行打分,只要有得分最高的从库出现,就把它选为新主库。

3. 通知

哨兵提升一个从库为新主库后,哨兵会把新主库的地址写入自己实例的 pubsub(switch-master)中。客户端需要订阅这个 pubsub,当这个 pubsub 有数据时,客户端就能感知到主库发生变更,同时可以拿到最新的主库地址,然后把写请求写到这个新主库即可。这是哨兵主动通知客户端。

问题:客户端错过了哨兵的通知?哨兵通知后客户端处理失败了?

解决:客户端需要支持主动去获取最新主从的地址进行访问。客户端访问主从库时,需要从哨兵集群中获取最新的地址(sentinel get-master-addr-by-name命令),这样当实例异常时,哨兵切换后或者客户端断开重连,都可以从哨兵集群中拿到最新的实例地址。Redis的 sdk 提供了通过哨兵拿到实例地址,在访问实例的方式。

三、哨兵集群

1. 基于 pub/sub 机制的哨兵集群组成

使用 Redis 的 pub/sub 机制(发布/订阅机制)。哨兵只要和主库建立起连接,就可以在主库上发布消息了,也可以从主库上订阅消息,获得其他哨兵发布的连接消息。当多个哨兵实例都在主库上做了发布和订阅操作之后,他们之间就能知道彼此的 IP 地址和端口。

哨兵之间的互相获取:Redis 中对发布/订阅的消息通过频道来区别,如主库上__sentinel__::hello 频道,不同哨兵就是通过它来互相发现实现通信的。

哨兵获取从库的信息:哨兵向主库发送 INFO 命令,主库会返回从库列表。

2. 基于 pub/sub 机制的客户端事件通知

每个哨兵实例提供 pub/sub 机制,客户端可以从哨兵订阅消息。如下列出一些重点的频道:

事件 相关频道
主库下线事件 +sdown(实例进入“主观下线”状态)
主库下线事件 -sdown(实例退出“主观下线”状态)
主库下线事件 +odown(实例进入“客观下线”状态)
主库下线事件 -odown(实例退出“客观下线”状态)
从库重新配置事件 +slave-reconf-sent(哨兵发送 SLAVEOF 命令重新配置从库)
从库重新配置事件 +slave-reconf-inprog(从库配置了新主库,但尚未进行同步)
从库重新配置事件 +slave-reconf-done(从库配置了新主库,且和新主库完成同步)
新主库切换 +switch-master(主库地址发生变化)

客户端可以从哨兵这里订阅消息了,客户端读取哨兵的配置文件后,可以获得哨兵的地址和端口,和哨兵建立网络连接。

问题:客户端会向所有的哨兵都订阅这些消息吗?

问题:如何确定哨兵集群中哪个哨兵来进行实际的主从切换呢?
解决:哨兵集群要判定主库“客观下线”,需要一定数量的实例都认为该主库已经“主观下线”。任何一个实例只要自身判断主库“主观下线”后,就会给其他实例发送 is-master-down-by-addr 命令。然后,其他实例会根据自己和主库的连接情况,做出 Y(赞成)或 N(反对) 的响应,一个哨兵获得了仲裁所需的赞成票后,就可以标记主库为“客观下线”。这个所需的赞成票是通过哨兵配置文件中的 quorum 配置项决定的。

问题:在投票环节,会出现所有哨兵都给自己投票的情况吗?投票投给谁的依据是什么?
解决:要发生所有哨兵都给自己投票,需要这三个哨兵同时判定主库客观下线,概率较小。其次,哨兵对主从库进行的在线检查等操作,一般用定时器(100ms)来执行一次这些事件,每个哨兵的定时器执行周期都会加上一个小小的时间偏移,目的是让每个哨兵执行上述操作的时间能稍微错开,避免他们都同时判定主库下线。最后,即使出现了都投自己一票的情况,导致无法选出 Leader,哨兵会停一段时间(一般是故障超时时间 failover_time 的2倍),然后再进行下一轮投票。
哨兵如果没有给自己投票,就会把票投给第一个给他发送投票请求的哨兵。后续再有投票请求来,哨兵就拒接投票了。

在投票环节,任何一个想成为 Leader 的哨兵,要满足两个条件:

  1. 拿到半数以上的赞成票
  2. 拿到的票数同时还需要大于等于哨兵配置文件中的 quorum 值

注意:如果哨兵集群只有2个实例,一个哨兵要想成为 Leader,必须获得2票,而不是1票。此时的哨兵集群是无法进行主从库切换的。因此通常至少会配置3个哨兵实例。

四、其他问题

问题1:哨兵集群多数达成共识,判断出主库“客观下线”后,由那个实例来执行主从切换呢?

哨兵集群判断出主库“主观下线”后,会选出一个“哨兵领导者”,之后整个过程由它来完成主从切换。
如何选择“哨兵领导者”?分布式系统中的共识算法,简单说每个哨兵设置一个随机超时时间,超时后每个哨兵会请求其他哨兵为自己投票,其他哨兵节点对收到的第一个请求进行投票确认,一轮投票下来后,首先达到多数选票的哨兵节点成为“哨兵领导者”,如果没有达到多数选票的哨兵节点,那么会重新选举,直到能够成功选出“哨兵领导者”。

问题2:哨兵集群有实例故障了,如何做?如何判定?这个还算一个哨兵吗?会参与选举哨兵Leader 活动吗?

哨兵节点被动挂掉,还会计算。哨兵节点主动挂掉,则不参与半数以上的投票规则。

问题3:哨兵实例是不是越多越好?如果同时调大 down-after-milliseconds 值,对减少误判是不是也有好处?

哨兵实例越多,误判率会越低,但是在判定主库下线和选举Leader时,实例需要拿到的赞成票数也越多,等待所有的哨兵投完票的时间可能也会相应增加,主从库切换的时间也会变长,客户端容易堆积较多的请求操作,可能会导致客户端请求溢出,从而造成请求丢失。如果业务层对Redis 的操作有响应时间要求,就可能会因为新主库一直没有选定,新操作无法执行而发生超时报警。

调大 down-after-milliseconds 后,可能会导致这样的情况:主库实际已经发生故障了,但是哨兵过了很长时间才判断出来,这就会影响到 Redis 对业务的可用性。