Redis Sentinel 本身文档介绍的很好了,这里一方面是对这个文档做一些精简的记录,再就是补充一些原理性的细节,记录一些 “坑”,为以后快速回忆用。

Sentinel 基本内容

功能

  • Monitor. 监控某个 Redis Master 和其 Slave 是否正常工作
  • Notification. 将监控 Redis 集群的状态以通知的形式发送出来
  • Automatic Failover. 自动的完成 Redis 集群的 Failover。当 Redis Master 不正常工作后,将 Redis Slave 提升为 Master,协助访问 Redis 的连接去访问新 Master,在老 Master 恢复后设置其 Slaveof 新 Master
  • Configuration Provider. 为 client 提供当前 redis 集群内 master 或 slave 的地址

集群结构

untitled diagram 9

  • 看到任意两个 Sentinel 之间都由两个连接相连,例如 Sentinel1 到 Sentinel2 一条,Sentinel2 到 Sentinel1 一条;
  • Sentinel Cluster 内每个 Sentinel 都会与被监控的 Redis Master, Redis Slave 建立连接,去探测他们的状态;
  • Sentinel 是分布式的服务,需要至少 3 个实例才能正常工作;
  • Sentinel 需要配置去监控某个 Redis Cluster 的 Master,Sentinel 会通过 info 指令从 Redis 的 Master 拿到 Redis Cluster 当前的 Salve 实例地址并与 Master 和 Salve 都建立连接;
  • Sentinel 会定时的给 Redis Master 和 Redis Slave 发 Ping 去探测他们是否正常工作,发现有 Redis 服务器不工作后,就 “打听” 其它 Sentinel 看他们是否都判定该 Redis 服务器不工作。当发现宕机的是 Master 且经过 “打听” 后,有超过 quorum 个 (在为 Sentinel 设置 monitor 的 Redis Master 地址时配置) Sentinel 都认为同一个 Master 宕机,则开始选举 Leader Sentinel 去执行 Failover;
  • 当发现 Slave 宕机后则后续 Failover 时,不会将该 Salve 作为 Failover 的 Candidate;
  • 当某个 Sentinel 发现有 Redis 服务宕机,这个叫做 Subjective Down。当 “打听” 到有足够多 Sentinel 都认为该 Redis 宕机后,叫做 Objective Down。不过对于 Redis Sentinel 来说,只有 Redis Master 宕机才会触发 Objective Down 接着触发 Failover;

基本东西就这么多,但魔鬼隐藏在细节中。可以带着下面的问题继续往下看。

  1. Sentinel 配置中一开始不用将所有 Sentinel 地址都配置上去,那 Sentinel 他们是怎么相互发现的?
  2. 多个 Sentinel 在做 Failover 时是怎么保证一致性的,不会将不同的 Slave 选为 Master?
  3. Sentinel 为什么至少需要 3 个实例?
  4. 听说 Sentinel 使用了 Raft 但是怎么使用 Raft 的,Raft 要求必须持久化 term、vote for、log 这些信息,Sentinel 有对应的去持久化这些东西吗?
  5. Redis Client 在支持 Sentinel 时候有什么需要注意的?

服务启动

Sentinel 启动必须要提供配置文件,因为 Sentinel 需要利用配置文件存储一些持久化信息,在 Sentinel 挂掉后能通过读取配置文件恢复挂掉前的状态。

配置示例如下:

# 列举要监控的 Redis 集群,每个集群都要有独立的名字,可以任意起名字,但每个 reids 集群要不同
# 下面举例是监控了名为 mymaster 和 resque 的两个 Redis 集群

# 不同监控集群的配置可以分开写,需要先列出来 master 的地址、端口 以及 quorum
sentinel monitor mymaster 127.0.0.1 6379 2
# 当多长时间无响应时认为目标机器 subjective down
sentinel down-after-milliseconds mymaster 60000
# 执行 failover 的超时时间,超过这么长时间依然没有 Failover 结束就放弃本次 Failover
sentinel failover-timeout mymaster 180000
# failover 发生时,能同时将多少 slave 指向新 master
sentinel parallel-syncs mymaster 1

# 下面是另一个 Redis Master 的配置
sentinel monitor resque 192.168.1.3 6380 4
sentinel down-after-milliseconds resque 10000
sentinel failover-timeout resque 180000
sentinel parallel-syncs resque 5
  • 设置 monitor 某个 Redis 的时候需要指定 quorum,表示当有 quorum 个 Sentinel 认为 Redis Master 宕机后就认为该 Master 为 Objective Down,要开始 Failover;
  • down-after-milliseconds 是 Sentinel 多长时间没收到 Redis 的 pong 后,认为该实例 Subjective Down。可以看到每个 Redis Master 可以设立不同的 down-after-milliseconds;
  • 假设 Master 有 3 个 Slave,如果 parallel-syncs 为 2,当 Master 宕机并找一个 Slave 作为新 Master 后,会同时将剩余两个 Slave 设置去 slaveof 新 Master,该操作会让这两个 Slave 立即清理当前所有数据通过同步新 Master 再重建数据。在重建完成前这段时间,没有一个 Slave 能正常访问。而如果将 parallel-syncs 设置成 1,同时只有一个 Slave 去同步新 Master,另一个 Slave 不会清理内部数据且能继续处于可访问状态,只是只能访问到老数据;

写好配置文件后就通过 redis-server /path/to/sentinel.conf --sentinel 命令将 Sentinel 启动起来。Sentinel 成功 Monitor Redis Master 后,可以通过 redis-cli 连上 Redis Master 执行 redis-cli -p 6379 DEBUG sleep 30 模拟 Master 不响应 Sentinel 的 ping 来测试 Sentinel 是否能正常做 Failover。

Sentinel 相互发现机制

从前面配置举例能看到,Sentinel 配置中不用列出来其它 Sentinel 地址。Sentinel 启动后会自动相互发现并且去探测其它 Sentinel 是否宕机。它是怎么做到的呢?

untitled diagram 5

1 通过 Monitor 指令类似 sentinel monitor mymaster 127.0.0.1 6379 2 让 Sentinel 监控某个 Redis Master 后,Sentinel 会以固定周期 (默认每10秒) 去发送 info 指令到 Redis Master 获取 Redis Master Role 信息以及所有 Salve 地址;

拿到 Slave 地址后,Sentinel 会更新本地的配置文件,将 Slave 信息写入配置文件。比如通过 info 拿到 Slave 在 127.0.0.1:6380,则会写:

