哨兵机制

Redis 中提供了[[Redis 的主从库模式以及主从复制|主从模式]]来提高可靠性,在这个模式下,从库宕机了客户端可以继续像主库或者其他从库发送请求,进行相关的操作;但是如果是主库发生故障了,那就直接会影响到从库的同步,因为从库没有相应的主库可以进行数据复制操作了。如下图所示:

无论是写服务终端,还是从库无法进行数据同步,都是不能接受的。所以,如果主库挂了,我们就需要运行一个新主库,比如说把一个从库切换为主库,把它当成主库。而这就会涉及到三个问题:

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

为解决以上三个问题,Redis 提供了哨兵机制,哨兵机制是实现主从库自动切换的关键机制,它有效地解决了主从复制模式下故障转移的三个问题。

哨兵机制的基本流程

哨兵其实就是一个运行在特殊模式下的 Redis 进程,主从库实例在运行的同时,哨兵也在运行。哨兵主要负责的就是三个任务:监控、选主(选择新主库)和通知。

  • 监控(监控主库运行状态,并判断主库是否客观下线)
  • 选主(在主库客观下线后,选取新主库)
  • 通知(选出新主库后,通知从库和客户端。)

监控

监控是指哨兵进程在运行时,周期性地给所有主从库发送 PING 命令,检测它们是否仍在线运行。如果从库在规定时间内响应哨兵的 PING 命令,哨兵就会把它标记为”下线状态“;同样,如果主库也没有在规定时间内响应哨兵的 PING 命令,哨兵就会判定主库下线,然后开始自动切换主库流程。

选主

自动切换主库也就是哨兵的第二个任务:选主。主库挂了以后,哨兵就需要从很多个从库里,按照一定的规则选择一个从库实例,把它作为新的主库。这一步完成之后,现在的集群里面就有了新主库。

通知

在执行通知任务时,哨兵会把新主库的连接信息发送给其他从库,让它们执行 replicaof 命令,和新主库建立连接,并进行数据复制。同时,哨兵会把新主库的连接信息通知给客户端,让它们把请求操作发送到新主库上。

在哨兵所执行的这 3 个任务中,通知任务相对来说比较简单,哨兵只需要把新主库信息发给从库和客户端,让它们和新主库建立连接就行,并不涉及决策的逻辑。但是,在监控和选主这两个任务中,哨兵需要做出两个决策:

  • 在监控任务中,哨兵需要判断主库是否处于下线状态;
  • 在选主任务中,哨兵也要决定选择哪个从库作为主库。

主观下线和客观下线

什么是主观下线?

哨兵进程会使用 PING 命令检测它自己和主、从库的网络连接情况,用来判断实例的状态。如果哨兵发现主库或从库对 PING 命令的响应超时了,那么,哨兵就会先把它标记为“主观下线”。

如果检测的是从库,那么,哨兵简单地把它标记为“主观下线”就行了,因为从库的下线影响一般不太大,集群的对外服务不会间断。

但是,如果检测的是主库,那么,哨兵还不能简单地把它标记为“主观下线”,开启主从切换。因为很有可能存在这么一个情况:那就是哨兵误判了,其实主库并没有故障。可是,一旦启动了主从切换,后续的选主和通知操作都会带来额外的计算和通信开销。

为了避免这些不必要的开销,我们要特别注意误判的情况。

首先,我们要知道啥叫误判。很简单,就是主库实际并没有下线,但是哨兵误以为它下线了。误判一般会发生在集群网络压力较大、网络拥塞,或者是主库本身压力较大的情况下。

一旦哨兵判断主库下线了,就会开始选择新主库,并让从库和新主库进行数据同步,这个过程本身就会有开销。

例如,哨兵要花时间选出新主库,从库也需要花时间和新主库同步。而在误判的情况下,主库本身根本就不需要进行切换的,所以这个过程的开销是没有价值的。正因为这样,我们需要判断是否有误判,以及减少误判。

那怎么减少误判呢?在日常生活中,当我们要对一些重要的事情做判断的时候,经常会和家人或朋友一起商量一下,然后再做决定。

