【Redis】哨兵(Sentinel)介绍及其工作原理
Redis 哨兵(Sentinel)是运行在特殊模式下的 Redis 服务器,不支持读写操作,它的作用是配合 Redis 的复制功能,实现对主从节点的监控、对下线的主节点进行故障转移和通知。
思考以下几个问题:
- 哨兵的主要作用是什么
- 哨兵是如何发现其他哨兵的
- 哨兵是如何发现其他从库的
- 哨兵是如何监控 Redis 服务的
- 哨兵是怎么样判断主节点下线的
- 哨兵为什么要选举一个 Leader
- 哨兵是怎么样选择新的主节点的
- 哨兵是怎么样切换到新的主节点的
- 哨兵是怎么样通知客户端主节点发生更换的
- 哨兵挂了怎么办
注:
官方资料
哨兵介绍
Redis 哨兵(Sentinel)是运行在特殊模式下的 Redis 服务器,不支持读写操作,它的作用是配合 Redis 的复制功能,实现对主从节点的监控、对下线的主节点进行故障转移和通知。
哨兵的存在与否不会对 Redis 主从复制服务造成影响,哨兵相当于额外的一层监控,而不是与 Redis 服务交织在一起。
哨兵不是一个单独的进程,而是有多个哨兵服务组成的分布式系统。哨兵间使用流言协议(gossip protocols)进行消息传播,使用投票协议(agreement protocols)决定是否执行自动故障迁移和选择新的主节点。
架构
哨兵集群独立于 Redis 集群,哨兵之间彼此建立连接,共同监控、管理所有的 Redis 节点。
作用
- 监 控:监控所有 Redis 节点的状态。
- 故障转移:当哨兵发现主节点下线时,会在所有从节点中选择一个作为新的主节点,并将所有其他节点的 Master 指向新的主节点。同时已下线的原主节点也会被降级为从节点,并修改配置将 Master 指向新的主节点,等到它重新上线时就会自动以从节点进行工作。
- 通 知:当哨兵选举了新的主节点之后,可以通过 API 向客户端进行通知。
搭建哨兵架构
哨兵的原理
从库发现
对于哨兵的配置,我们只需要配置主库的信息,哨兵在连接主库之后,会调用 INFO
命令获取主库的信息,再从中解析出连接主库的从库信息,再以此和其他从库建立连接进行监控。
INFO 中的 Replication 信息:
# Replication
role:master
connected_slaves:2
slave0:ip=172.25.0.102,port=6379,state=online,offset=258369,lag=1
slave1:ip=172.25.0.103,port=6379,state=online,offset=258508,lag=0
master_failover_state:no-failover
master_replid:a4a6a7f3b2e15d9a43c01d4ba6c842539e582d6a
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:258508
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:258508
哨兵对所有节点都会每隔 10s 发送一次 INFO
命令,从各节点获取 Redis 集群实时的拓扑图信息。如果新节点加入,哨兵就会去监控新的节点。
发布/订阅机制
哨兵们在连接同一个主库之后,是通过发布/订阅(pub/sub)模式来发现彼此的存在的。
发布/订阅(pub/sub)是一种消息通信模式,主要的目的是解耦消息发布者和消息订阅者之间的耦合。Redis 作为一个 pub/sub server,在订阅者和发布者之间起到了消息路由的功能。订阅者可以通过 subscribe 和 psubscribe 命令从 Redis 订阅自己感兴趣的消息类型,Redis 将消息类型称为频道(channel)。当发布者通过 publish 命令向 Redis 发送特定类型的消息时,该频道的全部订阅者都会收到此消息。这里消息的传递是多对多的。一个 client 可以订阅多个 channel,也可以向多个 channel 发送消息。1
在哨兵模式下,哨兵们会在每个 Redis 服务上创建并订阅一个名为 __sentinel__:hello
的频道,哨兵们就是通过它来相互发现,实现相互通信的。
订阅后,每个哨兵每隔 2 秒都会向 hello
频道发布一条携带自身信息的 hello 信息,这样哨兵就能知道其他哨兵的状态、监控的主节点和是否有新的哨兵加入:
127.0.0.1:6371> subscribe __sentinel__:hello
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "__sentinel__:hello"
3) (integer) 1
1) "message"
2) "__sentinel__:hello"
3) "172.25.0.202,26379,5134e342cc62ac76494c140b66b7fda80340e3a8,0,mymaster,172.25.0.101,6379,0"
1) "message"
2) "__sentinel__:hello"
3) "172.25.0.203,26379,5f5ce54a6f22f71c7d273cfb9eb14377b103d4ad,0,mymaster,172.25.0.101,6379,0"
1) "message"
2) "__sentinel__:hello"
3) "172.25.0.201,26379,4fa3486dfbaca9abc62b2976e821d18e697ab2db,0,mymaster,172.25.0.101,6379,0"
监控
哨兵在对 Redis 节点建立 TCP 连接之后,会周期性地发送 PING
命令给节点(默认是 1s),以此判断节点是否正常。如果在 down-after-millisenconds
时间内没有收到节点的响应,它就认为这个节点掉线了。
主观下线
当哨兵发现与自己连接的其他节点断开连接,它就会将该节点标记为主观下线(+sdown
),包括主节点、从节点或者其他哨兵都可以标记为 sdown
状态。
# 从节点主观下线
1:X 19 Aug 2021 07:26:29.837 # +sdown slave 172.25.0.103:6379 172.25.0.103 6379 @ mymaster 172.25.0.101 6379
# 哨兵主观下线
1:X 19 Aug 2021 08:19:19.799 # +sdown sentinel 5134e342cc62ac76494c140b66b7fda80340e3a8 172.25.0.202 26379 @ mymaster 172.25.0.101 6379
# 主节点主观下线
1:X 19 Aug 2021 08:24:06.612 # +sdown master mymaster 172.25.0.101 6379
当该节点重新连接之后,哨兵会取消对它的主观下线标记,操作是 -sdown
。
1:X 19 Aug 2021 08:20:04.811 # -sdown sentinel 5134e342cc62ac76494c140b66b7fda80340e3a8 172.25.0.202 26379 @ mymaster 172.25.0.101 6379
如果哨兵判断从节点或者其他哨兵节点主观下线,哨兵并不会执行其他操作。如果是主节点主观下线,哨兵就要采取措施,确定主节点是否真的宕机,并执行故障转移。
客观下线
哨兵确认主节点是否真的宕机这一步成为客观下线确认,如果主节点真的宕机了,哨兵就会将主节点标记为客观下线(+odown
)状态。
1:X 19 Aug 2021 08:24:06.612 # +sdown master mymaster 172.25.0.101 6379
1:X 19 Aug 2021 08:24:06.685 # +odown master mymaster 172.25.0.101 6379 #quorum 2/2
要判断主节点是否客观下线,需要与其他哨兵达成共识,如果大多数哨兵认为主节点主观下线了,哨兵才能确认主节点客观下线。达成共识的方式就是发起一轮投票,如果票数超过哨兵节点数的一半,并且大于等于 quorum
设置的数量,就是投票成功。否则哨兵就不能说主节点客观下线了。
quorum
是法定人数的意思,该信息在哨兵配置信息中进行配置:
# sentinel.conf
sentinel monitor <master-name> <ip> <redis-port> <quorum>
客观下线投票过程
- 当哨兵发现主节点下线,标记主节点为
sdown
状态。 - 哨兵向其他哨兵发送
SENTINEL is-master-down-by-addr
命令,询问其他哨兵该主节点是否已下线。SENTINEL is-master-down-by-addr <ip> <port> <epoch> <runId> > ip :哨兵判断下线的主节点 IP。 > port :哨兵判断下线的主节点端口。 > epoch:哨兵的 Epoch,可以理解为年龄,每当进行一轮故障转移,该值加一。 > runId:哨兵的 RunId。 # 此命令作用在客观下线确认阶段和故障转移的 Leader 选举阶段。 # 在客观线下确认阶段,`runId` 没有作用,的值为 `*`,如: SENTINEL is-master-down-by-addr 127.25.0.101 6379 3 *
- 其他哨兵在收到投票请求之后,会检查本地主缓存中主节点的状态并进行回复(
1
表示下线,0
表示正常)。 - 发起的询问的哨兵在接收到回复之后,会累加“下线”的得票数。
- 当下线的票数大于一半哨兵数量并且不小于
quorum
时,就会将主节点标记为odown
状态。并开始准备故障转移。注意,当哨兵把主节点标记为
odown
时,并不会通知其他哨兵,因为这样自己才更有机会进行故障转移。 - 发起投票的哨兵有一个投票倒计时,倒计时结束如果票数仍然不够的话,则放弃本次客观线下投票。并尝试继续与主节点建立连接。
如果多个哨兵在同一段时间内发现主节点下线,那么每个发现的哨兵都会发起投票,投票的结果只是让发起投票的哨兵能够确认主节点是否下线,并不会与其他哨兵共享。因此这个下线确认的动作是多个节点同时发起进行的。
故障转移
哨兵在将主节点标记为 odown
状态之后,就会马上开始尝试故障转移了。
故障转移主要由 sentinelFailoverStateMachineZ(sentinelRedisInstance)
函数负责2。该函数由一个状态机组成,共有五个状态,标志着故障转移共分为五个大步骤:
| SENTINEL_FAILOVER_STATE | desc | invoke |
|:------------------------|:---------------|:--------------------------------------------------------|
| `WAIT_START` | Leader 选举 | sentinelFailoverWaitStart(sentinelRedisInstance) |
| `SELECT_SLAVE` | Master 选取 | sentinelFailoverSelectSlave(sentinelRedisInstance) |
| `SEND_SLAVEOF_NOONE` | Slave 身份去除 | sentinelFailoverSendSlaveOfNoOne(sentinelRedisInstance) |
| `WAIT_PROMOTION` | 提升 Master | sentinelFailoverWaitPromotion(sentinelRedisInstance) |
| `RECONF_SLAVES` | 配置从节点 | sentinelFailoverReconfNextSlave(sentinelRedisInstance) |
SENTINEL_FAILOVER_STATE
是所有状态的前缀。
Leader 选举
哨兵首先进入 WATI_START
状态进行准备,等待哨兵成为哨兵集群的 Leader 才有资格进行故障转移。如果在超时时间之内哨兵都没有成为 Leader,则哨兵会调用 sentinelAbortFailover()
函数并结束本次故障转移。
当哨兵想要进行故障转移,首先需要得到多数哨兵的支持才能进行。而且,同一时间可能会有多个哨兵发起故障转移,所以故障转移前需要进行一轮竞选,得到多数选票的哨兵会被称为 Leader,只有 Leader 才能进行故障转移。
选举原理
哨兵属性
根据 Raft 算法,每个哨兵需要存储两个信息,是当前任期和心仪候选人,在 Redis 定义为 current_epoch
和 leader
,leander
字段用于存储心仪候选人的 runId
。
投票请求
同时,哨兵的投票请求依旧沿用 SENTINEL is-master-down-by-addr
指令,但此时除了附上自己的 Epoch 之外,还会在参数中带上自己的 runId
,标志投票的发起人。如:
SENTINEL is-master-down-by-addr 172.26.0.101 6379 4 9effe0cdc338e245391055caa45a05adf61fed37
投票方法
根据 Raft 算法,哨兵的投票原则就是:leader
字段是谁的 ID,就投给谁。
- 当哨兵要参加竞选,就会将自己的
current_epoch
字段加一,并将leader
字段指向自己。 - 当哨兵接收到投票请求,如果请求的
epoch
小于等于哨兵自身的current_epoch
,就投给自己leader
字段所指的哨兵。如果大于自身的current_epoch
,就将更新自己的leader
字段为请求中的runId
,再将票投给对方。 - 根据第一点和第二点,如果竞选哨兵收到了其他哨兵的投票请求,此时对方
epoch
与自己相等,永远都只会投给自己。
因此,同一轮 Epoch 的竞选中,不参选的哨兵会一直投给第一个发给它请求的哨兵。参选的哨兵会一直投给自己。这样,保证了同一轮 Epoch 竞选中,每个投票人只能投给一个人,保证了投票的正确性和公平性。
选举过程
总结哨兵的具体选举过程为:
-
哨兵确认主节点
odown
后,将自身current_epoch
加一,将leader
指向自己,并向其他哨兵发送投票请求。# 日志(sentinel-1):主节点客观掉线,current_epoch++ 变成 1,开始竞选 Leader 1:X 19 Aug 2021 08:24:06.612 # +sdown master mymaster 172.25.0.101 6379 1:X 19 Aug 2021 08:24:06.685 # +odown master mymaster 172.25.0.101 6379 #quorum 2/2 1:X 19 Aug 2021 08:24:06.685 # +new-epoch 1 1:X 19 Aug 2021 08:24:06.685 # +try-failover master mymaster 172.25.0.101 6379
-
其他哨兵接收到投票请求,判断请求中的
epoch
是否大于自己的current_epoch
: 大于则更新current_epoch
并将leader
指向发送方,然后投票给发送方。 小于或等于则将票投给自己leader
字段指向的哨兵(可能是它自己)。# 日志(sentinel-1):刚参与竞选,2 号就发来投票请求,果断投给自己。 1:X 19 Aug 2021 08:24:06.707 # +vote-for-leader 4fa3486dfbaca9abc62b2976e821d18e697ab2db 1
# 日志(sentinel-2):刚参与竞选,1 号就发来投票请求,果断投给自己。 1:X 19 Aug 2021 08:24:06.707 # +vote-for-leader 9effe0cdc338e245391055caa45a05adf61fed37 1
# 日志(sentinel-3):刚发现主节点宕机,就收到 1 号的投票请求 1:X 19 Aug 2021 08:24:06.589 # +sdown master mymaster 172.25.0.101 6379 # 当前节点 current_epoch 更新为 1,这一轮投给 1 号 1:X 19 Aug 2021 08:24:06.719 # +new-epoch 1 1:X 19 Aug 2021 08:24:06.731 # +vote-for-leader 4fa3486dfbaca9abc62b2976e821d18e697ab2db 1
-
哨兵每收到一个回复就会将对方的投票结果存储起来,并累计自己的得票数(投给自己的选票数加一,算上自己),当自己得票数超一半且不小于
quorum
时,成为 Leader 并向所有哨兵公示投票结果。# 日志(Sentinel-1):记录各哨兵投票数 1:X 19 Aug 2021 08:24:06.707 # 9effe0cdc338e245391055caa45a05adf61fed37 voted for 9effe0cdc338e245391055caa45a05adf61fed37 1 1:X 19 Aug 2021 08:24:06.731 # 5f5ce54a6f22f71c7d273cfb9eb14377b103d4ad voted for 4fa3486dfbaca9abc62b2976e821d18e697ab2db 1
-
如果到投票计时截止,哨兵自身的累计票数还没达标,哨兵就会宣告竞选失败,并进入一段随机的等待时间,等待结束之后会再次进行选举。
落选可能是有其他人当选,也可能没人能达标,当哨兵不关心有没有人胜选,因为如果有晋级者,它会主动宣告成功的。
# 日志(Sentinel-1):落选,并宣告下一轮参选时间 # 虽然得票 2:1 赢了,但是总共有 4 个哨兵,一个掉线了没有投票,没有哨兵票数超一半 1:X 19 Aug 2021 08:24:17.589 # -failover-abort-not-elected master mymaster 172.25.0.101 6379 1:X 19 Aug 2021 08:24:17.647 # Next failover delay: I will not start a failover before Thu Aug 19 08:30:07 2021
-
在等待期间如果没有任何哨兵宣布胜选,则等待时间结束后,哨兵会重新进行选举,此时回到步骤 1。
# 日志(Sentinel-2):率先发起选举,current_epoch++ = 2,第 2 轮拿下 3 票成为 Leader 1:X 19 Aug 2021 08:30:07.412 # +new-epoch 2 1:X 19 Aug 2021 08:30:07.412 # +try-failover master mymaster 172.25.0.101 6379 1:X 19 Aug 2021 08:30:07.443 # +vote-for-leader 9effe0cdc338e245391055caa45a05adf61fed37 2 1:X 19 Aug 2021 08:30:07.500 # 5f5ce54a6f22f71c7d273cfb9eb14377b103d4ad voted for 9effe0cdc338e245391055caa45a05adf61fed37 2 1:X 19 Aug 2021 08:30:07.507 # 4fa3486dfbaca9abc62b2976e821d18e697ab2db voted for 9effe0cdc338e245391055caa45a05adf61fed37 2 1:X 19 Aug 2021 08:30:07.520 # +elected-leader master mymaster 172.25.0.101 6379
Master 选取
当选 Leader 后哨兵会进入 SELECT_SLAVE
状态,选取新的主节点。
1:X 19 Aug 2021 08:30:07.520 # +failover-state-select-slave master mymaster 172.25.0.101 6379
选取新的主节点遵守以下规则:
- 排除:
- 已下线的从节点(
sdown
、odown
)。 - 连接断开的节点(
PING
超时,disconnected
状态)。 - 配置了不当 Master 的节点(
replica-priority = 0
)。 - 与宕机主节点断开时间过长的从节点(超 10 倍
down-after-milliseconds
)。
- 已下线的从节点(
- 优先级,从高到低排序:
- 优先值最高的节点(
replica-priority
最小)。 - 复制偏移量最大的节点。
- 配置了
runId
的节点。 - 随机
runId
字典序最小的节点。
- 优先值最高的节点(
如果选取失败,会隔一段时间进行重试,直到选取出新的主节点为止。
Slave 身份去除
当确定新的主节点后,哨兵会进入 SEND_SLAVEOF_NOONE
状态,撤销该节点的 Slave 状态。
1:X 19 Aug 2021 08:30:07.587 * +failover-state-send-slaveof-noone slave 172.25.0.102:6379 172.25.0.102 6379 @ mymaster 172.25.0.101 6379
哨兵会发送 slaveof NO ONE
指令给从节点,从节点接收到后会断开它与原主节点的网络连接,重置其复制 ID 并执行持久化重写,并开始将自己的复制身份转为 Master。
提升 Master
在发送指令之后,哨兵会进入 WAIT_PROMOTION
状态,等待该节点将自己提升为主节点。
1:X 19 Aug 2021 08:30:07.679 * +failover-state-wait-promotion slave 172.25.0.102:6379 172.25.0.102 6379 @ mymaster 172.25.0.101 6379
等待过程中哨兵会每隔一秒发送一次 INFO
命令给它,直到它的角色变成 Master。
配置从节点
当节点提升为 Master 之后,哨兵会进入 RECONF_SLAVES
状态,更新所有从节点的配置,让他们去复制新的 Master。
1:X 19 Aug 2021 08:30:08.374 # +promoted-slave slave 172.25.0.102:6379 172.25.0.102 6379 @ mymaster 172.25.0.101 6379
1:X 19 Aug 2021 08:30:08.374 # +failover-state-reconf-slaves master mymaster 172.25.0.101 6379
哨兵通过向从节点发送 slaveof <ip> <port>
命令即可修改从节点复制配置,并让从节点去复制新的主节点。
通知
当哨兵进行故障转移之后,哨兵会通知客户端主节点发生更换,让客户端去连接新的主节点。
在使用哨兵模式的时候,客户端通常应该使用 Redis 连接库的哨兵模式,例如使用 Jedis 需要使用:
JedisSentinelPool
。通过哨兵来获取主节点信息并建立连接,而不是直接在配置文件中写死主节点的信息,因为主节点是可变的。
哨兵同样是通过发布/订阅机制实现的客户端通知,每个连接哨兵的客户端,都会去订阅哨兵的 +switch-master
频道,当 Leader 进行故障转移后,会向其他哨兵发送新主节点配置,然后所有哨兵都会在 +switch-master
频道发布主节点切换信息,此时客户端监听到变化,就会去连接新的主节点。
# 日志
1:X 21 Aug 2021 08:16:05.963 # +failover-end master mymaster 172.25.0.101 6379
1:X 21 Aug 2021 08:16:05.963 # +switch-master mymaster 172.25.0.101 6379 172.25.0.103 637
# Sentinel-1(Leader)
127.0.0.1:26371> subscribe *
1) "pmessage"
2) "*"
3) "+failover-end"
4) "master mymaster 172.25.0.102 6379"
1) "pmessage"
2) "*"
3) "+switch-master"
4) "mymaster 172.25.0.102 6379 172.25.0.103 6379"
# Sentinel-2
127.0.0.1:26372> subscribe *
1) "pmessage"
2) "*"
3) "+config-update-from"
4) "sentinel 3fbda0aa37fbc1eb6dfde66677361a4ef09a40e3 172.25.0.201 26379 @ mymaster 172.25.0.102 6379"
1) "pmessage"
2) "*"
3) "+switch-master"
4) "mymaster 172.25.0.102 6379 172.25.0.103 6379"
# Sentinel-3
127.0.0.1:26373> subscribe *
1) "pmessage"
2) "*"
3) "+config-update-from"
4) "sentinel 3fbda0aa37fbc1eb6dfde66677361a4ef09a40e3 172.25.0.201 26379 @ mymaster 172.25.0.102 6379"
1) "pmessage"
2) "*"
3) "+switch-master"
4) "mymaster 172.25.0.102 6379 172.25.0.103 6379"
客户端代码参考:
// package redis.clients.jedis.JedisSentinelPool
jedis.subscribe(new JedisPubSub() {
@Override
public void onMessage(String channel, String message) {
log.debug("Sentinel {} published: {}.", hostPort, message);
// Message 实例参考:mymaster 172.25.0.102 6379 172.25.0.103 6379
String[] switchMasterMsg = message.split(" ");
if (switchMasterMsg.length > 3) {
if (masterName.equals(switchMasterMsg[0])) {
// 3 => 172.25.0.103, 4 => 6379
initMaster(toHostAndPort(Arrays.asList(switchMasterMsg[3], switchMasterMsg[4])));
} else {
log.debug("Ignoring message on +switch-master for master name {}, our master name is {}", switchMasterMsg[0], masterName);
}
} else {
log.error("Invalid message received on Sentinel {} on channel +switch-master: {}", hostPort, message);
}
}
}, "+switch-master");
客户端后台线程订阅 +switch-master
频道,接收到消息之后解析并重新初始化全局主节点 initMaster()
。
Footnotes
转载自:https://juejin.cn/post/6998564627525140494