sentinel known-slave mymaster 127.0.0.1 6380

Sentinel 的持久化工作都是这么通过写传入的配置文件来做的。有了 Slave 的信息并持久化后,如果 Redis Master 出现宕机才能正常去执行 Failover,不持久化的话如果该 Sentinel 宕机且 Redis Master 宕机,Sentinel 再起来后即使知道 Redis Master 需要去 Failover 也会因为忘记了 Salve 的地址而无法 Failover。

2 Sentinel 会订阅 Redis Master 的 __sentinel__:hello频道,并定期 (默认每两秒) 向该频道广播 "IP, PORT, SENTINEL-ID, EPOCH, MASTER_NAME, MASTER_IP, MASTER_EPOCH" ,同样的操作也会应用在 Sentinel 通过 info 指令发现的 Redis Slave 上。变成:

untitled diagram 17

是的,你没有看错,Sentinel 会 Subscribe Redis Slave 的频道,这是允许的。在 Redis 这里通过 Master 向某个 Channel Publish 消息,无论 Client 是在 Master 还是在 Slave 订阅的这个 Channel,都能收到通知。消息会从 Master 同步至 Slave。但如果是通过 Slave 向某个 Channel Publish 消息,则只能让在 Slave 订阅这个 Channel 的 Client 收到消息。

我理解这里 Sentinel 既订阅 Master 又订阅 Slave 是为了提高相互发现的可靠性,即使 Master 挂掉也能通过 Slave 发现监控同一个集群的 Sentinel,从而完成后续要做的投票,选举,Failover 等等事情。

3 因为每个 Monitor 相同 Redis Master 的 Sentinel 都会订阅 __sentinel__:hello 频道,并会广播自己的信息到这个频道,通过订阅这个频道就能收到其它也同样监控该 Redis Master 并订阅该频道的 Sentinel 的 IP、端口等信息,从而完成自动发现;

4 Sentinel 从 __sentinel__:hello 频道过滤出当前自己不认识也即未发现的 Sentinel 地址并与其创建连接 ( Sentinel 之间的连接,非 Subscribe);

5 Sentinel 将新发现的其它 Sentinel 地址存入本地配置文件。类似于:

sentinel known-sentinel mymaster 127.0.0.1 26381 e07d497d521074dfb580f936b51fc1f2b52ffbaa
sentinel known-sentinel mymaster 127.0.0.1 26380 e9a80378b5e8ebc71452c4e7217ca7dfd2c35bcb

持久化当前集群所有 Sentinel 是用 Raft 算法完成 Leader 选举的必要条件。能选出 Leader,才能在发现 Redis Master 进入 Objective Down 状态后,完成 Leader 选举后做 Failover。同一时刻只有 Leader 去执行 Failover 才能做到不会错误的将多个不同 Slave 提升为 Master。

Sentinel Failover 机制

Sentinel 所有创建的非 Pub/Sub 连接,即包含 Sentinel 之间的连接,也包含 Sentinel 与 Redis Master 或 Slave 创建的连接,都会以每秒一次的频率去 ping 对方服务器,看对方是否在线,是否异常。也就是说 Sentinel 相互之间会不断发 ping 做探测,Sentinel 也会给 Redis Master,Redis Slave 都发 ping 去做探测。当 Sentinel 发送 ping 后收到的响应不是有效的 pong,Sentinel 就会比对该连接最近一次正常 pong 距离当前时间是否大于配置的 down-after-milliseconds,大于则认为目标服务进入 Subjective Down。

加上前面说的 infopublish 命令 Sentinel 一共会定时的给 Redis 服务发送三个命令。ping 的频率一般最高,用于探测对方是否依然存活;Pub/Sub 频率次之,用于 Sentinel 之间相互发现以及同步配置信息 (下面会提到,就是某个 Sentinel 做完 Failover 后需要通知其他 Sentinel 更新配置);info 频率最低,主要是通过 Master 发现 Slave、检查 Redis Master 或 Slave 的配置是否符合预期、Failover 时查看 Slave 是否已经被成功提升为 Master 等。

untitled diagram 7

Sentinel 和 Sentinel 之间只会发 Ping,用于探测对方是否存在,不会建立 Subscribe 连接不会发 publish 也不会发 info

Sentinel 发现某个 Redis 进入 Subjective Down 后会做下面的事情:

  1. 广播 +sdown 即 Subjective Down 事件给所有在该 Sentinel 订阅 +sdown 频道的 Redis Client。Sentinel 还会 Publish 很多事件,Channel 名字就是事件类型,所以通过订阅各种事件就能获取到各个 Redis 集群当前状态,比如是不是在 Failover 等,还可以用 PSUBSCRIBE * 把所有频道都订阅了。具体有哪些事件能订阅需要参考 Redis 文档。
  2. 发送 sentinel is-master-down-by-addr <ip> <port> <current_epoch> <id> 请求给其它已知 Sentinel 们,当有 Majority 个 Sentinel 都返回说认为该 Master Subjective Down 后,Sentinel 开始准备选举 Leader。比如当前监控某 Redis Master 的一共有 5 个 Sentinel,其中有 3 个 Sentinel 认为该 Redis Master Subjective Down 后,Sentinel 正式进入 Failover 状态 SENTINEL_FAILOVER_STATE_WAIT_START,开始准备执行 Leader 选举;
  3. Leader 选举利用的 Raft 算法,先提升自己的 Epoch,Raft 保证每个 Epoch 下最多只有一个 Leader。选举开始时,Sentinel 选自己为 Leader,并发送 Vote 请求给其它 Sentinel 让其它 Sentinel 也选自己为 Leader。如果一个 Sentinel 收到 Vote 请求,且发现 Vote 请求的 Epoch 大于等于自己的 Epoch,且该 Epoch 下本 Sentinel 还未 Vote 任何 Sentinel,则回复 Vote 请求成功,即同意选举对方为 Leader,并且记录选举结果保证该 Epoch 下不会选别的 Sentinel 再当 Leader。(有没有 Vote 过其他 Sentinel 这个事情存在 master.leadermaster.leader_epoch 这俩内存结构下,后面会说) 如果某个 Epoch 下已经 Vote 了某个 Sentinel 为 Leader,再收到 Vote 请求申请选另一个 Sentinel 为 Leader,则直接拒绝该 Vote 请求;
  4. 当某个 Sentinel 收到 Majority 个 Sentinel 回复的 Vote 成功回应后,该 Sentinel 被选为 Leader,由 Leader 继续负责执行 Master 的 Failover,其它 Sentinel 会在 failover-timeout 后,如果该 Master 依然处在 Subjective Down 且还是 Master 时再次执行选举 Leader,重试 Failover。假设 Sentinel 当前的 epoch 都是 0,选举流程大概是下面图的样子。最终选出来 Sentinel 1 为 Leader。假设同时发现 Redis Master 宕机还有 Sentinel 3,它因为选举失败所以会等待 Failover Timeout 后才会再次重试选举,重试 Failover;

