Redis哨兵集群最核心的价值就是当主节点挂掉后,能够在短时间内自动完成故障转移,把某个从节点提升为新的主节点。但实际环境中经常出现一种诡异现象:主节点确实已经不可达,哨兵日志里也不断出现 +sdown、+odown 甚至 +failover-state-reconf-slaves 之类的状态变动,可故障转移就是一直卡在某个阶段,选举始终无法成功。遇到这种情况,很多运维人员的直觉是重启哨兵进程或者直接手动执行 failover 命令,但治标不治本,过一段时间同样的问题还会复现。

要彻底解决选举失败,必须先理解哨兵的选举机制。哨兵之间通过发布订阅和命令通道交换信息,每个哨兵根据自己的配置和观察结果独立判断主节点是否下线。只有当超过 quorum 数量的哨兵在同一个时间窗口内都认为主节点主观下线,才会触发客观下线,进而启动选举流程。因此,任何导致哨兵之间信息不同步、时间窗口错位或者票数不足的因素,都可能让选举卡死。下面从几个最容易被忽略的细节展开分析。
检查哨兵配置中的基础参数是否一致
很多选举失败的根因都藏在配置文件里。最典型的错误是 sentinel monitor 后面的 IP 和端口写得不完全一致。比如三个哨兵节点分别配置了 127.0.0.1、192.168.1.10 和 localhost,虽然它们指向同一台机器,但哨兵判断主节点身份时依赖的是字符串完全匹配,IP 写法不同会被当成三个不同的主节点,自然无法完成统一投票。
另一个高频问题是 sentinel known-replica 或旧版本中的 sentinel known-slave 参数缺失。哨兵在发现从节点后会通过配置文件持久化这些信息,如果某个从节点的 IP 或端口发生变化,但配置里没有及时清理旧的记录,会导致哨兵尝试连接一个已经不存在的从节点,从而影响后续的从节点选举和配置传播。建议在每次拓扑变更后执行 sentinel reset 命令,让哨兵重新扫描。
还有一个必须核对的参数是 sentinel down-after-milliseconds。这个值在每个哨兵上必须完全一致,至少不能相差太大。如果 A 哨兵设置 5000 毫秒触发主观下线,B 哨兵设置 30000 毫秒,那么当网络抖动时 A 可能已经判定主节点下线并开始拉票,B 却仍然认为主节点健康,导致投票无法达到 quorum。排查时可以用 sentinel sentinels mymaster 命令对比所有哨兵的配置输出。
分析哨兵日志中的投票状态和异常时间戳
当选举卡住时,第一时间应该查看每个哨兵节点的日志文件,而不是只盯着某一个哨兵看。哨兵在执行故障转移时会记录详细的 +try-failover、+vote-for-leader、+elected-leader 等状态。如果某一台哨兵频繁出现 +failover-state-wait-start 却始终没有进入 +failover-state-select-slave,说明它没有获得足够的票数成为领导者。
此时需要检查 sentinel leader-epoch 和 sentinel current-epoch 两个字段。每个哨兵在本地维护一个 current-epoch 计数器,每次发起投票都会递增。如果集群中某个哨兵的时钟出现了明显偏移,或者网络分区导致它长时间收不到其他哨兵的消息,它的 current-epoch 可能比其他节点落后很多。在这种情况下,即使它发起投票,也会因为 epoch 过低而被其他节点拒绝,结果就是永远选不出 leader。
解决时钟问题的标准做法是启用 NTP 服务,确保所有哨兵节点的时间差控制在几百毫秒以内。同时要检查防火墙规则,确认哨兵之间用来通信的 TCP 端口(默认是 26379)是双向放行的。很多人只开放了 Redis 数据端口 6379,却漏掉了哨兵自己的端口,导致哨兵之间只能单方向通信,投票根本传不出去。
处理主节点恢复后的脑裂和配置漂移
还有一种常见的选举失败场景发生在主节点短暂宕机又恢复之后。假设主节点因为内存溢出或者 CPU 满载导致几秒钟无法响应,哨兵判定它客观下线并开始故障转移,但还没完成切换时旧主节点又恢复了。这时旧主节点会以从节点的身份重新加入集群,但它可能还保留着旧的主节点配置,导致哨兵在同步配置时出现冲突。
具体表现是哨兵日志里反复出现 +convert-to-slave 和 +slave-reconf-sent,但旧主节点始终无法成功变成从节点,或者变成从节点后很快又因为配置不同步被踢出。这种情况下手动执行 redis-cli -p 6379 replicaof 新主IP 新主端口 可以临时解决,但更推荐的做法是在所有 Redis 节点上开启 replica-read-only yes,并把 replica-serve-stale-data 设置为 no 来减少旧数据干扰。
另一个容易忽视的点是 sentinel failover-timeout 参数。这个值默认是 180000 毫秒,表示整个故障转移流程必须在 3 分钟内完成。如果从节点数据量很大,重新同步需要的时间超过了这个阈值,哨兵会认为本次故障转移失败并重新发起选举,造成无限循环。对于数据量较大的集群,建议把该参数调整到 300000 或 600000 毫秒,同时确保从节点的 repl-backlog-size 足够大,尽量走增量同步而不是全量同步。
追查法定人数和网络分区的深层影响
哨兵的 quorum 设置直接决定了客观下线需要多少票。如果集群总共只有 3 个哨兵,而其中 2 个部署在同一台物理机上,那么当这台物理机出现故障时,剩余的 1 个哨兵永远无法达到 quorum=2 的要求,选举自然失败。因此生产环境至少要有 3 个独立物理节点部署哨兵,最好是 5 个,形成奇数个节点的仲裁机制。
网络分区带来的问题更加隐蔽。假设哨兵集群被分割成两个部分,一部分可以访问主节点,另一部分可以访问从节点。两部分都认为自己应该主导故障转移,但由于无法通信,它们各自独立递增 epoch 并尝试选举。最终当网络恢复时,会出现两个不同 epoch 的领导者,导致配置互相覆盖,主从关系混乱。缓解这个问题的关键是在防火墙和交换机层面避免出现单向链路故障,并且合理设置 sentinel parallel-syncs 参数,避免多个从节点同时向新主节点发起全量同步造成网络拥塞。
最后给出一份经过验证的最小化哨兵配置模板,可以作为排查后的基准配置:
# 哨兵监听的端口 port 26379 # 禁止守护进程模式,方便用 systemd 管理 daemonize no # 指定要监控的主节点,quorum 设为 2 sentinel monitor mymaster 192.168.1.100 6379 2 # 主观下线判定时间为 5 秒 sentinel down-after-milliseconds mymaster 5000 # 故障转移超时时间调整为 180 秒 sentinel failover-timeout mymaster 180000 # 同时只允许 1 个从节点进行同步 sentinel parallel-syncs mymaster 1 # 日志文件路径 logfile "/var/log/redis/sentinel.log" # 持久化哨兵状态,避免重启后丢失配置 dir "/var/lib/redis"
配置完成后重启所有哨兵节点,然后用 sentinel ckquorum mymaster 命令检查法定人数是否满足,再通过 sentinel get-master-addr-by-name mymaster 确认当前主节点地址是否一致。如果这些检查都通过,故障转移通常能够恢复正常。