通过部署多个哨兵实例,我们构建了一个哨兵集群,这个集群中的多个实例共同协作,以降低对主库下线的误判率。
然而,还有一个重要问题需要考虑:如果哨兵集群中的某个实例发生故障,主从库是否能够继续正常切换呢?
实际上,一旦多个实例组成了哨兵集群,即使有个别哨兵实例出现故障而无法正常运行,其他健康的哨兵实例仍然能够继续协同工作,完成主从库切换的各项任务,包括判断主库的下线状态、选择新的主库,以及通知从库和客户端。
如果你曾经部署过哨兵集群,你会发现,在配置哨兵信息时,我们只需要指定主库的 IP 和端口,而无需明确配置其他哨兵实例的连接信息。这是因为哨兵集群中的各个实例会相互感知和发现,形成一种自动协作的机制。
sentinel monitor <master-name> <ip> <redis-port> <quorum>
这些哨兵实例既然都不知道彼此的地址,又是怎么组成集群的呢?要弄明白这个问题,我们就需要学习一下哨兵集群的组成和运行机制了。
基于 pub/sub 机制的哨兵集群组成
哨兵实例之间可以相互发现,要归功于 Redis 提供的 pub/sub 机制,也就是发布 / 订阅机制。
哨兵只要和主库建立起了连接,就可以在主库上发布消息了,比如说发布它自己的连接信息(IP 和端口)。同时,它也可以从主库上订阅消息,获得其他哨兵发布的连接信息。当多个哨兵实例都在主库上做了发布和订阅操作后,它们之间就能知道彼此的 IP 地址和端口。
除了哨兵实例,我们自己编写的应用程序也可以通过 Redis 进行消息的发布和订阅。所以,为了区分不同应用的消息,Redis 会以频道的形式,对这些消息进行分门别类的管理。所谓的频道,实际上就是消息的类别。当消息类别相同时,它们就属于同一个频道。反之,就属于不同的频道。只有订阅了同一个频道的应用,才能通过发布的消息进行信息交换。
在主从集群中,主库上有一个名为“__sentinel__:hello”的频道,不同哨兵就是通过它来相互发现,实现互相通信的。
我来举个例子,具体说明一下。在下图中,哨兵 1 把自己的 IP(172.16.19.3)和端口(26579)发布到“__sentinel__:hello”频道上,哨兵 2 和 3 订阅了该频道。那么此时,哨兵 2 和 3 就可以从这个频道直接获取哨兵 1 的 IP 地址和端口号。
然后,哨兵 2、3 可以和哨兵 1 建立网络连接。通过这个方式,哨兵 2 和 3 也可以建立网络连接,这样一来,哨兵集群就形成了。它们相互间可以通过网络连接进行通信,比如说对主库有没有下线这件事儿进行判断和协商。
图片
哨兵除了彼此之间建立起连接形成集群外,还需要和从库建立连接。这是因为,在哨兵的监控任务中,它需要对主从库都进行心跳判断,而且在主从库切换完成后,它还需要通知从库,让它们和新主库进行同步。
那么,哨兵是如何知道从库的 IP 地址和端口的呢?
这是由哨兵向主库发送 INFO 命令来完成的。就像下图所示,哨兵 2 给主库发送 INFO 命令,主库接受到这个命令后,就会把从库列表返回给哨兵。接着,哨兵就可以根据从库列表中的连接信息,和每个从库建立连接,并在这个连接上持续地对从库进行监控。哨兵 1 和 3 可以通过相同的方法和从库建立连接。
图片
通过 pub/sub 机制,哨兵之间可以形成一个协作集群。此外,哨兵还能通过 INFO 命令获得从库的连接信息,建立连接并进行监控。
然而,哨兵的连接工作不仅仅限于主库和从库。当主从库发生切换后,客户端需要获取新主库的连接信息,以便继续发送请求操作。因此,哨兵的任务还包括将新主库的信息传达给客户端。
在实际使用哨兵时,有时候我们需要解决这样的问题:如何让客户端能够通过监控来了解哨兵进行主从切换的进度?具体来说,客户端需要了解主从切换进行到哪个步骤了。这就意味着客户端需要获取有关哨兵集群在监控、选主和切换等过程中发生的各种事件的信息。
在这种情况下,我们仍然可以借助 pub/sub 机制,以实现哨兵和客户端之间的信息同步。这种机制可以帮助客户端跟踪主从切换的各个步骤。
基于 pub/sub 机制的客户端事件通知
从根本上说,哨兵就是一个以特定模式运行的 Redis 实例。然而,它不会处理请求操作,而是专注于监控、选主和通知的任务。每个哨兵实例也提供 pub/sub 机制,允许客户端通过订阅消息来获取信息。这些消息订阅频道有众多,各自包含了主从库切换过程中的不同重要事件。
考虑到频道众多,尝试同时理解它们可能会让人感到不知所措。为了帮助你更轻松地理解,我将汇总一些关键频道,这些频道涵盖了几个重要事件,包括主库下线判断、新主库的选定和从库的重新配置。
图片
知道了这些频道之后,你就可以让客户端从哨兵这里订阅消息了。具体的操作步骤是,客户端读取哨兵的配置文件后,可以获得哨兵的地址和端口,和哨兵建立网络连接。然后,我们可以在客户端执行订阅命令,来获取不同的事件消息。
举个例子,你可以执行如下命令,来订阅“所有实例进入客观下线状态的事件”:
SUBSCRIBE +odown
当然,你也可以执行如下命令,订阅所有的事件:
PSUBSCRIBE *
当哨兵把新主库选择出来后,客户端就会看到下面的 switch-master 事件。这个事件表示主库已经切换了,新主库的 IP 地址和端口信息已经有了。这个时候,客户端就可以用这里面的新主库地址和端口进行通信了
switch-master <master name> <oldip> <oldport> <newip> <newport>
有了这些事件通知机制,客户端不仅可以获取新主库的连接信息,还可以跟踪主从库切换过程中的各个重要事件。这为客户端提供了切换的实时状态,使其了解切换的进展情况。
现在,借助 pub/sub 机制,哨兵与哨兵、哨兵与从库、哨兵与客户端之间都建立了紧密的联系。再加上之前我们介绍的主库下线判定和新主库选定的依据,哨兵集群的监控、选主和通知三大任务基本已经可以正常运行了。不过,我们仍需考虑一个问题:在主库发生故障后,哨兵集群中有多个实例,那么该由哪个哨兵来执行实际的主从切换呢?
由哪个哨兵执行主从切换?
确定由哪个哨兵来执行主从切换的过程,类似于主库“客观下线”的判断过程,它也是一个“投票仲裁”的过程。在详细探讨这个过程之前,我们先回顾一下判断主库“客观下线”的仲裁过程。
在哨兵集群中,要断定主库“客观下线”需要多个实例达成一致意见。我在之前的课程中分享了判断“客观下线”的原则,接下来,我们将深入探讨这个具体的判断过程。
一旦一个哨兵实例认为主库“主观下线”,它会向其他哨兵实例发送 is-master-down-by-addr 命令。其他实例根据它们自身与主库的连接情况,给出 Y 或 N 的响应。在这里,Y 代表赞成,N 代表反对。
图片
在获得足够的赞成票后,一个哨兵就可以将主库标记为“客观下线”。所需的赞成票数是通过哨兵配置文件中的 quorum 配置项来设置的。举个例子,如果现有 5 个哨兵,quorum 配置为 3,那么一个哨兵需要获得 3 张赞成票才能将主库标记为“客观下线”。这里的 3 张赞成票包括该哨兵自己的一张票和其他两个哨兵的赞成票。
当满足了所需的赞成票数后,该哨兵会向其他哨兵发送请求,表明自己希望执行主从切换,并请求进行投票,这个投票过程被称为“Leader选举”。因为最终执行主从切换的哨兵被称为Leader,而投票过程则决定了谁将成为这个Leader。
在Leader选举过程中,任何希望成为Leader的哨兵都必须满足两个条件:首先,必须获得半数以上的赞成票;其次,获得的票数还必须大于或等于哨兵配置文件中的quorum值。以拥有3个哨兵为例,如果quorum设置为2,那么任何想成为Leader的哨兵只需要获得2张以上的赞成票即可。
这么说你可能还不太好理解,我再画一张图片,展示一下 3 个哨兵、quorum 为 2 的选举过程。
图片
在T1时刻,S1判断主库已经“客观下线”,并试图成为Leader。首先,S1给自己投了一张赞成票,接着,S1发送请求命令给S2和S3,表明它想成为Leader。
在T2时刻,S3也判断主库已经“客观下线”并希望成为Leader。同样地,S3首先给自己投了一张赞成票,之后向S1和S2发送请求命令,表明它想成为Leader。
在T3时刻,S1收到了S3发来的Leader投票请求。因为S1已经投了一票赞成自己,所以无法再为其他哨兵投赞成票,于是S1回复N,表示不同意。同时,S2在T3时刻接到S3的Leader投票请求,由于S2之前没有进行投票,它会为首个向它发送投票请求的哨兵回复Y,而后续发送请求的哨兵则会收到N。因此,在T3时刻,S2回复S3,同意S3成为Leader。
在T4时刻,S2最终收到了T1时刻S1发来的投票请求。由于S2在T3时刻已经同意S3成为Leader,因此在T4时刻,S2回复S1,不同意S1成为Leader。此情况出现的原因可能是S3和S2之间的网络通信正常,而S1和S2之间的网络通信出现了阻塞,导致投票请求传输缓慢。
最后,在T5时刻,S1收到的票数包括一张赞成票Y(来自自己)和一张反对票N(来自S2)。而S3除了自己的一张赞成票Y,还收到了S2的一张赞成票Y。在这一时刻,S3不仅获得了半数以上的Leader赞成票,还达到了预设的quorum值(quorum为2),因此,S3最终成为Leader。接下来,S3将开始执行选主操作,并在选定新的主库后,通知其他从库和客户端新主库的信息。
如果S3未获得2张赞成票,那么这轮投票将不会产生Leader。哨兵集群将等待一段时间(即哨兵故障转移超时时间的2倍),然后重新进行选举。这是因为哨兵集群的成功投票在很大程度上取决于选举命令的正常网络传播。如果网络负载较大或短时阻塞发生,可能导致没有哨兵能够获得半数以上的赞成票。因此,在网络压力减轻后重新进行选举,将增加成功的机会。
需要注意的是,如果哨兵集群只有2个实例,那么一个哨兵要想成为Leader,必须获得2张赞成票而不是1张。因此,如果有一个哨兵出现故障,那么集群将无法执行主从库切换。因此,通常情况下,我们会配置至少3个哨兵实例,这一点非常重要,务必在实际应用中予以注意。
小结
一般情况下,当我们解决系统问题时,我们会引入新的机制或者设计新的功能层,就像我们在之前学习的内容一样:为了实现主从切换,我们引入了哨兵机制;为了应对单个哨兵故障导致无法进行主从切换的情况,以及为了降低误判率,我们引入了哨兵集群;而哨兵集群也需要一些机制来支持其正常运行。
本篇介绍了支持哨兵集群的关键机制,包括:
- 基于发布/订阅(pub/sub)机制的哨兵集群组成过程;
- 基于INFO命令的从库列表,用于帮助哨兵与从库建立连接;
- 基于哨兵自身的发布/订阅功能,实现了客户端和哨兵之间的事件通知。
在进行主从切换时,当然不是任何哨兵都可以随意执行的,否则会造成混乱。因此,哨兵集群需要经过投票仲裁来选举出一个领导者,由它负责实际的主从切换,即负责选择新的主库并通知从库和客户端。
最后,我想分享一个经验:要确保所有哨兵实例的配置保持一致,特别是主观下线的判断值 down-after-milliseconds。我们曾经因为这个值在不同的哨兵实例上配置不一致而遇到问题。这导致哨兵集群无法达成对有故障的主库的共识,最终导致集群服务不稳定。因此,请务必注意这条看似简单的经验。