哨兵机制也是类似的,它通常会采用多实例组成的集群模式进行部署,这也被称为哨兵集群。引入多个哨兵实例一起来判断,就可以避免单个哨兵因为自身网络状况不好,而误判主库下线的情况。同时,多个哨兵的网络同时不稳定的概率较小,由它们一起做决策,误判率也能降低。

在判断主库是否下线时,不能由一个哨兵说了算,只有大多数的哨兵实例,都判断主库已经“主观下线”了,主库才会被标记为“客观下线”,这个叫法也是表明主库下线成为一个客观事实了。这个判断原则就是:少数服从多数。同时,这会进一步触发哨兵开始主从切换流程。

简单来说,“客观下线”的标准就是,当有 N 个哨兵实例时,最好要有 N/2 + 1 个实例判断主库为“主观下线”,才能最终判定主库为“客观下线”。

这样一来,就可以减少误判的概率,也能避免误判带来的无谓的主从库切换。(当然,有多少个实例做出“主观下线”的判断才可以,可以由 Redis 管理员自行设定)。

如何选定新主库

一般来说,我把哨兵选择新主库的过程称为“筛选 + 打分”。简单来说,我们在多个从库中,先按照一定的筛选条件,把不符合条件的从库去掉。然后,我们再按照一定的规则,给剩下的从库逐个打分,将得分最高的从库选为新主库,如下图所示:

筛选条件

一般情况下,我们肯定要先保证所选的从库仍然在线运行。不过,在选主时从库正常在线只能表示从库的现状良好,并不代表它就是最适合做主库的。为什么?

打个比方,如果在选主时,一个从库正常运行,我们把它选为新主库开始使用。可是,很快它的网络出了故障,此时就又得重新选主了。

再比如,这个从库在选主之前网络都是断断续续的,网络状况才好一阵子就赶上选主,从库的正常在线也只能说此时状况良好,不能代表后续的情况也是如此。

所以,在选主时,除了要检查从库的当前在线状态,还要判断它之前的网络连接状态。如果从库总是和主库断连,而且断连次数超出了一定的阈值,我们就有理由相信,这个从库的网络状况并不是太好,就可以把这个从库筛掉了。

具体怎么判断?

在哨兵的 sentinel.conf 配置文件中,down-after-milliseconds 配置项中可以配置,这个配置项意思是当一个实例失去联系(要么不回复我们的请求,要么回复一个错误)超过了这个时间(毫秒为单位),Sentinel 就开始认为这个实例挂掉了。

这个配置项配置的时间越短,哨兵越敏感,哨兵集群认为主库在短时间内连不上就会发起主从切换,这种配置很可能因为网络拥塞但主库正常而发生不必要的切换,当然,当主库真正故障时,因为切换得及时,对业务的影响最小。

如果配置的时间比较长,哨兵越保守,这种情况可以减少哨兵误判的概率,但是主库故障发生时,业务写失败的时间也会比较久,缓存写请求数据量越多。

如果在 down-after-milliseconds 毫秒内,主从节点都没有通过网络联系上,我们就可以认为主从节点断连了。如果发生断连的次数超过了 10 次,就说明这个从库的网络状况不好,不适合作为新主库。

以上就会过滤掉了不适合做主库的从库,完成了筛选操作。

规则

接下来就要给剩余的从库打分了。我们可以分别按照三个规则依次进行三轮打分,这三个规则分别是从库优先级、从库复制进度以及从库 ID 号

只要在某一轮中,有从库得分最高,那么它就是主库了,选主过程到此结束。如果没有出现得分最高的从库,那么就继续进行下一轮。

第一轮:优先级最高的从库得分高

用户可以通过 redis.conf 配置文件中对 replica-priority 配置项进行配置,给不同的从库设置不同优先级,优先级数值越低优先级越高。

比如,你有两个从库,它们的内存大小不一样,你可以手动给内存大的实例设置一个高优先级。在选主时,哨兵会给优先级高的从库打高分,如果有一个从库优先级最高,那么它就是新主库了。如果从库的优先级都一样,那么哨兵开始第二轮打分。

