Redis哨兵模式:自动故障转移与高可用架构解析
发布时间:2026/10/5 3:00:42 作者:尧图编辑部 阅读量:1,286

主从复制在Redis里跑了半年多线上数据一直挺稳的。直到有一天半夜主节点所在的机器内存报警直接卡死。从节点老老实实待在那儿数据倒是都有可应用还在拼命往旧主节点写数据报错、重试、超时持续了快二十分钟直到我手动把从节点提升为主节点把重启后的旧主重新挂成从。那晚之后我开始认真啃哨兵模式把自动故障转移的整套机制彻底捋了一遍。这篇就仔细聊聊哨兵Sentinel到底是怎么做到自动发现主节点挂了、自动选出新主、还能让整个集群无感知切换的。哨兵模式是Redis官方提供的高可用方案能在不人工干预的情况下监控主从节点的运行状态并在主节点故障时自动完成故障转移failover把从节点提升为新的主节点。这套机制适合所有依赖Redis做缓存、会话存储、分布式锁等场景的生产环境尤其适合不允许长时间不可用的线上服务。这篇文章会从哨兵的监控机制讲起再拆解客观下线判定、Leader协商选举、新主选举规则这些核心逻辑同时附上我实际部署时踩过的坑和排查经验。1. 为什么需要哨兵从主从复制说起1.1 主从复制的致命短板Redis主从复制的核心思路很简单一台主节点负责接收写请求数据实时异步同步到若干从节点从节点分担读压力。这个架构解决的是“读写分离”和“数据备份”的问题但它有一个致命前提——主节点必须一直活着。主节点一旦宕机整个系统就瘫痪了。从节点不会自动上位客户端依然往主节点地址写数据所有写请求直接失败。更尴尬的是如果这时候你去手动切换主从还得考虑数据一致性的问题主节点宕机前可能有部分数据没来得及同步到从节点直接提升从节点会有少量数据丢失怎么取舍需要人来判断。这就暴露了主从复制的短板它只是一个数据层面的冗余方案不是高可用方案。高可用要求的是“当某台机器挂了系统还能继续正常工作”而主从复制遇到主节点宕机只能干瞪眼。所以要引入一个“监督者”角色专门盯着所有节点的健康状态在主节点挂掉后自动完成角色切换。1.2 哨兵到底做了什么哨兵Sentinel本质上是一个独立运行的Redis进程它不存储业务数据专门用于监控主从节点的运行状态。你可以把哨兵想成小区里的物业监控室里面有几个保安轮流盯着监控屏幕一旦发现某栋楼主节点出了问题立刻启动应急预案通知其他楼从节点调整职责再打电话通知业主客户端。哨兵的职责可以拆成四块监控持续检查主节点和从节点是否正常运行心跳机制每秒发送一次PING命令。通知某个被监控的Redis实例出现问题时哨兵通过Pub/Sub机制通知其他哨兵节点和客户端。自动故障转移主节点客观下线后自动从所有从节点中选出新的主节点并把其他从节点指向新主完成角色切换。配置提供客户端连接Redis时可以先向哨兵查询当前真正的主节点地址这样即使发生故障转移客户端也不用改配置。这里有个容易混淆的点哨兵本身也可以是多个实例组成一个哨兵集群。为什么不能只部署一个哨兵因为如果只有一个哨兵它自身挂掉了或者它的网络出现分区整个监控体系就失去了判断能力也没法做故障转移的投票决策。多个哨兵协作既能避免单点故障也能在判定主节点是否真正下线时互相印证防止误判。2. 哨兵的核心机制监控与下线判定2.1 主观下线sdown是怎么触发的哨兵对节点的健康检查靠的是定期发送PING命令。正常情况下被监控的节点会在毫秒级内回复PONG。但网络抖动、节点阻塞、机器负载过高都可能让PING迟迟没有回应。哨兵有一个配置项叫down-after-milliseconds意思是“如果持续多少毫秒没有收到有效回复就判定这个节点主观下线”。这个值的默认配置是30000ms也就是30秒。生产环境我一般会调成10秒到15秒因为30秒对用户来说感知很明显很多请求已经超时了故障转移还没开始但也不能调得太小比如设成1000ms万一Redis实例只是碰到一次长GC或者瞬时CPU打满就会被误判下线触发不必要的切换。注意这里“主观下线”的关键词是“主观”。某个哨兵只是根据自己的探测结果猜测这个节点可能挂了并不代表它真的挂了。还有一种情况Sentinel与主节点之间的网络被分区了其实主节点活得好好的但哨兵收不到回复也会判定为主观下线。所以主观下线只是一条“怀疑线索”不能作为故障转移的直接依据。在哨兵内部每个被监控的实例都有一个状态标志标记为S_DOWN主观下线。处于主观下线状态的主节点哨兵会提高PING的发送频率从每秒一次变为每秒十次用来快速确认这台机器是不是真的彻底没反应了。2.2 客观下线odown与quorum机制单个哨兵说主节点挂了其他哨兵未必同意。为了避免误判和脑裂Redis设计了客观下线判定机制。当一个哨兵发现主节点主观下线后它会通过Sentinel Pub/Sub频道向其他哨兵发起询问你们觉得主节点现在是什么状态如果在一定时间内收到足够多哨兵的确认也就是达到配置的quorum值这个哨兵才会把主节点标记为客观下线O_DOWN。我用一个例子来算清楚这个数。假设部署了5个哨兵节点配置quorum 3。哨兵A发现主节点主观下线于是向B、C、D、E广播主观下线消息。如果有至少2个其他哨兵也认为主节点下线加上A自己总共达到3个A就会把主节点标记为客观下线。如果只收到1个确认总数只有2达不到quorumA不会触发故障转移而是继续观察。quorum取值有个原则quorum 哨兵总数/2 1并且建议quorum取哨兵总数的一半以上。比如3个哨兵quorum至少是25个哨兵quorum建议是3。这样能保证在做下线判定或者故障转移投票时必须有多数节点参与决策不会出现两个哨兵各执一词的场面。如果部署3个哨兵却设quorum3一旦有一个哨兵宕机剩下2个哨兵永远无法达到quorum整个故障转移能力直接瘫痪高可用等于没有。2.3 哨兵之间的通信方式哨兵不是靠“猜测”来互相感知的它们之间有一套固定的通信机制。每个哨兵会每隔2秒向被监控的主从节点发布一条消息内容包含自己的IP、端口、运行ID和当前下线的判断状态。同时每个哨兵也会订阅__sentinel__:hello这个频道接收其他哨兵发布的消息。这套机制让每个哨兵都能维护一份关于其他哨兵以及主从节点的实时视图。通过交换消息哨兵能发现新加入的哨兵节点也能感知其他哨兵对某个节点的判定结果。更关键的是哨兵之间还会比对各自的“配置纪元”Configuration Epoch这个值是实现故障转移“一次只有一个指挥者”的核心后面会详细讲。在这个通信体系里哨兵还会用is-master-down-by-addr这个内部命令向其他哨兵询问主节点状态。这个过程存在推拉结合一边主动发消息告知自己的判断一边主动问别人要判断结果两种方式同时工作保证下线判定的消息能在毫秒级内传遍整个哨兵集群。3. 自动故障转移完整流程拆解3.1 Leader选举谁有资格执行故障转移当主节点被标记为客观下线后下一个问题是谁来执行故障转移。总不能让5个哨兵各做各的那会乱套。Redis的解决方案是发送故障转移请求从多个哨兵中选出一个Leader由这个Leader统一执行故障转移。这套选举机制借鉴了Raft算法里的思路但做了简化。每个哨兵都有投票权每轮选举有一个配置纪元epoch类似选举任期。整个流程是这样的哨兵A把主节点标记为客观下线后自增自己的配置纪元然后向其他哨兵发送SENTINEL is-master-down-by-addr命令请求对方投票给自己。其他哨兵收到投票请求如果之前没有投过票且请求里的配置纪元比自己当前记录的新就投票给请求方。候选哨兵统计自己收到的票数如果超过哨兵总数的一半注意不是quorum是半数以上就成为本轮Leader。Leader执行故障转移其他哨兵更新自己的配置纪元进入等待状态。这里有个很容易踩坑的点有人以为quorum就是选举需要的票数其实不对。quorum只决定“是否可以发起故障转移选举”而“能否成为Leader”需要的是哨兵总数的一半以上。比如5个哨兵quorum3客观下线需要3票但选举Leader需要至少3票这里刚好一样因为5 / 2 2.5向上取整是3。如果是6个哨兵quorum4才能下线选举Leader则需要4票6 / 2 1 4数字碰巧一样。但如果是4个哨兵quorum2就能标记客观下线而选举Leader需要3票4 / 2 1 3。这个差异很重要如果quorum设得太小可能出现“能判定下线、但选不出Leader”的尴尬局面故障转移就会一直卡在选举阶段。投票还有超时机制如果第一轮选举没有选出Leader候选哨兵会等一个随机时间后重新发起选举配置纪元再加1。这个随机时间避免多个哨兵同时抢票导致互相干扰。3.2 新主节点选举不是随便挑一个从节点选出了Leader哨兵接下来要决定哪个从节点能“转正”。不能随机选必须挑一个数据尽量全、网络尽量稳的从节点。Redis的选举规则按以下优先级依次比较第一优先级是slave-priority新版叫replica-priority配置项。这个值越小优先级越高默认值是100。你可以通过设置这个值来干预最终的选举结果比如某台从节点的机器性能特别强可以把优先级设成50如果某台从节点纯粹做备份不想让它当主节点可以设成0优先级0的从节点永远不会被选为主节点。第二优先级是复制偏移量replication offset。如果多台从节点优先级相同就比谁的数据更接近原主节点。复制偏移量记录的是从节点已接收到的复制流位置偏移量越大说明丢失的数据越少。实现上说从节点复制进展越靠后偏移量值越大。第三优先级是运行IDrunid。如果偏移量也一样就按运行ID的字典序排序小的被选中。用运行ID做最终兜底本质上是一个随机选择因为这玩意儿是每次启动Redis时随机生成的。在实际选举时还有一些“不健康”的从节点会被直接排除在外处于主观下线或客观下线状态的从节点直接排除。最近down-after-milliseconds毫秒内和Leader没有过通信的从节点直接排除这是为了避免选出一个网络已经断了的节点。优先级为0的从节点排除。复制断连时间过长的从节点排除因为它可能落后了太多数据。把候选列表筛完再用上面三个优先级逐级比较选出唯一的新主节点。整个过程是确定性的Leader拿着这套规则算一遍结果就是唯一的新主不会出现两个哨兵选出两个不同新主的情况。3.3 Leader执行故障转移的三板斧选好新主之后Leader要做三件事第一板斧把新主节点从“从节点”变成“主节点”。Leader会向候选从节点发送REPLICAOF no one命令让它停止复制旧主并且变成可接收写请求的主节点。这个操作会把新主节点提升为主节点同时清掉它作为从节点的状态。第二板斧通知其他从节点改认新主。Leader会向所有其他从节点发送REPLICAOF new_master_ip new_master_port命令让它们重新指向新的主节点开始从新主同步数据。第三板斧处理旧主节点。如果原主节点真的宕机了暂时不用管它但Leader会持续监控。一旦原主节点重新上线Leader会主动向它发送REPLICAOF new_master_ip new_master_port把它降级为新主的从节点自动恢复复制关系。这里额外说一个生产环境常见的场景旧主节点并不是宕机而只是网络分区导致被判定下线。当分区恢复后旧主节点上线如果它直接接收写请求就会和新主节点同时写入造成数据分叉。所以哨兵在旧主上线后必须强制把它变成从节点这也是为什么旧主恢复后你会看到它自动变成新主的从节点。整个故障转移过程客户端是无感知的。前提是客户端实现了哨兵协议能动态从哨兵获取主节点地址。比如Java的Lettuce和Jedis都支持通过Sentinel模式连接Redis应用配置里不写死Redis地址而是写哨兵地址和master名字客户端会订阅哨兵的消息主节点一变它自动切换连接。4. 关键配置与实际部署4.1 一套能跑起来的哨兵配置哨兵配置文件通常叫sentinel.conf我贴一份我在生产环境用过的精简配置带注释说明每个参数的作用port 26379 daemonize yes logfile /var/log/redis/sentinel.log pidfile /var/run/redis-sentinel.pid sentinel monitor mymaster 127.0.0.1 6379 2 sentinel down-after-milliseconds mymaster 10000 sentinel failover-timeout mymaster 30000 sentinel parallel-syncs mymaster 2逐行解释一下sentinel monitor mymaster 127.0.0.1 6379 2监控一个名为mymaster的主节点地址是127.0.0.1:6379quorum为2。这里quorum2意味着至少要2个哨兵同意主节点下线才会触发故障转移。sentinel down-after-milliseconds mymaster 1000010秒收不到PONG就判定主观下线。sentinel failover-timeout mymaster 30000故障转移的超时时间设置为30秒。这个值包含多个阶段判定、选举、向从节点发命令、等待新主同步等。如果30秒内没完成转移哨兵会重新尝试。sentinel parallel-syncs mymaster 2故障转移后同时允许几个从节点向新主发起全量同步。这个值不宜设置太大因为多个从节点同时全量同步会占用大量带宽可能把新主节点拖垮设成1最保守逐个同步更稳。哨兵的认证配置也别漏sentinel auth-pass mymaster yourpassword如果Redis实例开了requirepassSentinel必须配上同样的密码否则哨兵连监控都做不了。4.2 部署架构几个哨兵最合适生产环境我推荐最少3个哨兵节点分布在3台独立的机器上和Redis主从节点尽量分开部署。为什么强调3个因为哨兵选举需要半数以上投票3个哨兵能容忍1个哨兵宕机5个哨兵能容忍2个宕机。如果你追求更高的可用性可以部署5个哨兵再多意义不大因为哨兵本身是轻量进程它的核心价值是决策能力不是承载能力。还有一种常见的部署方式是“哨兵随从节点部署”从节点旁边各挂一个哨兵进程。这样节省机器但有个隐患如果一台机器同时挂了从节点和哨兵哨兵集群的决策能力就会下降。我在实际项目里更倾向于独立部署哨兵虽然多花两台机器但隔离性更好故障域更干净。客户端连接这块也要提前规划。使用哨兵模式时客户端配置不能只写主节点地址应该写所有哨兵的地址。生产里常见的问题是只配置了一个哨兵地址哨兵挂掉后客户端连不上触发不了自动切换。正确做法是把3个哨兵地址都配全客户端会依次尝试连接这些哨兵查询当前主节点地址。4.3 应用侧要改的连接方式以Java为例使用Spring Boot操作Redis有很多人直接配单机地址部署哨兵模式后连接方式要跟着调整。Spring Boot 2.x之后底层默认用的是Lettuce客户端配置哨兵模式的写法是这样的spring: data: redis: sentinel: master: mymaster nodes: - 192.168.1.10:26379 - 192.168.1.11:26379 - 192.168.1.12:26379 password: yourpassword配完之后客户端启动时会先连接哨兵获取主节点地址再建立真正的Redis连接。同时客户端会订阅哨兵的switch-master事件一旦发生故障转移客户端能及时感知并切换连接。这一步很关键否则故障转移完成了客户端还抱着旧连接不放照样写不进去。如果用的是原生Jedis写法也类似把jedis.sentinel配置项指到哨兵地址列表。无论哪种客户端核心思想只有一个不要写死Redis主节点的地址让客户端动态从哨兵获取。5. 常见问题与排查实录5.1 故障转移后客户端报错command timed out我在排查一个线上问题时遇到过这样的报错Redis command timed out; nested exception is io.lettuce.core.RedisCommandTimeoutException很多人的第一反应是Redis负载过高或者网络超时实际上我先去看了哨兵的日志确认故障转移发生的时间点。发现日志显示主节点在某个时刻挂了哨兵执行了切换但客户端一直在连接旧地址说明应用没有走哨兵连接模式而是配置了单个主的地址。排查步骤很简单先看应用配置里是不是写死了Redis主节点IP再看客户端是否订阅了哨兵事件。Spring Boot如果配置正确会在故障转移时输出switch-master相关的日志没有这类日志基本可以断定配置有问题。修复方式就是4.3里那一套把连接改成哨兵模式问题随即消失。5.2 误触发故障转移网络抖动不一定等于主节点挂了有一次线上Redis主节点没挂但哨兵触发了故障转移整个过程应用出现了大量写异常。排查后发现是数据中心内部网络抖动Sentinel所在的多台机器同时和主节点断连了几秒且持续超过了down-after-milliseconds阈值触发了主节点下线判定。这种问题在公有云环境尤其常见偶尔的链路抖动、交换机升级都可能导致瞬间丢包。我的经验是down-after-milliseconds不能太小建议至少10000ms以上同时可以把哨兵部署在和主节点不同的机架降低同区域网络故障对整体判断的影响。另外quorum要大于等于2单个哨兵的误判不会有决策权。还有一种误触发是主节点发生长时间GC停顿Java客户端调用Redis不会直接影响Redis本身但如果Redis所在机器上跑着Java应用且内存吃紧发生长GC时会阻塞所有线程Redis实例本身也会因为系统资源问题出现延迟导致哨兵PING超时。生产环境遇到过主节点在频繁执行BGSAVE时因为磁盘性能差把进程阻塞了将近20秒然后哨兵就判定下线了。这个问题的根因是fork子进程消耗内存导致的系统卡顿解决路径是优化持久化策略和磁盘性能而不是调整哨兵参数。5.3 Sentinel日志里两个关键事件怎么看哨兵排查绕不开日志。日志级别保持默认就行但要看懂几个关键标记sdown某个哨兵主观判定主节点下线。odown达到quorum客观下线开始选举和故障转移。failover-startLeader开始执行故障转移。failover-end故障转移完成。switch-master主节点地址已切换客户端应该更新连接。如果你发现日志停在sdown没有继续说明其他哨兵没有确认或者quorum没达标此时重点排查哨兵之间的网络连通性用redis-cli -h 哨兵IP -p 26379 ping逐个确认哨兵之间能互通。如果日志到了failover-start但迟迟没有failover-end大概率是选举新主的过程出了问题常见原因是从节点优先级都配置成了0导致没有候选者或者新主节点地址和端口配置错误哨兵连不上。5.4 数据丢失这个必须接受的现实哨兵模式能实现高可用但不等于数据零丢失。因为Redis主从复制是异步复制主节点写成功后返回客户端从节点可能还没来得及同步这些数据主节点就挂了。这时候提升从节点没同步过去的那部分数据就丢了。我之前负责的一个业务Redis里存的是会话数据故障转移后丢了大概几百条会话用户被迫重新登录。要想减少这种丢失有几个方向开启min-replicas-to-write和min-replicas-max-lag配置当从节点同步延迟过大时限制主节点写操作保证主从数据差距不会拉太大。尽量缩短故障转移时间减少故障窗口内写入的数据量。对于绝对不能丢的数据不要只依赖Redis要定期持久化到磁盘或同步到其他存储。如果业务对数据一致性要求极高Redis主从模式不是唯一方案需要引入更复杂的分布式一致性协议但这对大多数缓存场景来说没有必要。5.5 脑裂旧主恢复后出现双主脑裂是所有分布式系统的经典问题。在哨兵场景下场景大概是这样的主节点和哨兵之间的网络被分区哨兵判定主节点客观下线选出了新主但旧主其实还在运行并且继续接收写请求。等网络恢复后旧主发现新主存在会自动变成新主的从节点但它在分区期间写入的数据会被丢弃而且可能和新主的数据冲突。Redis解决这个问题的思路是靠配置约束min-replicas-to-write 1 min-replicas-max-lag 10第一行的意思是只有当至少1个从节点保持着正常复制关系时主节点才允许接收写请求第二行的意思是从节点延迟超过10秒就算作断连。这样在网络分区时旧主没有从节点可用会自动拒绝写请求从而避免脑裂期间写入脏数据。代价是牺牲一小部分可用性但保证了多主数据互斥。哨兵模式并不是Redis高可用的终点但说它是生产环境最务实的高可用方案一点不过分。我后来把它用在几个核心系统里故障转移从人工干预的十几分钟降到秒级自动完成半夜出问题也不会被电话吵醒了。如果你正在搭建Redis高可用先把哨兵这套原理吃透再小心处理那些边界场景线上基本能睡个踏实觉。个人实际体会是哨兵配置本身不难难的是对每个判定条件和时机的理解——弄懂了这些你连排查故障都有底气了。