注意:Sentinel 在通过咨询其他 Sentinel 发现 Redis Objective Down 后就进入 Failover 状态,会提升 epoch 然后进行选举等,如果因为选举失败、选 Slave 失败、提升 Slave 失败等等事情导致 Failover 失败,Sentinel 都必须等待本次 Failover 超时后才能退出 Failover 状态,进行下一轮 Failover。

下图 Sentinel 2 在将票投给 Sentinel 1 同意 Sentinel 1 当 Epoch 1 下的 Leader 后,从投票时刻开始至一个 failover-timeout 时间内,即使 Redis Master 持续宕机 Sentinel 2 都不会再尝试让自己选 Leader。收到更大的 Epoch 后会投票,但自己不会参选 Leader。Sentinel 3 收到两个 Reject 投票选 Leader 失败,会等待一个 min(SENTINEL_ELECTION_TIMEOUT, failover-timeout) 时间后,广播 -failover-abort-not-elected 事件放弃本次 Failover 进入下一次 Failover (如果 Redis 依然宕机),SENTINEL_ELECTION_TIMEOUT 默认是 10s。总之,一个 failover-timeout 时间范围内,Sentinel 1 的 Leader 状态是安全的,Sentinel 2 因为选 Sentinel 1 为 Leader 所以在从 Vote Sentinel 1 开始一个 failover-timeout 内不会再争抢 Leader,Sentinel 3 因为争抢 Leader 失败,所以从提升 Epoch 进入 Failover 状态开始一个 failover-timeout 内也不会再争抢 Leader。

untitled diagram 13

  1. 选好 Leader 后,作为 Leader 的 Sentinel 进入 SENTINEL_FAILOVER_STATE_SELECT_SLAVE 状态,继续执行 Failover。因为在 failover-timeout 时间内只有一个 Sentinel Leader,所以由该 Leader 去做 Failover 不会有一致性问题。Leader 在 SENTINEL_FAILOVER_STATE_SELECT_SLAVE 状态会从符合条件的 Slave 中挑选一个 Slave 作为新 Master,这个 Slave 必须不处于宕机状态,最近一次与 Sentinel 的 pong 的时间要小于一定时间范围,最近一次与 Master 成功通信的时间也要小于一定时间范围。我们还能通过为 Slave 指定优先级而让高优先级 Slave 优先被选为 Master。
  2. 选好 Slave 后,Leader 进入 SENTINEL_FAILOVER_STATE_SEND_SLAVEOF_NOONE 状态,通过 slaveof no one 指令让选出的 Salve 变成新 Master。
  3. 之后 Leader 进入 SENTINEL_FAILOVER_STATE_WAIT_PROMOTION 状态,通过频繁的info 命令去查询新 Master 是否已经变成 Master (Sentinel 正常时候会发 info 到 Redis Master 和 Slave,在进入 SENTINEL_FAILOVER_STATE_WAIT_PROMOTION 状态后会以更高的频率去新选的 Slave 发 info),变成 Master 后 Leader 更新本地配置文件并进入下一步。如果新 Master 一直不变成 Master 则放弃 Failover;
  4. 如果探测到成功提升 Slave 为 Master,Leader 进入 SENTINEL_FAILOVER_STATE_RECONF_SLAVES 状态,该状态下需要让所有老 Slave 根据 parallel-syncs 参数情况,去 slaveof 新 Master,成功后 Failover 结束,如果一直不成功比如 Slave 宕机了则等待一段时间后放弃更新这个 Slave 并且依然认为 Failover 成功,后续等 Slave 上线后 Sentinel 监听到这个 Slave 在错误的 Sync 一个 Master,就会修改这个 Slave 的配置;
  5. Leader Sentinel 通过 __sentinel__:hello频道广播新的 Master 地址,Master Epoch,Leader Sentinel 自己的地址,自己的 Epoch。Sentinel 因为既会订阅 Master 也会订阅 Slave 并且会频繁的发这个广播,所以其它 Sentinel 大概率会很快收到这个广播,当发现广播中自己 Monitor 的 Master 的 Epoch 比自己当前缓存的对应 Master 的 Epoch 大,就会更新本地配置数据,广播 +config-update-from 事件并打印日志。这也是为什么 Sentinel 支持对被监控的 Redis 配置做修复,但不会错误的将刚 Failover 过的 Master 配置再改回去。

假设 Sentinel 1 是 Leader 那流程大致如下:

untitled diagram 16

请注意 Sentinel 提升 Slave 为 Master 时候会发三个指令,这三个指令会放在一个事务当中。最重要的是 client kill 将 Slave 上的连接都断掉,同样 client kill 会发给老的 Master,让老 Master 上的连接都断掉。这个在说 Redis Client 时候会再提到。

上面过程有如下日志示例:

