上周半夜被一个电话叫醒,某业务线的订单服务雪崩式超时,排查下来不是代码问题,而是他们的 Redis 主节点所在的机器磁盘满了,主节点进程被系统干掉。从节点还在老老实实当从,业务方客户端连的是写死的 IP,于是一直往一个已经死掉的地址上发写请求。运维同学爬起来手动改配置、重启应用,前后折腾了四十分钟。这套架构其实已经跑了一年多,主从复制做得好好的,但主从复制从来就没有解决"谁说了算"这个问题。
Redis 哨兵模式的原理及部署,本质上就是在回答一个问题:当主节点不可依靠时,系统怎样在没有人工介入的前提下完成自我修复。它由一组特殊的 Redis 实例组成,不存业务数据,专职盯着主从集群的状态,一旦确认主节点真的挂了,就自动选一个新主出来,把其余从节点和客户端都牵引过去。这篇文章会从哨兵的监控骨架讲起,一路讲到三节点哨兵集群的实际部署、参数计算、故障演练和踩坑记录。适合已经用上 Redis 主从、但还没做自动故障转移的团队看,也适合正在纠结"用哨兵还是用 Cluster"的人做决策参考。
1. 先搞清楚哨兵到底补的是哪个窟窿
1.1 主从复制解决了读扩展,但没解决"谁说了算"
很多团队上主从复制的初衷很朴素:读写分离、分摊读压力、顺便存一份备份。配置也确实简单,从节点一行replicaof就完事,同步靠 RDB 全量加后续增量命令传播,稳定性没什么可挑剔的。但主从复制在架构上是一个单向的、静态的关系,主节点是绝对权威,从节点只能被动跟随,整个体系里没有任何一个组件负责"判断主节点是否健康"以及"主节点不健康时该让谁顶上去"。
这就导致几个非常现实的困境。第一,主节点宕机时,写请求全部失败,客户端要么报错要么长时间超时,业务能不能降级完全看客户端实现得够不够好。第二,即便运维半夜爬起来手动把一个从节点提升为主,还得逐一修改其余从节点的复制指向,再通知所有客户端改地址,整个流程对人工操作的依赖太重,恢复时间基本取决于"人多久能醒"。第三,人工切换非常容易出错,我见过不止一次因为提升了一个数据落后的从节点,导致一批订单数据被新主覆盖后彻底丢失的案例。
哨兵要做的就是把这个判断权和执行权交给一组自动化程序。它不存储业务数据,也不代理客户端流量,只是持续观察、达成共识、执行切换。理解这一点很重要,因为很多人一开始会误以为哨兵是"代理层",其实不是,客户端的请求并不经过哨兵,哨兵只负责提供"当前主节点是谁"这个情报。
1.2 哨兵的三件事:监控、通知、自动切换
官方对哨兵职责的描述很精炼,拆开看就是三件事,我在实际使用中也是按这三条来定位问题的。
监控,哨兵会以固定的节奏向主节点、从节点以及其他哨兵实例发送探测请求,同时通过INFO命令拉取整个集群的拓扑信息。它需要知道当前有哪几个从节点、每个从节点的复制偏移量是多少、角色是否正常。这套监控是持续不断在跑的,不是定时巡检那种粗粒度。
通知,当被监控的实例出现异常、或者发生了主从切换、或者有从节点被重新配置时,哨兵会通过发布订阅频道和可选的脚本回调把这些事件抛出来。这个能力在实际运维中价值极高,你可以把它接进告警系统,也可以挂一个脚本在切换后自动刷新某些缓存。我个人更推荐至少把+switch-master、+sdown、-sdown这几个事件接出来,出问题时排查会轻松很多。
自动故障转移,这是核心。确认主节点客观下线后,一组哨兵会先选出其中一个作为执行者,再由这个执行者从现有从节点中挑一个最合适的提升为主,指挥其余从节点转向新主,并在旧主恢复后把它降级为从。整个过程对外表现为一次"主节点地址变更",客户端需要具备重新获取地址的能力。
注意:哨兵不做数据分片,也不做请求转发。它的作用是让单主架构具备自动恢复能力,容量上限仍然由单个主节点的内存和机器规格决定。
1.3 这套方案的边界:不适合什么场景
在决定用哨兵之前,有几个边界得先想明白,否则后面容易推倒重来。
第一,数据量上限就是单机上限。哨兵模式下所有写请求仍然落在一个主节点上,如果你的数据量已经接近单机内存上限,或者写入 QPS 靠单节点扛不住,那要解决的是分片问题,应该考虑 Redis Cluster 或者客户端分片方案,哨兵帮不上忙。
第二,切换期间必然有一段不可写窗口。从确认下线、选举、提升新主到客户端感知并重连,这几秒钟内写请求大概率会失败。这个窗口没办法彻底消除,只能压短。业务侧的写操作如果对这几秒完全零容忍,就需要在客户端做好重试和幂等。
第三,异步复制带来的数据丢失无法根除。Redis 主从复制默认是异步的,主节点写完就返回成功,再从节点同步。如果主节点在把某条写命令传播出去之前就挂了,这条数据在新主上是不存在的。这是架构层面的权衡,哨兵能做的是通过配置尽可能减少窗口,而不是消灭它。
第四,哨兵节点本身也是需要被保护的。哨兵集群如果全部部署在同一台机器上,机器一挂,判断和执行能力就全没了。这一点在部署章节我会详细展开。
2. 哨兵内部是怎么运转的
2.1 三个定时任务构成监控骨架
哨兵看起来只是几个会发 PING 的进程,实际上内部有三条不同频率的定时任务在同时跑,理解了这三条任务,后面很多现象就能自己解释了。
第一条是每 10 秒一次的信息同步。哨兵会向所有被监控的主节点和从节点发送INFO命令,主节点返回的信息里包含它下面挂着哪些从节点,从节点返回的信息里包含它当前的复制偏移量、复制状态、主节点地址。哨兵就是靠这条命令动态发现新加入的从节点的。所以如果你后来给主节点加了一个从节点,不需要重启哨兵,最多十秒后它就会出现在哨兵的视野里。这个频率可以通过sentinel info-period调整,Redis 6.2 之后才支持,老版本是硬编码的 10 秒。
第二条是每 2 秒一次的握手广播。哨兵会向主节点和从节点上名为__sentinel__:hello的频道发布一条消息,内容包括自己的 IP、端口、运行 ID、当前的配置纪元,以及它认为的主节点地址和端口。同时,哨兵也订阅了这个频道,所以当有新的哨兵加入时,它会从频道里收到消息,从而发现同行的存在。这就是"哨兵自动发现彼此"的原理——你不需要在配置文件里写清楚所有哨兵的地址,只需要保证它们监控同一个主节点名。
第三条是每 1 秒一次的心跳探测。向所有被监控实例发送PING,正常情况下对方回PONG。这条任务是判断实例存活的基础,如果某个实例在down-after-milliseconds指定的时间内一直没有给出有效回复,哨兵就会把它标记为主观下线。
三条任务的频率差异是有讲究的。心跳最频繁,因为要尽快发现异常;信息同步和握手相对慢,因为它们主要维护拓扑,实时性要求没那么高。这也解释了一个常见现象:新加一个从节点后,哨兵的状态里要等一会儿才更新,这是正常的,不是配置没生效。
2.2 主观下线到客观下线:一次判死的完整链路
"下线"这个判断在哨兵里分成两级,很多人对这两级的区别含混不清,而线上很多误切换恰恰出在这一步。
主观下线(SDOWN)是单个哨兵的判断。哨兵 A 在down-after-milliseconds时间内没有收到主节点 B 的有效PING回复,就会在自己的内部状态里把 B 标记为 SDOWN。注意这只是 A 的一家之言,A 并不知道 B 是真挂了,还是 A 和 B 之间的网络断了,或者是 B 太忙了没来得及回复。如果 A 判断错了就直接切主,很容易造成"其实主没事,但被误切了"的灾难。
客观下线(ODOWN)是群体共识。哨兵 A 标记 SDOWN 之后,会向所有其他监控同一主节点的哨兵发送SENTINEL is-master-down-by-addr命令,询问"你们觉得这个主节点死了吗"。其他哨兵会返回自己的判断结果。当表示"我这也认为它挂了"的哨兵数量(包含 A 自己)达到配置文件里quorum设定的值,A 就会把 B 标记为 ODOWN。只有进入 ODOWN 状态,才会触发后续的故障转移。
这里有一个容易踩的坑:quorum只决定"是否需要进入客观下线",它不决定"谁能执行切换"。执行切换的哨兵需要拿到多数票,这个多数是独立的计算。举个例子,三个哨兵、quorum设为 2 的情况下,两个哨兵认为主挂了就能进入 ODOWN,但要真正执行切换,还得在选举中拿到至少两票。如果三个哨兵里恰好有一个网络隔离了,剩下两个之间仍然能正常判定和切换;但如果两个都隔离了,只剩一个哨兵,它能进入 ODOWN(如果 quorum 是 1),但永远拿不到多数票,切换不会发生,这是设计上的安全兜底。
提示:
down-after-milliseconds的值不要设得太激进。这个时间要覆盖"正常的网络抖动 + 主节点偶发的阻塞"。设成 3000 毫秒这种值,遇到一次稍微长一点的慢查询或者一次系统卡顿,就可能触发误切换,而一次误切换的代价远高于多等几秒。
2.3 谁是 Leader:基于 Raft 的投票机制
进入 ODOWN 之后,需要从这一组哨兵里选出一个执行者,这一步用的是类似 Raft 的投票算法。我把它拆成以下几个要点。
每个哨兵都有一个**配置纪元(epoch)**的概念,可以理解为一轮投票的编号。当一个哨兵确认主节点 ODOWN 后,它会把自己的纪元加一,然后向其他哨兵发起投票请求,请求里带上自己的纪元和自己的运行 ID。其他哨兵收到请求后,判断规则大致是:如果请求里的纪元比我当前的大,我更新自己的纪元,并且把票投给第一个请求的人;如果纪元比我小,直接拒绝;如果这个纪元我已经投过票了,只能投给同一个人,重复投给不同人会被拒绝。
当一个哨兵收集到的票数满足两个条件时,它就成为 Leader:票数达到多数派(majority),也就是哨兵总数除以二再加一;同时票数不低于quorum。这两个条件同时存在的原因是在不同规模下保证安全性。三个哨兵的场景下多数派是 2,如果quorum配成 2,那么需要 2 票;如果quorum配成 1,仍然需要 2 票,因为多数派的要求更高。
投票还有超时的概念,如果一轮投票内没有选出 Leader,会等待一段随机时间后重新发起,避免多个哨兵同时发起投票导致反复分裂。这也是为什么在三个哨兵的场景下,切换通常几秒内就能完成,而在五哨兵且网络状况不好的场景下,可能会看到不止一轮投票。
2.4 新主怎么选:四条规则的优先级
选出 Leader 之后,真正有意思的部分来了:从哪里挑一个新主。这个挑选过程不是随机的,而是一个有明确优先级的多轮筛选,我在排查"为什么切到了那个从节点"的问题时,反复用到了这几条规则。
第一轮是健康度过滤。所有从节点先过一遍筛子,以下情况会被排除:处于 SDOWN 或 ODOWN 状态的;在最近 5 秒内没有回复过PING的;INFO信息超时的,这里的超时阈值是down-after-milliseconds乘以 10,再加上从节点上报的与主节点之间的断开时长;还有主从连接已断开的。这一轮的目的是保证候选人是真的活着且数据是相对新鲜的。
第二轮比优先级。每个从节点可以在配置里设置replica-priority(老版本叫slave-priority),默认值是 100,数值越小优先级越高。如果你的集群里机器规格不一样,比如有一台配置明显更高的机器,就可以把它的优先级调低,让它优先被选中。反过来,如果某台机器只是临时挂上来做备份的,可以把优先级设成 0,表示"永远不要选我当主"。这一点在做灰度迁移或者临时扩容时特别有用。
第三轮比复制偏移量。如果优先级相同,就比较各从节点上报的复制偏移量,偏移量越大说明它从主节点同步到的数据越多,选它丢失的数据最少。这一轮其实是最贴近"数据完整性"的考量。
第四轮比运行 ID。如果前面全都一样,那就按运行 ID 的字典序取最小的。这一轮纯粹是为了让结果确定,避免多个哨兵选出不同的人。
实际应用里,我建议把replica-priority的差异设置好,因为一旦所有节点优先级都是 100,选谁就完全取决于偏移量,而偏移量在切换瞬间是动态的,结果不太可控。把优先级当成一种"人为指定主备顺序"的手段,比依赖偏移量更稳。
2.5 切换完成后,世界怎么知道变了
新主选定后,Leader 哨兵会依次执行这些动作:向选中的从节点发送SLAVEOF NO ONE,让它停止复制、成为主节点;然后向其余从节点发送SLAVEOF 新主地址,让它们转向新主;如果旧主还活着(比如只是网络分区),会把它也配置成新主的从节点;最后,通过+switch-master事件把新的主节点地址广播出去。
客户端感知变更主要有两条路。一条是主动订阅,客户端连上哨兵后订阅+switch-master频道,收到消息后更新内部的主节点地址池。另一条是被动查询,客户端在遇到连接错误时,重新向哨兵询问SENTINEL get-master-addr-by-name mymaster,拿到新地址后重连。主流客户端库基本都是这两条路结合着用。
这里必须提醒一句:哨兵的发布订阅是不可靠的,它用的是普通 Redis 的发布订阅机制,消息没有持久化也没有确认机制。如果客户端在切换的那一刻恰好断线了,它会错过这条消息。所以客户端的正确做法是在收到消息后更新地址,同时把"连接异常时重新问哨兵"作为兜底。只靠订阅不重试的客户端,在切换后大概率会一直连旧地址。我在一个项目里就遇到过客户端库版本太老、只实现了订阅没实现重连,切换后业务报错直到重启应用。
3. 部署前必须想清楚的规划
3.1 哨兵节点数量的数学问题
哨兵节点数必须是奇数吗?严格来说不是必须,但实际部署几乎都用奇数,原因在于多数派计算。多数派是"总数除以二再加一",三个哨兵需要 2 票,四个哨兵需要 3 票,五个需要 3 票。可以看到,四个节点相比三个节点并没有降低门槛,反而多了一台机器的成本和潜在的故障点,所以三个或者五个是常态。
数量定了,还要解决部署位置的问题。三个哨兵如果全放在同一台物理机或者同一个虚拟化宿主上,宿主一挂三个一起没,等于没做。正确的做法是分散到至少三台独立的机器上,有条件的话再分散到不同的机架或者可用区。主节点和哨兵混部是可以的,但要注意不要出现"主节点所在的机器上坐着两个哨兵,另外一台机器上一个"这种失衡情况,因为主节点机器宕机时会同时带走两个哨兵的判断能力。
还有一个更隐蔽的问题:主从节点和哨兵的机器资源争抢。哨兵本身很轻,内存占用通常在几十 MB 级别,但它对网络延迟敏感。如果和主节点混部,主节点发生内存溢出或者执行大 key 操作时,哨兵进程可能被一起拖慢,导致心跳超时误判。我个人的做法是哨兵尽量和主从节点分开部署,实在要混部,也至少保证哨兵不和主节点在同一台机器上。
3.2 时间参数怎么定:别拍脑袋
哨兵的配置里时间参数不多,但每一个都影响切换行为,靠拍脑袋设值是最常见的坑。
down-after-milliseconds决定多久没响应算主观下线,默认是 30 秒。这个值的设定需要同时考虑三件事:业务能容忍的故障发现时间、系统正常的抖动幅度、以及正常主从同步的耗时。如果业务要求故障后十秒内恢复,那这个值就得压到 5 秒左右,此时必须配合排查系统层面的卡顿源,比如 AOF 重写、大 key 删除、持久化 fork 造成的瞬时阻塞,否则误判概率会明显上升。反过来,如果业务对几十秒的中断不敏感,保持默认或者设到 60 秒都行,误判概率更低。
failover-timeout更复杂一些,它不是一个简单的"切换总超时",而是覆盖了多个阶段:一是切换失败后重新尝试的间隔,二是从节点复制新主数据的最长等待时间,三是强制从节点跟随新主的时间,四是取消一个正在进行中的切换的时间。默认值是 180 秒。这个值设太小,可能在新主数据量较大时强制中断同步,导致从节点反复全量同步;设太大,切换失败后重试的间隔会很长,恢复变慢。我的经验是新主数据量在几 GB 以内时,180 秒够用;如果有十几 GB 的实例,考虑调到 300 秒以上。
parallel-syncs控制的是切换后同时有几个从节点向新主发起同步,默认是 1。如果设成 1,意味着切换后有多个从节点时,它们会排队同步,整个集群恢复到全量冗余状态的时间变长;如果设得太大,新主同时被多个从节点拉取 RDB,网络和磁盘压力陡增,切换期间新主本身也可能被拖垮。对于从节点数量大于两个的场景,我一般保持 1 或者 2,宁可恢复慢一点,也不要让刚上任的新主一上来就被压死。
3.3 端口、目录与文件权限规划
哨兵的默认端口是 26379,和 Redis 的 6379 错开,方便区分。如果一台机器上要跑多个哨兵实例(不推荐但有时为了省机器会这么做),端口要依次错开,同时每个实例的配置文件、日志文件、工作目录都要完全独立。
配置文件这块有一个非常容易忽略的点:哨兵会自动重写自己的配置文件。它在运行过程中会把发现的新从节点、新哨兵、当前的配置纪元、当前的主节点地址这些信息写回配置文件。所以配置文件所在目录必须有写权限,而且不要在运行期间用配置管理工具定时覆盖这个文件,否则哨兵的状态信息会被清空,严重的会导致它重新学习拓扑,甚至引发重复切换。如果要统一管理配置,只在首次下发时写入,之后以哨兵重写后的文件为准。
日志方面,建议单独落盘。哨兵日志的信息量不算大,但故障期间会集中输出判定和切换过程,对排障非常有帮助。日志级别默认是 notice,一般够用,排查疑难问题时可以临时调到 debug,但不要长期开着,日志量会涨得很快。
3.4 数据一致性兜底参数
哨兵能降低故障时长,但无法保证切换时零数据丢失。不过有几个配置项可以显著缩小丢失窗口,值得一并写进主节点配置。
min-replicas-to-write指定主节点至少要有多少个健康从节点才允许接受写请求,min-replicas-max-lag指定从节点的延迟上限(单位秒)。两个配合使用,典型配置是min-replicas-to-write 1和min-replicas-max-lag 10,含义是:如果没有至少一个延迟在 10 秒以内的从节点,主节点就拒绝写入,直接返回错误。这个机制能避免在主从链路已经断掉的情况下还继续接受写入,否则切换时这些只存在于旧主的数据必然丢失。
代价是可用性会下降。网络抖动导致从节点延迟超过阈值时,主节点会短暂拒写,业务侧会看到错误。这是一个明确的权衡:要一致就牺牲一点可用。我的建议是核心交易类业务开启,日志、缓存类的数据不必要开。
4. 从零部署一套一主两从三哨兵
4.1 基础环境与目录准备
下面的部署方案是我在测试环境反复验证过的一套最小可用架构,三台机器,每台跑一个 Redis 实例加一个哨兵实例。虽然前面我建议哨兵和主节点分开部署,但三台机器的资源有限,这种混部方式在小规模场景下是可以接受的,关键是要保证哨兵不都集中在同一台。
假设三台机器的内网地址分别是 10.0.0.11、10.0.0.12、10.0.0.13,规划如下:
| 机器 | Redis 角色 | Redis 端口 | 哨兵端口 | 数据目录 |
|---|---|---|---|---|
| 10.0.0.11 | 主节点 | 6379 | 26379 | /data/redis/6379 |
| 10.0.0.12 | 从节点 | 6379 | 26379 | /data/redis/6379 |
| 10.0.0.13 | 从节点 | 6379 | 26379 | /data/redis/6379 |
目录结构统一规划成下面这样,方便后面写启动脚本:
mkdir -p /data/redis/6379/{conf,logs,data} mkdir -p /data/redis/26379/{conf,logs,data}三个节点上都要执行。Redis 建议用同一个版本,主从之间版本差异过大时,某些新命令的传播可能出问题,尤其是跨大版本(比如 5 和 7)混用,尽量避免。
4.2 主从节点的配置文件
主节点 10.0.0.11 的配置文件/data/redis/6379/conf/redis.conf,只列出和主从、持久化、安全相关的关键项:
bind 0.0.0.0 protected-mode yes port 6379 daemonize yes pidfile /data/redis/6379/redis.pid logfile /data/redis/6379/logs/redis.log dir /data/redis/6379/data # 密码,哨兵配置里要用同一个 requirepass YourStrongPass_2024 # 从节点连接主节点时使用的密码 masterauth YourStrongPass_2024 # 持久化,主从复制依赖 RDB 做初次全量同步 save 900 1 save 300 10 appendonly yes appendfsync everysec # 一致性兜底 min-replicas-to-write 1 min-replicas-max-lag 10两个从节点的配置基本一致,只是多一行指向主节点,另外把replica-read-only保持默认的 yes:
replicaof 10.0.0.11 6379 replica-read-only yes replica-priority 100关于replica-priority,两台从节点如果都设成 100,切换时选谁就取决于偏移量了。如果你希望 10.0.0.12 优先成为新主,把它设成 90,10.0.0.13 设成 100。这个改动很小,但能让切换结果变得可预期,我在生产环境里都会这么配。
注意:
masterauth一定要配,而且要和主节点的requirepass一致。漏配的典型表现是从节点能连上主节点但认证失败,INFO replication里master_link_status一直是down,日志里能看到NOAUTH相关的报错。这个问题在小规模部署里出现频率相当高。
4.3 哨兵配置文件逐行拆解
三个哨兵的配置文件除了sentinel monitor里的本机地址不同,其余完全一致。以 10.0.0.11 上的/data/redis/26379/conf/sentinel.conf为例:
port 26379 daemonize yes pidfile /data/redis/26379/sentinel.pid logfile /data/redis/26379/logs/sentinel.log dir /data/redis/26379/data # 监控的主节点名、地址、端口、quorum sentinel monitor mymaster 10.0.0.11 6379 2 # 认证信息 sentinel auth-pass mymaster YourStrongPass_2024 # 判定主观下线的超时时间 sentinel down-after-milliseconds mymaster 10000 # 切换相关超时 sentinel failover-timeout mymaster 180000 # 切换后同时向新主同步的从节点数 sentinel parallel-syncs mymaster 1逐条说一下容易出问题的地方。
第一行sentinel monitor mymaster 10.0.0.11 6379 2里,mymaster是主节点的逻辑名称,客户端连接哨兵时用的就是这个名称,所以这个名字一旦定下来就不要改,改了等于客户端要跟着改。后面的2是quorum,在三个哨兵的场景下设 2 是合适的,设 1 会导致一个哨兵的误判就能推动进入客观下线,放大了误切换风险。
sentinel auth-pass这一行的前缀是sentinel而不是requirepass,这是哨兵特有的写法,写错了哨兵会连不上 Redis。如果 Redis 开启了 ACL(6.0 之后),还需要配sentinel auth-user mymaster 用户名,两个一起配才生效。
down-after-milliseconds我给的是 10000,也就是 10 秒。这个值是在"发现要快"和"别误判"之间折中的结果,生产环境建议根据自己的实际抖动情况调整,先用 30 秒默认值跑一段时间,观察日志里有没有出现频繁的+sdown和-sdown交替再决定往下压。
配置文件写好后,一定要检查目录权限。哨兵需要对sentinel.conf有写权限,因为它会自动重写。用chown把整个目录交给运行用户,或者至少保证配置文件可写。我遇到过因为配置文件被设成只读,哨兵启动后一直报错退出,日志里写得也不够明显,查了挺久。
4.4 启动顺序与状态验证
启动顺序建议是:先起主节点,再起从节点,最后起哨兵。哨兵要能发现主从关系,主从先就位能减少它学习拓扑的时间。
# 三个节点上分别执行 redis-server /data/redis/6379/conf/redis.conf # 确认主从关系 redis-cli -a YourStrongPass_2024 -h 10.0.0.11 info replication在主节点上执行上面这条命令,应该能看到role:master和connected_slaves:2,以及两个从节点的地址和state=online。如果connected_slaves是 0,检查从节点配置里的replicaof和密码。
主从确认无误后启动哨兵:
# 三个节点上分别执行 redis-sentinel /data/redis/26379/conf/sentinel.conf注意这里用redis-sentinel命令,或者用redis-server sentinel.conf --sentinel,两种都可以,前者更直观。
启动后连接任意一个哨兵,用下面几条命令验证:
redis-cli -p 26379 # 查看主节点状态 sentinel master mymaster # 查看发现的从节点 sentinel replicas mymaster # 查看其他哨兵是否被发现 sentinel sentinels mymaster # 查询当前主节点地址,客户端用的就是这个 sentinel get-master-addr-by-name mymastersentinel master mymaster的输出里有几个关键字段值得关注:num-slaves应该是 2,num-other-sentinels应该是 2,flags里应该是master,如果有s_down或o_down说明有问题。sentinel sentinels mymaster如果返回为空,说明哨兵之间的互相发现还没完成,等十几秒再试,如果还是空,检查三个哨兵配置里的sentinel monitor是否完全一致。
4.5 故障演练:手动 kill 主节点看切换过程
部署完不做演练等于没部署。这一步我会在测试环境反复做,目的不只是验证"能切",更重要的是记录切换耗时和客户端表现,作为生产环境的预期基线。
演练步骤如下。先在主节点上持续写入一个带时间戳的 key,用一个简单脚本跑着:
while true; do redis-cli -a YourStrongPass_2024 -h 10.0.0.11 set probe:$(date +%s) 1 > /dev/null sleep 0.5 done然后另开一个窗口,在 10.0.0.11 上直接kill -9掉 Redis 进程,同时观察哨兵日志:
tail -f /data/redis/26379/logs/sentinel.log正常情况下,日志会依次出现这些事件:+sdown master mymaster(某个哨兵判定主观下线)、+odown master mymaster(达到 quorum,进入客观下线)、+try-failover(开始尝试切换)、+elected-leader(选举胜出)、+selected-slave(选中新主)、+failover-state-send-slaveof-noone、+switch-master mymaster 10.0.0.11 6379 10.0.0.12 6379(切换完成)、+slave-reconf-sent和+slave-reconf-done(其余从节点转向新主)。
从+sdown到+switch-master的耗时,就是业务侧的不可写窗口,我在配置为 10 秒判定、三个哨兵的条件下实测这个值是 12 到 15 秒左右,主要时间花在等待down-after-milliseconds到期上。如果把判定时间压到 5 秒,总耗时能到 7 到 9 秒。
切换完成后,用sentinel get-master-addr-by-name mymaster确认地址已经变成 10.0.0.12。然后把 10.0.0.11 的 Redis 重新拉起来,观察它是否被哨兵自动配置成新主的从节点。这里有个细节:旧主恢复后,哨兵会向它发送SLAVEOF命令,但旧主的配置文件是会被改写的(replicaof那一行会被加上),所以下次启动时它不再以主的身份起来。这个行为是符合预期的,但如果你的运维脚本里写死了"重启后检查 role 是不是 master",会误报。
4.6 客户端接入:三种主流写法
哨兵部署完,客户端接入方式决定了故障转移对业务是否真正透明。这里给出三种最常见的接入方式。
Spring Boot 项目用 Lettuce 的话,配置里写上哨兵节点和主节点名就行,Lettuce 内部会处理地址变更,不需要额外代码:
spring: redis: password: YourStrongPass_2024 sentinel: master: mymaster nodes: - 10.0.0.11:26379 - 10.0.0.12:26379 - 10.0.0.13:26379 lettuce: pool: max-active: 32 max-idle: 16 min-idle: 4Go 项目用 go-redis 的NewFailoverClient,这个客户端会周期性向哨兵查询主节点地址,切换后自动重连:
rdb := redis.NewFailoverClient(&redis.FailoverOptions{ MasterName: "mymaster", SentinelAddrs: []string{"10.0.0.11:26379", "10.0.0.12:26379", "10.0.0.13:26379"}, Password: "YourStrongPass_2024", SentinelPassword: "YourStrongPass_2024", DB: 0, PoolSize: 50, })Python 用 redis 库的 Sentinel 封装:
from redis.sentinel import Sentinel sentinel = Sentinel( [('10.0.0.11', 26379), ('10.0.0.12', 26379), ('10.0.0.13', 26379)], socket_timeout=0.5, password='YourStrongPass_2024', sentinel_kwargs={'password': 'YourStrongPass_2024'}, ) master = sentinel.master_for('mymaster', socket_timeout=1.0) slave = sentinel.slave_for('mymaster', socket_timeout=1.0)三种写法有一个共同点:它们都要求传入全部哨兵节点地址,而不是只传一个。原因很简单,如果你只传一个,那个哨兵挂了,客户端就再也问不到新地址了。客户端库通常会在内部做轮询或者随机选择。
提示:
sentinel_kwargs或者SentinelPassword这类参数,指的是"连接哨兵本身"的密码,如果你的哨兵没有配置requirepass,这一项可以不填。但强烈建议给哨兵也配上密码,否则任何能访问到 26379 端口的人都能执行sentinel failover手动触发切换。
5. 排障与踩坑实录
5.1 常见故障速查表
下面这张表是我这几年处理哨兵相关问题积累下来的,出问题时按症状对一遍,能省掉不少时间。
| 现象 | 大概率原因 | 排查动作 |
|---|---|---|
sentinel sentinels返回空 | 三个哨兵配置的monitor名称或地址不一致 | 逐台对比配置文件,重点看主节点名和地址 |
master_link_status一直为down | 从节点masterauth密码错误或未配置 | 从节点日志搜NOAUTH,检查masterauth |
频繁出现+sdown紧接-sdown | 判定时间过短,或主节点有长耗时操作 | 调大down-after-milliseconds,排查大 key 和 AOF 重写 |
进入+odown但始终不切换 | 存活哨兵数不足多数派 | 检查是否有哨兵宕机,检查网络分区 |
| 切换后客户端仍连旧主 | 客户端只订阅未重连,或用了单哨兵地址 | 升级客户端库,传入全部哨兵地址 |
| 哨兵启动即退出 | 配置文件不可写,或端口被占用 | 检查文件权限,检查 26379 端口占用 |
| 切换后旧主数据丢失 | 旧主恢复后被全量同步覆盖 | 开启min-replicas-to-write,排查异步丢失窗口 |
| 哨兵反复重写配置导致状态丢失 | 配置管理工具定时覆盖了sentinel.conf | 停止覆盖,只在首次下发时写入 |
5.2 几个让我熬夜的坑
有几个问题在文档里写得不多,但实际遇到过,这里展开说下。
坑一是哨兵被主节点的阻塞拖死。某次线上在凌晨执行了一次大批量删除,主节点因为要释放大量内存,出现了十几秒的卡顿,恰好哨兵和主节点混部在同一台机器上,哨兵的心跳探测也被拖慢,三个哨兵里有两个同时判定主观下线并进入客观下线,触发了切换。切换本身是成功的,但新主刚上任就迎来一波流量,加上原本的写入压力,很快也进入了高负载状态。事后我们把哨兵的down-after-milliseconds从 5000 调到 15000,并把哨兵从主节点机器上迁走,问题没再出现。这个教训是:判定时间要留出足够余量,覆盖掉系统层面可能的异常卡顿。
坑二是网络分区导致的"双主"误以为丢数据。有一次机房间网络出现短暂抖动,两个哨兵和主节点在一个机房,另一个哨兵在另一个机房。主节点被隔离的哨兵判定下线,但在主节点这边的哨兵因为还能看到它,没有进入客观下线,于是没有切换。看起来是对的,但麻烦的是隔离侧的那个哨兵一直在做判定,等网络恢复后,它的纪元已经涨上去了,反而引发了一次不必要的切换。后来我们调整了哨兵的机房分布,保证任意一个机房内都不会出现"多数派"的错觉,同时在网络恢复后重点观察一下sentinel master里的config-epoch是否异常跳变。
坑三是哨兵密码配置的坑。给哨兵配requirepass之后,客户端连接哨兵要先认证,这一步在 Spring Boot 的早期版本里没有直接的支持项,需要自定义RedisConnectionFactory或者在 URL 里带上密码。我当时的做法是升级到支持spring.redis.sentinel.password的版本,比写自定义 Bean 省事。如果你的版本比较老,要么升级,要么老老实实写配置类。
坑四是切换后的一段时间里读从节点还在读旧数据。切换完成后,从节点转向新主需要时间,如果在这段时间里有客户端在从库上读,会读到还没同步过来的旧数据。对于一致性要求高的读请求,建议在切换后的一小段时间内直接读主,或者业务侧接受这个短暂的不一致。这个问题不是哨兵的缺陷,而是主从异步复制的固有特性,只是切换会把窗口放大。
5.3 该盯哪些指标
哨兵本身没有复杂的监控体系,但有几个点必须盯住,否则出了问题只能靠猜。
第一是哨兵数量与存活状态。至少要保证多数派存活,如果只有两个哨兵活着,一旦其中一个也出问题,就再也无法完成切换。建议对每个哨兵的进程状态做存活检查,同时对sentinel master mymaster里的num-other-sentinels做监控,这个值掉下去说明有哨兵失联了。
第二是**config-epoch的变化**。这个值每发生一次切换就会增加,如果短时间内频繁增长,说明在反复切换,一定是某个环节有问题,比如判定时间太短、主节点不稳定、或者网络抖动严重。
第三是**master_link_down_since_seconds**。这个指标在主从链路上,表示从节点与主节点断开已持续多久。如果持续增长,说明复制链路有问题,即使哨兵还在工作,从节点的数据也在落后。
第四是切换事件的告警。把+switch-master、+odown、+sdown这几个事件接入告警通道,宁可多收几条告警,也不要等到业务报错才发现切换过了。特别是那些"切换后业务没报错"的情况,如果没人知道曾经切过一次,潜在的数据不一致可能会在很久以后才暴露。
我个人在实际操作中的体会是,哨兵这套方案的复杂度不在部署上,部署三台机器半小时就能搞定,真正难的是把它调到一个"该切的时候能切、不该切的时候绝对不动"的状态。这需要在测试环境反复演练,把判定时间、超时参数、优先级这些值根据自己系统的实际抖动情况调到位,然后把每次演练的耗时和日志记下来作为基线。上线后前两周重点关注切换事件和主从延迟,等这套参数稳定跑过一段时间,再去考虑要不要上 Cluster 解决容量问题——哨兵和 Cluster 解决的是两个不同维度的问题,别把它们混在一起想。
最后分享一个小技巧:在哨兵的配置文件里,sentinel monitor那一行下面加一行注释,写好这套集群的负责人、部署日期和当前的主备顺序,一年后接手的人会感谢你。这个习惯我是从一个老运维那里学来的,简单但很有用。