1. 从一次主从切换的深夜告警说起
凌晨两点被电话叫醒的滋味,做过线上运维的人都懂。那次是某业务系统的主库突然不可写,从库数据延迟一路飙到几十秒,业务侧开始大量报错。当时我们用的是最朴素的主从复制架构,主库挂了之后,整个写入链路直接瘫痪,只能人工登录、手动提升从库、改配置、重启应用。前后折腾了将近四十分钟,损失肉眼可见。
那次事故之后,我把高可用方案重新梳理了一遍,最终落到了 Redis 的哨兵机制上。这篇文章就围绕Redis 哨兵机制展开,把它的设计思路、核心原理、部署实操、参数调优和踩坑经验完整讲一遍。如果你正在用 Redis 做主从、又不想每次故障都靠人肉救火,或者你已经在用哨兵但总觉得切换不够稳、不够快,那这篇内容应该能帮到你。我会尽量把每个"为什么这么设计"讲清楚,而不是只丢一堆配置让你抄。
哨兵机制本质上解决的是一个很具体的问题:当主节点挂掉时,如何在没有人工介入的情况下,自动选出一个新的主节点,并让所有相关方都知道这件事。听起来简单,但要做到"不误判、不脑裂、切换快、通知准",里面的门道相当多。下面我按自己的理解顺序,一层层拆开。
2. 哨兵机制到底在解决什么问题
2.1 主从复制留下的那个"缺口"
先说清楚主从复制的定位。Redis 的主从复制解决的是读扩展和数据冗余:主节点负责写,从节点通过复制拉取数据,可以分担读请求,也能在主节点故障时保留一份数据。但它有一个天然的缺口——主节点是单点。
主节点一旦宕机,写请求无处可去,从节点虽然数据还在,但没人告诉它"你现在可以当主了",也没人告诉客户端"以后往新主写"。这个"决策"和"通知"的动作,主从复制本身是不做的。你可以把它理解成一个团队:主从复制让团队有了多个成员和备份,但队长倒下后,谁来当新队长、怎么通知所有人,这套机制是缺失的。
哨兵机制补的就是这块。它是一套独立的进程,不存储业务数据,专门干三件事:监控、判断、决策与通知。
2.2 哨兵要同时搞定三件事
第一件是监控。哨兵会周期性地向所有 Redis 节点(主、从、其他哨兵)发送心跳探测,确认对方是否还活着。这个探测不是随便发发,它决定了后续所有判断的基础。
第二件是故障判定。一个节点没响应,可能是它真挂了,也可能只是网络抖动、或者哨兵自己这边网络有问题。哨兵必须区分这两种情况,否则会频繁误切换,那比不切换还糟糕。
第三件是决策与通知。确认主节点真的挂了之后,哨兵要在多个从节点里选一个最合适的提升为主,然后通知其余从节点去复制新主,同时通知客户端新的主节点地址。
这三件事串起来,才构成一个完整的高可用闭环。很多人只关注"切换"这个动作,其实判定准不准、通知到不到位,才是决定这套机制好不好用的关键。
2.3 为什么不用别的方式
有人会问,为什么不用 Redis 集群(Cluster)?集群确实自带分片和高可用,但它的运维复杂度、对多 key 操作的限制、以及客户端改造成本都更高。对于数据量不大、但要求高可用的场景,主从加哨兵是更轻量的选择。
也有人用 Keepalived 之类的方案做 VIP 漂移。这种方案的问题在于,它判断的是"机器/端口是否存活",而不是"Redis 实例是否真的能提供服务"。一个 Redis 进程还在、但已经进入阻塞状态的情况,VIP 方案是识别不出来的。哨兵则是从 Redis 协议层面去探测,判断更贴近真实可用性。
所以选型的逻辑很清楚:要的是应用层的高可用,就用应用层能感知的方案。哨兵正好卡在这个位置上。
3. 哨兵的核心原理拆解
3.1 主观下线与客观下线:两道判断关卡
哨兵判断一个节点"挂了",分两步走,这是整个机制里最精妙的设计之一。
主观下线(SDOWN,Subjectively Down):单个哨兵自己觉得某个节点不行了。判断依据是,在down-after-milliseconds这个时间窗口内,一直没收到对方对心跳探测的有效回复。注意,这里只是"我自己觉得",一个哨兵的看法不代表全局。
客观下线(ODOWN,Objectively Down):当足够多的哨兵都认为主节点主观下线了,才升级为客观下线。这个"足够多"由quorum参数决定。比如你部署了 3 个哨兵,quorum 设为 2,那么至少 2 个哨兵都报告主节点失联,才会判定客观下线,进而触发切换。
为什么要分两步?因为单个哨兵的判断可能是它自己的网络出了问题。比如哨兵 A 和主节点之间的网络断了,但主节点其实活得好好的,其他哨兵都能正常通信。如果只凭 A 的判断就切换,那就是误切换。引入"多数派确认",就是为了过滤掉这种局部网络故障导致的误判。
注意:主观下线是针对所有节点(主、从、其他哨兵)都会做的判断,而客观下线只针对主节点。因为只有主节点才需要触发切换,从节点挂了不影响大局。
3.2 领导者选举:谁来执行切换
判定主节点客观下线之后,不能所有哨兵一拥而上都去执行切换,那样会乱套。哨兵之间要先选出一个领导者(Leader),由它单独负责这次故障转移。
选举用的是类似 Raft 的投票机制。大致流程是:发现客观下线的哨兵向其他哨兵拉票,每个哨兵在一个选举轮次里只能投一票,得票超过半数(且达到 quorum)的成为领导者。如果一轮没选出来,会等一个随机时间后重新发起,避免平票僵持。
这里有个关键点:选举需要多数派参与。如果你只部署了 2 个哨兵,一旦挂掉 1 个,剩下 1 个永远凑不齐多数派,就永远选不出领导者,也就无法切换。这就是为什么哨兵数量推荐奇数个、且至少 3 个。3 个哨兵挂 1 个还剩 2 个,能凑多数派;挂 2 个才失去切换能力。2 个哨兵的容错能力反而比 3 个差,这是个反直觉但很重要的结论。
3.3 新主节点的挑选规则
领导者选出来之后,它要从所有从节点里挑一个最合适的当新主。挑选不是随机的,而是按优先级逐层过滤:
- 排除已经下线的从节点,这个不用多说。
- 看优先级
replica-priority,值越小优先级越高。如果某个从节点配置了更小的优先级,它优先当选。默认都是 100。 - 优先级相同则看复制偏移量,偏移量越大说明数据越新,越新越优先。这保证了新主的数据尽可能接近旧主。
- 偏移量也相同则看 runid,选 runid 字典序较小的那个,纯粹是为了有个确定的规则,避免随机。
这个顺序背后的逻辑是:先看人为意愿(优先级),再看数据新鲜度(偏移量),最后用确定性规则兜底。实际运维中,如果某台从节点配置更高、机房位置更好,你可以把它的优先级调低,让它更容易当选。
3.4 故障转移的完整动作
领导者选出新主之后,会执行一系列动作:
- 向新主发送
REPLICAOF NO ONE,让它停止复制、成为主节点。 - 向其余从节点发送
REPLICAOF <新主IP> <新主端口>,让它们改去复制新主。 - 把旧主节点标记为从节点,等它恢复后自动去复制新主(这一步很关键,否则旧主恢复后会以为自己还是主,造成脑裂)。
- 通过发布订阅机制,把新主的地址通知给客户端。
这一套动作下来,整个集群就完成了主从角色的重新分配。客户端如果订阅了哨兵的切换事件,就能第一时间拿到新地址,实现无感切换。
4. 部署实操:从零搭一套三节点哨兵
4.1 环境规划与节点分配
假设我们有一主两从的 Redis 架构,加上三个哨兵,一共六个进程。为了演示清晰,我用端口区分,实际生产建议分布在不同物理机或虚拟机上。
| 角色 | 端口 | 说明 |
|---|---|---|
| Redis 主节点 | 6379 | 负责写 |
| Redis 从节点 1 | 6380 | 复制主节点 |
| Redis 从节点 2 | 6381 | 复制主节点 |
| 哨兵 1 | 26379 | 监控集群 |
| 哨兵 2 | 26380 | 监控集群 |
| 哨兵 3 | 26381 | 监控集群 |
提示:生产环境务必把哨兵和 Redis 实例分散到不同机器。如果三个哨兵都在同一台机器上,机器一挂全挂,高可用就成了摆设。
4.2 主从节点的基础配置
先配置主节点redis-6379.conf,核心几项:
port 6379 daemonize yes pidfile /var/run/redis-6379.pid logfile /var/log/redis-6379.log dir /data/redis/6379 # 设置访问密码,哨兵和从节点都要用 requirepass your_password # 主节点认证密码,用于主从复制 masterauth your_password从节点redis-6380.conf在此基础上加一行,指向主节点:
port 6380 daemonize yes pidfile /var/run/redis-6380.pid logfile /var/log/redis-6380.log dir /data/redis/6380 requirepass your_password masterauth your_password # 关键:指定主节点 replicaof 127.0.0.1 6379redis-6381.conf同理,只改端口和目录。启动三个实例后,用redis-cli -p 6379 info replication确认从节点已经连上,role:slave、master_link_status:up就说明复制正常。
4.3 哨兵配置文件详解
哨兵的核心配置在sentinel.conf里。三个哨兵的配置基本一致,只有端口不同。以sentinel-26379.conf为例:
port 26379 daemonize yes pidfile /var/run/sentinel-26379.pid logfile /var/log/sentinel-26379.log dir /data/sentinel/26379 # 监控名为 mymaster 的主节点,quorum 为 2 sentinel monitor mymaster 127.0.0.1 6379 2 # 主节点和哨兵都设了密码,这里要配认证 sentinel auth-pass mymaster your_password # 主观下线判定时间,30 秒 sentinel down-after-milliseconds mymaster 30000 # 故障转移时,同时向新主发起复制的从节点数量 sentinel parallel-syncs mymaster 1 # 故障转移超时时间,180 秒 sentinel failover-timeout mymaster 180000这里几个参数值得展开说:
sentinel monitor mymaster 127.0.0.1 6379 2里的mymaster是主节点别名,客户端连接哨兵时要用这个名字。最后的2就是 quorum,表示至少 2 个哨兵认为主节点下线才触发客观下线。
sentinel auth-pass是很多人会漏的一项。如果 Redis 设了密码而哨兵没配,哨兵连不上节点,会一直报认证失败,监控直接失效。
sentinel parallel-syncs控制切换后同时有几个从节点去同步新主。设成 1 表示一个一个来,避免新主瞬间被多个从节点的全量同步压垮。如果从节点多、且能承受压力,可以适当调大加快同步速度。
4.4 启动与验证
三个哨兵依次启动:
redis-sentinel /etc/redis/sentinel-26379.conf redis-sentinel /etc/redis/sentinel-26380.conf redis-sentinel /etc/redis/sentinel-26381.conf连上任意一个哨兵,执行sentinel master mymaster,能看到主节点的详细信息,包括num-slaves、num-other-sentinels等。再执行sentinel sentinels mymaster,应该能看到另外两个哨兵已经被互相发现。
哨兵之间会自动发现彼此,靠的是发布订阅频道__sentinel__:hello。每个哨兵启动后会往这个频道发消息,其他哨兵收到后就知道了对方的存在。所以你不需要手动配置哨兵之间的地址,这一点设计得很省心。
5. 故障切换实测与关键参数调优
5.1 手动模拟一次主节点宕机
配置搭好之后,最该做的事就是主动演练一次切换,别等真出事才第一次见它工作。我当时的做法是直接kill掉主节点进程,然后盯着哨兵日志看。
# 找到主节点进程并杀掉 kill -9 $(cat /var/run/redis-6379.pid)杀掉之后,哨兵日志里会依次出现这些关键信息:先是+sdown(主观下线),等凑够 quorum 后出现+odown(客观下线),接着是+vote-for-leader(拉票)、+elected-leader(当选领导者),最后是+switch-master(切换完成),日志里会明确写出新主的 IP 和端口。
整个过程我实测下来,从杀掉主节点到切换完成,大约在 30 到 40 秒之间。这个时间主要花在down-after-milliseconds的等待上。如果你把 30 秒调成 10 秒,切换会更快,但误判风险也会上升。
5.2 down-after-milliseconds 怎么定
这个参数是误判和切换速度之间的平衡点。设得太短,一次网络抖动、一次 GC 停顿就可能触发切换;设得太长,真故障时业务要等很久才能恢复。
我的经验是分场景:
- 内网环境、网络稳定,可以设 10 到 15 秒。
- 跨机房、网络质量一般,建议 30 秒甚至更长。
- 如果业务对写入中断极度敏感,可以配合客户端重试机制,把等待时间压到 10 秒左右,同时接受偶尔的误切换。
判断依据是:你观察到的正常网络抖动最长会持续多久。如果平时偶尔会有几秒的延迟尖峰,那down-after-milliseconds至少要大于这个尖峰,否则就是给自己找麻烦。
5.3 quorum 与多数派的关系
很多人搞不清 quorum 和"多数派"的区别。quorum 是判定客观下线的门槛,多数派是选举领导者的门槛,两者不是一回事。
举个例子,5 个哨兵,quorum 设为 2。那么 2 个哨兵认为主挂了,就能判定客观下线。但选举领导者需要超过半数,也就是至少 3 票。如果只有 2 个哨兵活着,虽然能判定下线,却选不出领导者,切换照样做不了。
所以 quorum 可以小于多数派,但实际能完成切换的前提是活着的哨兵数量超过半数。生产上我一般把 quorum 设为哨兵总数 / 2 + 1,让它和多数派一致,逻辑最清晰。
5.4 切换后旧主恢复会发生什么
这是很多人担心的场景:旧主只是短暂失联,切换完成后它又活过来了,会不会造成两个主节点同时写?
哨兵对此有处理。旧主恢复后,哨兵会向它发送REPLICAOF命令,把它降级为从节点,去复制新主。在它被降级之前,哨兵会确保它不会接受写请求。不过这里有个时间窗口,如果旧主在恢复的瞬间、还没被降级时就有客户端连上去写,理论上存在短暂的双写风险。
规避办法是:客户端不要硬编码主节点地址,而是通过哨兵获取当前主节点。这样切换后客户端拿到的一定是新主,不会往旧主写。这也是为什么我一直强调客户端要接入哨兵,而不是直连 Redis。
6. 常见问题与排查技巧实录
6.1 哨兵一直报无法连接主节点
最常见的原因是密码没配对。Redis 设了requirepass,但哨兵配置里漏了sentinel auth-pass,哨兵就会一直认证失败。排查时先看哨兵日志有没有NOAUTH相关的报错,有的话补上认证配置即可。
另一个原因是防火墙或安全组没放行哨兵到 Redis 的端口。哨兵需要能主动连接 Redis 节点,如果网络不通,监控直接失效。用telnet或nc从哨兵所在机器测一下端口连通性,很快能定位。
6.2 切换迟迟不触发
如果主节点明明挂了,哨兵却迟迟不切换,按这个顺序查:
- 确认活着的哨兵数量是否超过半数。少于半数永远选不出领导者。
- 确认
down-after-milliseconds是否设得过大,导致主观下线都还没判定。 - 确认 quorum 是否设得过高,导致客观下线凑不齐票。
- 看日志里有没有
failover-timeout相关的超时记录,可能是切换过程中卡住了。
我遇到过一次,是因为三个哨兵里有两个的配置文件写错了主节点端口,导致它们监控的根本不是同一个主节点,自然凑不齐客观下线。这种低级错误排查起来反而费时间,所以配置写完一定要逐个核对。
6.3 切换后客户端连不上新主
这种情况通常是客户端没有正确订阅哨兵事件。如果客户端缓存了旧主地址,切换后它还在往旧主连,而旧主已经被降级为从节点,写请求会被拒绝。
解决办法是客户端使用支持哨兵的连接库,比如 Java 的 Jedis、Lettuce 都支持传入哨兵地址列表和主节点别名,由库内部负责监听切换事件、自动更新连接。不要自己手写地址解析逻辑,坑太多。
6.4 常见问题速查表
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 哨兵报 NOAUTH | 密码未配置 | 检查 sentinel auth-pass |
| 无法判定客观下线 | quorum 过高或哨兵数不足 | 核对 quorum 与存活哨兵数 |
| 切换后双写 | 客户端直连旧主 | 改为通过哨兵获取主节点 |
| 切换时间过长 | down-after-milliseconds 过大 | 结合网络质量适当调小 |
| 哨兵互相发现不了 | 发布订阅频道被阻断 | 检查哨兵间网络与端口 |
| 从节点同步失败 | 新主密码或网络问题 | 检查 masterauth 与连通性 |
6.5 几条踩坑心得
第一,哨兵数量一定要是奇数且至少 3 个。我见过有人图省事只部署 2 个,结果挂 1 个就彻底失去切换能力,等于白搭。
第二,所有节点的时间要同步。哨兵日志的时间戳、故障判定的时间窗口都依赖系统时间,时间不同步会导致判断混乱。用 NTP 把各机器时间对齐,这是基础中的基础。
第三,切换演练要定期做。配置好不代表能用,网络环境、版本升级、配置变更都可能影响切换。我一般每个季度会主动演练一次,确认切换链路始终是通的。
第四,日志级别别设太高。哨兵的logfile要保留足够详细的信息,出问题时日志是唯一的线索。生产上建议至少保留 info 级别,并做好日志轮转,别让日志把磁盘写满。
7. 我对哨兵机制的一点个人体会
用了几年哨兵下来,最大的感受是:它不是一个"配好就不用管"的组件,而是一个需要你理解它判断逻辑、并据此调参的系统。默认配置能跑,但未必适合你的网络环境和业务节奏。down-after-milliseconds、quorum、parallel-syncs这几个参数,每一个都对应着一种权衡,调之前先想清楚你在拿什么换什么。
另外,哨兵解决的是"主节点故障自动切换",它不解决数据一致性问题。主从复制本身是异步的,切换时如果旧主还有没同步到从节点的数据,那部分数据就丢了。对数据一致性要求极高的场景,光靠哨兵不够,还得在应用层做补偿或者用更强的同步机制。这一点在选型时就要想明白,别等丢了数据才后悔。
最后分享一个小技巧:把哨兵的切换事件通过发布订阅接出来,推到你的告警系统里。这样每次切换你都能第一时间知道,而不是等业务报错才发现。切换本身不可怕,可怕的是切换了你却不知道。