Skip to content

Redis 哨兵选举机制(Raft)

问题

Redis Sentinel 选举主节点的算法是什么?跟 Raft 协议是什么关系?生产环境有哪些容易踩的坑?

分析

哨兵干了什么

Redis Sentinel(哨兵)负责三件事:监控、通知、自动故障转移。当主节点挂了,哨兵集群需要自己选出一个 leader,由这个 leader 执行"从节点晋升新主"的操作。

选 leader 这一步,参考了 Raft 协议,但不是完整实现,属于"Raft-like"。

Raft 核心机制回顾(为了对比)

要理解 Sentinel 选举的局限,先快速过一遍 Raft 的核心机制。Raft 把共识问题拆成三个子问题:Leader 选举、日志复制、安全性

  • Term(任期):时间划分为递增的 term,每个 term 最多一个 leader
  • 选举超时(Election Timeout):Follower 在规定时间内没收到 leader 心跳,转为 Candidate 发起选举,超时时间 150-300ms 随机化
  • 投票规则:每个 term 每台服务器只能投一票,Candidate 必须拿到 > N/2
  • 日志复制:leader 把客户端的写操作包装成 Log Entry,通过 AppendEntries RPC 复制到所有 Follower,过半确认后才可以提交(commit)
  • 安全性:Candidate 的日志必须至少和 Follower 一样新(比较 lastLogIndex 和 lastLogTerm),否则 Follower 拒绝投票。这保证了已提交的日志不会丢失

Sentinel 只复用了"选 leader"这一层,而且砍掉了日志复制,所以叫 Raft-like。

选举流程拆解

第一步:确认主节点真挂了(S_DOWN → O_DOWN)

单个哨兵发现主节点 ping 超时,先标记为 S_DOWN(主观下线)。然后问其他哨兵:"你觉得主节点挂了没?" 如果至少 quorum 个哨兵都确认挂了,标记为 O_DOWN(客观下线),触发选举。

python
# 伪代码:哨兵判断 ODOWN
def check_odown(sentinel, master, quorum):
    votes = 1  # 自己先算一票
    for peer in sentinel.peers:
        if peer.sentinel_master_down(master.name):
            votes += 1
    if votes >= quorum:
        master.odown = True
        # 触发选举
        start_election(sentinel, master)

这里的 quorum 通常在 sentinel.conf 里配置。生产坑:quorum 设得太高(比如 5 节点哨兵设 quorum=5),网络抖动丢掉一个哨兵,永远凑不够 O_DOWN,故障转移失效。

第二步:自荐 + 拉票

触发选举的哨兵给自己投一票(leader_epoch += 1),并向其他哨兵发 SENTINEL is-master-down-by-addr 请求,附带自己的 leader_epoch

注意这里的 leader_epoch 对应 Raft 的 term。但和 Raft 的一个关键区别:Raft 的 term 必须持久化到磁盘,选举恢复后重新读;Sentinel 的 epoch 只在内存中维护。哨兵全部重启后 epoch 归零,如果旧主还活着但没来得及降级,可能出现双写。

第三步:投票规则

其他哨兵收到拉票请求,判断逻辑:

  • 如果当前 epoch 还没投过票,同意
  • 如果已经有更高 epoch 的候选投过票,拒绝(Raft 的"每 term 一票"原则)
  • 如果收到更低 epoch 的拉票,忽略

第四步:胜出

拿到超过半数(> N/2)票的哨兵成为 leader,然后执行故障转移。

python
# 伪代码:选举计票
def elect_leader(sentinel, master):
    votes = {}
    for peer in sentinel.peers:
        if peer.vote_for == sentinel.id:
            votes[peer.id] = peer.epoch
    if len(votes) > len(sentinel.peers) / 2:
        return sentinel  # 胜出
    return None

选举超时:Sentinel 没有 Raft 的随机化选举超时机制,而是依赖 sentinel failover-timeout(默认 3 分钟)。这意味着 Sentinel 的选举触发速度比 Raft 慢几个数量级——Raft 在 300ms 内完成,Sentinel 可能要等几十秒甚至几分钟。

