likes
comments
collection
share

【Redis】哨兵(Sentinel)介绍及其工作原理

作者站长头像
站长
· 阅读数 18

Redis 哨兵(Sentinel)是运行在特殊模式下的 Redis 服务器,不支持读写操作,它的作用是配合 Redis 的复制功能,实现对主从节点的监控、对下线的主节点进行故障转移和通知。

思考以下几个问题:

  • 哨兵的主要作用是什么
  • 哨兵是如何发现其他哨兵的
  • 哨兵是如何发现其他从库的
  • 哨兵是如何监控 Redis 服务的
  • 哨兵是怎么样判断主节点下线的
  • 哨兵为什么要选举一个 Leader
  • 哨兵是怎么样选择新的主节点的
  • 哨兵是怎么样切换到新的主节点的
  • 哨兵是怎么样通知客户端主节点发生更换的
  • 哨兵挂了怎么办

注:

官方资料

哨兵介绍

Redis 哨兵(Sentinel)是运行在特殊模式下的 Redis 服务器,不支持读写操作,它的作用是配合 Redis 的复制功能,实现对主从节点的监控、对下线的主节点进行故障转移和通知。

哨兵的存在与否不会对 Redis 主从复制服务造成影响,哨兵相当于额外的一层监控,而不是与 Redis 服务交织在一起。

哨兵不是一个单独的进程,而是有多个哨兵服务组成的分布式系统。哨兵间使用流言协议(gossip protocols)进行消息传播,使用投票协议(agreement protocols)决定是否执行自动故障迁移和选择新的主节点。

架构

【Redis】哨兵(Sentinel)介绍及其工作原理

哨兵集群独立于 Redis 集群,哨兵之间彼此建立连接,共同监控、管理所有的 Redis 节点。

作用

  1. 监  控:监控所有 Redis 节点的状态。
  2. 故障转移:当哨兵发现主节点下线时,会在所有从节点中选择一个作为新的主节点,并将所有其他节点的 Master 指向新的主节点。同时已下线的原主节点也会被降级为从节点,并修改配置将 Master 指向新的主节点,等到它重新上线时就会自动以从节点进行工作。
  3. 通  知:当哨兵选举了新的主节点之后,可以通过 API 向客户端进行通知。

搭建哨兵架构

哨兵的原理

从库发现

对于哨兵的配置,我们只需要配置主库的信息,哨兵在连接主库之后,会调用 INFO 命令获取主库的信息,再从中解析出连接主库的从库信息,再以此和其他从库建立连接进行监控。

【Redis】哨兵(Sentinel)介绍及其工作原理

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 的频道,哨兵们就是通过它来相互发现,实现相互通信的。

【Redis】哨兵(Sentinel)介绍及其工作原理

订阅后,每个哨兵每隔 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>

客观下线投票过程

  1. 当哨兵发现主节点下线,标记主节点为 sdown 状态。
  2. 哨兵向其他哨兵发送 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 *
    
  3. 其他哨兵在收到投票请求之后,会检查本地主缓存中主节点的状态并进行回复(1 表示下线,0 表示正常)。
  4. 发起的询问的哨兵在接收到回复之后,会累加“下线”的得票数。
  5. 当下线的票数大于一半哨兵数量并且不小于 quorum 时,就会将主节点标记为 odown 状态。并开始准备故障转移。

    注意,当哨兵把主节点标记为 odown 时,并不会通知其他哨兵,因为这样自己才更有机会进行故障转移。

  6. 发起投票的哨兵有一个投票倒计时,倒计时结束如果票数仍然不够的话,则放弃本次客观线下投票。并尝试继续与主节点建立连接。

如果多个哨兵在同一段时间内发现主节点下线,那么每个发现的哨兵都会发起投票,投票的结果只是让发起投票的哨兵能够确认主节点是否下线,并不会与其他哨兵共享。因此这个下线确认的动作是多个节点同时发起进行的。

故障转移

哨兵在将主节点标记为 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_epochleaderleander 字段用于存储心仪候选人的 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,就投给谁

  1. 当哨兵要参加竞选,就会将自己的 current_epoch 字段加一,并将 leader 字段指向自己。
  2. 当哨兵接收到投票请求,如果请求的 epoch 小于等于哨兵自身的 current_epoch,就投给自己 leader 字段所指的哨兵。如果大于自身的 current_epoch,就将更新自己的 leader 字段为请求中的 runId,再将票投给对方。
  3. 根据第一点和第二点,如果竞选哨兵收到了其他哨兵的投票请求,此时对方 epoch 与自己相等,永远都只会投给自己。

因此,同一轮 Epoch 的竞选中,不参选的哨兵会一直投给第一个发给它请求的哨兵参选的哨兵会一直投给自己。这样,保证了同一轮 Epoch 竞选中,每个投票人只能投给一个人,保证了投票的正确性和公平性

选举过程

总结哨兵的具体选举过程为:

  1. 哨兵确认主节点 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
    
  2. 其他哨兵接收到投票请求,判断请求中的 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
    
  3. 哨兵每收到一个回复就会将对方的投票结果存储起来,并累计自己的得票数(投给自己的选票数加一,算上自己),当自己得票数超一半且不小于 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
    
  4. 如果到投票计时截止,哨兵自身的累计票数还没达标,哨兵就会宣告竞选失败,并进入一段随机的等待时间,等待结束之后会再次进行选举

    落选可能是有其他人当选,也可能没人能达标,当哨兵不关心有没有人胜选,因为如果有晋级者,它会主动宣告成功的。

    # 日志(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
    
  5. 在等待期间如果没有任何哨兵宣布胜选,则等待时间结束后,哨兵会重新进行选举,此时回到步骤 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

选取新的主节点遵守以下规则:

  • 排除
    • 已下线的从节点(sdownodown)。
    • 连接断开的节点(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

  1. redis 订阅、发布 | 晓的技术博客 (lanjingling.github.io)

  2. Redis Sentinel原理与实现 (下) - 云+社区 - 腾讯云 (tencent.com)