上节课,我们学习了哨兵机制,哨兵机制可以实现主从库的自动切换。
通过部署多个哨兵实例,就形成了一个哨兵集群。
哨兵集群中的多个实例共同判断,可以降低对主库下线的误判率。

但是,我们要考虑一个问题:如果有哨兵实例在运行时发生了故障,主从库还能正常切换吗?
实际上,一旦多个哨兵实例组成了哨兵集群,即使有哨兵实例出现故障挂掉了,其他哨兵还能继续协作完成主从库切换的工作,包括:判断主库的下线状态、选定新主库、通知从库 和 客户端。

如果你部署过哨兵集群的话就会知道,在配置哨兵实例的信息时,我们只需要用到下面的这个配置项,设置主库的 IP 和 端口,并没有配置其他哨兵实例的连接信息。
sentinel monitor <master-name> <ip> <redis-port> <quorum>
这些哨兵实例不知道彼此的地址,又是怎么组成集哨兵群的呢?
要弄明白这个问题,我们就需要学习一下哨兵集群的组成和运行机制了。

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

哨兵实例之间可以相互发现,归功于 Redis 提供的 pub/sub 机制,也就是 发布/订阅 机制。

哨兵实例只要和主库建立了连接,它就可以在主库上发布消息了,
比如:发布该哨兵实例的连接信息(IP 和端口)。
该哨兵实例也可以从主库上订阅消息,获得其他哨兵实例发布的连接信息。
当多个哨兵实例都在主库上做了发布和订阅操作后,它们之间就能知道彼此的 IP地址 和 端口。

除了哨兵实例,我们自己编写的应用程序也可以通过 Redis 进行消息的发布和订阅。
所以,为了区分不同应用的消息,Redis 会以频道的形式,对这些消息进行分门别类的管理。
频道,就是消息的类别。
当消息类别相同时,它们就属于同一个频道。反之,就属于不同的频道。
只有订阅了同一个频道的应用,才能通过发布的消息进行信息交换。


在主从集群中,主库上有一个名为“sentinel:hello”的频道,
不同哨兵就是通过该频道来相互发现,实现互相通信的。

哨兵实例彼此之间如何建立网络连接形成集群?
哨兵实例是如何知道其他哨兵实例的 IP 和 端口的?
举例说明:在下图中,哨兵实例 1 把自己的 IP (172.16.19.3) 和 端口 (26579) 发布到“sentinel:hello”频道上,哨兵实例 2 和 哨兵实例 3 订阅了该频道。
那么,哨兵实例 2 和 哨兵实例 3 就可以从这个频道直接获取哨兵实例 1 的 IP 地址 和 端口号了。
然后,哨兵实例 2 和 哨兵实例 3 可以和哨兵实例 1 建立网络连接。
通过这个方式,哨兵实例 2 和 哨兵实例 3 也可以建立网络连接,这样一来哨兵集群就形成了。
这些哨兵实例之间可以通过网络连接进行通信,比如:判断主库的下线状态 并 协商。
ca42698128aa4c8a374efbc575ea22b1.webp
哨兵实例除了彼此之间建立网络连接形成集群外,还需要和从库建立连接。
哨兵实例要和从库建立连接的原因:在哨兵实例的监控任务中,哨兵实例需要对主从库都进行心跳判断,而且在主从库切换完成后,哨兵实例还需要通知其他从库,让 其他从库 和 新主库 进行数据同步。

哨兵实例和从库如何建立网络连接?
哨兵实例是如何知道从库的 IP 和 端口的?
是由哨兵实例向主库发送 INFO 命令来获取从库的 IP 和 端口信息的。
如下图所示,哨兵实例 2 给主库发送 INFO 命令,主库接受到这个命令后,就会把从库列表返回给哨兵实例 2。
接着,哨兵实例 2 就可以根据从库列表中的连接信息,和每个从库建立网络连接,并在这个网络连接上持续地对从库进行监控。
哨兵实例 1 和 哨兵实例 3 可以通过相同的方法和从库建立连接。
88fdc68eb94c44efbdf7357260091de0.webp
通过 pub/sub 机制,哨兵实例之间可以组成哨兵集群,同时,哨兵实例又通过 INFO 命令,获得了从库的网络连接信息,也能和从库建立网络连接,并对从库进行监控。

但是,哨兵实例不能只和主、从库建立网络连接。
因为,主从库切换后,客户端也需要知道新主库的网络连接信息,才能向新主库发送请求操作。
所以,哨兵实例还需要把新主库的网络连接信息告诉客户端务。

而且,在实际使用哨兵机制时,我们有时会遇到这样的问题:如何在客户端通过监控了解哨兵实例进行主从切换的过程呢?比如:主从库切换进行到哪一步了?
这其实就是:要求客户端能够获取到哨兵集群在监控、选主、切换这个过程中发生的各种事件。
仍然可以依赖 pub/sub 机制,来帮助我们完成哨兵和客户端间的信息同步。

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