第二轮:和旧主库同步程度最接近的从库得分高

这个规则的依据是,如果选择和旧主库同步最接近的那个从库作为主库,那么,这个新主库上就有最新的数据。如何判断从库和旧主库间的同步进度呢?

如何判断从库和旧主库间的同步进度呢?

Redis 中[[Redis 的主从库模式以及主从复制#数据同步|主从模式的数据同步]]中,主从库同步时有个命令传播的过程。在这个过程中,主库会用 master_repl_offset 偏移量记录当前的最新写操作在 repl_backlog_buffer 环形缓冲区里,主库和从库都能访问到这片缓冲区,从库使用 slave_repl_offset 偏移量记录当前的复制进度。

现在主库挂掉了,无法拿到 master_repl_offset,那不同从库的 slave_repl_offset 就要进行比较,找出最大的也就是与 master_repl_offset 最接近的从库,得分最高可以作为主库。

就像下图所示,旧主库的 master_repl_offset 是 1000,从库 1、2 和 3 的 slave_repl_offset 分别是 950、990 和 900,那么,从库 2 就应该被选为新主库。

当然,如果有两个从库的 slave_repl_offset 值大小是一样的(例如,从库 1 和从库 2 的 slave_repl_offset 值都是 990),我们就需要给它们进行第三轮打分了。

第三轮:ID 号小的从库得分高

每个实例都会有一个 ID,这个 ID 就类似于这里的从库的编号,Redis server 启动时,会生成一个 40 字节长的随机字符串作为 runID,具体算法用的是 SHA-1 算法。

目前,Redis 在选主库时,有一个默认的规定:在优先级和复制进度都相同的情况下,ID 号最小的从库得分最高,会被选为新主库

到这里,新主库就被选出来了。

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

切换过程中,客户端能否正常进行请求操作?

如果客户端使用了读写分离,那么读请求可以在从库上正常执行,不会受到影响

但是由于此时主库已经挂了,而且哨兵还没有选出新的主库,所以在这期间写请求会失败失败持续的时间 = 哨兵切换主从的时间 + 客户端感知到新主库 的时间。

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

1
既然存在在切换过程中,写请求会失败的问题,那该如何让客户端对此过程无感知呢?

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

除此之外,也需要哨兵和客户端配合做改造:

  • 哨兵主动通知

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

这里的 switch-master 是 Redis 中订阅/发布中的通道

  • 客户端主动获取

如果客户端因为某些原因错过了哨兵的通知,或者哨兵通知后客户端处理失败了,安全起见,客户端也需要支持主动去获取最新主从的地址进行访问。

所以,客户端需要访问主从库时,不能直接写死主从库的地址了,而是需要从哨兵集群中获取最新的地址(sentinel get-master-addr-by-name 命令),这样当实例异常时,哨兵切换后或者客户端断开重连,都可以从哨兵集群中拿到最新的实例地址。

一般 Redis 的 SDK 都提供了通过哨兵拿到实例地址,再访问实例的方式,我们直接使用即可,不需要自己实现这些逻辑。当然,对于只有主从实例的情况,客户端需要和哨兵配合使用,而在分片集群模式下,这些逻辑都可以做在 proxy 层,这样客户端也不需要关心这些逻辑了,Codis 就是这么做的。

哨兵集群

Redis 的哨兵机制可以实现主从库的自动切换,但是因为网络原因,主库可能只是网络阻塞问题没有及时回复哨兵的心跳,导致哨兵误判从而启动主从库的切换。而这切换也会带来很大的开销,为了防止这种误判的情况概率出现,一般都是通过部署多个哨兵实例组成集群,通过集群中多个实例共同判断,可以降低主库下线的误判率。

但凡是允许的实例机器,都有宕机的可能,那如果在哨兵集群中的实例在运行中发生了故障导致宕机,主从库还能正常切换吗?

实际上,一旦多个实例组成了哨兵集群,即使有哨兵实例出现故障挂掉了,其他哨兵还能继续协作完成主从库切换的工作,包括判断主库是不是处于下线状态,选择新主库,以及通知从库和客户端。

创建配置文件,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 配置文件目录 
dir /opt/sentinel/
# 日志文件位置
logfile "./sentinel-26381.log"
# pid文件
pidfile /var/run/sentinel_26381.pid
# 是否后台运行
daemonize yes
# 端口
port 26381
# 监控主服务器master的名字:mymaster,IP:192.168.200.129,port:6379,最后的数字2表示当 Sentinel集群中有2个Sentinel认为master存在故障不可用,则进行自动故障转移
sentinel monitor mymaster 192.168.200.129 6379 2
# master响应超时时间(毫秒),Sentinel会向master发送ping来确认master,如果在20秒内,ping 不通master,则主观认为master不可用
sentinel down-after-milliseconds mymaster 60000
# 故障转移超时时间(毫秒),如果3分钟内没有完成故障转移操作,则视为转移失败
sentinel failover-timeout mymaster 180000
# 故障转移之后,进行新的主从复制,配置项指定了最多有多少个slave对新的master进行同步,那可以理 解为1是串行复制,大于1是并行复制
sentinel parallel-syncs mymaster 1
# 指定mymaster主的密码(没有就不指定)
sentinel auth-pass mymaster 123456

启动一个哨兵有 2 种启动方式:

1
2
方式1:redis-server sentinel.conf --sentinel 
方式2:redis-sentinel sentinel.conf

这里选择第二种方式,这三个哨兵使用不同的端口号、主目录、pid 文件目录以及日志文件目录

1
2
3
redis-sentinel sentinel/1/sentinel.conf 
redis-sentinel sentinel/2/sentinel.conf
redis-sentinel sentinel/3/sentinel.conf

然后使用 redis-cli 工具连接到哨兵,使用 info sentinel 命令查看哨兵信息

1
2
3
4
5
6
7
8
9
10
redis-cli -p 26379
127.0.0.1:26379> info sentinel
# Sentinel
sentinel_masters:1
sentinel_tilt:0
sentinel_tilt_since_seconds:-1
sentinel_running_scripts:0
sentinel_scripts_queue_length:0
sentinel_simulate_failure_flags:0
master0:name=mymaster,status=ok,address=127.0.0.1:6379,slaves=0,sentinels=3

在最后一行中就可以看出 master 的地址和哨兵数量,哨兵数量为 3

基于 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 机制的客户端事件通知

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

而哨兵也提供了以下频道,给客户端监听到主从库切换过程中的不同关键事件。

事件 相关频道
主库下线事件 +sdown(实例进行“主观下线”状态)
-sdown(实例退出“主观下线”状态)
+odown(实例进入“客观下线”状态)
-odown(实例退出“客观下线”状态)
新主库切换 +switch-master(主库地址发生变化)

具体的操作步骤是,客户端读取哨兵的配置文件后,可以获得哨兵的地址和端口,和哨兵建立网络连接。然后,我们可以在客户端执行订阅命令,来获取不同的事件消息。

举个例子,可以通过 redis-cli 连接到哨兵,执行以下命令,来订阅”所有实例进入客观下线状态的时间“:

1
2
redis-cli -p 26379
127.0.0.1:26379> SCBSCRIBE +odown

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

返回的信息体结构:

1
switch-master <master name> <oldip> <oldport> <newip> <newport>

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

由哪个哨兵执行主从切换?

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

主库的”客观下线“判断过程是怎么样的呢?

任何一个实例只要自身判断主库“主观下线”后,就会给其他实例发送 is-master-down-by-addr 命令。接着,其他实例会根据自己和主库的连接情况,做出 Y 或 N 的响应,Y 相当于赞成票,N 相当于反对票。如图所示:

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

上文中所提到的 quorum 对应的是配置文件配置项:
sentinel monitor <master-name> <ip> <redis-port> <quorum>
示例:
sentinel monitor mymaster 127.0.0.1 6381 2

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

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

  • 第一,拿到半数以上的赞成票;
  • 第二,拿到的票数同时还需要大于等于哨兵配置文件中的 quorum 值。

以 3 个哨兵为例,假设此时的 quorum 设置为 2,那么,任何一个想成为 Leader 的哨兵只要拿到 2 张赞成票(因为还要算上自己本身的一票),就可以了。

示例如图:

在 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。发生这种情况,是因为 S3 和 S2 之间的网络传输正常,而 S1 和 S2 之间的网络传输可能正好拥塞了,导致投票请求传输慢了。

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

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

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

搭建哨兵集群

现在实现 1 主 2 从 3 哨兵的配置,当主库挂掉之后,要求最少有 2 个哨兵主观认为主库挂掉,才进行故障转移。

现在这些配置都在同一台物理机上面运行,通过端口来区分 6 个不同的节点,节点配置如下表:

节点角色 节点名称 端口号
主库 master 6379
从库 slave1 6380
从库 slave2 6381
哨兵 sentinel1 26379
哨兵 sentinel2 26380
哨兵 sentinel3 26381

环境准备

  1. 创建本次测试的目录

    1
    2
    3
    4
    5
    6
    7
    8
    ## 创建目录
    mkdir -p redis-test/log
    mkdir -p redis-test/master
    mkdir -p redis-test/slave1
    mkdir -p redis-test/slave2
    mkdir -p redis-test/sentinel/1
    mkdir -p redis-test/sentinel/2
    mkdir -p redis-test/sentinel/3
  2. 拷贝配置文件
    配置文件有 2 个,分别是 redis 的配置文件和哨兵的配置文件,这 2 个配置文件在进行解压 redis 文件夹的目录下

1
2
3
4
5
6
7
8
9
## 将redis配置文件拷贝到master和2个slave中
cd redis-test/master
cp /home/yunzhou/Library/redis-7.0.5/redis-7.0.5/redis.conf .

cd ../slave1
cp /home/yunzhou/Library/redis-7.0.5/redis-7.0.5/redis.conf .

cd ../slave2
cp /home/yunzhou/Library/redis-7.0.5/redis-7.0.5/redis.conf .

拷贝 sentinel 配置文件

1
2
3
4
5
cd redis-test/sentinel/1
cp /home/yunzhou/Library/redis-7.0.5/redis-7.0.5/sentinel.conf .

cp sentinel.conf ../2
cp sentinel.conf ../3

运行主库

主库的配置文件如下:

1
2
3
4
5
6
7
## 其余均保存默认配置
bind 127.0.0.1 -::1
port 6379
daemonize yes
logfile /home/yunzhou/software/redis-test/log/master.log
dir /home/yunzhou/software/redis-test/master
requirepass yunzhou

运行以下命令运行主库:

1
2
## 注意,redis.conf 在我执行目录下,属于相对位置
redis-server redis.conf

运行从库

从库的配置文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#redis.conf是redis原配置文件,内部包含了很多默认的配置,这里使用include将其引用,相当于把
#redis.conf内容直接贴进来了
include /home/yunzhou/software/redis-test/master/redis.conf
daemonize yes
bind 127.0.0.1
#配置密码
requirepass yunzhou
dir /home/yunzhou/software/redis-test/slave1
logfile /home/yunzhou/software/redis-test/log/slave1.log
#端口
port 6380
#rdb文件
dbfilename dump_6380.rdb
#pid文件
pidfile /home/yunzhou/software/redis-test/slave1/6380.pid

#replicaof 主机ip 端口
replicaof 127.0.0.1 6379
#主机的密码
masterauth yunzhou

第二个从库的配置文件就不贴出来了,大致上与上面的一致,只不过在 log 文件、pid 文件以及工作目录的命名从 slave1 修改成了 slave2,用于区分。

运行以下命令启动从库:

1
2
redis-server slave1/redis.conf
redis-server slave2/redis.conf

使用 redis-cli 命令去查看主库是否将从库连接上:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
yunzhou@yunzhou-PC:~/software/redis-test$ redis-cli
127.0.0.1:6379> auth yunzhou
OK
127.0.0.1:6379>info Replication
# Replication
role:master
connected_slaves:2
slave0:ip=127.0.0.1,port=6380,state=online,offset=37030,lag=0
slave1:ip=127.0.0.1,port=6381,state=online,offset=37030,lag=0
master_failover_state:no-failover
master_replid:d5c4212d150c0d9ba80630c1960793d677b7d675
master_replid2:02433ed156eaa70f93938f57d2657387f9c1b422
master_repl_offset:37030
second_repl_offset:36877
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:36877
repl_backlog_histlen:154

输出以上信息就代表两个从库已经连接到主库了。

运行哨兵

哨兵的配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 配置文件目录
dir "/home/yunzhou/software/redis-test/sentinel/3"
# 日志文件位置
logfile "/home/yunzhou/software/redis-test/log/sentinel3.log"
# pid文件
pidfile "/home/yunzhou/software/redis-test/sentinel/3.pid"
# 是否后台运行
daemonize yes
# 端口
port 26379
# 监控主服务器master的名字:mymaster,IP:192.168.200.129,port:6379,最后的数字2表示当Sentinel集群中有2个Sentinel认为master存在故障不可用,则进行自动故障转移
sentinel monitor mymaster 127.0.0.1 6379 2
# master响应超时时间(毫秒),Sentinel会向master发送ping来确认master,如果在10秒内,ping不通master,则主观认为master不可用
sentinel down-after-milliseconds mymaster 10000
# 故障转移超时时间(毫秒),如果3分钟内没有完成故障转移操作,则视为转移失败
sentinel failover-timeout mymaster 180000
# 故障转移之后,进行新的主从复制,配置项指定了最多有多少个slave对新的master进行同步,那可以理解为1是串行复制,大于1是并行复制
sentinel parallel-syncs mymaster 1
# 指定mymaster主的密码(没有就不指定)
sentinel auth-pass mymaster yunzhou

其他的 2 个哨兵通过引入哨兵 1 的配置,对 pidfile、logfile、dir 这些关键配置做修改,其他保持一致,此处就不贴出来了。

使用以下命令来运行哨兵:

1
2
3
redis-sentinel sentinel/1/sentinel.conf
redis-sentinel sentinel/2/sentinel.conf
redis-sentinel sentinel/3/sentinel.conf

使用 redis-cli 命令行连接工具去连接,然后执行 info sentinel 命令查询:

1
2
3
4
5
6
7
8
9
10
11
12
yunzhou@yunzhou-PC:~/software/redis-test/sentinel/1$ redis-cli -p 26379
127.0.0.1:26379>
127.0.0.1:26379> info sentinel
# Sentinel
sentinel_masters:1
sentinel_tilt:0
sentinel_tilt_since_seconds:-1
sentinel_running_scripts:0
sentinel_scripts_queue_length:0
sentinel_simulate_failure_flags:0
## 在这一行可以看到,从库有2个,哨兵有3个
master0:name=mymaster,status=ok,address=127.0.0.1:6379,slaves=2,sentinels=3

模拟主库故障

在哨兵的配置文件里面,配置了哨兵在 10 秒内没有回心跳信息的话,哨兵就会主观认为主库下线,故障切换时间超过是 3 分钟就代表切换失败。

在模拟主库故障前,我们可以通过 redis-cli 工具连接到 sentinel 里,使用以下命令订阅 +odown、-odown 和+switch-master 频道,监听事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
yunzhou@yunzhou-PC:~$ redis-cli -p 26379
127.0.0.1:26379> subscribe +sdown -sdown +odown -odown +switch-master
1) "subscribe"
2) "+sdown"
3) (integer) 1
1) "subscribe"
2) "-sdown"
3) (integer) 2
1) "subscribe"
2) "+odown"
3) (integer) 3
1) "subscribe"
2) "-odown"
3) (integer) 4
1) "subscribe"
2) "+switch-master"
3) (integer) 5

