连载中 12/16

哨兵:主库挂了,谁说了算

2026-05-21 · 5580 阅读 · 0 评论 · 0 赞

凌晨三点的主库宕机,我不希望你亲历

讲一次没有哨兵的凌晨。主库失联告警响起,我蹬着 VPN 上服务器:确认进程、确认机器、挑一个从库、改主从关系、改应用配置、重启、观察流量……一整套人肉操作做完,二十分钟过去了,业务就这么停了二十分钟。更要命的是手抖的风险——选错从库、漏改配置,都可能把事故变成灾难。

如果当时有哨兵(Sentinel),这二十分钟会被压缩成三十秒:主库失联、多数哨兵确认、自动选新主、通知客户端,全程没人碰键盘。这篇就把哨兵这套自动化流程拆开看。

哨兵是什么:不存数据的观察员

哨兵是独立于 Redis 实例运行的小进程,自己不存任何业务数据,专职干四件事:

任务说明
监控每秒 ping 一遍主库、从库和其他哨兵
通知实例出问题时,把消息推给运维或客户端
自动故障转移主库确认下线后,从从库里选一个晋升为新主
配置中心客户端来问「主库是谁」,哨兵随时答得上来

一个关键的设计:哨兵自己也是分布式的,至少部署 3 个、且是奇数。一个哨兵自己挂了就误报,两个哨兵 disagreement 时永远对不齐——奇数个才能凑出稳定的「多数派」。这条规则贯穿全文的每个环节。

判定下线:先主观,再客观

哨兵怎么确认「主库真的挂了」?分两阶段。某个哨兵 ping 主库超过 down-after-milliseconds 没回音,它标记主库为主观下线——注意,这只是它一个人的看法,也可能是它自己网断了。于是它去问别的哨兵:你们也 ping 不通吗?当超过 quorum 个哨兵都同意,升级为客观下线——大家投票确认,这主库是真不行了。

为什么要两阶段?想象主库没事,只是某个哨兵和主库之间的网络断了:单哨兵直接切换就是误杀。多数派投票机制保证了「一票否决」式的误判不会发生——这是把「防误删别人的锁」(第 8 篇)的思路用到了集群管理上:重要结论,不能由单点说了算。

谁来执行切换:哨兵也要先选领导

确认客观下线后,还有一个问题:这么多哨兵,谁动手执行切换?答案还是投票——哨兵之间先选出一个领导者(内部是一套类 Raft 选举),拿到多数派(majority)选票的哨兵当选,由它执行故障转移。

这里有两个容易混的数字:quorum 只用来判定「客观下线」,majority 用来选出「领导者」。3 个哨兵配 quorum=2 时,判定下线要 2 票,选领导要 2 票(3 的多数);5 个哨兵配 quorum=3,判定要 3 票,选领导要 3 票。但两者含义不同——quorum 可以小于多数派,比如 5 个哨兵配 quorum=2:判定下线更容易(2 票就行),选领导依然要 3 票。

领导者上任后执行四步:

  • 挑新主:规则三条,优先级(replica-priority,数字小的优先,设 0 表示永不参选)→ 复制进度(offset 最大、数据最全)→ runid 最小(兜底的随机平局裁决)。
  • 改换门庭:让其余从库执行 replicaof 新主。
  • 处置旧主:旧主恢复后自动降级为从库,跟着新主复制。
  • 通知客户端:把新主的地址广播出去。

脑裂:切换期间最疼的坑

现在看最阴险的场景。主库没挂,只是它和哨兵、从库之间的网络断了(机房级网络分区):哨兵们按流程判定客观下线、选出新主、客户端开始往新主写数据;而旧主那边还活着,孤岛上照样在接写请求。等网络恢复,旧主被降级为从库、清空数据跟新主同步——它孤岛上收到的那些写入,就这么没了。

缓解配置是第 11 篇提过的老朋友:min-replicas-to-write + min-replicas-max-lag——主库发现没有从库跟上,就拒绝写入。孤岛上的旧主接不了写,数据就不至于丢。但这只是止血:真正的兜底还是那句老话,关键数据以数据库为准,Redis 不做唯一的真相源。

配置与客户端接入

三个哨兵节点的核心配置,每个节点一份:

# sentinel.conf:监控名为 mymaster 的主库,quorum 为 2
sentinel monitor mymaster 10.0.0.1 6379 2
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 60000
sentinel parallel-syncs mymaster 1

客户端不再直连主库,而是「问哨兵要主库地址」并跟随切换。Lettuce 的接法:

// 客户端连哨兵,自动发现主库并跟随故障转移
RedisURI uri = RedisURI.builder()
        .withSentinel("10.0.0.11", 26379)
        .withSentinel("10.0.0.12", 26379)
        .withSentinel("10.0.0.13", 26379)
        .withMaster("mymaster")
        .build();

注意连接字符串里写的全是哨兵地址和主库名,没有一个真正的 Redis 数据节点——主库是谁,运行时问哨兵,切换了它自然会告诉你新的。


总结:哨兵用「多数派」贯穿始终——判定下线要多数同意,选领导要多数投票,奇数个节点才玩得转;故障转移四步走,脑裂用写入限流止血。但哨兵解决的是「高可用」,不是「容量」:数据再多,主从还是一份数据。数据量大到单机装不下怎么办?下一篇聊聊 Cluster 集群:数据怎么切片?哈希槽是什么?为什么 Redis 作者放弃了一致性哈希?下篇见。

咖啡凉了,记得趁热喝。

503

10 年全栈工程师 · 503咖啡馆主理人

#Redis#哨兵#高可用#故障转移#脑裂

评论 (0)

相关推荐

连载中 12/20

排查四件套:jstack、jmap、jstat、jcmd 的实战分工

jstack 看线程在干什么,jmap 看堆里装了什么,jstat 看运行时在变什么,jcmd 是统一入口。四把刀各管一段,配合着用没有查不动的现场。

#jstack#jmap#jstat#jcmd#排查工具
2026-09-16 · 4 阅读 · 0 评论 · 0 赞
连载中 11/20

GC 日志:把回收过程翻译成人话

一行 GC 日志里塞着七种信息:谁触发的、收了哪、停了多久、活了哪些。加上 -Xlog 配置,再加上日志分析工具,GC 不再是只能盯监控曲线的黑盒。

#GC日志#Xlog#日志分析#GC监控#Full GC排查
2026-09-16 · 13 阅读 · 0 评论 · 0 赞
连载中 10/20

ZGC:亚毫秒停顿是怎么炼成的

百 G 大堆停顿不到一毫秒,靠的是把搬家全部挪到并发阶段——着色指针让引用自带状态,读屏障让搬运中的对象依然可访问。代价是吞吐与内存,收益是停顿与堆大小解耦。

#ZGC#着色指针#读屏障#亚毫秒停顿#分代ZGC
2026-09-15 · 8 阅读 · 0 评论 · 0 赞