# mymaster 的 Master Subjective Down
24585:X 13 Jan 12:24:36.702 # +sdown master mymaster 127.0.0.1 6379 
# 询问了一圈 Sentinel 后得到该 Master Objective Down
24585:X 13 Jan 12:24:36.702 # +odown master mymaster 127.0.0.1 6379 #quorum 2/2
# 提升 epoch
24585:X 13 Jan 12:24:36.702 # +new-epoch 95
# 进入 Failover 流程,等待被选为 Leader 后开始 Failover
24585:X 13 Jan 12:24:36.702 # +try-failover master mymaster 127.0.0.1 6379
# 发出 Vote,Vote 自己
24585:X 13 Jan 12:24:36.703 # +vote-for-leader d14c1b416d38ae4e5a030b829e4ae6326c773f7c 95
# 因为是 95 这个 epoch 下第一个发出 Vote 请求的,所以其它 Sentinel 均 Vote 了自己
24585:X 13 Jan 12:24:36.704 # e07d497d521074dfb580f936b51fc1f2b52ffbaa voted for d14c1b416d38ae4e5a030b829e4ae6326c773f7c 95
24585:X 13 Jan 12:24:36.704 # e9a80378b5e8ebc71452c4e7217ca7dfd2c35bcb voted for d14c1b416d38ae4e5a030b829e4ae6326c773f7c 95
# 自己被选为 Leader 负责去 Failover mymaster 127.0.0.1 6379 这个 Redis Master
24585:X 13 Jan 12:24:36.759 # +elected-leader master mymaster 127.0.0.1 6379
# failover-state 是个前缀,表明当前进行到 Failover 的哪一个步骤了
# 下面这个是走到选 Salve 为 Master 这一步了
24585:X 13 Jan 12:24:36.759 # +failover-state-select-slave master mymaster 127.0.0.1 6379
## 选好了 slave
24585:X 13 Jan 12:24:36.843 # +selected-slave slave 127.0.0.1:6380 127.0.0.1 6380 @ mymaster 127.0.0.1 6379
# failover 进行到将 Slave 提升为 Master,通过 slaveof no one 命令去提升 Master
24585:X 13 Jan 12:24:36.843 * +failover-state-send-slaveof-noone slave 127.0.0.1:6380 127.0.0.1 6380 @ mymaster 127.0.0.1 6379
# 指令发出后得等待对方变成 Master,等待期间会不断定时发起 INFO 请求查看对方状态
24585:X 13 Jan 12:24:36.917 * +failover-state-wait-promotion slave 127.0.0.1:6380 127.0.0.1 6380 @ mymaster 127.0.0.1 6379
# 发现 Slave 被提升为 Master 了
24585:X 13 Jan 12:24:37.748 # +promoted-slave slave 127.0.0.1:6380 127.0.0.1 6380 @ mymaster 127.0.0.1 6379
# Failover 进行到 reconf-slaves 这一步
24585:X 13 Jan 12:24:37.748 # +failover-state-reconf-slaves master mymaster 127.0.0.1 6379
# Failover 结束
24585:X 13 Jan 12:24:37.829 # +failover-end master mymaster 127.0.0.1 6379
# Master 切换
24585:X 13 Jan 12:24:37.829 # +switch-master mymaster 127.0.0.1 6379 127.0.0.1 6380

从上面能看出来 Sentinel 虽然使用了 Raft 但只用了 Raft 最基础的选举 Leader 部分,且还是 Raft 简化版的 Leader 选举,所以才能做到只持久化 Epoch 就够了。

下面是一些可能会有的小问题和回答。

  • 如果 Epoch 变化时,有个 Sentinel 挂掉了,等它再醒来时候他是怎么知道新 Epoch 的?

Sentinel 订阅 __sentinel__:hello 频道后,会从该频道收到其它 Sentinel 的 Epoch,Sentinel 会更新到最大的那个 Epoch。

  • 看 Redis 代码时候有 master.leader_epoch, sentinel.config_epoch, master.failover_epoch, master.config_epoch 都是什么?

看 Redis 代码时候可能对 epoch 这个东西会很晕,有这么多 epoch 。sentinel.config_epoch 表示当前 Sentinel 认为最大的 epoch,会记录在文件中,即:

sentinel current-epoch 540

还有跟 master name 相关的两个 epoch:master.config_epochmaster.leader_epoch 也会记录在文件当中:

sentinel config-epoch mymaster 450
sentinel leader-epoch mymaster 540

这些 Epoch 都跟 Failover 过程相关,所以又得提及 Failover 过程。假设现在有 Sentinel 1 2 3,监控的 Master 名字叫 mymaster。如果 Sentinel 1 发现 Redis Objective 宕机,还未收到别的 Sentinel 要求 Vote 它的请求,Sentinel 1 进入 Failover 状态,提升 Epoch 即 sentinel.config_epoch,存入配置文件并记录该 Epoch 为 mymaster.failover_epoch。这是 Failover Epoch。在 Failover 成功后,Sentinel 1 会将 Failover Epoch 存入 mymaster.config_epoch。这是 master.config_epoch,可以理解为是 Redis Master 配置的版本号。后续其它 Sentinel 会比对 master.config_epoch 来确认自己是不是该更新 Master 地址。在下面一个问题里详细说。

master.leader_epoch 是用来选举的。每当 Sentinel Vote 了某个 Sentinel 为 Leader,可能是自己也可能是别的 Sentinel,就会将 master.leader_epoch 记录为投票的 Leader 的 Epoch,还会将这个信息写入配置文件,从而保证同一个 Epoch 下 Vote 过一个 Leader 后不会再 Vote 别的 Leader。比如 Sentinel 1 的 Epoch 是 100, Sentinel 2 收到 Sentinel 1 的 Vote 请求,Sentinel 2 决定投 Sentinel 1 在 Epoch 100 下为 Leader,于是在回复 Vote 前,Sentinel 2 会存 mymaster.leader = Sentinel 1, mymaster.leader_epoch = 100 在内存,并写 mymaster.leader_epoch = 100 到磁盘。之后再收到 Sentinel 3 的投票请求时,会比较 Sentinel 3 的 Epoch 是否大于 mymaster.leader_epoch 只有大于才会投 Sentinel 3 为 mymaster 集群新 Epoch 下的 Leader。并且因为在内存中记录了某个 Master 某 Epoch 下的 Leader ID,所以还支持根据 Epoch 来查询该 Epoch 下 Leader 是什么。当然因为 mymaster.leadermymaster.leader_epoch 只有一个,所以只会记录最新的 Leader 信息,比如问 Sentinel 2,Epoch 20 时候 Leader 是谁 Sentinel 2 肯定不记得,也没必要记得了。

另外看到写入配置文件的只有 mymaster.leader_epoch = 100 没有 mymaster.leader = Sentinel 1,所以 Sentinel 2 如果重启,假设 (虽然不会) 它再次收到 Sentinel 1 在 Epoch 100 下发来的投票请求,它会拒绝 Sentinel 1 的 Vote 请求,即使自己之前曾经确实 Vote 了 Sentinel 1 为 Leader。这也是跟 Raft 不同之处,Raft 因为会持久化 VoteFor 信息,所以如果 vote 了 Leader,即使 Follower 宕机它起来以后假设收到 Leader 的 Vote 请求则还会 Vote 原 Leader 为 Leader。

  • leader 选举成功后,是怎么通知给别的 sentinel 的?