然后开始模拟主库故障,主库故障可以通过 redis-cli 连接到主库然后使用 shutdown 命令关闭服务,也可以查看主库的进程使用 kill -9 命令杀死进程,这里使用 shutdown 的方式

1
2
yunzhou@yunzhou-PC:~$ redis-cli
127.0.0.1:6379> shutdown

主库故障之后,在等待 10 秒后,监听频道的哨兵窗口就会打印出以下信息:

1
2
3
4
5
6
7
8
9
1) "message"
2) "+sdown"
3) "master mymaster 127.0.0.1 6379"
1) "message"
2) "+switch-master"
3) "mymaster 127.0.0.1 6379 127.0.0.1 6381"
1) "message"
2) "+sdown"
3) "slave 127.0.0.1:6379 127.0.0.1 6379 @ mymaster 127.0.0.1 6381"

从以上事件可以看出,在+sdown(实例进行“主观下线”状态)频道收到信息,master 127.0.0.1 6379 进入主观下线状态,在等待一会后+switch-master(主库地址发生变化)频道收到信息,新的主库地址是 127.0.0.1:6381。

然后登录到 6381 上查看信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
yunzhou@yunzhou-PC:~$ redis-cli -p 6381
127.0.0.1:6381> info Replication
# Replication
role:master
connected_slaves:1
slave0:ip=127.0.0.1,port=6380,state=online,offset=1203204,lag=1
master_failover_state:no-failover
master_replid:242d54ce5a9915c37d86c21ddf77bde9636bc341
master_replid2:c1850638690d94e891cb6b883aa67362750cbaea
master_repl_offset:1203337
second_repl_offset:1139293
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1126320
repl_backlog_histlen:77018
127.0.0.1:6381>

