Redis——哨兵机制
前言
在上一篇博客中,讲解了有关 Redis 的主从模式,在这个模式下,如果从库发生故障了,客户端可以继续向主库或者其他从库发送请求,进行相关操作。但如果是主库挂了呢?就会直接影响从库之间的数据同步,因为从库没有相应的主库可以进行数据复制操作了。
这个时候,如果客户端发送的是读请求,那么集群中的从库还可以继续提供服务,也就是说在纯读的业务场景下还能被接受。可一旦有写请求的操作了,按照主从模式下的读写分离要求,需要由主库来完成写操作。此时,因为主库挂了,没有实例可以来服务客户端的写操作请求了。
无论是写服务中断,还是从库无法进行数据同步,在实际生产环境中都是不可接受的。所以,当主库挂了之后,需要一个新的主库来保持 Redis 集群的正常运行。通常会选择把集群中的一个从库切换为主库,这个时候就会涉及到以下三个问题:
- 主库真的挂了吗?
- 该选择哪个从库作为主库?
- 怎么把新主库的相关信息通知给从库和客户端呢?
这就是 Redis 的哨兵机制的作用。在 Redis 主从集群中,哨兵机制是实现主从库自动切换的关键机制,它有效地解决了主从复制模式下故障转移的这三个问题。
基本流程
哨兵其实就是一个运行在特殊模式下的 Redis 进程,主从库实例运行的同时,它也在运行。哨兵主要负责的就是三个任务:监控、选主和通知。
监控指的是哨兵在运行时,会周期性地给所有的主从库发送 PING 命令,检测集群中的节点是否在正常运行。如果从库的响应超时,哨兵就会把它标记为“下线状态”;同样,如果主库没有在规定时间内响应哨兵的 PING 命令,哨兵就会判定主库下线,然后开始自动切换主库的流程。
这也就会用到哨兵的第二个任务,选主。主库挂了之后,哨兵就需要从很多个从库里,按照一定的规则选择一个从库实例,把它作为新的主库。这一步完成后,集群中也有有了新的主库。
然后,哨兵会执行最后一个任务:通知。在执行通知任务时,哨兵会把新主库的连接信息发送给其他从库,让它们执行 replicaof 命令,和新主库建立连接,并进行数据复制。同时,哨兵会把新主库的连接信息通知给客户端,让它们把请求操作发到新主库上。
在上述的三个任务中,通知任务相对来说比较简单,哨兵只需要把新主库的信息发送给从库和客户端,让它们重新和新主库建立连接即可,并不涉及到决策的逻辑。但是,在监控和选主这两个任务重,哨兵需要做出两个决策:
- 在监控任务中,哨兵需要判断主库是否处于下线状态;
- 在选择任务中,哨兵也需要决定选择哪一个从库实例作为新的主库。
主观下线和客观下线
先来看一下什么是“主观下线”。哨兵进程会使用 PING 命令检测它自己和主从库的网络连接情况,用来判断集群中各个实例的状态。如果某个节点的响应超时了,哨兵就会先把它标记为“主观下线”。
这里对于主从库的后续处理会有所不同。如果检测的是从库,处理比较简单,只需要将其标记为“客观下线”就行了,因为从库的下线影响一般不会太大,集群对外的服务也就不会间断。
但如果检测到的是主库响应 PING 命令超时,哨兵就不能简单地将其标记为“主观下线”,开启主从切换。因为很有可能存在这么一个情况:哨兵误判了,主库并没有故障。如果直接开始主从切换,后续的选主和通知任务都会带来额外的计算和通信开销。为了避免这些不必要的开销,就需要特别注意误判的情况。
首先,得搞清楚误判是什么。简单来说,就是主库实际上并没有下线,但是哨兵误以为它挂了。误判通常会发生在集群网络压力较大、网络阻塞,或者是主库本身压力较大的情况下。
一旦哨兵判断主库下线了,就会开始选择新主库,并让从库和新主库进行数据同步,这过程本身就会有开销,例如,哨兵要花时间选出新主库,从库也要花时间和主库建立连接,然后进行数据同步。而在误判的情况下,主库本身根本就不需要进行切换的,所以这个过程的开销是没有价值的。所以,我们要尽可能地减少误判。
俗话说,一个好汉三个帮,我们可以采用多实例组成的哨兵集群来解决这个问题。引入多个哨兵实例一起来判断,就可以避免单个哨兵因为自身网络状况不好,而误判主库下线的情况。同时,多个哨兵的网络同时不稳定的概率比较小,通过哨兵集群一起做决策,误判率也会降低。
在判断主库是否下线时,不能由一个哨兵说了算,只有大多数的哨兵实例,都判断主库已经“客观下线”了,主库才会被标记为“客观下线”。这个判断的原则是:少数服从多数。同时,这会进一步触发哨兵开始主从切换的流程。
如下图所示,Redis 主从集群有一个主库、三个从库,还有三个哨兵实例。在图片的左边,哨兵 2 判断主库为“主观下线”,但哨兵 1 和 3 却判定主库是上线状态,此时,主库仍然被判断为处于上线状态。在图片的右边,哨兵 1 和 2 都判断主库为“主观下线”,此时,即使哨兵 3 仍然判断主库为上线状态,主库也被标记为“客观下线”了。
简单来说,“客观下线”的标准就是,当有 N 个哨兵实例时,最好要有 N/2+1 个哨兵实例都认为主库是“主观下线”了,才能最终将主库标记为“客观下线”。这样一来,就可以减少误判的概率,也能避免误判带来的主从切换。
如何选主?
一般来说,可以把哨兵选择新主库的过程称为“筛选 + 打分”。简单来说,在多个从库中,先按照一定的筛选条件,把不符合条件的从库去掉。然后,再按照一定的规则,给剩下的从库逐个打分,将得分最高的从库选为新主库,如下图所示:
一般情况下,我们要先保证所选的从库仍然在线运行。不过,在选主时从库正常在线,这只能表示从库的现状良好,并不代表它就是最适合做主库的。比如,在选主时,一个从库正常运行,我们就把它选为新主库开始使用了。可是,没过一会它的网络出现了故障,这就导致不得不重新选主,是一件很糟糕的事情。
所以,在选主时,除了要检查从库的当前在线状态,还要判断它之前的网络连接状态。如果从库和主库断连,而且断连的次数超过了一定的阈值,凭借这一点就可以说,这个从库的网络状况并不是很好,从而可以排除这个从库。
具体怎么判断呢?可以使用配置项 down-after-milliseconds * 10
。其中,down-after-milliseconds 是认定主从库断连的最大连接超时时间。如果在 down-after-milliseconds 毫秒内,主从节点都没有通过网络联系上,就可以认为主从节点断连了。如果发生断连的次数超过了 10 次,就说明这个从库的网络状况不好,不适合作为新主库。
基于上述内容就可以过滤掉不适合做主库的从库,完成了筛选工作。
接下来就需要给剩余的从库进行打分,可以分别按照三个规则依次进行三轮打分,这三个规则分别是从库优先级、从库复制进度以及从库 ID 号。只要在某一轮中,有从库得分最高,那么它就是主库了,选主过程到此结束。如果没有出现得分最高的从库,那么就继续进行下一轮。
第一轮:优先级最高的从库得分高。
用户可以通过 slave-priority
配置项,给不同的从库设置优先级。假设现在有两个从库,它们的内存大小不一样,就可以手动给内存大的实例设置一个高优先级。在选主时,哨兵会给优先级高的从库打高分,如果有一个从库优先级最高,那么它就是新主库了。如果从库的优先级相同,则哨兵会开始第二轮打分。
第二轮:复制进度最高的从库得分高。
这个规则的依据是,如果选择和旧主库同步最接近的从库作为主库,那么,这个新主库上就有最新的数据。如何判断从库和旧主库间的同步进度呢?
在上一篇博客中有说到,主从库同步时有个命令传播的过程。在这个过程中,主库会使用 master_repl_offset 记录当前的最新写操作在 repl_backlog_buffer 中的位置,而从库会用 slave_repl_offset 这个值记录当前的复制进度。此时,想要找的从库,它的 slave_repl_offset 需要最接近 master_repl_offset。如果在所有从库中,有从库的 slave_repl_offset 最接近 master_repl_offset,那么它的得分就最高,可以作为新主库。
就像下图所示,旧主库的 master_repl_offset 是 100,从库 1、2 和 3 的 slave_repl_offset 分别是 95、99 和 90,那么,从库 2 就应该被选为新主库。
不过,有时候也会出现两个从库的 salve_repl_offset 的值大小是一样的,这种情况下,哨兵会继续进行第三轮打分。
第三轮:ID 号最高的从库得分高。
每个实例都会有一个 ID,这个 ID 类似于这里的从库编号。目前,Redis 在选主从库时,有一个默认的规定:在优先级和复制进度都相同的情况下,ID 号最小的从库得分最高,会被选为新主库。
到这里,新主库就被选出来了,“选主”这个过程就完成了。
简单总结如下:
首先,哨兵会按照在线状态、网络状态,筛选过滤掉一部分不符合要求的从库,然后,依次按照优先级、复制进度、ID 号大小再对剩余的从库进行打分,只要有得分最高的从库出现,就把它选为新主库。
总结
在这篇博客中,了解了哨兵机制,它是实现 Redis 不间断服务的重要保证。具体来说,主从集群的数据同步是数据可靠的基础保证;而在主库发生故障时,自动的主从切换时服务不间断的关键支撑。
Redis 的哨兵机制自动完成了以下三大功能,从而实现了主从库的自动切换,可以降低 Redis 集群的运维开销:
- 监控主库运行状态,并判断主库是否客观下线;
- 在主库客观下线后,选取新主库;
- 选出新主库后,通知从库和客户端。
为了降低误判率,在实际应用时,哨兵机制通常采用多实例的方式进行部署,多个哨兵实例通过“少数服从多数”的原则,来判断主库是否客观下线。一般来说,可以部署哨兵集群来进一步提升判断准确率。