主从复制与哨兵机制
参考:
假设我们的 Redis 只有一个实例,那么当这个实例宕机,那就无法继续提供服务了。我们总说 Redis 具有高可靠性,其实有两层含义:
- 一是数据尽量少丢失。
- 二是服务尽量少中断。
AOF 和 RDB 保证了前者,而对于后者,Redis 的做法就是增加副本冗余量,这样当一个实例出现故障后,其他实例可以继续提供服务。
Redis 使用了主从复制的模式来做数据复制,主从库之间采用的是“读写分离”的方式:
- 读操作:主库、从库都可以接收;
- 写操作:首先到主库执行,然后,主库将写操作同步给从库。
主从复制的模式就面临 DDIA 数据复制 中所讨论的一系列问题。下面我们对 Redis 在该模式上的实践进行讨论。
# 1. 数据同步:主从库如何实现数据一致?
这一大节主要讨论主从库同步的原理,以及应对网络断连风险的方案。
首先看一下主从库间的第一次同步是如何进行的,这也是 Redis 实例建立主从库模式后的规定动作。
# 1.1 主从库间如何进行第一次同步?
当我们启动多个 Redis 实例的时候,它们相互之间就可以通过 replicaof(Redis 5.0 之前使用 slaveof)命令形成主库和从库的关系,之后会按照三个阶段完成数据的第一次同步。
例如,现在有实例1(ip:172.16.19.3)和实例2(ip:172.16.19.5),我们在实例2上执行以下这个命令后,实例2就变成了实例1的从库,并从实例1上复制数据:
replicaof 172.16.19.3 6379
接下来,我们就要学习主从库间数据第一次同步的三个阶段了:
第一阶段是主从库间建立连接、协商同步的过程,主要是为全量复制做准备。在这一步,从库和主库建立起连接,并告诉主库即将进行同步,主库确认回复后,主从库间就可以开始同步了。
具体来说,从库给主库发送 psync 命令,表示要进行数据同步,主库根据这个命令的参数来启动复制。psync命令包含了主库的 runID和复制进度 offset 两个参数:
- runID:是每个 Redis 实例启动时都会自动生成的一个随机 ID,用来唯一标记这个实例。当从库和主库第一次复制时,因为不知道主库的 runID,所以将runID设为“?”。
- offset:此时设为 -1,表示第一次复制。
主库收到 psync 命令后,会用 FULLRESYNC 响应命令带上两个参数:主库 runID 和主库目前的复制进度 offset,返回给从库。从库收到响应后,会记录下这两个参数。
这里有个地方需要注意,FULLRESYNC 响应表示第一次复制采用的全量复制,也就是说,主库会把当前所有的数据都复制给从库。
在第二阶段,主库将所有数据同步给从库。从库收到数据后,在本地完成数据加载。这个过程依赖于内存快照生成的 RDB 文件。
具体来说,主库执行 bgsave 命令,生成 RDB 文件,接着将文件发给从库。从库接收到 RDB 文件后,会先清空当前数据库,然后加载 RDB 文件。这是因为从库在通过 replicaof 命令开始和主库同步前,可能保存了其他数据。为了避免之前数据的影响,从库需要先把当前数据库清空。
在主库将数据同步给从库的过程中,主库不会被阻塞,仍然可以正常接收请求。否则,Redis 的服务就被中断了。但是,这些请求中的写操作并没有记录到刚刚生成的 RDB 文件中。为了保证主从库的数据一致性,主库会在内存中用专门的 replication buffer,记录 RDB 文件生成后收到的所有写操作。
最后,也就是第三阶段,主库会把第二阶段执行过程中新收到的写命令,再发送给从库。具体的操作是,当主库完成 RDB 文件发送后,就会把此时 replication buffer 中的修改操作发给从库,从库再重新执行这些操作。这样一来,主从库就实现同步了。
# 1.2 主从级联模式分担全量复制时的主库压力
在主从同步时,全量复制的阶段需要主库完成两个耗时的操作:生成 RDB 和传输 RDB。
若从库数量很多且都需要进行全量复制,会让主库忙于 fork 子进程生成 RDB,导致主库响应请求速度变慢。同时传输 RDB 也需要占用主库的带宽。那么,有没有好的解决方法来分担主库压力呢?这就是“主-从-从”模式。
刚刚介绍的主从模式中,所有的从库都是从主库同步而来的,现在我们可以通过“主-从-从”的模式将主库生成的 RDB 和传输 RDB 的压力以级联的方式分散到从库上。
简单来说,我们在部署主从集群的时候,可以手动选择一个从库(比如选择内存资源配置较高的从库),用于级联其他的从库。然后,我们可以再选择一些从库(例如三分之一的从库),在这些从库上执行如下命令,让它们和刚才所选的从库,建立起主从关系:
replicaof 所选从库的IP 6379
这样一来,这些从库就会知道,在进行同步时,不用再和主库进行交互了,只要和级联的从库进行写操作同步就行了,这就可以减轻主库上的压力,如下图所示:
以上就是“主-从-从”模式的同步。一旦主从库完成了全量复制,它们之间就会一直维护一个网络连接,主库会通过这个连接将后续陆续收到的命令操作再同步给从库,这个过程也称为基于长连接的命令传播,可以避免频繁建立连接的开销。
这个数据同步的过程存在一个风险点:网络断连或阻塞。如果网络断连,主从库之间就无法进行命令传播了,从库的数据自然也就没办法和主库保持一致了,客户端就可能从从库读到旧数据。接下来,我们就来聊聊网络断连后的解决办法。
# 1.3 主从库间网络断了怎么办?
在 Redis 2.8 之前,如果主从库在命令传播时出现了网络闪断,那么,从库就会和主库重新进行一次全量复制,开销非常大。
从 Redis 2.8 开始,网络断了之后,主从库会采用增量复制的方式继续同步。也就是只会把网络断连期间主库收到的命令,同步给从库。
那么,增量复制时,主从库之间具体是怎么保持同步的呢?这里的奥妙就在于 repl_backlog_buffer 这个缓冲区。我们先来看下它是如何用于增量命令的同步的。
当主从库断连后,主库会把断连期间收到的写操作命令,写入 replication buffer,同时也会把这些操作命令也写入 repl_backlog_buffer 这个缓冲区。repl_backlog_buffer 是一个环形缓冲区,主库会记录自己写到的位置,从库则会记录自己已经读到的位置。
刚开始的时候,主库和从库的写读位置在一起,这算是它们的起始位置。随着主库不断接收新的写操作,它在缓冲区中的写位置会逐步偏离起始位置,我们通常用偏移量来衡量这个偏移距离的大小,对主库来说,对应的偏移量就是 master_repl_offset。主库接收的新写操作越多,这个值就会越大。同样,从库在复制完写操作命令后,它在缓冲区中的读位置也开始逐步偏移刚才的起始位置,此时,从库已复制的偏移量 slave_repl_offset 也在不断增加。正常情况下,这两个偏移量基本相等。
主从库的连接恢复之后,从库首先会给主库发送 psync 命令,并把自己当前的 slave_repl_offset 发给主库,主库会判断自己的 master_repl_offset 和 slave_repl_offset 之间的差距。在网络断连阶段,主库可能会收到新的写操作命令,所以,一般来说,master_repl_offset 会大于 slave_repl_offset。此时,主库只用把 master_repl_offset 和 slave_repl_offset 之间的命令操作同步给从库就行。
就像刚刚示意图的中间部分,主库和从库之间相差了 put d e
和 put d f
两个操作,在增量复制时,主库只需要把它们同步给从库就行了。
说到这里,我们再借助一张图,回顾下增量复制的流程:
不过由于 repl_backlog_buffer 是一个环形缓冲区,所以在缓冲区写满后,主库会继续写入,此时,就会覆盖掉之前写入的操作。如果从库的读取速度比较慢,就有可能导致从库还未读取的操作被主库新写的操作覆盖了,这会导致主从库间的数据不一致。因此,我们要想办法避免这一情况,一般而言,我们可以调整 repl_backlog_size 这个参数。这个参数和所需的缓冲空间大小有关:
举个例子,如果主库每秒写入 2000 个操作,每个操作的大小为 2KB,网络每秒能传输 1000 个操作,那么,有 1000 个操作需要缓冲起来,这就至少需要 2MB 的缓冲空间。否则,新写的命令就会覆盖掉旧操作了。为了应对可能的突发压力,我们最终把 repl_backlog_size 设为 4MB。
这样一来,增量复制时主从库的数据不一致风险就降低了。不过,如果并发请求量非常大,连两倍的缓冲空间都存不下新操作请求的话,此时,主从库数据仍然可能不一致。针对这种情况:
- 一方面,根据服务器的资源继续适当增加 repl_backlog_size 值。
- 一方面,可以考虑使用切片集群来分担单个主库的请求压力。(后面会讲)
# 1.4 小结
这一节主要讲了 Redis 主从同步的三种模式:
- 全量复制:第一次同步不可避免,耗时。为避免压力,引入了“主-从-从”的级联模式。
- 长连接复制:是主从库正常运行后的常规同步阶段,主从之间通过命令传播实现同步。
- 增量复制:发生网络断连后,就需要增量复制来保证数据的同步。
不过,主从模式面临主库故障的潜在风险,下面将聊聊主库故障后,保证服务可靠性的解决方案。
本节问题:为什么主从库间的复制不使用 AOF?
有两个原因:
- RDB 文件是二进制文件,无论是要把 RDB 写入磁盘,还是要通过网络传输 RDB,IO 效率都比记录和传输 AOF 的高。
- 在从库端进行恢复时,用 RDB 的恢复效率要高于用 AOF。
# 2. 哨兵机制:主库挂了,如何不间断服务?
上节学习了主从库的集群模式,在这个模式下,如果从库发生故障了,客户端可以继续向主库或其他从库发送请求,但是如果主库发生故障了,从库就没有主库可以进行数据复制了。如果请求是只读的话还好,但如果有写请求,那么此时就没有实例可以完成这个操作了。这种情况是不可接受的。
所以,如果主库挂了,我们就需要运行一个新主库,比如说把一个从库切换为主库,但这就涉及到三个问题:
- 主库真的挂了吗?
- 该选择哪个从库作为主库?
- 怎么把新主库的相关信息通知给从库和客户端呢?
这就要提到哨兵机制了。在 Redis 主从集群中,哨兵机制是实现主从库自动切换的关键机制,它有效地解决了主从复制模式下故障转移的这三个问题。
# 2.1 哨兵机制的基本流程
哨兵其实就是一个运行在特殊模式下的 Redis 进程,主从库实例运行的同时,它也在运行。哨兵主要负责的就是三个任务:监控、选主(选择主库)和通知。
- 监控是指哨兵进程在运行时,周期性地给所有的主从库发送 PING 命令,检测它们是否仍然在线运行。如果从库没有在规定时间内响应哨兵的PING命令,哨兵就会把它标记为“下线状态”;同样,如果主库也没有在规定时间内响应哨兵的PING命令,哨兵就会判定主库下线,然后开始自动切换主库的流程。
- 自动切换主库的流程就是哨兵的选主的任务:主库挂了以后,哨兵就需要从很多个从库里,按照一定的规则选择一个从库实例,把它作为新的主库。这一步完成后,现在的集群里就有了新主库。
- 然后,哨兵会执行最后一个任务:通知:哨兵把新主库的连接信息发给其他从库,让它们执行 replicaof 命令,和新主库建立连接,并进行数据复制。同时,哨兵会把新主库的连接信息通知给客户端,让它们把请求操作发到新主库上。
在这三个任务中,通知任务相对来说比较简单,哨兵只需要把新主库信息发给从库和客户端,让它们和新主库建立连接就行,并不涉及决策的逻辑。但是,在监控和选主这两个任务中,哨兵需要做出两个决策:
- 在监控任务中,哨兵需要判断主库是否处于下线状态;
- 在选主任务中,哨兵也要决定选择哪个从库实例作为主库。
接下来先说一下如何判断主库的下线状态。哨兵对主库的下线判断有“主观下线”和“客观下线”两种。
# 2.2 主观下线和客观下线
我先解释下什么是“主观下线”。
哨兵进程会使用 PING 命令检测它自己和主、从库的网络连接情况,用来判断实例的状态。如果哨兵发现主库或从库对 PING 命令的响应超时了,那么,哨兵就会先把它标记为“主观下线”:
- 如果检测的是从库,那么哨兵简单地把它标记为主观下线就行了,因为从库的下线影响一般不太大,集群的对外服务不会间断。
- 但如果检测是主库,那么哨兵在标记为主观下线后,还需要开启主从切换。
但存在哨兵误判主库的下线状态的问题,因为一旦开启主从切换,就会带来额外开销,因此需要特别注意避免误判的情况。误判就是说主库实际没有下线,但哨兵误以为它下线了,这通常发生在集群的网络压力较大的情况下。
那怎样减少误判呢?哨兵机制通常会由多台实例组成一个哨兵集群,让多个哨兵实例一起来判断,从而避免单个哨兵因网络问题而导致的误判。这样,只有当大多数的哨兵都认为主库已经主观下线了,那主库才被标记为客观下线。这个叫法表明主库下线成为一个客观事实了,判断的原则就是少数服从多数。这时会进一步出发哨兵进行主从切换的流程。
上述标记为“主观下线”和“客观下线”的流程如下图所示:
简单来说,“客观下线”的标准就是,当有 N 个哨兵实例时,最好要有 N/2 + 1 个实例判断主库为“主观下线”,才能最终判定主库为“客观下线”。这样一来,就可以减少误判的概率,也能避免误判带来的无谓的主从库切换。(当然,有多少个实例做出“主观下线”的判断才可以,可以由 Redis 管理员自行设定)
到这里我们可以看到,借助于多个哨兵实例来共同判断主库是否处于下线状态。如果主库的确下线了,哨兵就要开始下一个决策过程:选主。
# 2.3 如何选定新主库?
哨兵选择新主库的过程可以称为“筛选+打分”:
- 筛选:在多个从库中按照一定的筛选条件来去掉不符合条件的从库。
- 打分:再按照一定的规则,给剩下的从库逐个打分,评选得分最高的从库为新主库。
这个过程如下图所示:
在刚刚那段话中,有两个“一定”,我们看看分别是什么:
# 2.3.1 筛选
我们在多个从库中选主的时候,除了要检查从库的当前在线状态,还要判断它之前的网络连接状态,从而选出网络情况最好的实例。如果从库总是和主库断连,而且断连次数超出了一定的阈值,我们就有理由相信,这个从库的网络状况并不是太好,就可以把这个从库筛掉了。
具体怎么判断呢?你使用配置项 down-after-milliseconds * 10。其中,down-after-milliseconds 是我们认定主从库断连的最大连接超时时间。如果在 down-after-milliseconds 毫秒内,主从节点都没有通过网络联系上,我们就可以认为主从节点断连了。如果发生断连的次数超过了 10 次,就说明这个从库的网络状况不好,不适合作为新主库。
由此就过滤掉了不合适做主库的从库,完成了筛选工作。
# 2.3.2 打分
接下来就要给剩余的从库打分了。我们可以分别按照三个规则依次进行三轮打分:
- 从库优先级
- 从库复制进度
- 从库 ID 号
只要在某一轮中,有从库得分最高,那么它就是主库了,选主过程到此结束。如果没有出现得分最高的从库,那么就继续进行下一轮。
第一轮:优先级最高的从库得分高。
用户可以通过 slave-priority 配置项,给不同的从库设置不同优先级。比如,你有两个从库,它们的内存大小不一样,你可以手动给内存大的实例设置一个高优先级。在选主时,哨兵会给优先级高的从库打高分,如果有一个从库优先级最高,那么它就是新主库了。如果从库的优先级都一样,那么哨兵开始第二轮打分。
第二轮:和旧主库同步程度最接近的从库得分高。
这个规则的依据是,如果选择和旧主库同步最接近的那个从库作为主库,那么,这个新主库上就有最新的数据。其实也就是哪个从库的 slave_repl_offset 最接近主库的 master_repl_offset 谁就得分高。如下图所示:
第三轮:ID号小的从库得分高。
每个实例都会有一个ID,这个ID就类似于这里的从库的编号。目前,Redis 在选主库时,有一个默认的规定:在优先级和复制进度都相同的情况下,ID 号最小的从库得分最高,会被选为新主库。
到这里,“选主”这个过程就完成了。我们在回顾一下这个流程:
- 首先,哨兵会按照在线状态、网络状态,筛选过滤掉一部分不符合要求的从库;
- 然后,依次按照优先级、复制进度、ID 号大小再对剩余的从库进行打分,只要有得分最高的从库出现,就把它选为新主库。
# 2.4 小结
哨兵机制是实现 Redis 不间断服务的重要保证,它自动完成了以下三个功能,并实现了主从切换:
- 监控主库运行状态,并判断主库是否客观下线;
- 在主库客观下线后,选取新主库;
- 选出新主库后,通知从库和客户端。
为了降低误判率,哨兵机制通常采用多实例部署,通过“少数服从多数”的原则,来判断主库是否客观下线。
但哨兵集群同时也带来了问题:
- 哨兵集群中有实例挂了,怎么办,会影响主库状态判断和选主吗?
- 哨兵集群多数实例达成共识,判断出主库“客观下线”后,由哪个实例来执行主从切换呢?
为了搞懂这些问题,需要进一步了解哨兵集群。
本节问题 1:在主从切换过程中,客户端能否正常地进行请求操作呢?
主从集群一般是采用读写分离模式,当主库故障后,客户端仍然可以把读请求发送给从库,让从库服务。但是,对于写请求操作,客户端就无法执行了。
本节问题 2:如果想要应用程序不感知服务的中断,还需要哨兵或客户端再做些什么吗?
- 一方面,客户端需要能缓存应用发送的写请求。只要不是同步写操作(Redis 应用场景一般也没有同步写),写请求通常不会在应用程序的关键路径上,所以,客户端缓存写请求后,给应用程序返回一个确认就行。
- 另一方面,主从切换完成后,客户端要能和新主库重新建立连接,哨兵需要提供订阅频道,让客户端能够订阅到新主库的信息。同时,客户端也需要能主动和哨兵通信,询问新主库的信息。
# 3. 哨兵集群:哨兵挂了,主从库还能切换吗?
上节讲的哨兵机制实现了主从切换,但如果有哨兵实例在运行时发生了故障,主从库还能正常切换吗?实际上,一旦多个实例组成了哨兵集群,即使有哨兵实例挂掉了,其他哨兵还能继续协作完成主从切换的工作,包括判定主库是不是处于下线状态、选择新主库,以及通知从库和客户端。
如果你部署过哨兵集群的话就会知道,在配置哨兵的信息时,我们只需要用到下面的这个配置项,即设置主库的 IP和端口,并没有配置其他哨兵的连接信息:
sentinel monitor <master-name> <ip> <redis-port> <quorum>
这些哨兵实例既然都不知道彼此的地址,又是怎么组成集群的呢?要弄明白这个问题,我们就需要学习一下哨兵集群的组成和运行机制了。
# 3.1 基于 pub/sub 机制的哨兵集群组成
哨兵实例之间可以相互发现,要归功于 Redis 提供的 pub/sub 机制,也就是发布/订阅机制。哨兵只要和主库建立起了连接,就可以在主库上发布消息了,比如说发布它自己的连接信息(IP和端口)。同时,它也可以从主库上订阅消息,获得其他哨兵发布的连接信息。当多个哨兵实例都在主库上做了发布和订阅操作后,它们之间就能知道彼此的 IP 地址和端口。
除了哨兵实例,我们自己编写的应用程序也可以通过Redis进行消息的发布和订阅。所以,为了区分不同应用的消息,Redis 会以频道的形式,对这些消息进行分门别类的管理。所谓的频道,实际上就是消息的类别。当消息类别相同时,它们就属于同一个频道。反之,就属于不同的频道。只有订阅了同一个频道的应用,才能通过发布的消息进行信息交换。
在主从集群中,主库上有一个名为 __sentinel__: hello
的频道,不同哨兵就是通过它来相互发现,实现互相通信的。如下图示例:
借助于上图所示的 pub/sub 机制,哨兵集群就形成了,它们之间可以通过网络连接进行通信,并对主库是否下线的判断进行协商。哨兵除了彼此之间建立起连接形成集群外,还需要和从库建立连接。这是因为,在哨兵的监控任务中,它需要对主从库都进行心跳判断,而且在主从库切换完成后,它还需要通知从库,让它们和新主库进行同步。
那么, 哨兵是如何知道从库的 IP 地址和端口的呢?如下图所示,这是由哨兵向主库发送 INFO 命令来完成的,然后主库会响应给哨兵一个 slave 列表。由此,哨兵就可以与从库建立连接了。
你看,通过 pub/sub 机制,哨兵之间可以组成集群,同时,哨兵又通过 INFO 命令,获得了从库连接信息,也能和从库建立连接,并进行监控了。
但是,哨兵不能只和主、从库连接。因为主从库切换后,客户端也需要知道新主库的连接信息,才能向新主库发送请求操作。所以,哨兵还需要完成把新主库的信息告诉客户端这个任务。而且,在实际使用哨兵时,我们有时会遇到这样的问题:如何在客户端通过监控了解哨兵进行主从切换的过程呢?比如说,主从切换进行到哪一步了?这其实就是要求,客户端能够获取到哨兵集群在监控、选主、切换这个过程中发生的各种事件。
此时,我们仍然可以依赖 pub/sub 机制,来帮助我们完成哨兵和客户端间的信息同步。
# 3.2 基于 pub/sub 机制的客户端事件通知
从本质上说,哨兵就是一个运行在特定模式下的 Redis 实例,只不过它并不服务请求操作,只是完成监控、选主和通知的任务。所以,每个哨兵实例也提供 pub/sub 机制,客户端可以从哨兵订阅消息。哨兵提供的消息订阅频道有很多,不同频道包含了主从库切换过程中的不同关键事件。下面列出了一些重要的频道:
知道了这些频道之后,你就可以让客户端从哨兵那里订阅消息了。具体的操作步骤是,客户端读取哨兵的配置文件后,可以获得哨兵的地址和端口,和哨兵建立网络连接。然后,我们可以在客户端执行订阅命令,来获取不同的事件消息。
举个例子,你可以执行 SUBSCRIBE +odown
命令,来订阅“所有实例进入客观下线状态的事件”,也可以执行 PSUBSCRIBE *
来订阅所有的事件。
当哨兵把新主库选择出来后,客户端就会看到下面的 switch-master 事件。这个事件表示主库已经切换了,新主库的 IP 址和端口信息已经有了。这个时候,客户端就可以用这里面的新主库地址和端口进行通信了:
switch-master <master name> <oldip> <oldport> <newip> <newport>
有了这些事件通知,客户端不仅可以在主从切换后得到新主库的连接信息,还可以监控到主从库切换过程中发生的各个重要事件。这样,客户端就可以知道主从切换进行到哪一步了,有助于了解切换进度。
好了,有了 pub/sub 机制,哨兵和哨兵之间、哨兵和从库之间、哨兵和客户端之间就都能建立起连接了,再加上我们上节课介绍主库下线判断和选主依据,哨兵集群的监控、选主和通知三个任务就基本可以正常工作了。不过,我们还需要考虑一个问题:主库故障以后,哨兵集群有多个实例,那怎么确定由哪个哨兵来进行实际的主从切换呢?
# 3.3 由哪个哨兵执行主从切换?
确定由哪个哨兵执行主从切换的过程,和主库“客观下线”的判断过程类似,也是一个“投票仲裁”的过程。在具体了解这个过程前,我们再来看下,判断“客观下线”的仲裁过程。
哨兵集群要判定主库“客观下线”,需要有一定数量的实例都认为该主库已经“主观下线”了。我在上节课向你介绍了判断“客观下线”的原则,接下来,我介绍下具体的判断过程:
任何一个实例只要自身判断主库“主观下线”后,就会给其他实例发送 is-master-down-by-addr命令。接着,其他实例会根据自己和主库的连接情况,做出 Y 或 N 的响应,Y 相当于赞成票,N 相当于反对票。如下图所示:
一个哨兵获得了仲裁所需的赞成票数后,就可以标记主库为“客观下线”。这个所需的赞成票数是通过哨兵配置文件中的quorum配置项设定的。例如,现在有 5 个哨兵,quorum 配置的是 3,那么,一个哨兵需要3张赞成票,就可以标记主库为“客观下线”了。这3张赞成票包括哨兵自己的一张赞成票和另外两个哨兵的赞成票。
此时,这个哨兵就可以再给其他哨兵发送命令,表明希望由自己来执行主从切换,并让所有其他哨兵进行投票。这个投票过程称为“Leader 选举”。因为最终执行主从切换的哨兵称为 Leader,投票过程就是确定 Leader。在投票过程中,任何一个想成为 Leader 的哨兵,要满足两个条件:
- 拿到半数以上的赞成票;
- 拿到的票数同时还需要大于等于哨兵配置文件中的 quorum 值。以3个哨兵为例,假设此时的 quorum 设置为 2,那么,任何一个想成为 Leader 的哨兵只要拿到2张赞成票,就可以了。
下图就展示了 3 个哨兵、quorum=2 的选举过程:
需要注意的是,如果哨兵集群只有 2 个实例,此时,一个哨兵要想成为 Leader,必须获得 2 票,而不是 1 票。所以,如果有个哨兵挂掉了,那么,此时的集群是无法进行主从库切换的。因此,通常我们至少会配置 3 个哨兵实例。这一点很重要,你在实际应用时可不能忽略了。
# 3.4 小结
通常,我们在解决一个系统问题的时候,会引入一个新机制,或者设计一层新功能,就像我们在这两节课学习的内容:为了实现主从切换,我们引入了哨兵;为了避免单个哨兵故障后无法进行主从切换,以及为了减少误判率,又引入了哨兵集群;哨兵集群又需要有一些机制来支撑它的正常运行。
这一节主要讲了哨兵集群的一些关键机制:
- 基于 pub/sub 机制的哨兵集群组成过程;
- 基于 INFO 命令的从库列表,这可以帮助哨兵和从库建立连接;
- 基于哨兵自身的 pub/sub 功能,这实现了客户端和哨兵之间的事件通知。
对于主从切换,需要哨兵集群在判断了主库“客观下线”后,经过投票仲裁,选举一个 Leader 出来,由它负责实际的主从切换,即由它来完成新主库的选择以及通知从库与客户端。
最后,我想再给你分享一个经验:要保证所有哨兵实例的配置是一致的,尤其是主观下线的判断值 down-after-milliseconds。我们曾经就踩过一个“坑”。当时,在我们的项目中,因为这个值在不同的哨兵实例上配置不一致,导致哨兵集群一直没有对有故障的主库形成共识,也就没有及时切换主库,最终的结果就是集群服务不稳定。所以,你一定不要忽略这条看似简单的经验。
本节问题 1:5 个哨兵实例的集群,quorum 值设为 2。在运行过程中,如果有 3 个哨兵实例都发生故障了,此时,Redis 主库如果有故障,还能正确地判断主库“客观下线”吗?如果可以的话,还能进行主从库自动切换吗?
因为判定主库“客观下线”的依据是,认为主库“主观下线”的哨兵个数要大于等于quorum值,现在还剩2个哨兵实例,个数正好等于quorum值,所以还能正常判断主库是否处于“客观下线”状态。如果一个哨兵想要执行主从切换,就要获到半数以上的哨兵投票赞成,也就是至少需要3个哨兵投票赞成。但是,现在只有2个哨兵了,所以就无法进行主从切换了。
本节问题 2:哨兵实例是不是越多越好呢?如果同时调大 down-after-milliseconds 值,对减少误判是不是也有好处?
哨兵实例越多,误判率会越低,但是在判定主库下线和选举Leader时,实例需要拿到的赞成票数也越多,等待所有哨兵投完票的时间可能也会相应增加,主从库切换的时间也会变长,客户端容易堆积较多的请求操作,可能会导致客户端请求溢出,从而造成请求丢失。如果业务层对Redis的操作有响应时间要求,就可能会因为新主库一直没有选定,新操作无法执行而发生超时报警。
调大down-after-milliseconds后,可能会导致这样的情况:主库实际已经发生故障了,但是哨兵过了很长时间才判断出来,这就会影响到Redis对业务的可用性。