ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Redis 哨兵模式深度实践:主观下线、客观下线与 Raft 选举的源码级排障

Redis 哨兵模式深度实践:主观下线、客观下线与 Raft 选举的源码级排障 Redis 哨兵模式深度实践主观下线、客观下线与 Raft 选举的源码级排障凌晨两点监控群里弹出一条告警业务写入 Redis 失败报错READONLY You cant write against a read only replica。值班同学第一反应是主库挂了于是登录跳板机打开哨兵日志看到满屏sdown、odown、vote-for-leader但主库地址迟迟没有切换。半小时后业务恢复复盘时发现真正的根因不是 Redis 宕机而是两台哨兵所在宿主机时钟漂移加上一个哨兵进程被 OOM Killer 杀掉导致quorum长期不满足客观下线根本没达成。这个场景里问题不在“Redis 会不会挂”而在“谁来判断它挂了、几个人同意才算挂、挂之后谁来接班”。这三件事分别对应哨兵模式的主观下线、客观下线和 Raft 选举。下面先把整体框架立起来再逐一拆开。1. 先记住一个最小模型哨兵不是代理而是带投票权的看门人可以先把它理解成Redis 哨兵Sentinel是一组独立运行、互相通信的进程它们不转发业务请求只做四件事——持续探测主库与从库是否存活、在多个哨兵之间对“主库是否真挂了”达成共识、从从库中选出一个新主库、把新主库地址通知给其余从库和客户端。业务客户端通过哨兵拿到当前主库地址再直连 Redis 读写所以哨兵挂了通常不会立刻断业务但会失去自动切换能力。最小模型里有三种角色、两条链路。三种角色是主库master唯一接受写、从库replica异步复制主库数据、哨兵sentinel独立进程多个组成哨兵集群。两条链路是数据复制链路主库把写命令传播给从库和哨兵控制链路哨兵之间用PUBLISH/SUBSCRIBE在__sentinel__:hello频道交换心跳与投票信息同时每个哨兵用PING探测主从库。业务客户端 | 1. 询问当前 master 地址 v --------------------- | Sentinel 集群 | | S1 --- S2 --- S3| 2. 互相 PING、交换 hello -------------------- | 3. 探测主从库 v --------------------- | Master (可写) | -------------------- | 4. 异步复制 v --------------------- | Replica A / B | ---------------------一次故障转移的完整顺序是某个哨兵先发现主库无响应并标记“主观下线”sdown→ 它向其他哨兵询问达到quorum后升级为“客观下线”odown→ 触发选举参选哨兵向其他哨兵拉票 → 获得多数票的哨兵成为 Leader → Leader 从从库中挑一个升级为主库并让其他从库复制新主库。理解这条主线后面所有配置项和日志都能归位。2. 主观下线一个哨兵的“个人判断”不构成结论“主观下线”解决的是单点探测问题。每个哨兵每隔down-after-milliseconds毫秒向被监控实例发PING如果在这个窗口内没收到有效回复PONG、-LOADING、-MASTERDOWN都算有效其他回复或超时不算就把这个实例标记为SRI_S_DOWN日志里出现sdown。关键点是这只是单个哨兵的个人判断此刻不触发任何切换因为可能是这个哨兵自己网络抖动、宿主机负载高、或者对方只是短暂卡顿。这里最容易误解的是down-after-milliseconds不是“超过这个时间就切换”而是“单个哨兵从最后一次有效响应起经过这个时长仍未得到有效响应就进入主观下线”。切换还需要客观下线与选举两个关口所以真实切换时间通常是down-after-milliseconds加上一轮PING周期、quorum收集时间、选举时间生产上常见几秒到十几秒。用一个具体数字推演哨兵 S1 配置down-after-milliseconds 30000主库在 10:00:00 最后一次正常响应之后每 1 秒被 PING 一次。到 10:00:30S1 仍无有效响应于是 S1 标记 master 为 sdown。此时 S2、S3 可能仍然能正常连上主库比如只是 S1 到主库的链路故障它们不会认同 sdown客观下线就不会达成也就不会切换——这正是我们想要的行为。# 源码节选sentinel.c逻辑示意不能直接运行 if (now - ri-last_pong_time SENTINEL_PING_PERIOD * down_after_period) { /* 单哨兵判定主观下线 */ if (ri-flags SRI_MASTER) { sentinelFlagSubjectiveDown(ri); // 记录 sdown 事件 } }这段逻辑在sentinel.c的sentinelCheckSubjectivelyDown附近核心就是“本地超时即 sdown”。它的作用是让每个哨兵独立表达意见避免单点误判直接引发切换它不能替代客观下线也就是说 sdown 只是“我怀疑”不是“大家都同意”。3. 客观下线凑够 quorum 才能把怀疑变成结论“客观下线”解决的是共识问题。当哨兵把主库标记为 sdown 后它会向其他哨兵发送SENTINEL is-master-down-by-addr询问其他哨兵根据自己的视图回复 master 是否也下线。只有主库和从库才有客观下线概念从库宕机一般不会触发切换除非是replica-priority等场景因为从库挂掉不影响写入。判断规则是当前哨兵统计所有回答“master 已下线”的数量包含自己的 sdown 判断如果数量达到quorum就把 master 标记为SRI_O_DOWN日志出现odown。注意quorum只用于认定客观下线不直接决定选举胜出选举需要的是哨兵总数的多数票。这两个阈值经常被混为一谈是排障时最常见的坑。举例说明假设 3 个哨兵quorum设为 2。主库真挂了S1、S2 都把它标记为 sdownS1 询问后收到 S2 的肯定答复加上自己共 2 票达到 quorumS1 标记 odown 并尝试发起选举。如果只剩 1 个哨兵存活它只有自己 1 票永远达不到 quorumodown 就无法达成也就永远不会切换。所以哨兵数量与 quorum 必须成对设计。概念判定主体触发条件是否触发切换常用日志主观下线 sdown单个哨兵该哨兵 PING 超时否sdown客观下线 odown哨兵集群同意下线的哨兵数 ≥ quorum是进入选举odown、try-failover选举 Leader哨兵集群获得多数哨兵投票是执行切换vote-for-leader、elected-leader故障转移完成Leader 哨兵新主库切换并通知成功是switch-master、promoted-slave4. Raft 选举只学它需要的部分但关键规则必须严格哨兵使用的是一套受 Raft 启发的选举算法不是完整 Raft但核心规则一致每个哨兵有任期epoch任期单调递增每个任期最多投一票竞选者需要获得超过半数哨兵的投票。为什么要类比 Raft因为分布式共识里只有“多数派”才能保证同一任期不会选出两个 Leader从而避免脑裂时的双主写入。选举的触发时机是某个哨兵达成客观下线后它自己作为候选人把当前任期加一向所有其他哨兵发送SENTINEL is-master-down-by-addr ip port current_epoch runid其中runid填自己的运行 ID 表示“我参选”。收到请求的哨兵如果在本任期没投过票且认为请求者合法例如主库确实客观下线就回复同意如果已经投给别人就回复自己支持的 runid。候选人统计票数获得超过半数例如 5 个哨兵需要 3 票即成为 Leader日志出现elected-leader。如果第一轮没有哨兵拿到多数票就会进入下一轮任期继续增加等待一段随机时间后再发起。这个随机等待是为了避免多个候选人无限互相否决。排障时如果看到任期号epoch快速增长、vote-for-leader反复出现但始终没有elected-leader说明选举反复失败通常是因为存活哨兵数少于需要投票的多数派或者存在网络分区。# Sentinel 故障转移时序ASCII 示意 S1 S2 S3 Master |--- PING -------| | | |-- PONG ---------| | | |--- PING -------------------------------------------| X 超时 | 标记 sdown(master) | |--- is-master-down-by-addr (询问) --| | |-- master 已下线 ------------------| | | 达到 quorum标记 odown | |--- 拉票 current_epoch1, runidS1 -| | |-- 同意投票 -----------------------| | | 获得多数票成为 Leader | |--- SLAVEOF NO ONE 给新主库 ------------------------| |--- SLAVEOF new-master 给其余从库 -----------------|5. sentinel.conf每个参数在流程里的位置哨兵的配置决定它怎么探测、怎么投票、怎么选新主库。下面是一份三哨兵、一主两从的最小配置注释说明每一项在流程里的作用。注意哨兵配置不要在运行时随意改文件部分参数需要通过SENTINEL SET、SENTINEL MONITOR命令修改否则重启后可能与运行时视图不一致。# sentinel.conf 示例三哨兵集群中的一台 port 26379 bind 0.0.0.0 daemonize yes logfile /var/log/redis/sentinel.log dir /var/lib/redis/sentinel # 监控名为 mymaster 的主库quorum2 表示至少 2 个哨兵同意才客观下线 sentinel monitor mymaster 192.168.1.10 6379 2 # 主库 30 秒无有效响应则主观下线 sentinel down-after-milliseconds mymaster 30000 # 故障转移时最多允许 1 个从库同时向新主库同步避免新主库被压垮 sentinel parallel-syncs mymaster 1 # 故障转移超时超时后进入下一轮选举 sentinel failover-timeout mymaster 180000 # 从库优先级值越小越优先被选为新主库0 表示永不参选 sentinel replica-priority mymaster 100 # 哨兵之间以及哨兵到 Redis 的认证密码 sentinel auth-pass mymaster YourStrongPasswordsentinel monitor里的quorum只参与客观下线选举多数派是“哨兵总数 / 2 1”。所以 3 个哨兵时 quorum 通常设为 25 个哨兵时通常设为 3。把 quorum 设成 3 而只有 3 个哨兵意味着必须全部存活才能判定 odown容错性变差把 quorum 设成 1则单个哨兵可能因自身网络抖动误判主库下线并推动选举风险更大。配置项作用阶段推荐区间配置过小的风险配置过大的风险quorum客观下线哨兵数/21单点误判引发切换故障时无法达成 odowndown-after-milliseconds主观下线5000~30000网络抖动就 sdown切换延迟明显parallel-syncs故障转移1~3新主库复制压力大从库同步慢failover-timeout选举与切换60000~180000选举频繁重试故障恢复慢replica-priority选新主库按机房/规格弱机器被选为主全部为 0 则无候选6. 完整示例一用 Docker 搭一套可复现的哨兵环境目标在本地搭起一主两从三哨兵手动 kill 主库观察 sdown、odown、选举、故障转移全过程。前置环境已安装 Docker 与 Docker Compose机器内存至少 2GB。输入下面这份docker-compose.yml。version:3.8services:redis-master:image:redis:7.2container_name:redis-mastercommand:[redis-server,--port,6379,--requirepass,redis123]ports:[6379:6379]redis-replica1:image:redis:7.2container_name:redis-replica1command:[redis-server,--port,6379,--replicaof,redis-master,6379,--masterauth,redis123,--requirepass,redis123]depends_on:[redis-master]redis-replica2:image:redis:7.2container_name:redis-replica2command:[redis-server,--port,6379,--replicaof,redis-master,6379,--masterauth,redis123,--requirepass,redis123]depends_on:[redis-master]sentinel1:image:redis:7.2container_name:sentinel1command:sh -c cp /etc/sentinel.conf /tmp/sentinel.conf redis-sentinel /tmp/sentinel.confvolumes:[./sentinel.conf:/etc/sentinel.conf:ro]depends_on:[redis-master,redis-replica1,redis-replica2]sentinel2:image:redis:7.2container_name:sentinel2command:sh -c cp /etc/sentinel.conf /tmp/sentinel.conf redis-sentinel /tmp/sentinel.confvolumes:[./sentinel.conf:/etc/sentinel.conf:ro]depends_on:[redis-master,redis-replica1,redis-replica2]sentinel3:image:redis:7.2container_name:sentinel3command:sh -c cp /etc/sentinel.conf /tmp/sentinel.conf redis-sentinel /tmp/sentinel.confvolumes:[./sentinel.conf:/etc/sentinel.conf:ro]depends_on:[redis-master,redis-replica1,redis-replica2]操作步骤# 1. 准备 sentinel.confmonitor 指向容器名或自定义网络内的主机名dockercompose up-ddockerexec-itsentinel1 redis-cli-p26379sentinel master mymaster# 2. 确认三个哨兵都发现了彼此forsinsentinel1 sentinel2 sentinel3;dodockerexec-it$sredis-cli-p26379sentinel sentinels mymaster|head-5done# 3. 模拟主库宕机dockerstop redis-master# 4. 实时观察切换日志dockerlogs-fsentinel1|grep-Esdown|odown|vote|switch-master|elected预期输出与观察点停止主库后约down-after-milliseconds时间内三个哨兵日志先出现sdown master mymaster随后达到 quorum 的哨兵出现odown master mymaster接着出现vote-for-leader和elected-leader最后出现switch-master mymaster 旧地址 新地址。适用场景本地验证配置和故障转移流程。容易改错的地方sentinel monitor中的主机名若在容器网络里解析不到哨兵会一直 sdownauth-pass必须与 Redis 的requirepass一致否则 PING 被拒也会被当成超时。7. 完整示例二Spring Boot 通过哨兵连接并处理切换目标让 Java 客户端在哨兵完成切换后自动连上新主库并演示一个常见的错误用法。前置环境JDK 17、Spring Boot 3.2、spring-boot-starter-data-redis。packagecom.example.redis;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;importorg.springframework.data.redis.connection.RedisPassword;importorg.springframework.data.redis.connection.RedisSentinelConfiguration;importorg.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;importorg.springframework.data.redis.core.StringRedisTemplate;ConfigurationpublicclassSentinelRedisConfig{BeanpublicLettuceConnectionFactoryredisConnectionFactory(){RedisSentinelConfigurationsentinelnewRedisSentinelConfiguration().master(mymaster).sentinel(127.0.0.1,26379).sentinel(127.0.0.1,26380).sentinel(127.0.0.1,26381);sentinel.setPassword(RedisPassword.of(redis123));LettuceConnectionFactoryfactorynewLettuceConnectionFactory(sentinel);factory.setTimeout(2000);returnfactory;}BeanpublicStringRedisTemplatestringRedisTemplate(LettuceConnectionFactoryfactory){returnnewStringRedisTemplate(factory);}}packagecom.example.redis;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.data.redis.RedisConnectionFailureException;importorg.springframework.data.redis.core.StringRedisTemplate;importorg.springframework.web.bind.annotation.GetMapping;importorg.springframework.web.bind.annotation.RestController;RestControllerpublicclassWriteController{AutowiredprivateStringRedisTemplateredisTemplate;GetMapping(/write)publicStringwrite(){intretries3;for(inti1;iretries;i){try{redisTemplate.opsForValue().set(user:1001:name,alice);returnok, retryi;}catch(RedisConnectionFailureExceptione){if(iretries){throwe;}try{Thread.sleep(500L*i);}catch(InterruptedExceptionie){Thread.currentThread().interrupt();thrownewIllegalStateException(ie);}}}returnfailed;}}关键步骤LettuceConnectionFactory连接的是哨兵地址而不是主库地址Lettuce 内部会订阅哨兵的switch-master消息并刷新拓扑。预期结果停止主库后前几次写可能抛RedisConnectionFailureException重试若干次后成功写入新主库。适用场景中小规模、需要自动故障转移的缓存或会话存储。边界重试必须有上限和退避否则在哨兵长时间未完成切换时会拖垮线程池setTimeout过短会放大切换期间的失败率。8. 完整示例三一次真实排障——quorum 配置错误导致永不切换目标复现并定位“主库挂了但始终不切换”的故障。前置环境沿用示例一的环境但故意把sentinel monitor的 quorum 改为 3并且只启动 2 个哨兵。输入与步骤# 只启动两个哨兵quorum 配为 3dockercompose up-dredis-master redis-replica1 redis-replica2 sentinel1 sentinel2# 停止主库dockerstop redis-master# 观察 30 秒以上dockerlogs sentinel1|tail-30可观察结果日志里会持续出现sdown master mymaster但不会出现odown也不会出现try-failover。原因是只有 2 个哨兵即使两个都同意票数也只有 2达不到 quorum3。修复方式把 quorum 改为 2或补足第三个哨兵。适用场景生产环境排查“哨兵在跑、日志有 sdown、但主库地址不变”。容易改错的地方把 quorum 与选举多数派混淆以为 3 个哨兵 quorum 必须写 3实际上 quorum 是客观下线门槛而选举需要多数派3 个哨兵需 2 票。故障现象排查路径ASCII -------------------- | 主库不可写/连接失败 | ------------------- v -------------------- | 查 sentinel 日志 | | 有无 sdown? | ------------------- | 无 | 有 v v 检查网络/超时 有无 odown? | | 无 有 v v 检查 quorum 检查 elected-leader 与存活哨兵数 是否为 0/反复失败9. 脑裂为什么“多数派”这个规则不能省脑裂指的是网络分区时出现两个“当前主库”两边都可能接受写入导致数据分叉。哨兵本身对脑裂的防护是切换必须由获得多数票的 Leader 执行。如果原来的主库只是与部分哨兵失联但它自己仍存活多数派一侧会选出新主库旧主库恢复网络后就会被降级为从库并在复制新主库时丢弃自己分区期间的写入。如果旧主库与它所在分区内的客户端仍能写那部分写入就会丢失。所以从业务视角看脑裂的损失通常发生在“旧主库所在分区仍有客户端可写”的窗口。工程上常用补充措施在客户端侧使用带redis.clients.jedis、Lettuce 的主从感知拓扑让写请求只发往从哨兵获知的主库在主库侧用min-replicas-to-write和min-replicas-max-lag限制“从库太少或太慢时拒绝写入”降低旧主库在被分区期间继续接受写入的概率。防护手段作用点收益代价或边界哨兵多数派选举切换决策防止双 Leader 切换存活哨兵不足则无法切换min-replicas-to-write主库写路径降低分区旧主库写入从库少时正常写入也会失败客户端拓扑刷新客户端请求快速指向新主库切换窗口内仍可能失败合理 quorum客观下线减少误判过高会降低容错10. 常见误区这五个判断最容易出错第一把down-after-milliseconds当作切换总耗时。它只决定单个哨兵何时进入 sdown后面还有客观下线和选举。第二把quorum当作选举票数。客观下线用 quorum选举用多数派。第三认为哨兵数量越多越好。哨兵数增加会提高多数派门槛和通信开销通常 3 或 5 个足够且应分布在不同的故障域。第四认为主库恢复后会自动恢复为主。哨兵完成切换后旧主库恢复只会作为从库加入不会自动抢回主库除非再次发生故障转移或人工执行SENTINEL FAILOVER。第五忽略认证和地址解析。auth-pass不一致、容器主机名解析失败、防火墙拦截哨兵端口都会表现为 sdown让人误以为是 Redis 本身的问题。11. 生产实践建议把可用性和误切换风险一起算进去哨兵部署建议 3 个或 5 个分布在不同的物理机、机架或可用区避免同一宿主机故障同时带走多个哨兵。quorum设为多数派down-after-milliseconds根据网络质量设 5000~30000 毫秒内网稳定可取 10 秒左右跨可用区可适当放宽。parallel-syncs建议从 1 开始确认新主库容量后再调整。客户端侧必须配置重试与退避并监控“写入失败但哨兵未报 odown”这一类指标。同时建议开启 Redis 慢日志和主从复制延迟监控因为复制延迟过大时即便切换成功也可能丢数据。对于强一致要求高的场景哨兵模式的异步复制本质上无法完全避免丢数据应改用 Redis Cluster 或在上层用更严格的写入确认策略。12. 排障清单从现象到动作现象优先检查常见根因处置动作有sdown无odownquorum、哨兵存活数quorum 过高或哨兵不足调整 quorum 或补齐哨兵有odown无elected-leader投票日志、epoch多数派不足、网络分区恢复网络、检查分区反复vote-for-leaderepoch 增长速度选举陷入僵局检查时钟、哨兵数量已切换但客户端仍报只读客户端拓扑、DNS客户端未刷新拓扑检查客户端 sentinel 配置切换后数据丢失复制延迟、旧主写入脑裂窗口写入启用min-replicas-to-write排障时先看哨兵日志的四个里程碑sdown、odown、elected-leader、switch-master。缺哪个就聚焦哪个环节的配置和网络不要一上来就重启哨兵因为重启会丢失运行时的 epoch 和视图反而让问题更难复现。13. 面试/复盘问题检验是否真的理解主观下线和客观下线的判定主体分别是谁各自触发什么动作quorum和选举多数派有什么区别3 个哨兵时分别应设为多少哨兵选举为什么需要任期epoch没有任期会怎样旧主库恢复后为什么不会自动变回主库如何降低脑裂期间旧主库继续接受写入的概率客户端在哨兵切换窗口内应该怎样重试如果 5 个哨兵中有 2 个同时宕机还能完成切换吗14. 总结回到开头那条sdown日志现在应该能快速定位先确认是几个哨兵、quorum 是多少、有多少哨兵真的在运行再判断卡在主观下线、客观下线还是选举。整体框架可以压缩成一张表单个哨兵超时产生 sdown集群凑够 quorum 产生 odown拿到多数票的哨兵成为 Leader 并执行 switch-master。所有配置项和日志都围绕这三步展开。最后再给一个决策清单小规模主从高可用选哨兵需要分片和更大写入吞吐选 Cluster对强一致有要求时不要把哨兵当作银弹要在应用层接受“切换窗口可能失败、异步复制可能丢数据”这两个边界。把 quorum、哨兵分布、客户端重试和监控四项配置清楚哨兵模式在多数中小规模场景下依然是可靠且成本较低的高可用方案。参考资料Redis 官方文档Redis Sentinel Documentationhttps://redis.io/docs/latest/operate/oss_and_stack/management/sentinel/Redis 源码仓库sentinel.chttps://github.com/redis/redis/blob/unstable/src/sentinel.cRedis 官方文档High availability with Redis Sentinelhttps://redis.io/docs/latest/operate/oss_and_stack/management/sentinel/Redis 官方文档Replicationhttps://redis.io/docs/latest/operate/oss_and_stack/management/replication/Spring Data Redis 官方文档Redis Sentinel Supporthttps://docs.spring.io/spring-data/redis/reference/redis/sentinel.htmlDiego Ongaro, John Ousterhout. In Search of an Understandable Consensus Algorithm (Raft), USENIX ATC 2014.
返回列表