从本质上说,哨兵就是一个运行在特定模式下的 Redis 实例,只不过哨兵并不服务请求操作,只是完成监控、选主和通知的任务。
所以,每个哨兵实例也提供 pub/sub 机制,客户端可以从哨兵订阅消息。
哨兵提供的消息订阅频道有很多,不同频道包含了主从库切换过程中的不同关键事件。
重要的频道如下表所示,其中涉及几个关键事件,包括判断主库的下线状态、选定新主库、重新配置从库。


主库下线事件

相关频道 描述
+sdown 实例进入“主观下线”状态
-sdown 实例退出“主观下线”状态
+odown 实例进入“客观下线”状态
-odown 实例退出“客观下线”状态

从库重新配置事件

相关频道 描述
+slave-reconf-sent 哨兵发送 slaveof 命令重新配置从库
+slave-reconf-inprog 从库配置了新主库,但尚未进行数据同步
+slave-reconf-done 从库配置了新主库,且和新主库完成数据同步

新主库切换事件

相关频道 描述
+switch-master 主库地址发生变化

知道了这些频之后,你就可以让客户端从哨兵这里订阅消息了。
客户端从哨兵这里订阅消息,具体的操作步骤是:
客户端读取哨兵的配置文件后,可以获得哨兵的地址和端口信息,客户端和哨兵建立网络连接。
然后,我们可以在客户端执行订阅命令,获取不同的事件消息。

举例说明:你可以执行如下命令,来订阅“所有实例进入客观下线状态的事件”:subscribe +odown 。
也可以执行如下命令,订阅所有的事件:psubscribe *。

当哨兵把新主库选择出来后,客户端就会看到下面的 switch-master 事件。
switch-master <master name> <oldip> <oldport> <newip> <newport>
switch-master 事件表示主库已经切换了,新主库的 IP 地址和端口信息已经有了。
客户端就可以用新主库地址和端口进行通信了。

有了事件通知,客户端不仅可以在主从切换后得到新主库的连接信息,
还可以监控到主从库切换过程中发生的各个重要事件。
这样,客户端就可以知道主从切换进行到哪一步了,有助于了解切换进度。

有了 pub/sub 机制,哨兵和哨兵之间、哨兵和从库之间、哨兵和客户端之间就都能建立起网络连接了,
再加上主库下线判断和选主依据,哨兵集群的监控、选主和通知,这三个任务就基本可以正常工作了。

我们还需要考虑一个问题:
主库故障以后,哨兵集群有多个哨兵实例,那怎么确定由哪个哨兵实例来进行实际的主从切换呢?

由哪个哨兵执行主从切换

确定由哪个哨兵执行主从切换的过程,和主库“客观下线”的判断过程类似,也是一个“投票仲裁”的过程。

在具体了解这个过程前,我们再来看下,判断“客观下线”的仲裁过程。
哨兵集群要判定主库“客观下线”,需要有一定数量的哨兵实例认为该主库已经“主观下线”了。
在上节课介绍了判断“客观下线”的原则,下面介绍判断“客观下线”的具体过程
任何一个哨兵实例只要自身判断主库“主观下线”后,就会给其他哨兵实例发送 is-master-down-by-addr 命令。接着,其他哨兵实例会根据自己和主库的连接情况,做出 Y 或 N 的响应,Y 相当于赞成票,N 相当于反对票。
e0832d432c14c98066a94e0ef86af384.webp
一个哨兵实例获得了仲裁所需的赞成票数后,就可以标记主库为“客观下线”了。

这个所需的赞成票数是通过哨兵配置文件中的 quorum 配置项设定的。
比如:现在有 5 个哨兵实例,quorum 配置的是 3,那么,一个哨兵实例有至少 3 张赞成票,就可以标记主库为“客观下线”了。
这 3 张赞成票包括哨兵实例自己的一张赞成票 和 另外两个哨兵实例的赞成票。

此时,这个哨兵实例就可以再给其他哨兵发送命令,表明希望由自己来执行主从库切换,并让所有其他哨兵实例进行投票。
这个投票过程称为“Leader 选举”。
因为最终执行主从库切换的哨兵实例称为 Leader,投票过程就是确定 Leader。

在投票的过程中,任何一个想成为 Leader 的哨兵实例,要满足两个条件:

  1. 拿到半数以上的赞成票
  2. 拿到的票数同时还必须 >= 哨兵配置文件中的 quorum 值。

以 3 个哨兵为例,假设此时的 quorum 设置为 2,
那么任何一个想成为 Leader 的哨兵只要拿到 2 张赞成票,就可以了。
5f6ceeb9337e158cc759e23c0f375fd9.webp
在 T1 时刻,S1 判断主库为“客观下线”,它想成为 Leader,
就先给自己投一张赞成票,然后分别向 S2 和 S3 发送命令,表示要成为Leader。

在 T2 时刻,S3 判断主库为“客观下线”,它也想成为 Leader,
所以也先给自己投一张赞成票,再分别向 S1 和 S2 发送命令,表示要成为 Leader。