角色已经从 slave 变成了 master,这时主库故障的 6379 此时恢复,查看这机器是否会自动加入集群。

1
2
3
4
5
6
7
8
yunzhou@yunzhou-PC:~$ redis-cli
127.0.0.1:6379> info Replication
# Replication
role:slave
master_host:127.0.0.1
master_port:6381
master_link_status:down
## 下面信息省略

却发现 master 的节点信息是正确的,但是链路状态确实 down,什么原因导致链路状态失败?

这是因为之前 6379 作为 master 节点启动时,在配置文件配置了验证密码,6380 和 6381 作为 slave 启动时配置文件也配置了验证密码,密码一致自然就能作为 slave 加入集群;

在 6379 宕机后,6381 被选举为主库,此时的配置文件也是有验证密码,但 6379 之前作为 master 节点是没有配置 masterauth 该配置项的,所以加入集群失败;

所以想要 6379 作为 slave 加入集群,只需要在配置文件加入 masterauth 配置项,与 master 节点配置保持一致即可。

修改配置文件后重新启动,在 6381 查看信息

1
2
3
4
5
6
7
127.0.0.1:6381> info Replication
# Replication
role:master
connected_slaves:2
slave0:ip=127.0.0.1,port=6380,state=online,offset=1252920,lag=1
slave1:ip=127.0.0.1,port=6379,state=online,offset=1252920,lag=0
##省略

从以上信息可以看出,6379 已经加入集群当中,模拟主库故障哨兵切换主库成功。

小结

哨兵机制是实现主从库自动切换的关键机制,它有效地解决了主从复制模式下故障转移下的三个问题:

  • 监控主库运行状态,并判断主库是否客观下线;
  • 在主库客观下线后,选取新主库;
  • 选出新主库后,通知从库和客户端。

为了降低误判率,在实际应用时,哨兵机制通常采用多实例的方式进行部署,多个哨兵实例通过“少数服从多数”的原则,来判断主库是否客观下线。一般来说,我们可以部署三个哨兵,如果有两个哨兵认定主库“主观下线”,就可以开始切换过程。当然,如果你希望进一步提升判断准确率,也可以再适当增加哨兵个数,比如说使用五个哨兵。

在确认主库客观下线后,哨兵会经过投票仲裁,选举一个 Leader 出来,由它负责实际的主从切换,即由它来完成新主库的选择以及通知从库与客户端。

在哨兵集群进行主从库切换时,基于 pub/sub 功能客户端可以实现主库的切换事件通知以及哨兵集群组成过程。