Redis哨兵机制详解:从主从复制到自动故障转移
凌晨两点半手机突然开始连续震动。打开运维群一看Redis 主节点挂了。如果没有哨兵接下来的流程你比谁都清楚——先登录一台从节点执行SLAVEOF NO ONE把它提升成主节点再手动把其他从节点重新挂到新主上面最后还得到应用服务器上去改连接配置。运气好十分钟搞定运气不好大家对着日志猜来猜去一小时就过去了。这个系列前面几篇聊了基础数据类型、持久化、主从复制今天这篇就补上高可用的最后一块拼图Redis 哨兵Sentinel机制看看它如何把“半夜爬起来切换主节点”变成“系统自己完成故障转移”。1. 先理清楚主从复制模式下“高可用”到底缺了什么1.1 一主多从的高可用只是“高读可用”Redis 主从复制能做的事情很多人第一反应是“数据冗余”和“读写分离”读流量可以分散到从节点主节点挂了还有一份数据备份在从节点上看起来确实比单机稳多了。但仔细想一想从节点在复制架构里有一个天然身份——它默认不接收写命令所有写请求仍然集中在主节点。也就是说主从复制解决的是“读的高可用”不是“写的高可用”。只要主节点还活着架构是通的一旦主节点宕机从节点只能继续提供读服务写请求会直接失败。如果业务只是缓存场景、读多写少系统还能硬扛一阵但如果是写入量很大的在线业务主节点宕机几分钟意味着大量超时和报错更可怕的是缓存一断流量直接穿透到数据库数据库也可能跟着被压垮。很多人最初学习主从复制时没有意识到这一点复制本身不提供故障转移它只负责把数据同步到从节点至于“主节点挂了谁来接替”这个问题完全不在它的能力范围内。1.2 没有哨兵时的典型事故现场没有哨兵时恢复流程完全靠人工我简单列一下你会经历的操作找到一台数据最新的从节点执行SLAVEOF NO ONE让它变成新的主节点。在其余从节点上执行SLAVEOF 新主IP 新主端口把它们的复制关系重新指向新主。修改所有客户端的连接配置把写请求切换到新主节点。修复或重启原来的主节点确认它恢复后重新挂到新主下作为从节点。这几步看起来不复杂但每一步都依赖人就意味着要有值班、要有人清楚当前拓扑、要有人敢在半夜执行命令。更麻烦的是“怎么判断原主真的起不来了”——网络抖动、进程假死、服务器负载极高导致的心跳超时都有可能造成误判。人工误判之后再手动切换很容易出现多人操作互相覆盖、主从关系混乱的场面。所以哨兵机制的意义不只是“自动化”它首先是一套有判断逻辑、能区分“真挂”和“误报”的决策系统。1.3 哨兵的四个职责监控、通知、自动故障转移、配置提供者哨兵Sentinel是 Redis 官方提供的高可用解决方案它是一个独立运行的进程可以部署多个实例组成哨兵集群。官方文档把哨兵要做的事情归纳为四件监控持续检查主节点、从节点以及其他哨兵实例是否在线。通知被监控的实例发生故障时哨兵可以通过日志、事件等渠道通知管理员或上层应用。自动故障转移当主节点不可用时哨兵会从从节点中选举一个成为新主并把其他从节点重新挂到新主下。配置提供者客户端连接哨兵查询“当前主节点是谁”哨兵相当于一个实时的地址簿。这四个职责里配置提供者这个角色特别容易被忽略。哨兵不是代理数据读写请求不经过它它只负责告诉客户端“现在应该连哪个节点”。理解这一点后面很多设计逻辑就顺理成章了。2. 哨兵核心链路拆解从 sdown 到 odown 再到 failover 到底发生了什么2.1 心跳监控与“主观下线”单个哨兵说了不算哨兵进程启动后会以每秒一次的频率向所有主节点、从节点和其他哨兵发送 PING 命令。如果被监控实例在配置的down-after-milliseconds时间段内没有给出有效回复哨兵就会把这个实例标记为“主观下线”Subjectively Down简称 sdown。请注意“主观”这个词——这只是当前这个哨兵单方面认为对方下线了可能是因为对方真的宕机也可能只是当前哨兵到那个节点的网络链路出了状况。对从节点和哨兵实例来说被标记 sdown 基本可以断定它大概率不可用但对主节点而言不能仅凭一个哨兵的主观判断就执行故障转移否则一个哨兵自己的网络抖动就可能引发一次完全没有必要的切换后果比节点故障本身还严重。2.2 “客观下线”quorum 个哨兵确认后才算数主节点被标记 sdown 后发现问题的哨兵会向其他哨兵发送SENTINEL is-master-down-by-addr命令询问它们眼中的主节点状态。当足够数量的哨兵都确认主节点已经主观下线时主节点才会被标记为“客观下线”Objectively Down简称 odown。这个“足够数量”就是sentinel monitor配置里的quorum参数。这里有非常重要的一点客观下线只是敲定了“主节点确实可能挂了”这个共识真正执行故障转移还需要另一轮多人投票。我把 sdown 和 odown 的区别整理成一张表方便对照对比维度sdown主观下线odown客观下线判定主体单个哨兵多个哨兵共同确认触发条件在down-after-milliseconds内 PING 无响应收到至少 quorum 个哨兵确认主节点为 sdown适用范围主、从、哨兵实例都会判断只针对主节点后续动作继续观察不立即切换触发领导者选举和故障转移流程2.3 领导者选举为什么故障转移只能由唯一 leader 执行主节点被判定 odown 后最先发现问题的哨兵会尝试竞选“领头哨兵”leader向其他哨兵请求投票。在同一个配置纪元epoch内每个哨兵只能投一票获得超过一半哨兵数量majority票数的哨兵成为 leader。只有 leader 才有资格执行故障转移。这个机制的思想和 Raft 协议非常接近核心目的只有一个保证任何时刻最多只有一个哨兵在操作拓扑。否则两个哨兵同时发现故障、同时去做切换可能出现两个从节点都被提升为主节点的混乱局面。选举过程是自动完成的不需要人工介入整个时间通常非常短。2.4 故障转移执行过程选新主、切换复制关系、配置重写leader 产生后真正的工作才开始。完整的故障转移流程大致如下从当前在线的从节点中筛选候选者过滤掉断线超过down-after-milliseconds的、与主节点失联过久的、复制偏移量落后太多的从节点。在候选从节点中按优先级排序slave-priority数值越小越优先如果优先级相同复制偏移量越大越优先仍然无法区分时runid 最小的胜出。leader 向选中的从节点发送SLAVEOF NO ONE让它脱离复制关系成为新的主节点。leader 向其他从节点发送SLAVEOF 新主IP 新主端口让它们重新对齐到新主。leader 持续监视原来的主节点如果它后续恢复上线强制将它降级为新主的从节点。整个流程中从第 3 步到第 4 步之间通常会有几十毫秒到几秒的间隔具体受网络状况和failover-timeout参数影响。与此同时哨兵还会自动更新自己的配置文件把最新的主节点地址写入磁盘这样哨兵进程重启后也能恢复到当前正确的拓扑状态。3. 哨兵设计里几个反直觉的点3实例、quorum与majority、客户端感知3.1 为什么至少 3 个哨兵还必须是奇数单台哨兵当然也能工作主节点挂了它直接判定下线然后切换。但哨兵自己挂了怎么办如果业务接受“哨兵挂了就恢复人工值守”那单哨兵方案也能凑合。可生产环境下哨兵本身也需要高可用一般会部署至少 3 个实例。为什么是 3 而不是 2因为两个哨兵实例时如果其中一个网络分区它拿不到另一个的同意就只能停在那里不敢切换而且两台哨兵中有一台宕机后剩下一台要获得“超过一半”的选票才能成为 leader2 的一半以上是 2 票它只有自己这 1 票永远选不出 leader故障转移能力直接瘫痪。3 台实例时挂掉 1 台还剩 2 台多数派恰好是 2 票可以正常完成选举和切换。至于“为什么是奇数”核心原因是奇数集群在容错能力上更划算3 台能容忍 1 台故障5 台能容忍 2 台故障7 台能容忍 3 台故障规则是“最多容忍 N/2 台下线”。偶数集群不仅存在选举死锁风险扩容时也要多增加一台才能提升容错等级所以生产实践里几乎都采用奇数部署。3.2 quorum 和 majority判定门槛与执行授权是两回事这是面试里和实战里都很容易混淆的一对概念。quorum是sentinel monitor配置中写死的数字代表判定主节点客观下线需要的同意票数majority是动态计算的代表总哨兵数量的一半以上。举个例子5 个哨兵quorum设为 2。那么只要有 2 个哨兵确认主节点 sdown就能标记为 odown。但是真正执行故障转移的 leader 必须获得 majority 票也就是 3 票以上。这意味着可能出现一种情况3 个哨兵认为主节点正常2 个哨兵认为主节点挂了odown 虽然成立但发出切换请求的哨兵最多只能拿到 2 票无法成为 leader切换不会执行。这个设计避免了“少数哨兵误判就盲目切换”的风险。生产环境一般怎么配我通常的做法是3 个哨兵配quorum25 个哨兵配quorum3也就是让 quorum 至少等于 majority 值。如果 quorum 设置得比 majority 还大客观下线门槛变高切换会更保守如果设置得太小误判时容易引发不必要切换。3.3 哨兵不代理数据流量客户端怎么找到新主很多人第一次接触哨兵时都会有一个疑问主节点切换后客户端连接的 IP 变了数据请求到底怎么知道要连哪个节点答案不是让哨兵帮你转发哨兵不代理任何数据流量它是“配置提供者”。主流 Redis 客户端比如 Jedis、Lettuce、StackExchange.Redis都支持 Sentinel 模式。以 Java 应用为例客户端启动时会连接哨兵节点通过SENTINEL get-master-addr-by-name master-name拿到当前主节点的地址当主节点发生切换后客户端会从哨兵那里得到新的主节点地址并重新建立连接。如果用 Spring Boot配置大概长这样spring.redis.sentinel.mastermymaster spring.redis.sentinel.nodes127.0.0.1:26379,127.0.0.1:26380,127.0.0.1:26381要特别注意一点哨兵保证的是拓扑最终会恢复而不是请求一个都不失败。在切换发生的几秒窗口期内应用侧依然可能遇到连接超时或写入失败客户端超时设置和重试策略是高可用方案的另一半不能完全甩给哨兵。3.4 脑裂是怎么被 majority 机制压住的脑裂split brain是分布式系统里最让人头疼的问题。在哨兵机制里如果只有 2 个哨兵且 quorum1完全可能出现这样的场景一个哨兵和主节点网络被隔离它单方面判定主节点挂了并执行切换而此时主节点其实还活着还在接收写请求另一个哨兵和主节点通信正常认为一切正常。于是一段时间内出现了“两个主节点”都在接收写入数据开始分叉。哨兵通过 majority 授权来规避这种情况要达到执行故障转移的条件必须有超过半数的哨兵一致同意“主节点确实不可用”。也就是说“主节点不可用”是一个需要过半节点共同确认的结论而不是单个节点的猜测。真正的主节点宕机通常是多数派的共同观察结果脑裂概率因此大幅下降。4. 手把手部署 1主2从3哨兵配置、启动顺序与一次完整的故障演练4.1 最小高可用拓扑与端口规划下面我按一个特别适合学习的最小拓扑来演示1 个主节点、2 个从节点、3 个哨兵。这些实例可以全部跑在一台机器上用不同端口模拟生产环境则建议主、从、哨兵尽量跨机器、跨可用区至少不要让所有哨兵都在同一台物理机上否则“哨兵高可用”就成了摆设。角色端口说明主节点6379接收写请求从节点 16380复制主节点数据提供读能力从节点 26381复制主节点数据提供读能力哨兵 126379监控与故障转移哨兵 226380监控与故障转移哨兵 326381监控与故障转移同一台机器上跑多个实例一定要注意port、pidfile、logfile、dbfilename不能互相冲突否则启动时会报错或者数据文件互相覆盖。4.2 主从节点配置replicaof 与 masterauth主节点本身不需要特殊配置从节点只需要加上replicaof指定主节点地址即可。下面是一份最小化配置# 从节点1 redis_6380.conf port 6380 daemonize yes pidfile /var/run/redis_6380.pid logfile /var/log/redis_6380.log dbfilename dump_6380.rdb replicaof 127.0.0.1 6379# 从节点2 redis_6381.conf port 6381 daemonize yes pidfile /var/run/redis_6381.pid logfile /var/log/redis_6381.log dbfilename dump_6381.rdb replicaof 127.0.0.1 6379如果配置了密码环境有三个参数非常关键主节点需要requirepass来限制客户端访问从节点必须配置masterauth也就是从节点连接主节点做复制时使用的密码哨兵后续也要配置sentinel auth-pass。很多生产事故都是只配了requirepass忘了从节点的masterauth从节点日志一直报MASTER auth failed数据永远同步不起来。4.3 哨兵配置详解monitor、down-after-milliseconds、failover-timeout哨兵的配置文件叫sentinel.conf一份最简配置长这样# sentinel_26379.conf port 26379 daemonize yes pidfile /var/run/redis-sentinel_26379.pid logfile /var/log/redis-sentinel_26379.log # 监控主节点 # mymaster 是自定义的主节点组名称客户端也要用这个名字查询 # 127.0.0.1 6379 是初始主节点地址 # 最后的 2 是 quorum sentinel monitor mymaster 127.0.0.1 6379 2 # 超过3秒没有响应就认为主观下线 sentinel down-after-milliseconds mymaster 3000 # 故障转移超时时间 sentinel failover-timeout mymaster 15000 # 同时允许几个从节点同步新主建议保持1 sentinel parallel-syncs mymaster 1三个哨兵除了port和文件路径不同核心配置完全一样。如果主节点有密码还要在哨兵配置里加sentinel auth-pass mymaster 密码。启动哨兵使用如下命令redis-server /path/to/sentinel_26379.conf --sentinel--sentinel参数可以显式声明以哨兵模式启动即使不加只要配置里有sentinel monitorRedis 也会自动以哨兵模式运行但建议还是写清楚方便别人阅读。4.4 启动顺序和验证命令先主从、后哨兵启动顺序有一点讲究先启动主节点再启动两个从节点最后启动三个哨兵。原因很简单——先让主从复制建立好哨兵起来后看到的拓扑就是稳定的如果哨兵先于主从启动它会先记录一个不完整的拓扑虽然之后会自动纠正但没必要给自己增加变量。启动完成后用下面一组命令验证redis-cli -p 6379 INFO replication redis-cli -p 26379 SENTINEL masters redis-cli -p 26379 SENTINEL get-master-addr-by-name mymaster redis-cli -p 26379 SENTINEL replicas mymaster redis-cli -p 26379 SENTINEL sentinels mymasterSENTINEL sentinels mymaster会列出当前哨兵集群中已知的其他哨兵正常应该看到另外两个。如果只看到自己或者数量不对检查三个哨兵的sentinel monitor配置是否一致。执行INFO sentinel可以查看当前哨兵视角里的整体状态正常情况下应该能看到master0:namemymaster,statusok。4.5 故障演练杀掉主节点观察哨兵日志中的切换全过程部署完成不能只看statusok就收工建议做一次完整的故障演练。最简单的做法是直接停掉主节点redis-cli -p 6379 SHUTDOWN NOSAVE几秒之后再查哨兵redis-cli -p 26379 SENTINEL get-master-addr-by-name mymaster正常情况下返回的新主地址会变成某个从节点的 IP 和端口。哨兵日志里会出现类似这样的关键行monitor master mymaster 127.0.0.1 6379 sdown master mymaster 127.0.0.1 6379 odown master mymaster 127.0.0.1 6379 failover-state-select-slave master mymaster selected-slave slave 127.0.0.1:6380 promoted-slave slave 127.0.0.1:6380 failover-state-send-slaveof-noone slave 127.0.0.1:6380 failover-end master mymaster switch-master mymaster 127.0.0.1:6379 127.0.0.1:6380看到switch-master就说明切换已经完成。如果不想真正关机还可以用redis-cli -p 6379 DEBUG SLEEP 30让主节点阻塞 30 秒同样会触发哨兵判定和切换30 秒后节点恢复会被哨兵自动降级为从节点。这种演练方式比SHUTDOWN更适合反复测试因为不需要手动重启节点。5. 生产环境摸爬滚打出来的坑哨兵机制最容易翻车的几个细节5.1 down-after-milliseconds 过小慢节点引发的连环误判调试环境里为了快速验证我经常把down-after-milliseconds设成 3000 甚至更小。但到了生产环境这个值必须慎重。Redis 节点在 CPU 飙高、内存 swap、网络瞬时拥塞时PING 延迟超过 3 秒是非常常见的。哨兵把这视为 sdown接着触发 odown 判定和 failover可实际情况是主节点只是“慢”而不是“挂”。切换后旧主恢复又会产生一段时间的双主状态数据一致性风险很大。生产环境我一般至少配置到 10000 毫秒以上并结合业务对故障恢复时间的容忍度来权衡。10 秒的判定延迟对多数业务是完全可接受的如果业务对恢复时间要求特别苛刻更应该从网络和资源稳定性上优化而不是单纯调小哨兵阈值那样只会放大误判风险。5.2 failover 期间写入失败客户端超时与重试才是高可用的另一半哨兵能自动切换但 failover 发生的那几秒窗口期内客户端仍然可能拿着旧主地址尝试写入这时会遇到连接拒绝或超时。所以“高可用”从来不是哨兵单方面能搞定的。应用端的 Redis 客户端要开启 Sentinel 模式同时配置合理的连接超时、读写超时和重试策略。以 Lettuce 为例超时设置太短切换期间大量请求会快速失败设置太长整个接口容易被拖垮。我的习惯是写操作超时控制在 1 到 2 秒做有限次数的重试切换期间的失败交给业务层做有限重试或快速熔断绝不在客户端层无限重连那只会把故障放大成雪崩。5.3 sentinel.conf 会自动重写别把它当静态文件很多第一次部署哨兵的人都会遇到这个现象配置文件明明写得挺好跑了一阵子后打开文件发现注释没了sentinel monitor里的地址也变了。这不是丢配置而是哨兵的自动重写机制。每次主节点切换后哨兵都会更新内存状态并且把最新拓扑写回 sentinel.conf这样哨兵重启后就能恢复到正确状态。这个机制带来的直接教训是不要把哨兵配置文件设置成只读权限也不要在哨兵运行期间手动修改文件后直接重启——你手改的内容很容易被哨兵落盘的旧状态覆盖或者你自己覆盖了哨兵刚更新的状态。想要安全变更应该先停掉哨兵修改配置再启动或者修改后确认哨兵已经完成新的落盘再重启。生产环境里我还会定期备份 sentinel.conf防止异常恢复时配置丢失。5.4 密码配置三件套requirepass、masterauth、auth-pass 缺一不可涉及认证时密码配置需要三处全部到位主节点配置requirepass限制普通客户端访问。从节点配置masterauth用于从节点连接主节点复制数据。哨兵配置sentinel auth-pass mymaster 密码用于哨兵监控和后续查询。三处配置最容易漏的是从节点的masterauth。漏掉之后从节点会不断报认证失败复制建立不起来故障转移后如果新主节点本身是从节点它沿用的还是自己原来的认证配置哨兵和客户端连接时如果认证对不上依然会卡在验证环节。所以多实例环境里我建议统一密码管理避免给哨兵机制增加额外复杂度。5.5 哨兵还是 Cluster先看看数据能不能塞进单机内存聊哨兵机制时总有人问既然哨兵能实现高可用是不是可以直接用Redis Cluster 又用来干什么我的理解是哨兵解决的是主从高可用数据全部落在同一个逻辑主节点上容量受单机内存限制Cluster 解决的是分片加高可用数据分散在多个主节点槽位上容量可以横向扩展。两者解决的问题不同选型不是越复杂越好。对比维度Sentinel 主从Redis Cluster数据存储全量在同一逻辑主节点容量受单机内存限制数据分片到多个主节点容量可横向扩展高可用自动故障转移主从切换自动故障转移槽位迁移客户端要求通过哨兵获取主地址兼容性好必须支持 Cluster 协议处理 MOVED 重定向运维复杂度部署简单拓扑固定运维复杂跨槽位操作有限制适用场景数据量中等、需要主从高可用数据量大、写请求和容量需要横向扩展很多团队一上来就上 Cluster结果被跨 slot 操作、批量 key 限制等问题折腾得够呛也有团队在单机都放不下数据时还硬撑哨兵属于拿错了工具。选型前先测一下数据增长速度和单节点能力这是最朴素也最有效的判断标准。6. 我的哨兵落地习惯从部署规范到日常演练最后分享一点我个人的落地习惯。第一哨兵数量固定为 3 起步业务规模再大也优先考虑 3 或 5不要出现 2 这样的偶数quorum 统一配置为 majority 值。第二所有哨兵和 Redis 实例的密码统一管理配置模板化部署时用脚本批量生成避免手工写错。第三每次做主节点变更或网络调整都要把故障演练重跑一遍演练不只是验证哨兵也是验证客户端在切换期间超时和重试策略是否符合预期。第四监控面板上不只看主节点存活状态还要盯哨兵自身的内存、CPU、网络和日志——哨兵部署得再稳如果它自己先出问题同样会成为新的故障点。这套习惯维护下来Redis 主节点故障时基本能做到分钟级自动恢复真正让“半夜人工救火”变成历史。如果你也遇到过哨兵机制里其他离谱的问题欢迎交流。