Raft 里在 Leader 选举成功后,需要立即发送 ping 或者空的 RPC 将最新的 Term,Log Index 发送给其它 Raft 成员,从而让其它 Raft 成员不要再进行选举,变成 Follower 去追随 Leader。但 Sentinel 这里因为比 Raft 简单,它只需要在其它 Sentinel 选举 Leader 失败后,一个 Failover Timeout 时间内不要再执行 Leader 选举,就能保证 Leader 在执行 Failover 过程中至少一个 Failover Timeout 时间内不受干扰,选 Leader 失败的那个 Sentinel 无需知道谁被选为 Leader 了。所以 Sentinel 在选举成功后,并不需要去通知所有其它 Sentinel,也不需要其它 Sentinel 去 Follow Leader,只需要在 Leader 完成 Failover 后让所有 Sentinel 更新 Redis Master 配置,这个时候需要有个通知。我们一步步看看这些过程。

首先从前面提到过的投票的过程说起。假设现在有 Sentinel 1 2 3,如果 Sentinel 1 发现 Redis Objective 宕机,还未收到别的 Sentinel 要求 Vote 它的请求,Sentinel 1 进入 Failover 状态,提升 Epoch 并记录该 Epoch 为 failover_epoch,之后发起投票,要求别的 Sentinel 都投它。Sentinel 2 和 3 如果还未进入 Failover 状态未 Vote 自己,则在收到 Sentinel 1 的投票请求后,也记录投票时间为 Failover Start 时间,并保证在 Failover Start + Failover Timeout 时间前不会再发起投票。如果已经进入 Failover 状态,Vote 了自己,则和 Sentinel 1 一起去争抢另一个 Sentinel 的投票,抢不到则也是在 Failover Start + Failover Timeout 时间前不会再发起投票。

假设 Sentinel 1 拿到了 Leader,等选好 Slave 进入 SENTINEL_FAILOVER_STATE_WAIT_PROMOTION 状态后,Sentinel 1 会将之前记录的 failover_epoch 存入这个 Master Name 下的 config_epoch 中。之后通过 publish Event 到 __sentinel__:hello 频道,Event 中会带着这个新的 master.config_epoch,其它 Sentinel 因为在 Failover Timeout 之前一定不会同时进行 Failover,所以他们存储的 Master Name 下的 master.config_epoch 会比 Sentinel 1 通过 Publish 发来的 master.config_epoch 小,从而知道目标 Master Name 的配置发生了变化,会发出 +config-update-from 事件并打印:

16350:X 17 Jan 20:31:12.204 # +config-update-from sentinel e9a80378b5e8ebc71452c4e7217ca7dfd2c35bcb 127.0.0.1 26380 @ mymaster 127.0.0.1 6380

之后更新本地 Monitor 的 Master 信息,去 Monitor 新的 Master。

  • Raft 要求持久化 VoteFor 信息,即一个 Raft Server vote 过某个 Raft 后,如果 epoch(term) 不变,则不能再 Vote 别的 Raft。而 Sentinel 没有持久化 Vote For 信息,那会不会出现比如有 Sentinel A B C, epoch 100 下 SentinelA 在返回 Vote Sentinel B 后挂掉了,起来后收到 epoch 100 下 SentinelC 发的 Vote 请求,A 会不会又再 Vote Sentinel C 一次?

不会。上面这个问题是从 Raft 角度来看 Redis 提出来的。事实上对这个问题前面已经解释了,Sentinel A 在回复 Vote Sentinel B 前就会写 master.leader_epoch 为 100,启动后收到 Sentinel C 的 Vote 请求因为 Sentinel C 的 Epoch 还是 100,并不比 Sentinel A 当前存的 master.leader_epoch 大,所以不会给 Sentinel C 返回 Vote 成功。

  • 如果 Failover timeout 期间 Leader 没能完成 Failover 怎么办?

放弃 Failover。对于 Failover 失败的 Leader 来说,下一次 Failover 的开始时间是上一次 Failover Start Time + 2 * Failover Timeout。对选这个 Leader 为 Leader 的 Sentinel 来说,下一次 Failover 开始时间是 Vote 投票发出开始 + Failover Timeout;对于竞争 Leader 失败的 Sentinel,下一次 Failover 开始时间是该 Sentinel 上一次进入 Failover 的 Start Time + Failover Timeout。

Redis 配置纠正

Sentinel 在定时通过 info 获取 Redis Master 或 Slave 服务的状态信息后,会比较目标服务的 Role 和自己期望的 Role 是否一致,Slave 的 Master 是否和自己一致,不一致时候会就去纠正目标服务的配置。

比如 redis-cli 连上 Redis Slave 后,执行 slaveof no one,强制将 Slave 提升为 Master,但当有任意一个 Sentinel 发现这个事情后,会立即通过 config rewrite 命令将被错误提升为 Master 的 Slave 改回 Slave。如果手工修改 Slave 去 Sync 另一个 Master,Sentinel 发现这个事情后也会把它改回来。

如果强制把 Master 改为 Slave 呢?Sentinel 不会去纠正被错误改为 Slave 的 Master,会让它继续处在 Slave 状态,并且会通过后续 Failover 过程尝试选新 Master 出来,但会一直 Failover 失败无法恢复。原因是 Sentinel 判定 Master 下线进入 +sdown 有两个情况,一个是给 Master 发 ping 超过 down_after_milliseconds 时间未回复 pong,再有就是这里说的问题场景,Sentinel 认为对方是 Master 但对方回复自己是 Slave,持续超过 down_after_milliseconds + 2 * SENTINEL_INFO_PERIOD 时间。此时会开始 Failover,而 Failover 时候需要选 Slave,要求 Slave 最近一次与 Master 成功通信时间必须小于 5 * SENTINEL_PING_PERIOD,这个时间一般远小于 down_after_milliseconds + 2 * SENTINEL_INFO_PERIOD ,所以所有 Slave 都会不满足要求,从而无法选出合适的 Master 致使 Failover 持续失败。