在 T3 时刻,S1 收到了 S3 的 Leader 投票请求。
因为 S1 已经给自己投了一票 Y,所以它不能再给其他哨兵投赞成票了,所以 S1 回复 N 表示不同意。

同时,S2 收到了 T2 时 S3 发送的 Leader 投票请求。
因为 S2 之前没有投过票,它会给第一个向它发送投票请求的哨兵回复 Y,给后续再发送投票请求的哨兵回复 N,所以,在 T3 时,S2 回复 S3,同意 S3 成为 Leader。

在 T4 时刻,S2 才收到 T1 时 S1 发送的投票命令。
因为 S2 已经在 T3 时同意了 S3 的投票请求,此时,S2 给 S1 回复 N,表示不同意 S1 成为 Leader。

发生 S1 最先发投票请求,但是 S2 却比较靠后时刻收到 S1 的请求,这种情况是因为:S3 和 S2 之间的网络传输正常,而 S1 和 S2 之间的网络传输可能正好拥塞了,导致投票请求传输慢了。

最后,在 T5 时刻,S1 得到的票数是来自它自己的赞成票 Y 和来自 S2 的一票 N。
而 S3 除了自己的赞成票 Y 以外,还收到了来自 S2 的一票 Y。
此时,S3 不仅获得了半数以上的 Leader 赞成票,也达到预设的 quorum 值(quorum 为 2),
所以 S3 最终成为为了 Leader。
接着,S3 会开始执行选主操作,而且在选定新主库后,会给其他从库和客户端通知新主库的信息。

如果 S3 没有拿到 2 票 Y,那么这轮投票就不会产生 Leader。
哨兵集群会等待一段时间(也就是哨兵故障转移超时时间的 2 倍),再重新选举。
这是因为,哨兵集群能够进行成功投票,很大程度上依赖于选举命令的正常网络传播。
如果网络压力较大或有短时堵塞,就可能导致没有一个哨兵能拿到半数以上的赞成票。
所以,等到网络拥塞好转之后,再进行投票选举,成功的概率就会增加。

需要注意的是:如果哨兵集群只有 2 个实例,一个哨兵要想成为 Leader,必须获得 2 票,而不是 1 票。
所以,如果有个哨兵挂掉了,此时,哨兵集群是无法进行主从库切换的。
因此,通常我们至少会配置 3 个哨兵实例。
这一点很重要,你在实际应用时可不能忽略了。

小结

通常,我们在解决一个系统问题的时候,会引入一个新机制,或者设计一层新功能,

就像我们在这两节课学习的内容:
为了实现主从切换,我们引入了哨兵机制;
为了避免单个哨兵故障后无法进行主从切换,以及为了减少误判率,又引入了哨兵集群
哨兵集群又需要有一些机制来支撑它的正常运行。

这节课上,我就向你介绍了支持哨兵集群的这些关键机制,包括:

  • 基于 pub/sub 机制的哨兵集群组成过程;
  • 基于 info 命令的从库列表,这可以帮助哨兵和从库建立网络连接;
  • 基于哨兵自身的 pub/sub 功能,这实现了客户端和哨兵之间的事件通知。

对于主从库切换,当然不是哪个哨兵想执行就可以执行的,否则就乱套了。
所以,这就需要哨兵集群在判断了主库“客观下线”后,经过投票仲裁,选举一个 Leader 出来,由它负责实际的主从库切换,即由它来完成新主库的选择以及通知从库与客户端。

最后,我想再给你分享一个经验:要保证所有哨兵实例的配置是一致的,尤其是判断主库是否主观下线的 响应最大超时时间 down-after-milliseconds 毫秒。

我们曾经就踩过这一个“坑”。
当时,在我们的项目中,因为这个值在不同的哨兵实例上配置不一致,导致哨兵集群一直没有对有故障的主库形成共识,也就没有及时切换主库,最终的结果就是集群服务不稳定。
所以,你一定不要忽略这条看似简单的经验。

每课一问

假设有一个 Redis 集群,是“一主四从”,同时配置了包含 5 个哨兵实例的集群,quorum 值设为 2。
在运行过程中,如果有 3 个哨兵实例都发生故障了,
此时,Redis 主库如果有故障,还能正确地判断主库“客观下线”吗?
如果可以的话,还能进行主从库自动切换吗?
此外,哨兵实例是不是越多越好呢,
如果同时调大down-after-milliseconds值,对减少误判是不是也有好处呢?


问题 1
因为判定主库“客观下线”的依据是,认为主库“主观下线”的哨兵个数要大于等于 quorum 值,
现在还剩 2 个哨兵实例,个数正好等于 quorum 值,所以还能正常判断主库是否处于“客观下线”状态。
如果一个哨兵想要执行主从切换,就要获到半数以上的哨兵投票赞成,也就是至少需要 3 个哨兵投票赞成。
但是,现在只有 2 个哨兵了,所以就无法进行主从切换了。


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

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