第五步:故障转移——选从节点

leader 选出来之后,从候选从节点中挑一个升主,筛选规则:

  1. 优先级slave-priority)越小越优先,0 表示不参与选举
  2. 复制偏移量(replication offset)越大越优先,数据越新
  3. runid 越小越优先(兜底)
python
# 伪代码:选主节点
def select_new_master(slaves):
    # 过滤掉断连、优先级为 0 的
    candidates = [s for s in slaves if s.is_connected and s.priority > 0]
    # 按优先级升序,相同按复制偏移量降序,再相同按 runid 升序
    candidates.sort(key=lambda s: (s.priority, -s.replication_offset, s.runid))
    return candidates[0] if candidates else None

这里有个隐含陷阱:复制偏移量只反映主从之间的数据同步进度,不代表数据一致性。如果从节点在同步过程中断连了,它的偏移量停在某个位置,leader 选择它升主后,丢失的数据只能靠主节点重启后追回。所以 slave-priority 配置要配合预期的一致性和可用性要求来设置。

选出来后,leader 向其他从节点发送 SLAVEOF new_master,把原主节点标记为从节点(恢复后自动切换)。

完整时序图

Sentinel-1         Sentinel-2         Sentinel-3         Master    Slave-A    Slave-B
    |                   |                   |               |         |           |
    |--- ping timeout -->|                   |               |         |           |
    |<-- S_DOWN --------|                   |               |         |           |
    |                   |                   |               |         |           |
    |--- is-master-down? -->|               |               |         |           |
    |<-- yes ------------|                   |               |         |           |
    |--- is-master-down? ------>|           |               |         |           |
    |<-- no -------------|                   |               |         |           |
    |                   |                   |               |         |           |
    | quorum=2, 2/3 确认, O_DOWN              |               |         |           |
    | leader_epoch=3                        |               |         |           |
    |                   |                   |               |         |           |
    |--- vote_for(epoch=3) -->|             |               |         |           |
    |<-- vote_yes ----------|               |               |         |           |
    |--- vote_for(epoch=3) ------>|         |               |         |           |
    |<-- vote_no (已投 Sentinel-2) |         |               |         |           |
    |                   |                   |               |         |           |
    | [2/3 票, 胜出]                         |               |         |           |
    |                   |                   |               |         |           |
    |----------- SLAVEOF Slave-A ------------->|           |         |           |
    |--- SLAVEOF Slave-A ------------------------>|        |         |           |
    |                   |                   |               |         |           |
    |--- 标记旧主为 slave ---->|               |               |         |           |
    |                   |                   |               |         |           |
    | [故障转移完成]                             |         Slave-A 旧主    |
    |                   |                   |               | 升 NEW  |            |
    |                   |                   |               | Master  |            |

哨兵选举 vs Raft 的关键区别

维度RaftSentinel
日志复制有严格的日志复制机制,写操作必须过半确认才提交没有日志复制,只关心谁当 leader,不关心数据完整性
Term 持久化term 持久化到磁盘,重启后恢复epoch 只在内存中维护,哨兵全挂后归零
Term 连续性严格递增,未参与 Term 的节点不能投票epoch 递增,但无严格连续性保障
选举超时150-300ms 随机化,避免 split vote固定 failover_timeout(默认 3 分钟),速度慢 2-3 个数量级
安全性保证通过日志比较(lastLogIndex, lastLogTerm)保证已提交日志不丢失无日志比较,选从节点只看复制偏移量,不保证一致性
脑裂保护通过日志复制 + 多数派保证线性一致性靠 epoch 标记拒绝旧主写入,不保证强一致
心跳机制空 AppendEntries RPC,周期性发送ping 命令,超时时间可配置

核心差异一句话:Raft 是强一致共识算法,Sentinel 是"够用就行"的 Raft-like 实现

生产环境实战坑

坑一:哨兵最少几个节点

3 个(至少 2 个确认才 ODOWN)。如果只有 2 个哨兵,一个挂了,另一个无法凑够 quorum,故障转移永远触发不了。

生产建议:3 个或 5 个奇数节点,quorum 设为 2 或 3。