这里标题叫做 Redis 配置纠正实际 Sentinel 去修改有问题的 Slave 的配置不是为了解决 Master 或 Slave 的配置被异常修改,而只是处理 Failover 过程中的必要步骤。比如 Failover 过程中,如果 Master 下线,选出新 Master 后这个老 Master 再次上线,此时老 Master 认为自己是 Master,但 Sentinel 认为其是 Slave,所以需要 Sentinel 支持将老 Master 修改为 Slavel。并且因为 Failover 过程是有时间限制的,中不能一直等着老 Master 再次上线等着把老 Master 配置修改完后才算 Failover 结束,所以只能是在新 Master 被设置好后,尝试设置所有 Slave 去 Sync 新 Master 完就算 Failover 结束,即使有部分节点没有被设置为 Slave 没有去 Sync 新 Master,在等待一段时间依然不成功后,就也结束 Failover 也算 Failover 成功,等后续 Sentinel 执行 info 操作时候发现目标服务的 Role 或 Sync 的目标和 Sentinel 认为的不符时再进行修改。

注意:配置纠正有效的前提是 Master 处于正常状态,在纠正 Slave 配置前,Sentinel 如果检查 Master 发现 Master 异常,比如 Master 处于 +sdown+odown,Master 不认为自己是 Master,超过两个 SENTINEL_INFO_PERIOD 周期未回复 info,则不会对 Slave 配置进行纠正。

Redis Client 使用 Sentinel

Client 通过 Sentinel 连接 Redis 的步骤

  1. 连上一个 Sentinel,记得设置超时时间,如果一个 Sentinel 不可用或访问失败则连下一个 Sentinel;
  2. 使用 SENTINEL get-master-addr-by-name master-name 去 Sentinel 询问某个 Master Name 当前的 Master 地址。如果请求失败则回到步骤 1;
  3. 连上目标 Redis Master 后,用 role 命令获取 Master 的 Role,判断其是不是 Master,不是的话回到步骤 1;

处理 Failover

Sentinel 在执行 Failover 时,我们在前面提到过,当切换了某个 Redis 的 Role 后,会立即发送 client kill type normal 指令给这个 Role 被切换的 Redis,该 Redis 会立即关闭所有 Client 的连接。

连接断开是 Client 最可靠的获知 Redis Master 发生切换的途径。但关闭连接这个事情 Client 无法区分出来是网络问题还是真的有 Master 切换,Redis 推荐 Client 在出现异常关闭时候都重新走一遍上述 Client 连接 Redis 的三个步骤,否则就可能会导致 Client 无法正常工作或者无法按照预期进行工作,比如期望连 Master 但因为 Role 切换并且没有重新去 Sentinel 解析新 Redis 地址而连上了 Slave 等。

并且如果使用了 Connection Pool,只要有一个 Connection 重连,都得去 Sentinel 再次获取 Redis Master 地址,如果 Master 地址变化需要将整个 Pool 中所有连接清理掉,不要让他们再重连原来的地址。

订阅 Sentinel 的 Event 以更快的获取 Master 变化

Sentinel 在执行 Failover 的过程中,会发出很多的 event,最重要的可能就是 switch-master 这个 Event,通过订阅这个 Event 能知道 Master 有切换。

但要注意的是 Redis Pub/Sub 机制不保证 Event 可达,订阅连接断开后 Event 就丢了。所以订阅 Event 只是能做到加速知道 Master 切换了,然后再去执行上面三个步骤获取最新 Master 地址,但不能只依靠订阅去保证获取最新 Master 地址。

使用 Sentinel 时遇到的问题

看到目前为止可能感觉都是棒棒棒,好的不行,可能偶尔看到的注意事项能隐隐感觉到有一些可能出问题的地方。下面介绍一些个人在使用 Sentinel 时候遇到的问题,我看过的 Redis Client 库不多,工作上因为主要使用 Clojure 语言,所以使用 Sentinel 的时候用的Carmine-Sentinel 这个库,它是基于 Clojure 语言下用的非常多的 Carmine 库开发的。除了 Carmine-Sentinel 以外再就是使用过 Jedis 这个库,所以下面提到的问题基于这两个库来说。

目前从这两个库里发现了这么一些问题:

  • 从 Sentinel 获取 Master 地址后,没连上 Master 再去确认 Master 的角色;
  • 过度依赖 Sentinel 的 PubSub 通知 Master 发生切换的 Event;
  • 想要利用 Sentinel PubSub 快速获取 Master 切换的通知,但只订阅了一个 Sentinel;
  • Client 支持 Redis 订阅操作,但不支持订阅连接通过 Ping/Pong 探测 Redis 是否还正常;
  • 不在连接断开或无法在连接断开时清理所有现有连接、现有缓存的 Master 地址重新去 Sentinel 解析 Master;

Jedis

Jedis 代码在:https://github.com/xetorthio/jedis/blob/jedis-3.0.1/src/main/java/redis/clients/jedis/JedisSentinelPool.java#L151

Jedis 做 Sentinel 解析 Master 地址的步骤大概是这样:

master_addr = null
for sentinel in sentinels:
  conn = connect_to_sentinel(sentinel)
  master_addr = conn.get_master_addr(master_name)
  if master_addr != null:
    break
for sentinel in sentinels:
  setup_master_listener_threads(sentinel, master_name)
return master_addr
  1. 遍历 Sentinel 列表,查询 Master 地址,只要拿到一个 Master 地址就认为解析 Master 成功;
  2. 再次遍历 Sentinel 列表,为每个 Sentinel 配置一个 master listener
  3. Master Listener 是单独的线程,里面是个 Loop,基本就是 Subscribe 每个 Sentinel 订阅 +switch-master 获取 Master 切换信息,类似于:
while (not_stop):
  conn = connect_to_sentinel(sentinel)
  master_addr = conn.get_master_addr(master_name)
  init_master_connection_pool(master_addr)
  conn.subscribe("+switch-master", handle_master_switch)

Jedis 有这么些问题:

  • 它只从一个 Sentinel 获取了 Master 地址。我们知道 Sentinel 是个集群,每个 Sentinel 之间对 Redis Master 的看法可能是不同的,可能有的认为 Master 是 A 有的认为 Master 是 B,只是 Majority 个 Sentinel 会认为 Master 是同一个地址。所以只从一个 Sentinel 获取 Master 地址可能拿到的并不是 Master;
  • 拿到 Master 后,不去连上 Master 用 role 看一下它是不是真的是 Master,这么直接连上去无法保证连的一定是 Master;
  • Master Listener 线程启动获取 Master 地址后,订阅 Sentinel 的 +switch-master 频道前,如果 Master 发生切换,Master Listener 不会重新初始化连接池,因为它无法收到 Master 切换通知,如果没有下一次 Master 切换 Master Listener 线程会卡在 subscribe 操作里永远阻塞下去;
  • 每次 Subscribe 连接异常断开后,Redis Master 都可能发生切换,并且切换时候可能因为还没有 Subscribe 的连接存在而丢失 Master 切换通知;
  • 没有处理与 Master 的连接断开时重新解析 Redis Master

