面试官把“听说你精通Redis来说说Redis哨兵”这句话甩过来的时候多半不是想听你复述三个定时任务的间隔秒数。真正想探的是你有没有在凌晨三点被一次误切换叫起来过有没有见过主从切换完成后业务还在往旧主上写数据的场面有没有算过 quorum 配成 1 之后会发生什么。Redis 哨兵这套东西配置起来不到二十行坑却能把人埋三层。下面这些内容是我自己搭过、拆过、也修过若干套哨兵集群之后留下的记录从它在解决什么问题讲起一路讲到故障注入、客户端跟着换人、以及那些文档里不会写但生产上一定会遇到的情况。不管你是刚接触主从架构的新手还是已经能背出 sentinel.conf 全部参数的老手应该都能在里面捡到一两点能直接用的东西。1. 哨兵要解决的从来不是“高可用”这四个字很多人对哨兵的理解停留在“主挂了自动换一个”这句话没错但太粗。粗到最后你会以为装上哨兵就万事大吉然后第一次真实故障就翻车。要搞明白哨兵得先把它要填的窟窿看清楚再看它用什么姿势去填。1.1 主从复制留下的三个窟窿Redis 的主从复制本身是个很克制的设计一台主扛写若干台从扛读主把写命令以异步方式甩给从。它解决了读扩展和一定程度的冗余但它对“主没了怎么办”这个问题是完全沉默的。第一个窟窿是角色是静态的。你在配置文件里写下replicaof 10.0.0.11 6379这台机器就永远认为自己是某台的从。主的进程一旦消失从还是从它只会不停地重连那个不存在的地址每秒一次日志里刷满MASTERDOWN。没有谁站出来说“从现在起我是主”。第二个窟窿是故障发现只能靠人。监控系统告警了值班的人打开电脑确认主真的挂了然后手动在某台从上敲REPLICAOF NO ONE再把其他从的配置改掉再通知业务方改连接地址。这一套动作在凌晨做手抖一次就够喝一壶。更麻烦的是决策本身有模糊地带——主是彻底挂了还是网络抖了一下如果只是抖动你手动切了等原主恢复回来两份数据谁对谁错第三个窟窿是客户端不知道拓扑变了。业务代码里写死一个 IP主换了地址代码得重启。就算你把地址写在配置中心谁来触发那一次配置推送推送之后连接池里的旧连接怎么处理这些事分到各个业务团队实现方式五花八门最后变成一堆甩不掉的运维债。哨兵就是把这三件事一次性收编它自己是一组进程互相看着发现主不行了就投票决定换谁换完把新拓扑广播出去客户端只要订阅或者定期问一句就行。所以准确地说哨兵不是“高可用”它是一套带投票机制的主节点托管服务。这个措辞很重要因为它解释了为什么哨兵必须是奇数个、为什么它自己也会挂、为什么它救不了数据一致性问题。1.2 哨兵的本质一支会投票的值班小队把哨兵想象成一个值班室里面坐着三个值班员每个人手里都有一份相同的花名册名单上写着“当前的主是谁、有哪几个从、还有哪几个值班员”。每个值班员做的事情很简单每秒给名单上所有人打个电话PING每十秒问一次主从“你现在是什么角色、复制到哪了”INFO每两秒在值班室的小黑板上写一句“我是谁、我认为主是谁”往__sentinel__:hello频道发消息。小黑板的作用是让新来的值班员能自动加入也让所有人对“当前主是谁”这件事逐渐对齐。当某个值班员连续打了几次电话都打不通主他会在自己的小本子上记一笔“主可能不行了”这叫主观下线。但这只是他一个人的判断不能作数因为可能只是他自己和主之间的线路断了。于是他挨个问其他值班员“你们看主还活着吗”如果凑够约定的人数quorum都说不行这件事就升级成客观下线开始走换人流程。换人之前先得选出一个牵头的值班员这一步靠投票投的是“这一轮谁来主持换人”。选出来之后牵头的那位去从里面挑一个最合适的扶正通知其他从改口叫新主最后在小黑板上广播“主换人了新地址是某某”。整个流程听下来你会发现它对抗的是“单个观察者的误判”和“没人愿意负责”这两件事。它不解决的问题也很清楚主挂掉之前那一瞬间还没同步出去的写命令不会因为换了主就回来。这一点后面单独说。2. 把哨兵的内部机制拆到骨头面试里最容易露怯的地方是把“哨兵做了什么”和“哨兵怎么做到的”混在一起答。前者是背概念后者才是理解。下面这四块是哨兵的骨架。2.1 三个定时任务撑起全部感知能力哨兵没有额外的通信机制它所有信息都来自三个周期性任务间隔分别是 10 秒、2 秒、1 秒。这三个数字不要死记要理解它们为什么是这个量级。每 10 秒一次的 INFO 采集。哨兵向被监控的主节点和所有从节点发送INFO命令解析返回内容得到当前主的信息role、connected_slaves、run_id、偏移量等和每个从的信息。拿到从的地址后哨兵还会顺带向从发送INFO确认它的角色和复制状态。这个间隔决定了拓扑变化的发现速度你新挂了一台从最多十秒后哨兵才会知道。之所以是 10 秒而不是 1 秒是因为 INFO 的输出相对重主节点上挂五个从、三个哨兵每秒发一次 INFO 的开销就不可忽视了。每 2 秒一次的 hello 广播。哨兵向主节点和所有从节点上的__sentinel__:hello频道发布一条消息内容包括自己的 IP、端口、run_id、当前的配置纪元config epoch以及它认为的主节点地址和纪元。同时它也订阅这个频道所以别人发的消息它能收到——这就是哨兵之间互相发现的唯一途径。这条消息的载荷里带着纪元直接导致了后面的“谁说了算”问题。每 1 秒一次的 PING。哨兵向主、从、以及其他哨兵都发 PING判断对方是否可达。注意有效响应不只是PONG-LOADING和-MASTERDOWN也算“活着”因为这两个回复说明进程还在、网络还通只是暂时不能服务。只有连接超时、连接被拒、或者收到完全无关的响应才算一次失败。这个细节很关键主节点在做全量持久化的时候可能短暂返回-LOADING如果你把它当成挂了就会误切。三个任务合起来的开销其实很小一套三节点的哨兵集群跑在 1 核 2G 的机器上都绰绰有余。但这也解释了哨兵的一个性格它是个秒级的观察者不是毫秒级的。你指望它在一秒内发现故障并完成切换那是为难它。真实的切换耗时通常在十秒到几十秒之间取决于down-after-milliseconds和故障转移本身的执行速度。2.2 主观下线与客观下线一字之差一条命主观下线sdown的判定条件是在sentinel down-after-milliseconds master-name 毫秒规定的时间内没有收到有效的 PING 响应。这个时间默认是 30 秒我一般会压到 10 秒到 15 秒之间理由放在后面运维那节讲。判定为主观下线之后哨兵做的事情是向其他哨兵发送SENTINEL is-master-down-by-addr ip port current_epoch runid问“你们怎么看”。注意这里的 runid 字段如果传自己的 runid是在请求投票选自己当 leader如果传*就纯粹是询问状态。这一步是两个语义共用一条命令很多资料讲不清楚。其他哨兵收到询问后根据自己的判断回复主对我来说是不是也不可达、我现在认为的 leader 纪元是多少。发起询问的哨兵统计回复如果认为主下线的哨兵数量含自己达到 quorum就把状态升级为客观下线odown。这里有个必须澄清的点quorum 只用于判定客观下线它不参与后面的 leader 选举。很多人把这两个数字混为一谈写成 quorum1 觉得“一台说挂就算挂多快”结果就是单点误判直接触发切换因为 quorum1 时它自己一票就够了后面的 leader 选举它也会投自己整个流程变成单人独裁。还有一类判定容易被忽略从节点和哨兵节点也会被判定为 sdown但它们永远不会变成 odown。从节点挂了不需要换主哨兵节点挂了不需要投票只有主节点的下线才需要走完整流程。这个不对称性经常被问到。2.3 Leader 选举epoch 才是真正的计票器客观下线成立之后参与的哨兵要选出一个 leader 来执行故障转移。这里借用了 Raft 的一些概念但实现上更简单也更粗糙。每个哨兵维护一个current_epoch可以理解为“第几届”。当一次故障转移被发起纪元会加一。选举的规则是某个哨兵认为自己应该当 leader就向其他人发is-master-down-by-addrrunid 填自己的epoch 填自己的当前纪元。收到请求的哨兵如果在这个纪元里还没投过票就投给它并记录“这个纪元我投过谁了”。一旦投过票同一纪元内不再改投。拿到超过半数的票majority不是 quorum的那个哨兵成为 leader开始执行故障转移。注意关键点是超过半数不是达到 quorum。三个哨兵的集群quorum 配 2那 leader 需要 2 票超过 1.5。五个哨兵的集群quorum 配 3leader 需要 3 票。如果集群里只有两个哨兵活着无论 quorum 怎么配都拿不到超过半数的票故障转移就卡住了。这是哨兵的一个硬性设计它宁可不切也不允许少数派擅自决定。之所以强调 epoch是因为它解决了“两个哨兵在不同轮次里各自选自己”的冲突。低纪元的选择结果会被高纪元覆盖配置更新时也是按纪元比较大小。你在sentinel.conf里会看到sentinel current-epoch这一行它被自动维护不要手动改改了会让整个集群的决策逻辑混乱。还有一个细节leader 选出来之后如果在failover-timeout内没有完成故障转移其他哨兵会认为它失败了纪元加一重新选。failover-timeout默认 180 秒这个值不是“故障转移最多花 180 秒”而是“同一 master 的两次故障转移之间的最小间隔”加上一堆重试判定的阈值语义比名字复杂后面单独拆。2.4 挑新主的三级排序比你想象的更保守leader 选出来了接下来挑谁当新主。这一步筛得比很多人想的严格不是随便挑一台从就行。第一轮排除不合格的。以下从节点直接被剔除处于 sdown 状态超过down-after-milliseconds乘以 10 的说明它自己也病了很久与主节点断连时间超过同样阈值的优先级replica-priority老版本叫slave-priority配置为 0 的——这个值是明确表示“我不参与选主”常用来让某台弱机器只做只读副本。第二轮按三个维度排序。剩下的从节点依次比较排序维度比较方式设计意图优先级数值小者胜让你手动指定“我更希望谁当主”比如同机房、配置更好的机器复制偏移量数值大者胜偏移量越大说明数据越新丢的数据越少run_id字典序小者胜前两项打平时的兜底保证结果确定这套排序说明了一件事哨兵优先尊重人的意图优先级其次才是数据新鲜度。如果你把优先级配反了明明数据最新的那台优先级是 100另一台落后一大截的优先级是 10哨兵会毫不犹豫地选落后的那台然后把新的写请求压上去你之前没同步过去的那部分数据就永久丢了。这个坑我在早期项目里踩过代价是几个小时的日志回捞。选定之后leader 向目标从节点发送REPLICAOF NO ONE然后每秒发一次INFO确认它的 role 变成了 master确认之后向其他所有从节点发送REPLICAOF 新主IP 新主端口最后通过 hello 频道广播新拓扑。整个过程里parallel-syncs参数控制的是“同时有几个从开始向新主做全量同步”默认 1。设大了切换后新主会被多个全量同步的 RDB 传输打满设 1 则是慢慢排队切换窗口拉长但更稳。我一般保持 1除非你的从节点数量很多又急着恢复读能力。3. 三节点哨兵从零搭到能切换理论讲完动手。这一节的所有配置都可以直接抄改改 IP 和密码就能用。环境假设是一主两从加三个哨兵六台机器如果资源紧张可以让哨兵和 Redis 实例混部但不要让哨兵和它监控的主节点在同一台机器上——主挂了机器也挂了那个哨兵跟着一起消失白白损失一票。3.1 机器与端口规划附一张表角色地址端口说明master10.0.0.116379初始主节点replica-110.0.0.126379从节点优先级 10replica-210.0.0.136379从节点优先级 20sentinel-110.0.0.2126379哨兵独立机器sentinel-210.0.0.2226379哨兵独立机器sentinel-310.0.0.2326379哨兵独立机器为什么是三个哨兵单个哨兵是新的单点没意义两个哨兵在任何一方挂掉时都凑不齐多数票等于没有冗余三个哨兵可以容忍任意一台挂掉仍是多数。五个哨兵可以容忍两台再往上收益递减而运维成本上升所以三个是绝大多数场景的答案。三个哨兵分布在三台独立机器上是为了避免“一台物理机挂了带走两个哨兵”这种局面。如果条件不允许至少保证不会有两台哨兵落在同一台机器上。主从的搭建很简单# 主节点 redis.conf 关键项 port 6379 daemonize yes appendonly yes requirepass YourStrongPass masterauth YourStrongPass # 从节点 redis.conf port 6379 daemonize yes appendonly yes requirepass YourStrongPass masterauth YourStrongPass replicaof 10.0.0.11 6379 replica-priority 10从节点上的masterauth和requirepass必须都配。requirepass是让自己能接受带密码的连接masterauth是在自己去连主的时候出示密码。只配一个的后果是初始复制能建立因为你手动起的进程但故障转移后新主连旧从的时候认证失败复制关系断裂从节点永远停在旧数据上。3.2 sentinel.conf 逐行拆解哨兵的配置文件不叫 sentinel.conf 也行但是习惯上就这么放。三台机器的内容除了port和日志路径基本一样。port 26379 daemonize yes logfile /var/log/redis/sentinel-26379.log dir /var/lib/redis/sentinel # 监听与保护 bind 0.0.0.0 protected-mode no # 监控目标名字、地址、端口、quorum sentinel monitor mymaster 10.0.0.11 6379 2 # 认证 sentinel auth-pass mymaster YourStrongPass # 判定与重试 sentinel down-after-milliseconds mymaster 10000 sentinel failover-timeout mymaster 180000 sentinel parallel-syncs mymaster 1 # 安全 sentinel deny-scripts-reconfig yes逐项说几个值的来历。sentinel monitor里的名字mymaster是自己取的标识客户端也要用这个名字来查主所以改了名字等于所有客户端要跟着改。地址写主节点当前的 IP哨兵启动后如果发现这个地址已经是别人的从会自动纠正所以初始化顺序无所谓先把主从跑起来再起哨兵最省事。quorum写 2 还是 3取决于哨兵数量。三节点配 2 意味着两台认为主挂了才升级容忍一台误判。配 3 更保守但只挂一台哨兵的时候就凑不够客观下线什么都不会发生。绝大多数场景三节点配 2。down-after-milliseconds我给的 10000比默认的 30000 激进。原因是业务侧对主不可用的容忍通常做不到三十秒而 10 秒在绝大多数内网环境里已经足以排除网络抖动。如果你的环境跨机房网络质量一般乖乖用 20000 到 30000别学我。failover-timeout保持 180000。它的实际语义包含好几层同一次故障转移中各个子步骤的超时、两次故障转移之间的最小间隔、以及一个被降级的从重新加入时的等待时间。名字很有误导性不要因为“想让切换更快”就把它调小。注意sentinel auth-pass这一行配的是被监控 Redis 的密码不是哨兵自己的密码。哨兵之间通信默认是无认证的如果要做得配requirepass在所有哨兵的配置里并且版本要在 5.0.1 以上。生产环境这块容易被忽略导致内网里任何人连上 26379 端口都能执行sentinel failover强行切换这就是个可控的运维事故按钮。配置里没有出现的那些项比如sentinel known-sentinel、sentinel known-replica、sentinel current-epoch会在运行过程中被自动写入文件。所以这个文件是可写的部署时不要挂只读文件系统也不要用配置管理工具定时覆盖它后面单独讲。3.3 启动验证三条命令确认拓扑拉起来了三个节点都启动后用三条命令确认状态。# 1. 确认监控的主信息 redis-cli -p 26379 sentinel master mymaster输出里重点看几个字段num-slaves是不是 2num-other-sentinels是不是 2flags里有没有masterquorum是不是 2。# 2. 确认其他哨兵都被发现了 redis-cli -p 26379 sentinel sentinels mymaster三个哨兵互相发现靠的是 hello 广播因为是每 2 秒一次启动后等几秒再查。如果这里始终只有一个或者两个说明某个哨兵的bind或防火墙挡了或者两个哨兵互相连不上或者一侧配了认证另一侧没配。# 3. 确认从节点信息 redis-cli -p 26379 sentinel replicas mymaster能看到两台从并且flags里没有s_down或disconnected。补一条最实用的redis-cli -p 26379 sentinel get-master-addr-by-name mymaster这不是给运维看的是你的客户端或者健康检查脚本最应该调用的命令。它返回当前主节点的 IP 和端口切换后立刻变化。我自己写的探活脚本就靠它比解析INFO sentinel的输出稳得多。3.4 一次可控的故障注入演练没有演练过的哨兵集群等于没有哨兵。你永远不知道那个down-after-milliseconds设得对不对也永远不知道业务客户端会不会跟着切换。演练有两种方式我推荐第二种。暴力方式直接在主机上kill -9Redis 进程或者SHUTDOWN NOSAVE。redis-cli -p 6379 -a YourStrongPass SHUTDOWN NOSAVE这种方式最接近真实故障但主进程完全消失恢复需要手动重启并指望哨兵把它降级成从。演练完记得检查老主起来后有没有被正确降级——这是哨兵的职责但很多时候它需要额外的SLAVEOF指令才能完成具体见后面的坑。温和方式用DEBUG SLEEP让主节点阻塞。redis-cli -p 6379 -a YourStrongPass DEBUG SLEEP 30这条命令会让主节点在 30 秒内不响应任何请求但进程活着。哨兵在 10 秒内判定 sdown凑够 quorum 后升级为 odown完成切换。30 秒后主节点醒来发现自己被抛弃了哨兵会通知它改为从。整个过程不需要重启进程演练完拓扑自动收敛非常适合反复做。为了让阻塞时间盖住整个切换流程DEBUG SLEEP的秒数要比down-after-milliseconds大出足够的余量。我给 30 秒是因为切换通常在 15 秒内完成。如果只想验证判定逻辑而不想真切可以把这个值设成略小于down-after-milliseconds的秒数。演练过程中盯着两个东西一是哨兵日志里有没有完整的sdown→odown→try-failover→elected-leader→switch-master序列二是你的业务侧有没有报错、报错持续了多久、有没有出现往旧主写数据的情况。日志里的关键标记sdown master mymaster 10.0.0.11 6379 odown master mymaster 10.0.0.11 6379 #quorum 2/2 try-failover master mymaster 10.0.0.11 6379 vote-for-leader runid 2 elected-leader master mymaster 10.0.0.11 6379 selected-slave slave 10.0.0.12:6379 failover-state-send-slaveof-noone slave 10.0.0.12:6379 promoted-slave slave 10.0.0.12:6379 failover-state-reconf-slaves master mymaster 10.0.0.11 6379 switch-master mymaster 10.0.0.11 6379 10.0.0.12 6379switch-master这一行是终点也是客户端能感知到变化的时刻。看到它之后立刻用get-master-addr-by-name验证应该是新的地址。还有一个专门为演练准备的命令redis-cli -p 26379 sentinel failover mymaster这是手动触发一次完整故障转移不依赖任何故障。它走的是和自动切换完全一样的流程但有个区别手动切换不需要满足客观下线条件。所以如果你只想验证“切换流程本身能不能跑通”而不想制造故障用这个。我在做巡检脚本的时候会定期对预发环境执行一次确保切换链路没退化。3.5 客户端怎么跟着换人哨兵做得再好客户端不认也是白搭。客户端感知切换的方式有两种主动查询和订阅推送。主动查询就是定期或者每次重连前调用sentinel get-master-addr-by-name或者用客户端库内置的哨兵支持。这是最稳妥的方式主流客户端库都是这么实现的。Spring Boot 2.x 的配置spring: redis: password: YourStrongPass timeout: 3000 sentinel: master: mymaster nodes: 10.0.0.21:26379,10.0.0.22:26379,10.0.0.23:26379 password: YourStrongPassSpring Boot 3.x 前缀变了spring: data: redis: sentinel: master: mymaster nodes: 10.0.0.21:26379,10.0.0.22:26379,10.0.0.23:26379 password: YourStrongPass password: YourStrongPass注意sentinel.password指的是哨兵节点的密码如果哨兵本身没配密码这一项不要写写了反而连不上。这一点坑过很多人看着报认证错误就往所有 password 字段里填越填越乱。Lettuce 手动构造RedisSentinelConfiguration config new RedisSentinelConfiguration() .master(mymaster) .sentinel(10.0.0.21, 26379) .sentinel(10.0.0.22, 26379) .sentinel(10.0.0.23, 26379); config.setPassword(RedisPassword.of(YourStrongPass)); LettuceConnectionFactory factory new LettuceConnectionFactory(config); factory.afterPropertiesSet();Jedis 池化方式SetString sentinels new HashSet(Arrays.asList( 10.0.0.21:26379, 10.0.0.22:26379, 10.0.0.23:26379)); JedisPoolConfig poolConfig new JedisPoolConfig(); poolConfig.setMaxTotal(64); poolConfig.setMaxIdle(16); poolConfig.setTestOnBorrow(true); JedisSentinelPool pool new JedisSentinelPool( mymaster, sentinels, poolConfig, 3000, YourStrongPass);Jedis 的JedisSentinelPool有一个必须知道的特性它在内部维护一个后台线程通过订阅哨兵的switch-master消息来感知切换而不是每次获取连接都去查一次。这带来两个后果一是监听线程万一下线池子会一直用旧地址二是订阅需要哨兵本身可连接如果哨兵配了密码而这个池子没提供哨兵密码老版本构造函数就没有这个参数监听会失败但不会报错表现就是切换后连接全部失效。所以我个人更倾向 Lettuce它对哨兵密码的支持更完整。不管用哪个客户端有一条原则不要自己维护一套“主节点地址缓存”再配合固定 IP 连接。你以为省了一次查询实际上制造了一个只在切换时才暴露的定时炸弹。4. 生产上真正会让你半夜爬起来的那几个坑前面都是铺垫这一节才是重点。下面这些情况没有一个是“配置写错了”全都是“配置写对了但它就是不按你想的来”。4.1 脑裂与异步复制切换瞬间到底丢多少数据先明确一件事哨兵在故障转移时一定会丢数据区别只是丢多少。Redis 的复制是异步的主执行完写命令就返回客户端成功然后再把命令发给从。主在发出后、从收到前挂掉这段数据就没了。切换只能保证“新主有的数据是它自己有的”不能保证“所有写过的东西都还在”。丢多少取决于主从延迟和你的写入量。写入每秒几千条、网络延迟 1 毫秒的环境丢失窗口可能只有几十条如果从节点因为全量同步或者网络拥塞落后了几秒那丢的就是几秒的写入。这是架构决定的哨兵救不了能救的只有WAIT命令同步复制确认或者换用其他强一致的方案。真正危险的是脑裂它会放大数据丢失。场景是这样的主节点 A 和哨兵集群之间的网络断了或者主节点所在机器因为负载过高响应不过来哨兵判定 A 下线选了从节点 B 当新主业务开始往 B 写。而 A 其实还活着它那边的客户端连得好好的继续往 A 写。等网络恢复A 被哨兵降级为 B 的从A 上那段时间写入的数据会被全量同步清空——不是丢一点是整段抹掉。对抗脑裂的手段在主节点的配置文件里min-replicas-to-write 1 min-replicas-max-lag 10语义是如果健康从节点延迟不超过min-replicas-max-lag秒的数量少于min-replicas-to-write个主节点就拒绝写入返回错误。上面这组配置的意思是“至少有一个从在 10 秒内跟我同步过否则我不接写”。这样在网络分区时A 发现自己可用的从数量变成 0直接拒绝写入客户端报错业务侧感知到问题就不会产生那些注定被抹掉的数据。代价是主节点在从少或从慢的时候会短暂拒绝写入需要业务侧有重试或降级。这两个参数不在 sentinel.conf 里在 redis.conf 里这点经常被搞混。而且它们只防脑裂不防普通的异步丢数据。4.2 quorum 和 majority 被混为一谈的后果这是概念性问题但后果是实打实的。quorum决定什么时候判客观下线majority超过半数决定谁有资格执行故障转移。三节点集群 quorum2两票就能判下线但要拿两票才能当选 leader3 的一半是 1.5超过就是 2。数量刚好一样容易让人以为是同一件事。五个节点的集群就看得出区别了。quorum 配 3意味着三台认为主挂了就判下线但 leader 需要 3 票。如果此时只有三台哨兵活着能选出 leader能切如果只有两台活着即使它们都认为主挂了也凑不够 quorum不会切——这个还算合理。真正的问题在于quorum配得过大或过小。配 1 的后果前面说了单点误判即切。配成哨兵总数比如 3 个哨兵配 quorum3任何一次故障转移都需要全员在线等于放弃了冗余一台哨兵挂了整个自动切换能力就没了。稳妥的配法是quorum floor(n/2) 1即三节点配 2五节点配 3。还有一种更隐蔽的情况哨兵集群自己发生了网络分区2 台在一侧1 台在另一侧。2 那一侧能选出 leader1 那一侧不能所以只有一侧会执行切换。但如果主节点恰好和那 1 台在同一个分区里那 1 台会一直认为主是好的不会触发任何动作而另一侧已经完成切换。等分区恢复那 1 台的纪元是旧的会被纠正但它在这段时间里可能一直在为旧主提供“主还活着”的错误信息。这类问题没法完全避免只能靠监控哨兵之间互相能不能看见来提前发现。4.3 密码链路三处认证少一处都不行Redis 加了密码之后链路上会出现三个独立的认证点任何一个没配对表现都不一样。认证点配置位置没配的后果客户端连 Redisrequirepass业务连不上报 NOAUTH从连主masterauth初始能连手动起的切换后新主连旧从失败复制断哨兵连 Redissentinel auth-pass哨兵 INFO 拿不到主一直被当成 sdown第三处最坑因为它的表现不是报错是哨兵永远不发客观下线。你在日志里能看到哨兵不停地 PING 主节点主节点也活着但就是因为 INFO 被拒哨兵解析不出从节点列表num-slaves一直是 0也不影响它 PING 通所以看起来风平浪静。等真出故障了你发现哨兵压根没动作再去翻日志才发现一堆NOAUTH。还有一个变体哨兵自己配了requirepass5.0.1 支持但客户端连哨兵的时候没带这个密码。这种时候客户端连哨兵这一步就直接失败配置中心查不到主节点地址直接报连接拒绝反倒容易排查。排查的时候最简单的一招是手动用同样的命令连一遍redis-cli -p 26379 127.0.0.1:26379 SENTINEL master mymaster只要这条能返回完整信息说明哨兵到主这条链路是通的返回空或者报错就从监听地址、防火墙、密码三个方向查。4.4 配置被自动重写带来的麻烦哨兵的配置文件是可写且会被自己改写的。每次拓扑发生变化、每次发现新哨兵、每次纪元递增它都会把当前状态写回文件。你打开sentinel.conf会看到一堆不是你写的行sentinel known-replica mymaster 10.0.0.12 6379 sentinel known-replica mymaster 10.0.0.13 6379 sentinel known-sentinel mymaster 10.0.0.22 26379 runid sentinel known-sentinel mymaster 10.0.0.23 26379 runid sentinel current-epoch 3这些行是哨兵的记忆。如果你用配置管理工具比如定时下发配置、或者容器启动时覆盖配置文件把文件重置回初始版本哨兵的记忆就丢了。丢失记忆本身不致命它会在几十秒内通过 hello 广播重新发现但current-epoch回退会造成短暂的决策混乱尤其在切换刚完成的时候重置可能引发重复选主。所以部署上要注意两条一是配置文件所在目录必须可写别挂只读卷二是容器化部署时把配置文件放在持久化卷里或者至少在容器重启时不要覆盖。我见过容器滚动发布时把哨兵配置重置结果五台哨兵各自认为纪元是 0互相之间谁也不服整个集群几十分钟没恢复正常。如果确实要做配置管理思路是只管理静态部分动态部分让哨兵自己长。把sentinel monitor、密码、超时这些写进模板但不要在运行中反复覆盖只在初始化时生成一次。4.5 客户端缓存旧主与重试风暴切换完成哨兵日志一片绿但业务在报错通常是客户端那边的问题。第一种是连接池里留着旧主的连接。旧主被降级成从之后连接不会断客户端继续往它写Redis 返回READONLY You cant write against a read only replica。这个错误很容易识别看到就说明客户端没跟上新拓扑。Lettuce 收到这个错误会自动触发拓扑刷新重连新主有些客户端不会需要你配置一个错误处理。第二种是重试风暴。切换那一瞬间所有连接同时失效所有请求同时重试新主刚被扶正还没来得及处理完切换收尾就先被一波重连打满然后超时然后重试更多。这在 QPS 高的系统里很常见。缓解手段是给重试加退避和随机抖动别让所有客户端在同一毫秒同时重连。第三种是订阅者掉了。用 Jedis Sentinels 池的项目切换靠的是后台订阅线程。如果那个线程因为网络问题断过它会重连但如果断的时间比较长或者哨兵那边限制连接数它可能连不上。表现就是池子里的连接全是旧的业务持续报错但客户端日志里没有任何切换记录。排查方法是看客户端的连接数、看有没有订阅到switch-master。我见过最离谱的一个案例是业务侧自己写了个主节点健康检查每五秒查一次主节点的INFO一旦查不到就把本地的地址变量改成另一台——而这个另一台是写死在代码里的一台从。结果哨兵正常切换了它的逻辑却把地址带偏了反向切回了一个只读节点。不要自己造哨兵的轮子要用就完整用要么就干脆不用哨兵自己搞客户端侧的服务发现。5. 排查速查表与演练节奏这一节是我自己的笔记整理遇到问题先对照表对不上再往下钻。5.1 常见故障对照表现象大概率原因快速确认处理方向主挂了但没切换quorum 不够或哨兵数量不足sentinel master看 quorum 和 num-other-sentinels补足哨兵节点核对 quorum切了但业务还连旧主客户端没订阅或池子没刷新查客户端日志有无 READONLY 报错换用支持哨兵的客户端配置加错误处理哨兵看不到从节点sentinel auth-pass没配sentinel replicas返回空配上被监控实例的密码反复切换来回跳failover-timeout太小或从节点频繁掉线日志里有多个switch-master调大超时先修从节点稳定性新主数据比预期旧优先级配置反了选中了落后的从sentinel replicas看 offset 和 priority调整replica-priority哨兵之间看不到彼此bind/防火墙/认证不一致sentinel sentinels数量少于预期逐项排查网络和配置重启哨兵后纪元回退配置文件被覆盖看sentinel current-epoch保证配置目录持久化且不被覆盖主恢复后数据被抹掉脑裂主在分区期间继续收写事件时间线对得上分区窗口配min-replicas-to-write排查顺序我一般固定为先看哨兵日志的标记序列再看sentinel master的状态字段最后看客户端。因为哨兵的日志是按事件打点的sdown、odown、switch-master这些标记能直接把时间线拉出来比翻业务日志快得多。5.2 演练和监控该盯什么演练频率我建议至少一个季度一次核心链路变更后加一次。演练内容就三件事用DEBUG SLEEP制造一次自动切换用sentinel failover手动切一次把一台哨兵停掉确认剩下两台还能正常判定和投票。监控方面除了常规的 Redis 指标哨兵这边至少要盯这几个每个哨兵实例是否存活26379 端口的连通性sentinel master mymaster返回的num-other-sentinels是否等于预期值减一num-slaves是否等于预期从节点数flags里是否出现了s_down或o_down记录switch-master事件的时间点事后能对上业务异常窗口我自己写了一个心跳脚本每 30 秒对三个哨兵各执行一次get-master-addr-by-name如果三者返回的地址不一致立刻告警。这个检查比看单个哨兵的状态更有效因为三个哨兵对当前主有分歧本身就是最危险的信号而单看某一个哨兵的输出是完全看不出来的。另外提一句监控之外的准备切换后旧主的处理要有预案。旧主恢复上线时哨兵会尝试把它变成一个从但它需要先清空自己、做一次全量同步。如果旧主数据量大这次全量同步会消耗带宽和时间期间它不可用。如果你的从节点数量有限切换后读能力会在一段时间内下降这个要在容量规划里留出余量。我个人在实际操作中的体会是哨兵这套东西的复杂度不在配置而在它把故障变成了一个持续的、需要被观察的状态。你配完那一刻它是能用的但真正的考验在于你有没有能力在半年后它悄悄切换过一次时从日志里把那次切换的前因后果还原出来。所以我现在的习惯是每套哨兵集群上线时顺手建一个文档记录三件事哨兵节点清单和版本、down-after-milliseconds和quorum的取值理由、以及一次完整演练的日志截图。这份东西平时没人看出事的时候能省下大量时间。
阅读完成 · 觉得有帮助?