坑的变形:哨兵 3 节点但 quorum=3,一次网络抖动丢一个哨兵,剩下 2 个哨兵中 1 个把主标记 S_DOWN,但另一个哨兵不确认,永远凑不够 O_DOWN。实际遇到过——某业务线线上 Redis 主挂了 2 分钟没触发故障转移,排查发现 quorum=3 但一个哨兵机器硬盘 IO 打满导致响应超时,实际只有 2 个哨兵活着。

坑二:密码配置不一致

Sentinel 连接从节点也需要密码。如果 requirepassmasterauth 配置不一致,从节点升主后会因为密码不对连不上,导致故障转移失败。

bash
# 务必保持一致
requirepass YourPassword
masterauth YourPassword

实际案例:某生产环境升级 Redis 6 到 7,改了 requirepass 但忘了改 masterauth,一个月后主节点宕机,从节点升主后新主无法认证旧从,集群陷入"无主"状态 8 分钟,手工介入才恢复。

坑三:脑裂(split-brain)

网络分区发生时,两个分区都可能选出哨兵 leader。但集群纪元(epoch)保证唯一性——故障转移后 current_epoch 递增,旧主恢复后收到 epoch 更新的消息,发现自己落后,自动降级为从节点。

python
# 脑裂保护示意
epoch_new = 5
epoch_old_master = 3  # 旧主还在用旧 epoch
if epoch_new > epoch_old_master:
    old_master.reject_write()  # 拒绝写入
    old_master.become_slave(new_master)

但这里有个坑:旧主恢复后拒绝写入之前的几秒,客户端可能已经写入了新数据。如果旧主和新主分区期间都接受了写入,epoch 机制只能保证后续一致性,不能挽回已丢失的数据。

实际场景:某电商 Redis 集群在 AWS 可用区之间出现网络延迟,两个哨兵各选出一个 leader,两个主节点都接受了写入,导致约 500 条订单数据不一致。最终靠业务日志逐条修复。

坑四:主观下线误判

网络抖动可能导致哨兵误判 S_DOWN,触发不必要的选举。调优 down-after-milliseconds(默认 30000ms),根据网络状况适当放大。

建议:如果部署在同一机房内网,down-after-milliseconds 可以设 10000-15000ms;跨可用区则设 30000-50000ms,避免频繁选举。

坑五:哨兵自身脑裂(选举失败)

极端情况:3 个哨兵分布在 3 个可用区,如果两个可用区之间网络中断,哨兵可能永远选不出 leader(两个分区都只有 1 个哨兵在线,各自拿到 1 票,达不到 2 票)。这种情况下的妥协方案:将哨兵部署在同一个可用区,跨 AZ 用 Redis Cluster 而非 Sentinel

坑六:从节点数据过期

Sentinel 选从节点只看复制偏移量,不考虑从节点数据是否过期(比如断连了 2 天)。如果选了断连很久的从节点升主,客户端读取到的数据是 2 天前的。通过 min-slaves-to-writemin-slaves-max-lag 参数可以部分缓解,但不能完全解决。

bash
# 至少 1 个从节点同步延迟不超过 10 秒,才允许主节点写入
min-slaves-to-write 1
min-slaves-max-lag 10

总结

  • Sentinel 选举是 Raft-like,不是完整 Raft 实现,没有日志复制
  • 选举流程:O_DOWN 确认 → 自荐拉票 → 过半胜出 → 执行故障转移
  • 选从节点:优先级 > 复制偏移量 > runid
  • 核心差异:Sentinel 没有日志复制和持久化 term,选举超时长达 3 分钟,安全性远弱于 Raft
  • 生产配置:3 个奇数哨兵、密码一致、down-after-milliseconds 合理设置
  • 最坑的脑裂问题靠 epoch 机制兜底,但不保证强一致,网络分区期间可能丢数据
  • 面试加分:能说出 Sentinel 和 Raft 在 term 持久化、日志复制、选举超时随机化三个维度的差异,以及脑裂场景下 epoch 机制的实际局限

手撕 → 框架 → 生产化,一步步把 AI Agent 工程化搞透。