Carmine-Sentinel

发现的问题 issue 或 PR 有:

  • https://github.com/killme2008/carmine-sentinel/pull/14
  • https://github.com/killme2008/carmine-sentinel/issues/13
  • https://github.com/killme2008/carmine-sentinel/issues/12
  • https://github.com/killme2008/carmine-sentinel/pull/18

不过下面提到的问题都已经得到修复,因为这个库没有打 Tag 现在已经不能很方便的看到之前代码是怎样的了,简单来说它当时的逻辑是这样:

master_addr = null
for sentinel in sentinels:
  conn = connect_to_sentinel(sentinel)
  master_addr = conn.get_master_addr(master_name)
  new_thread_run(conn.subscribe("+switch-master", clean_master_addr_cache))
  master_conn = connect_to_master(master_addr)
  if master_conn.is_master():
    cache_master_addr(master_addr)
    break
return master_addr
  1. 遍历 Sentinel 列表,连上一个 Sentinel 后获取 Master 地址;
  2. 订阅 Sentinel 的 +switch-master 频道,如果收到 Master 切换会将缓存的 Master 地址清理;
  3. 连上 Master 通过 role 命令看看它是不是真的是 Master;
  4. 如果是 Master 则认为找到了 Master,将 Master 的地址缓存起来,后续请求都会用这个缓存的地址;

它的问题是:

  1. 假若在用 role 判断 Master 确实是 Master 后 Master 发生了切换,即使从 +switch-master 频道收到了 Master 切换的通知,因为此时还未缓存 Master 地址,所以 clean_master_addr_cache 清理操作是无效的,之后主线程会将已经被切换为 Slave 的 Redis 地址当做 Master 地址存入缓存;
  2. 假若确认 Master 是 Master 后出现了 Master 切换,但订阅 +switch-master 频道的连接恰好断了,或者因为订阅 +switch-master 的线程还未启动还未执行 Subscribe,则会丢失 Redis Master 切换的通知,没收到这个通知不会清理缓存的 Master 地址,所以 Client 会继续使用被切换走的 Master 做后续操作;
  3. 与 Master 连接断开后没有再次去解析 Master 地址,而是继续使用同一个 Master 地址做重连操作。如果连接断开时因为 Master 切换,又恰好因为什么原因没有收到 +switch-master 的 Event,Client 就会一直去连 Slave,而不是 Master;
  4. 仅靠 Sentinel PubSub 去获知 Master 切换是不够的,只订阅一个 Sentinel 获知 Master 切换这个事情也是不够的。因为只订阅一个 Sentinel 这个 Sentinel 可能与其他 Sentinel 之间有网络 Partition 导致根本不知道 Master 发生了切换;
  5. Carmine 这个库的 Pub/Sub 命令不支持 ping / pong 探测,Subscribe 后如果没有设置超时时间会一直阻塞线程等待 Redis 发来的 Event,但如果此时 Redis 突然宕机,Client 短时间内是不可能知道对面的 Redis 宕机,从而一直会傻等着永远不会来的 Event。而 Carmine 库文档推荐 Subscribe 操作是不要设置 timeout 的,所以大多数时候使用 Carmine 库做 Subscribe 都不会设置 timeout,Carmine-Sentinel 库之前也是没有 timeout,也会在 Sentinel 宕机时卡住;

问题解决办法

看到上面问题多多,但关键的感知 Master 变化的办法就是在遇到 Client 与 Redis 连接断开时候重新去 Sentinel 解析一遍 Master 地址,重建所有连接。再一个关键的事情就是,不要过度依赖 Redis 的 Pub/Sub 机制,不把这个事情记在心里就容易踩坑。

还有个方法是使用一个专门的线程定时去依次连接所有当前解析出来的 Master 地址,通过 Role 命令检查当前解析的 Master 是不是真的还是 Master。

最近还从同事那里得到一个在某些场景下相对更优雅的方法,是用 HAProxy。大致原理就是 HAProxy 负责去定时探测背后的 Redis 是不是 Master,谁是 Master,将请求发送给是 Master 的那个 Redis,并且当发现所有配置的 Redis 都认为自己是 Master 的时候,就给用户请求抛错,不把请求发送到任何一个 Redis 上,等待 Sentinel 将 Redis 配置修复选出真正的 Master 后再将请求重新发送到真的 Master 那里。

据我同事说这个方案是从某个其它地方看到的,因为时间久远所以很难在这里列出最初的出处了,非常抱歉。我只列一下配置示例:

# 后端配置,可以看到是手写了一堆 Redis 的指令去做后端 Redis 服务的检查,看到关键是有用 INFO 检查 Redis 角色
backend bk_some-cache-123
  option tcp-check
  tcp-check connect
  tcp-check send AUTH\ XXXXX\r\n
  tcp-check expect string +OK
  tcp-check send PING\r\n
  tcp-check expect string +PONG
  tcp-check send info\ replication\r\n
  tcp-check expect string role:master
  tcp-check send QUIT\r\n
  tcp-check expect string +OK
# 明确列出来 Redis 服务的 IP 端口号,指定 1s 检查一次
  server some-cache-123_0 10.10.10.1:22222 check inter 1s
  server some-cache-123_1 10.10.10.2:23333 check inter 1s  

# 前端配置就是暴露出去一个端口,用户连这个端口时候会去找后端的 bk_some-cache-123
frontend ft_some-cache-123
  bind *:8888 name some-cache-123 maxconn 200
  default_backend bk_some-cache-123
# 关键是这里有个检查,判断后端是不是只有一个 master 连接,如果超过两个 Master 就完全放弃请求
  acl single_master nbsrv(bk_some-cache-123) eq 1
  tcp-request connection reject if !single_master
  use_backend bk_some-cache-123 if single_master

这个方式的好处就是对于 client 来说无需知道谁是 Master 谁是 Slave,出现 Master Slave 切换时候也不需要做额外的操作,等待 Sentinel 去切换 Master Slave 即可。缺点就是得明确写出来有哪些 Redis,并且如果出现网络隔离,两个 Redis 都认为自己是 Master 就直接不工作,而 Sentinel 模式下至少理论上有能取到 Sentinel 认为的那个 Master 去访问这个 Master。

