ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Redis分片集群故障转移实战:从原理到参数调优

Redis分片集群故障转移实战:从原理到参数调优 分片集群故障转移可能是 Redis 运维里最需要被认真对待、却又最容易被“跑通一次就完事”掩盖掉细节的一个场景。我最近在本地用 3 主 3 从搭了一套 Redis 分片集群特意把其中一个主节点直接 kill 掉全程盯着cluster nodes和 Redis 日志看完了整场 failover这篇文章就把原理、搭建、实操、参数调优和排障经验一次讲清楚。无论你是被 Redis 面试题逼着补集群原理还是生产环境准备做一次主从切换演练这套方法都值得照着做一遍。1. 分片集群故障转移先理解这台“投票机器”Redis 分片集群和普通主从复制最大的区别不只是多了一堆节点而是它有一个完整的故障发现和选举机制。很多人第一次接触集群时只记住了 16384 个哈希槽和MOVED重定向等到真正做故障转移演练时才发现集群内部的消息交互比想象中复杂得多。先把这个机制搞明白后面才能看懂日志、调好参数。1.1 为什么分片集群必须考虑故障转移分片集群的作用是把数据分摊到多个主节点上每个主节点只负责一部分哈希槽。这样做的好处很直接单机内存上限不再是瓶颈写入吞吐也能横向扩展。但代价也随之而来——任何一个主节点挂了它负责的那部分哈希槽就没人提供读写了。整个集群的可用性就取决于最脆弱的那台机器。Redis Cluster 的设计者没有选择“手动切主从”这种笨办法而是在集群内部内置了自动故障转移机制。一个主节点不可达时它下面的从节点会尝试提升自己为新主节点继续承担原主节点的哈希槽。这相当于给每个分片配了一个“候补值班人员”主值班人员失联后候补人员自动顶上去。对业务来说故障转移期间的写入抖动窗口越短越好这就是我们要演练的目标。没有这套机制会怎样比如一个 3 主 3 从的集群某台主节点宕机后如果只靠人工去执行CLUSTER FAILOVER可能需要几分钟甚至更久。对于线上高并发场景这几分钟意味着大量报错和超时。自动故障转移的价值就是把不可用窗口从“人工操作分钟级”压缩到“集群自动秒级”。1.2 主节点挂掉之后集群内部到底发生了什么我过去一直以为 Redis 集群的故障转移是“监控程序发现主节点挂了然后喊从节点上位”。实际完全不是这样Redis Cluster 是去中心化的没有一个专门的监控节点。每个节点都会通过 cluster bus 端口和其他节点持续交换 gossip 消息故障判断也一样。当一个主节点在cluster-node-timeout时间内没有响应时其他节点会先把这个主节点标记为PFAILpossibly failed可能下线。注意这只是一个节点的主观判断不代表它真的挂了可能是网络抖动或者 GC 停顿。接下来这个PFAIL状态会随着 gossip 消息在集群里传播当多数主节点都认为它不可达时状态才会从PFAIL升级为FAIL此时才进入真正的故障转移流程。从节点看到自己的主节点处于FAIL状态会发起一轮类似 Raft 的选举每个从节点把当前的currentEpoch加一然后向集群里其他主节点请求投票获得多数主节点投票的从节点胜出。胜出的从节点会把自己的configEpoch更新成集群里最新且唯一的数值然后向集群广播“我接替了原主节点”之后这套分片下的哈希槽就归属到新主节点了。这里值得特别强调Redis Cluster 的故障转移不是靠“谁复制得最多”直接决定的而是通过“多数投票”这种民主机制来避免脑裂。如果主节点只是网络分区但还在运行而选举已经完成那么旧主节点重新通信后会发现自己的configEpoch已经落后自己会主动降级为从节点重新复制新主节点。这种设计非常聪明不是“谁的机器配置高谁上位”而是“谁先拿到多数票谁接管”。1.3 自动、强制、手动三种故障转移的适用场景Redis Cluster 里故障转移并不是只有自动一种还有几条和故障转移相关的命令很多人在面试或者实际运维中容易搞混。下面这张表是我日常使用时的理解方式触发命令适用场景风险自动故障转移节点被多数主节点标记为 FAIL 后自动选举主节点宕机、网络分区等突发情况自动切换无需人工干预但切换时间受cluster-node-timeout影响手动故障转移在从节点执行CLUSTER FAILOVER计划内维护、主节点版本升级、机房割接比较安全主从会先做数据对齐再切换角色强制故障转移CLUSTER FAILOVER FORCE主节点失联且你确定没有数据一致性问题会跳过部分检查极端情况下可能丢数据接管故障转移CLUSTER FAILOVER TAKEOVER主节点物理损坏需要从节点直接强行上位会忽略多数投票一般建议谨慎使用最常用的其实是自动故障转移和手动故障转移。生产环境计划内维护时我会先在从节点上执行CLUSTER FAILOVER做一次优雅切换让主节点的角色先转移走再对旧主节点做维护。这样业务侧只是出现轻微的连接重连不会触发复杂的自动选举流程。1.4 客户端视角MOVED、ASK 和拓扑刷新故障转移对集群内部来说是角色变更但对外部客户端来说问题会更直接。客户端原本连的是旧主节点它持有的哈希槽路由表也指向旧主节点。故障转移之后同一个 key 可能被重定向到新主节点这时候客户端必须正确处理服务端返回的MOVED重定向。MOVED表示目标 key 所在的哈希槽已经永久迁移到了另一个节点普通单机模式的 Redis 客户端不会自动跟随这个重定向只有开启了集群模式比如redis-cli -c或者 JedisCluster、Lettuce 的集群连接才会重新解析路由并刷新本地拓扑缓存。还有个ASK重定向通常出现在哈希槽迁移期间它只是一种临时询问客户端处理完当前命令后不需要永久更新路由表。故障转移和槽位迁移是两件事但很多新手会把它们混在一起。这也是为什么我一直不建议用 Redis Desktop Manager 这类可视化工具去验证集群故障转移。这类工具通常按单节点设计连接的是一个具体端口不会像集群 SDK 一样自动处理重定向。可视化工具只适合看单个节点的内存、键值分布真正的故障转移验证必须用支持集群模式的客户端或者在命令行用redis-cli -c。2. 一套能随便“搞事”的 3 主 3 从测试环境纸上谈兵永远不如直接演练。我建议你在本地搭一套 3 主 3 从的 Redis Cluster然后放心大胆地 kill 主节点开关机也不会影响任何人。搭建方法很多最省事的是用 Docker或者直接用 Redis 官方提供的redis-trib.rb脚本不过在 Redis 5 之后更推荐直接用redis-cli --cluster子命令。2.1 环境准备用 Docker 拉起 6 个 Redis 节点我使用的镜像是redis:7.2-alpine配置参数如下开启集群模式、使用独立端口、开启 AOF、设置cluster-node-timeout。我习惯用一个 shell 循环把 6 个节点一次性拉起来for port in 7000 7001 7002 7003 7004 7005; do docker run -d \ --name redis-cluster-${port} \ --network host \ -v ~/redis-cluster/${port}:/data \ redis:7.2-alpine \ redis-server --port ${port} \ --cluster-enabled yes \ --cluster-config-file nodes-${port}.conf \ --cluster-node-timeout 5000 \ --appendonly yes done这里用--network host是最省事的方案节点之间直接用127.0.0.1通信不需要额外处理端口映射也不需要配置cluster-announce-ip。如果你用的是 macOS 或者 Windows 的 Docker Desktophost 网络模式在某些版本下有限制那就需要把业务端口和集群总线端口都映射出来比如启动 7000 节点时加上-p 7000:7000 -p 17000:17000并且给 Redis 加上--cluster-announce-ip 127.0.0.1 --cluster-announce-bus-port 17000否则容器里的节点会用容器 ID 或内网 IP 互相通信集群会建得一团糟。如果你是 Windows 用户网上有大量“redis 下载安装配置”的教程但我强烈不建议在 Windows 原生环境里跑多节点集群权限和网络隔离问题会让你怀疑人生。更好的选择是用 WSL2 里的 Linux 环境或者直接装个 Docker Desktop。2.2 把 6 个节点拉成 3 主 3 从6 个 Redis 进程都跑起来后执行下面这一行命令redis-cli --cluster create \ 127.0.0.1:7000 127.0.0.1:7001 127.0.0.1:7002 \ 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005 \ --cluster-replicas 1命令中的--cluster-replicas 1表示给每个主节点分配 1 个从节点。Redis 会按顺序把前三个节点设为主节点后面三个节点按复制最少的策略自动挂到对应主节点下面。执行后会出现一个哈希槽分配计划表确认无误后输入yes稍等几秒集群就创建完成了。创建完成后用redis-cli -p 7000 cluster info检查cluster_state:ok cluster_slots_assigned:16384 cluster_slots_ok:16384 cluster_slots_pfail:0 cluster_slots_fail:0 cluster_known_nodes:6看到cluster_state:ok和cluster_slots_ok:16384就说明一切正常。再用redis-cli -p 7000 cluster nodes | head看一下节点关系输出里带master且后面跟着一段0-5460之类哈希槽范围的是主节点带slave的是从节点。2.3 先确认拓扑并准备好日志和监控命令动手搞故障转移之前一定要先确认每个主节点的从节点是谁。尤其要注意一个点Redis Cluster 的从节点不一定是按端口顺序匹配的有可能 7000 的从节点是 7005而不是 7003。判断方法很简单redis-cli -p 7000 cluster nodes | grep slave或者直接看cluster nodes完整输出找到slave标记后面跟的master的 node id。这个信息非常重要否则你可能以为自己 kill 的是某个主节点结果发现真正失效的分片完全不在预期位置。同时还要把 Redis 日志打开或者准备好docker logs -f。我一般会提前执行docker logs -f redis-cluster-7000 另外准备一个长驻观察窗口watch -n 1 redis-cli -p 7001 cluster nodes | grep -E 7000|7003这样故障转移全程的状态变化都能实时看到。别嫌这一步多余我见过很多人做演练时只盯着cluster info结果节点状态已经变成FAIL自己却完全没意识到切换已经完成了。3. 实战让一个主节点“意外死亡”环境就绪后就可以开始做真正的故障注入实验。我的目标很简单找到某一个主节点直接docker stop掉然后观察集群能否自动完成故障转移。整个过程我会记录时间点、观察日志、测试读写最后再把这台节点重新加回集群。3.1 定位主节点然后 stop 掉假设我们已经确认 7000 端口是一个主节点并且它的从节点是 7003 端口。为了让实验结果更干净你可以先往 7000 写入一批带集群路由的 keyfor i in {1..100}; do redis-cli -p 7000 set key:$i value:$i; done然后记录当前时间执行docker stop redis-cluster-7000这里用docker stop模拟的是彻底宕机相当于物理断电不是优雅退出。如果只 kill 进程Redis 可能还会发一些关闭消息给其他节点故障判断会更快但不够“真实”。生产环境里大多数宕机都是突然的所以用docker stop更合适。3.2 观察自动故障转移的完整过程执行docker stop后我立刻切到watch页面。通常在几秒内就会看到 7000 的节点状态从master变成fail?随后 7003 从slave变成master并且接管原来属于 7000 的哈希槽范围。我用一条时间线把观察到的过程整理成了下表时间点现象集群状态T0s执行docker stop redis-cluster-7000cluster_state:okT1s7001、7002 开始记录 PFAILcluster_state:okT3s7003 从节点状态发生变化gossip 开始传播cluster_state:okT7s多数主节点确认 FAILcluster_state:fail 或短暂异常T9s7003 发起选举并获胜cluster_state:okT11s7003 成为新主节点哈希槽 0-5460 全部接管cluster_state:ok不同版本的 Redis、不同cluster-node-timeout下时间会有些差异。我们配置的是 5000ms所以从 kill 到完成切换大约 10 秒左右。如果你把cluster-node-timeout调成 15000ms那么整个故障转移可能要 30 秒以上这也是生产环境选参时最纠结的地方。在这期间如果你尝试读取 7000 分片上的 key可能会看到类似这样的错误CLUSTERDOWN Hash slot not served别慌这说明集群正在切换中旧的 slot 归属已经失效新的 slot 归属还没完全生效。等到cluster_state恢复成ok后再次读写就正常了。3.3 数据写入与读取验证故障转移完成后最关键的是验证新主节点能正常服务。我习惯用集群模式连接一个还活着的节点redis-cli -c -p 7001 127.0.0.1:7001 get key:50 - Redirected to slot [xxxx] located at 127.0.0.1:7003 value:50注意这里的Redirected to slot信息客户端在 7001 上发起请求但 Redis 发现目标 key 的哈希槽现在由 7003 负责于是返回MOVEDredis-cli -c自动跟着重定向到 7003 并把结果读出来。这正好验证了故障转移后整条链路是通的。还需要检查一下新主节点的复制关系redis-cli -p 7003 info replication正常情况下role会显示为master下面会带着一个slave也就是原来的 7000此时可能还在线也可能已经离线。如果 7000 已经被 stop那么 7003 当前没有可用的从节点但主节点角色已经稳定能继续对外提供服务。这里我要格外说明数据一致性Redis 主从复制默认是异步的故障转移发生前的最后一批写入如果还没来得及同步给从节点在故障转移后就可能丢失。所以如果你的业务要求“主库写入成功就必须有副本收到”可以考虑用WAIT命令等待复制确认但代价是每次写入都要多等一次网络往返。到底要不要牺牲性能换强一致只能由业务场景来决定。3.4 让原主节点回归并做一次“无感切换”自动故障转移结束后原来的主节点 7000 还处于 stop 状态。把它重新启动docker start redis-cluster-7000启动后 7000 会重新加入集群。由于集群的configEpoch已经更新7000 发现自己已经不是当前分片的主节点会自动降级为从节点并向现在的新主节点 7003 发起全量或部分同步。等它同步完成后cluster nodes里 7000 的状态会显示为slave后面跟着的 master 是 7003。这时候如果想“切回去”让 7000 重新当主节点就可以在 7000 这个从节点上执行redis-cli -p 7000 cluster failover手动故障转移和自动故障转移最大的不同是它会先做一次数据对齐。主节点 7003 会把自己当前的复制偏移量信息发给从节点 7000等 7000 追上最后一个偏移量后再完成角色交换。整个过程中理论上业务侧只会看到一次短暂的连接重连不会像自动切换那样出现比较明显的CLUSTERDOWN报错。执行完手动切换后再用redis-cli -p 7000 cluster nodes查看7000 重新变回master7003 变回slave。这一步在真实运维中非常有用比如你要给新主节点升级硬件就可以用同样的方式把角色切到另一台机器上。4. 常见故障场景与排障实录故障转移实验做多了总会遇到各种“看起来没问题、实际很坑”的场景。这里挑几个我真实遇到过的案例覆盖网络分区、切换延迟、客户端超时和容器环境问题每个都可以直接对照排查。4.1 主从切换后业务大面积超时现象集群里发生了一次故障转移Redis 节点本身一切正常cluster_state也很快恢复成ok但业务那边报了一堆超时异常。日志里最常见的就是io.lettuce.core.RedisCommandTimeoutException: Redis command timed out或者 Spring Boot 项目里的RedisSystemException: Redis command timed out。这类问题十有八九不是 Redis 挂了而是客户端还在使用旧的拓扑缓存。Lettuce 这类客户端在启动时会建立到若干节点的连接并缓存哈希槽和节点的映射关系。故障转移后映射关系已经变了如果客户端没有及时刷新拓扑它还会把请求发到原来的旧主节点上然后收到MOVED重定向。有些版本默认不自动刷新拓扑或者刷新周期很长于是连接池里的旧连接不断超时。解决办法是调整客户端的集群拓扑刷新策略。在 Spring Boot 项目中可以开启 Lettuce 的自动刷新spring: data: redis: lettuce: cluster: refresh: adaptive: true period: 5sadaptive: true表示在收到MOVED或ASK后主动触发拓扑刷新period: 5s作为兜底周期。这个配置在不同版本的 Spring Boot 里写法略有差异但思路一致。使用 JedisCluster 时也需要关注JedisClientConfig里的setMaxRedirections适当调大重定向次数避免在故障转移期间一次重定向失败就直接抛异常。4.2 网络分区和脑裂怎么判断故障转移最怕的不是节点宕机而是节点还活着但它和集群其他节点之间网络不通。当一个主节点被隔离但它自己不知道时它可能会继续接收写入。此时另外一部分主节点和从节点看到它失联重新选举出一个新主节点整个分片出现了两个“主节点”在同时提供写入服务这就是脑裂场景。Redis Cluster 靠多数投票来解决这个问题。新主节点必须获得多数主节点的投票才能上位而被隔离的旧主节点无法获得多数票只能在自己的小分区里继续“自嗨”。当网络恢复后旧主节点发现自己持有的configEpoch已经落后它会把自己降级为从节点并且丢弃一段在分区期间写入的数据。这也是分布式系统里最常见的取舍优先保证集群可用和多数一致性而不是保留少数分区的写入。所以判断是否有脑裂风险时第一反应不是去查业务日志而是看当前集群有几个主节点还能互相通信。如果你只有一个 3 主 3 从集群网络故障把两个节点隔离成了 1 主和 2 主两个分区那个只有 1 主节点的分区是不可能完成新选举的因为拿不到多数票最终表现就是cluster_state:fail整个集群不可写。这里还需要注意cluster-require-full-coverage参数。默认是yes意思是只要有任何一个哈希槽没有被覆盖集群就拒绝对外服务。如果你设成no在部分 slot 缺失时其他 slot 还能正常读写但业务方可能会遇到“部分请求成功、部分失败”的奇怪现象。到底开还是不开要根据业务对可用性和一致性的要求来定。4.3 切换慢、切换抖动先查这几个参数如果你发现故障转移耗时总是远超预期大概率是cluster-node-timeout设置过大。这个参数不仅影响节点不可达的判定速度也影响PFAIL提升为FAIL的时间。默认值是 15000ms也就是 15 秒这对于很多互联网场景来说太久了。cluster-node-timeout也不是越小越好。我见过有人为了追求快速切换直接把它调成 1000ms结果一次普通的 GC 停顿或网络抖动就触发了主从切换集群频繁发生无意义的故障转移比宕机还伤。内部网络稳定、节点同机房部署时建议设置为 5000ms 左右跨机房部署时阈值要再放宽至少 10000ms 起步。这是一个需要反复压测后才能确定的参数。还有一个容易被忽略的参数是cluster-replica-validity-factor。它控制从节点在什么情况下才允许参与选举。从节点断连时间越长它落后主节点的数据就越多。如果这个因子设置得太小某些从节点会因为“失联太久”而失去候选资格。如果集群每个分片只有一个从节点而它又被判定为不具备选举资格那整个分片就无法故障转移。所以千万别盲目调小这个值除非你确定每个分片都准备了多个从节点。4.4 容器重建和 K8s 环境下的“新节点迷路”用 Docker 做故障转移实验时最常见的坑是容器被删除后重建。Redis 集群的每个节点都依赖nodes.conf文件持久保存自己的 node id 和集群信息。如果你把容器删掉只重新 run 一个同端口的新容器它的 node id 变了旧 nodes.conf 也丢了新容器加入集群时会变成一个“新节点”集群里其他节点可能还在期望旧 node id导致状态一直对不上。处理办法分两种情况如果是临时测试环境直接清掉所有容器的/data下的 nodes.conf重新--cluster create如果是生产环境务必使用持久化存储或者像 K8s 里部署 Redis Cluster 那样使用 StatefulSet让每个 Pod 有稳定的网络标识和持久化存储。K8s 里跑 Redis 集群比裸机更麻烦一点因为 Pod 重建后 IP 会变化。如果客户端和节点都通过 IP 通信一个 Pod 重启就可能导致其他节点一直尝试连接旧 IP。Redis 7.0 之后支持cluster-announce-hostname可以配合 StatefulSet 的 headless service 让节点用稳定的 DNS 名称互相通信。这部分内容展开是一篇长文但核心思路就是不要让 Redis 节点依赖易变的 IP要让它们有一个稳定的身份。4.5 日志和监控指标速查清单最后给出一份我平时排障时对照的速查表都是故障转移场景下最高频出现的日志关键词和监控指标日志或指标含义处理建议Cluster state changed: ok集群状态变为正常无需处理但可以记录时间点Failover auth granted to ...某个从节点获得了投票授权说明选举正在进行Failover auth denied投票被拒绝检查是否有多个从节点同时竞选cluster_state:fail集群进入不可用状态检查哈希槽是否缺失、主节点是否过少master_link_status:down从节点与主节点断连检查网络、主节点存活状态slave_repl_offset长时间不变从节点复制停滞检查主从连接和 backlog 配置监控层面我至少会盯四个指标cluster_state、cluster_slots_fail、当前各节点的master_link_status、以及复制偏移量差值。任何一项出现异常都大概率会在故障转移前后出现。5. 调优建议与这几年的真实体会演练做完、问题排完最后还是要把经验固化到生产参数和运维流程里。Redis 集群的故障转移不是“能用就行”不同业务对可用性和一致性的要求完全不同参数配置也需要有针对性。5.1 参数调整的推荐起点如果是同机房部署我会先把cluster-node-timeout定在 5000ms 到 10000ms 之间。假设内网 RTT 在 1ms 以内5000ms 已经足够宽裕如果跨机房、跨可用区RTT 会明显增加建议不要低于 10000ms。故障转移耗时大致是cluster-node-timeout的 1.5 到 2 倍所以你可以按“业务能接受几秒不可用”来倒推这个值。cluster-require-full-coverage通常保持默认yes一旦集群出现部分分片不可用宁可整体拒绝写入也不要让业务出现“写一半成功一半失败”的状态。只有当业务侧已经做了完善的降级方案并且你能接受部分数据缺失时才考虑改成no。从节点选举优先级也值得关注。Redis 的从节点有一个replica-priority参数默认是 100数字越小越优先被选为主节点。如果你在两地三中心架构里希望优先在同可用区选举可以把本可用区的从节点优先级调低。不过要注意同一分片里如果有多个从节点它们的优先级差距太大会让选举行为过于可预测某些从节点可能永远没机会上位这对硬件负载均衡并不友好。5.2 别把“有从节点”当成“一定安全”很多团队认为只要每个分片配了一个从节点故障转移就万无一失。但实际上如果唯一的从节点本身也处于断连状态或者复制进度落后太多主节点宕机后是无法完成切换的。更极端的场景是主节点和它的从节点恰好部署在同一台物理机或同一个机架上机柜断电时一起宕机整个分片就完全没有可用节点。所以我的建议是核心业务的分片至少保留 2 个从节点如果实在资源有限至少要保证主节点和从节点分布在不同的故障域。做故障转移演练时也应该把“主从同机”这种反模式列为重点排查对象。可以用redis-cli --cluster check检查集群的副本分布看看有没有多个节点落在同一台宿主机上。5.3 写在最后我的几点真实体会最后分享几条从实际踩坑中得来的经验。第一故障转移能不能顺利执行和参数细节关系极大不要只测“节点挂了之后能恢复”要测网络分区、从节点断连、主节点假死等多种情况。第二客户端拓扑刷新比集群本身更容易成为故障转移的瓶颈Redis 内部切换得再快客户端不及时更新路由业务该超时还是超时。第三所有故障转移参数都可能误伤正常流量调参要反复验证不要一上来就把超时时间压到极致。我自己维护集群时每次故障转移演练后都会做一件事把当天的日志、客户端报错、监控告警全部归档然后对比切换时间是否符合预期。下次遇到类似故障转移我不会急着重启节点而是先看PFAIL/FAIL日志和复制偏移量很多问题都能少掉一半排查时间。这套流程虽然朴素但比临时翻文档可靠得多。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进