最后想说一点是,Sentinel 能让 Client 库这么费劲有这么多问题原因就是通过连接断开来通知 Master 和 Slave 发生了切换。让这么重要一个事情用另一种不清晰的方式去通知 Client。虽然看着上面写的是 Client 库的问题,但归根揭底是 Sentinel 机制实现的并不优雅,至少说 Master 切换事件通知机制并不优雅。拍脑袋想,是不是连接在建立时候就能有默认的鉴权机制去保证我希望连 Master 时候确认对方真的是 Master 不然就报错是不是好一点。

运维方面的注意事项

  • 强行改一个 Redis Master 或 Slave 的 Role 是不行的

有时候因为某些原因可能会强行修改 Redis 的 Role,比如想扩大 Redis 集群的内存,可能得先升级一下 Slave 的内存,之后把 Slave 提升为 Master,再升级原来 Master 的内存。但这里如果强行连上 Redis 通过 slaveof no one 或者 slaveof XXX 去修改 Role 是没用的,前面说过,如果把 Slave 提升为 Master,在被 Sentinel 发现后会给你改回去。把 Master 降为 Slave,Sentinel 会开始 Failover。

实际做法是得用 Sentinel 的 API,SENTINEL failover <master name>去强制做一次 Master 切换。如果有多个 Slave,可以先给期望变成新 Master 的 Slave 设置更高优先级。

  • Sentinel 不会忘记一个 Master 的 Slave,如果一个 Redis 本来是 Slave 因为什么原因被想去拿去当 Master,只要 IP 端口不变,Sentinel 会把它再改回 Slave

这个是我同事提供的例子。因为运维原因想把一个很久很久很久以前当过 Slave 的实例拿来当做 Master 用,结果配置好以后被 Sentinel 发现了又把它强行改回 Slave。只要 IP port 不变,Sentinel 就不会忘记它,并且在它上线后会去检查它配置。

解决办法是用 SENTINEL reset <pattern> 将目标 Redis Master Name 下现有信息清理掉,不光会清理掉 Slave 信息,还会清理掉相互发现的 Sentinel 信息。全部重来。

  • 增加 Sentinel 时候过快

Sentinel 选 Leader 是收到 Majority 个投票的 Sentinel 会变成 Leader。在对 Sentinel 进行扩容或缩容时候,Majority 个 Sentinel 的个数会发生变化。比如开始 5 个 Sentinel,如果一口气补充到 10 个,可能有的 Sentinel 会认为当前有 10 个 Sentinel,有的认为现在只有 5 个 Sentinel,这个时候如果发生 Master Slave 切换,可能就会选出不只一个 Leader。因为对于认为只有 5 个 Sentinel 的 Sentinel 来说,拿到 3 个投票就够当 Leader 了,但集群内实际有 10 个 Sentinel。

解决办法就是增加 Sentinel 节点的时候都慢一点,一次只增加一个节点,并且增加节点完毕去每个 Sentinel 配置确认当前它认为集群内有多少 Sentinel。

  • Sentinel 减节点时候不会忘记被移除的 Sentinel

Sentinel 的机制是只要发现了新的 Sentinel 就进行持久化并且不忘记这个事情,即使一个 Sentinel 想下线直接移除是不行的,移除掉以后 Sentinel 在对投票计数时候还是按原先 Sentinel 集群规模计数,计算是否超过 Majority。所以下线 Sentinel 时候需要使用 SENTINEL reset <pattern>,将当前所有发现的 Sentinel 全部忘记,重组集群。。。

TILT 模式

Sentinel 运行时候能看到类似这样的日志:

778:X 14 Jan 19:07:04.570 # +tilt #tilt mode entered
778:X 14 Jan 19:07:34.616 # -tilt #tilt mode exited

这是 Sentinel 一个特殊的运行模式。Sentinel 内部类似有个大的循环,每个循环开始都会做类似判断一下要不要去 Ping 某些连接,要不要发送 INFO 到 Redis Master,要不要查看一下 Slave 是否有 Promot 到 Master 等。每个循环间隔 100ms,如果 Redis 发现这个间隔时间变的不合理的大它就会进入 TILT 模式,在该模式下它依然会监控 Redis 服务,但不会做任何事情,不会进行主从切换,不会发 Event 等等。直到它发现每次循环间隔时间缩小到某个合理值并保持一段时间 (默认 30 秒) 后,才会退出 TILT 模式。

提高 Failover 时的一致性

最后一个想记录的就是有两个配置能去提高 Failover 时候的一致性。

Sentinel 只保证最终一致性,即出现 Failover 后,所有数据会以新选出来的 Master 为准,其它 Slave 不管数据比这个新 Master 新还是更老,都全部丢弃后从新 Master 重新 Sync,从而保证出现 Failover 后,Redis 集群内数据最终能保持一致。比如下图,一开始 Redis 3 是 Master,之后出现了网络隔离,Sentinel 1 和 2 将 Redis 1 选为新 Master,并且有了 Client A 往 Redis 1 开始写数据。而老的 Redis 3 因为网络隔离原因,它并不知道自己被选为 Slave,还认为自己是 Master,并且 Client B 在持续向它写数据。最终网络恢复的时候,Redis 3 会被设置为 Slave 去 Sync Redis 1 的数据,从而导致网络隔离期间,Client B 写入的数据全部丢失。

default

图片来自:https://redis.io/topics/sentinel

这时候有两个选项能帮助你在网络出现隔离时候牺牲可用性换取相对高一点的一致性:

# 至少有一个 Slave 可写的时候 Master 才接收 Client 的写请求
min-slaves-to-write 1
# Slave 和 Master 之间的 Lag 必须小于 10
min-slaves-max-lag 10

min-slaves-to-write 比较有迷惑性,可能会以为是像某些 DB 那样表示写操作必须在 Master 和至少一个 Slave 写成功后才算真的写入成功。而 Redis 因为 Master 和 Slave 之间是异步的,所以 Client 写操作无法等待 Slave 写成功后才返回成功,所以min-slaves-to-write变成了 Master 至少有一个活着的 Slave 时候才可写入。

有了这两个配置就能做到当 Slave 和 Master 的 Lag 过大,或者太久未回复异步写请求的 ack 时,Slave 就会被 Master 认为不可写。