
1. 为什么分片集群需要自动故障转移我之前接手一个电商后端项目时缓存量上来之后最先扛不住的不是 CPU而是单机 Redis 的内存。数据量一涨maxmemory一设淘汰策略一触发缓存命中率唰唰往下掉业务方天天找上门。后来把架构从“单机 主从哨兵”切到 Redis 分片集群Cluster这一刀切完容量是够了可真正上线前做故障演练时才发现“自动故障转移”这六个字背后藏着一条完整的判定链路从节点标记、投票、晋升、槽位接管到客户端感知任何一个环节配错故障转移要么不触发要么触发了业务照样超时。这篇不是复制官方文档是我把一套 6 节点的 Redis Cluster 从部署、模拟宕机到观察故障转移全流程跑完之后的经验总结。适合谁看适合正在从哨兵方案迁移到 Cluster、准备做高可用压测、或者被“redis command timed out”这类偶发超时折腾的运维和 Java/Go 后端同学。看完之后你能回答这几个问题从节点什么时候有资格晋升、为什么有些集群宕了一台主节点半天切不过来、故障转移期间客户端到底会不会断连、以及哪些参数是必须调的。1.1 从“主从哨兵”到“分片集群”是一次架构必然先别急着上手敲命令得先搞清楚为什么一定要有自动故障转移。Redis 主从复制解决的是数据冗余和读扩展哨兵Sentinel解决的是主节点宕机后的自动切换。这套组合拳在小数据量场景下非常好用主节点写、从节点读主节点挂了哨兵把从节点提升上来业务无感知。但它的天花板也很明显所有写流量还是压在一台主节点上单机内存上限决定了整个缓存集群的容量上限。网上很多教程拿“黑马点评”这类电商实战项目练习时最开始用的也是单机或主从模式等到你要缓存商品详情、库存、用户会话、热点榜单时一台 8GB 内存的机器根本不够放。此时你面临的选择是纵向扩容换更大内存的机器贵且到头或横向分片。横向分片的本质就是把 key 按照某种规则散到多台机器上每台只存一部分数据。Redis Cluster 用的是哈希槽hash slot方案一共 16384 个槽通过CRC16(key) % 16384算出 key 落在哪个槽再由槽映射到具体节点。这样加机器时把一部分槽挪过去就能水平扩展数据不用全量重新分布。但“分片”只是解决了容量问题没解决可用性问题。任意一台分片节点宕机它负责的那部分槽就无法读写整个集群等于残废。所以 Redis Cluster 的每个分片内部仍然保留了“一主一从”甚至“一主多从”的结构分片内主节点负责读写从节点只是复制数据一旦主节点故障从节点要能自动顶上否则分片集群依然是脆弱的。1.2 “分片”和“自动故障转移”为什么必须一起谈很多初次用 Cluster 的同学会误解一件事以为只要把多个 Redis 实例用redis-cli --cluster create串起来集群就天然高可用了。实测不是这样。Cluster 的自动故障转移是一个独立运行的机制需要同时满足节点心跳超时判定、客观下线确认、从节点竞选、槽位接管四个阶段才算完整。我见过一个真实案例某团队部署了三主三从的 Cluster压测时拔掉一台主节点网线等了两分钟业务还在报错查cluster nodes发现从节点一直处于disconnected状态根本没有发起竞选。后来排查发现是cluster-node-timeout设置成了 60 秒主观下线判定拖太久且部分节点的cluster-replica-validity-factor默认值加复制延迟导致从节点主动放弃了竞选资格。所以说自动故障转移不是某个开关一打开就完事它背后是一整套分布式共识流程。下一节我把这条链路从头到尾拆开讲你看完再去配置才知道每个参数为什么要这么设而不是照着别人的redis.conf抄。2. 拆解故障转移闭环从心跳超时到新主接管自动故障转移全流程可以拆成四步主观下线PFAIL、客观下线FAIL、从节点竞选failover election、角色切换与槽位接管。很多人只知道“主节点挂了会自动切换”但没搞懂每一步的触发条件和投票规则排查问题时就会非常被动。2.1 主观下线 PFAIL 与客观下线 FAIL 的判定差异Redis Cluster 中的每个节点都通过 Gossip 协议定时向其他节点发送 ping/pong 消息消息里带上自己视角的部分节点状态。正常网络下节点之间每秒有一次心跳。如果节点 A 在cluster-node-timeout时间内没有收到节点 B 的有效响应A 会在本地把 B 标记为PFAILpossible failure主观下线。主观下线的“主观”二字很关键它只是 A 单方面的判断可能是网络抖动、GC 停顿、CPU 飙高甚至负载均衡层丢包导致的误判所以 PFAIL 状态并不会直接触发故障转移。要让 Cluster 真正认为某个主节点挂了需要走客观下线流程节点 A 会通过 Gossip 消息把“我认为 B 是 PFAIL”的信息传播出去当集群中持有槽位的主节点里大多数都认为 B 不可达时B 才会被标记为FAIL客观下线。这里要注意“大多数”不是全集群节点而是持有槽位的主节点。比如三主三从集群三个主节点中至少两个都认为 B 不可达B 才能从 PFAIL 变成 FAIL。这样设计的目的是避免一个节点误判就导致整个分片切换。默认的客观下线流程要求多数持有槽位的主节点确认这意味着如果网络分区比较恶劣只有一部分主节点能收到 PFAIL 消息客观下线就会悬而不决从节点一直等不到晋升信号。2.2 从节点竞选的资格与投票规则主节点被标记为 FAIL 之后持有槽位的主节点会向集群广播一条 FAIL 消息此时处于该分片内的从节点开始判断自己是否有资格竞选。判定的核心指标是复制偏移量replication offset和cluster-replica-validity-factor。复制偏移量代表从节点已经同步到主节点的数据位置。如果主节点宕机前刚写入了一条数据从节点还没来得及复制此时从节点晋升就会丢这条写请求。cluster-replica-validity-factor默认是 10它乘以cluster-node-timeout得到允许的最大复制延迟如果从节点的复制偏移量落后主节点太多超过这个阈值它会放弃竞选因为晋升上去也是数据残缺不如等原主恢复或人工介入。符合资格的从节点不会立刻发起竞选而是等待一个随机延迟延迟范围是0 到 cluster-node-timeout * (replica-priority 1)中的一个随机值其中replica-priority越小代表优先级越高。这样设计是防止多个从节点同时投票导致票数分散、谁都无法过半也让数据最新、优先级最高的从节点大概率先发起请求并拿到多数票。选举采用 Raft 风格的规则从节点发送FAILOVER_AUTH_REQUEST只有持有槽位且状态正常的主节点有权回复FAILOVER_AUTH_ACK从节点拿到超过半数主节点的 ACK 后就赢得本次故障转移的胜利。2.3 槽位接管的真实机制与 epoch 纪元我最初有个误解以为主节点下线后从节点晋升还要把槽里的数据从其他节点搬过来后来看代码和实测才发现根本不是一回事。从节点在平时一直在复制主节点的全量数据两者数据几乎一致所以从节点晋升后是直接“继承”原主节点持有的所有槽位不需要跨节点迁移数据。实现方式是通过 epoch纪元机制。每轮故障转移都会产生一个新的currentEpoch赢得竞选的从节点把自己的 epoch 更新为更大的值并向全集群广播 PONG 消息声明“我现在是这些槽的新主节点”。其他主节点收到这个消息后更新自己的槽位映射表后续客户端无论连到哪个节点都会通过 MOVED 重定向到新主节点。如果原主节点之后恢复上线它会发现当前集群纪元比自己记录的 epoch 更大同时自己的槽位已经被别人接管于是自动降级尝试临时复制新主节点来继续同步数据。这个设计保证了故障转移后只要复制链路没断裂数据不会出现大规模丢失但也带来了一个经典的分布式系统问题——网络分区下旧主节点仍在运行导致的写冲突窗口这个在第四章细聊。3. 实操6 节点分片集群部署与故障转移演练理论讲了半天现在开始动手。我用的是 Redis 7.0 版本在单台 Linux 服务器上用 Docker 起 6 个节点3 主 3 从然后杀掉其中一个主节点完整观察自动故障转移的全过程。这套流程你在自己机器上也能复现30 分钟跑完。3.1 准备 6 个 Redis 节点的 docker 配置用 Docker 是最快的但有一个要注意的点Redis Cluster 的节点之间通过 Gossip 端口默认是当前端口 10000即 7000 对应 17000通信容器部署时必须把两个端口都映射出来否则节点之间互相发现不了。我习惯用一个目录统一放配置文件每个实例一个子目录。先创建配置文件模板以 7000 端口为例/opt/redis-cluster/7000/redis.conf内容如下port 7000 cluster-enabled yes cluster-config-file nodes-7000.conf cluster-node-timeout 5000 appendonly yes appendfilename appendonly-7000.aof protected-mode no daemonize no解释几个关键参数cluster-enabled yes是开启集群模式cluster-config-file是集群拓扑信息的持久化文件Redis 会把槽位映射、节点 ID 等信息写进这个文件下次启动时直接恢复cluster-node-timeout 5000表示节点心跳超过 5 秒没收到响应就标记 PFAIL这是我建议的默认值太大会拖慢故障转移太小容易误判。剩下 7001 到 7005 五个目录里的配置文件只改端口号和文件名。启动容器时我在一个自定义网络里依次启动docker network create cluster-net for port in 7000 7001 7002 7003 7004 7005; do docker run -d --name redis-$port \ --network cluster-net \ -p $port:7000 \ -p $((port 10000)):17000 \ -v /opt/redis-cluster/$port/redis.conf:/usr/local/etc/redis/redis.conf \ redis:7.0 redis-server /usr/local/etc/redis/redis.conf done容器内固定使用 7000 和 17000 端口宿主机把 7000~7005 映射到容器的 7000把 17000~17005 映射到容器的 17000。容器之间通过cluster-net自定义网络直接用内网 IP 通信不依赖宿主机端口转发宿主机映射端口只是为了方便本机用redis-cli -p 7000连接调试。提示不要试图把 Gossip 端口改成 10001、10002 这类自定义值除非你同时改cluster-port配置并在所有节点上保持一致。节点之间互通时会从配置里读取端口信息一旦各节点端口不统一拓扑会发现“我连了你的 7000但你告诉我你的端口是 17001”非常容易错乱。3.2 创建集群并确认主从与槽位分布六个节点都起来后用官方的集群创建命令一把梭redis-cli --cluster create \ 172.20.0.2:7000 172.20.0.3:7001 172.20.0.4:7002 \ 172.20.0.5:7003 172.20.0.6:7004 172.20.0.7:7005 \ --cluster-replicas 1这里我直接用了容器在cluster-net网络里的 IP因为节点之间的 Gossip 要把 IP 写进自己的拓扑文件如果用127.0.0.1创建跨容器通信会全部失败。如果你的环境不方便手动查 IP可以用docker inspect redis-7000 | grep IPAddress获取。命令里的--cluster-replicas 1表示每个主节点配 1 个从节点。Redis 会自动分配主从角色尽量把主节点和它的从节点分到不同容器。创建成功后看一下节点状态redis-cli -p 7000 cluster nodes输出类似node_id_7000 172.20.0.2:700017000 myself,master - 0 0 1 connected 0-5460 node_id_7001 172.20.0.3:700117001 master - 0 0 2 connected 5461-10922 node_id_7002 172.20.0.4:700217002 master - 0 0 3 connected 10923-16383 node_id_7003 172.20.0.5:700317003 slave node_id_7000 ... node_id_7004 172.20.0.6:700417004 slave node_id_7001 ... node_id_7005 172.20.0.7:700517005 slave node_id_7002 ...确认三个主节点分别持有 0-5460、5461-10922、10923-16383 三组槽位三个从节点各挂一个主节点。此时cluster info里的cluster_state:ok、cluster_slots_assigned:16384才是健康状态。3.3 模拟主节点宕机观察自动切换全过程演练开始前我先把一批测试 key 写进 7000 节点负责的槽位区域方便之后验证数据是否可读for i in {1..1000}; do redis-cli -c -p 7000 set key:$i value:$i /dev/null done-c参数表示集群模式客户端会自动处理 MOVED 重定向。确认 key 都写进去后直接杀掉 7000 节点的容器docker kill redis-7000注意这里我用kill而不是pause因为进程直接消失会让节点心跳立刻失联。如果是模拟网络抖动用pause更合适。接下来观察 7003 从节点的状态变化。因为cluster-node-timeout是 5 秒所以大约 5 秒后其他节点会开始把 7000 标记为 PFAIL再经过一轮 Gossip 传播和投票真正完成客观下线还需要 1 到 2 秒。实测从 kill 到 7003 成功晋升为主节点大约用了 6 到 8 秒。期间不停执行redis-cli -c -p 7001 cluster nodes | grep 7003约 8 秒后输出变成node_id_7003 172.20.0.5:700317003 myself,master - 0 0 4 connected 0-5460这意味着 7003 已经从slave变成了master并且完整接过了原来的0-5460槽位。再执行redis-cli -p 7003 get key:500能正常返回数据。整个故障转移过程中写入命令会短暂报错CLUSTERDOWN在客观下线达成前或者MOVED重定向客户端如果配置了自动重连和拓扑刷新业务侧只会感受到一次短暂的命令超时。3.4 原主恢复后的角色回退机制演练还没有结束。我把 7000 容器重新启动等它重新加入集群后查看cluster nodes会发现原来的 7000 主节点已经变成了slave并且挂在了新的 7003 主节点下面。Redis 用 epoch 机制识别出自己已经“过气”自动放下身段开始复制新主节点的数据。这里有个实用技巧如果原主节点恢复后不想让它作为从节点挂在老分片里想让它重新拿回主角色可以先redis-cli -p 7000 cluster failover手动让 7000 从 7003 手里接管槽位演练时就按“原主降级为从新主继续服务”来观察即可。这个设计保证故障转移后集群不会出现两个节点同时声称持有同一批槽位的情况。4. 自动故障转移常见坑与排查实录部署和演练都跑完了接下来聊点我在多个项目里真刀真枪遇过的故障转移问题。这些问题在官方文档里都有解释但文档只告诉你“是什么”不会告诉你排起来有多头大。4.1 从节点不竞选先查这三项配置有人给我看过他集群的cluster nodes输出主节点已经显示FAIL但对应的从节点一直停在slave状态过了十几秒才手动切换成功。这种情况十有八九是配置问题我建议按这个顺序排查。首先看cluster-node-timeout是否设得过大。有人图省事直接抄网上配置把超时设成 30000 甚至 60000主观下线就得等 30 秒客观下线还要等一轮 Gossip 周期故障转移总耗时自然被拉长。这个值我建议生产环境 5000 到 10000 之间太小会把 GC 停顿当故障太大则切换太慢。其次看cluster-replica-validity-factor。如果从节点长时间和主节点断连、复制偏移量落后太多它会主动放弃竞选。你可以在从节点上执行redis-cli -p 7003 info replication观察master_link_status和master_repl_offset如果两个值不正常说明复制链路本身有问题故障转移当然不会触发。最后还有个隐蔽问题故障发生在网络分区场景时比如主节点 A 所在子网只有少数持有槽位的主节点那么 A 收到的 FAIL 报告不足以构成多数客观下线就会卡住从节点自然不会发起竞选。这种场景不是配置错了而是部署拓扑本身没有考虑机房冗余排查时要站在分区角度重新审视节点分布。4.2 转移窗口期的客户端超时与连接池适配Java 项目里最常见的报错就是redis command timed out; nested exception is io.lettuce.core.RedisCommandTimeoutException。故障转移窗口期主观下线判定 投票晋升一般有几秒到十几秒这窗口内原主节点已不可用新主节点还没接管客户端的命令就会堵塞到超时。很多帖子把这个报错归结为“网络慢”或者“连接池不够”其实是没意识到自己连接的是 Cluster而 Cluster 在做高可用切换。客户端侧能做的有三件事一是把命令超时时间设得比cluster-node-timeout略大比如 5 秒的心跳超时就配 3 到 5 秒的命令超时给故障转移留出缓冲区二是配置连接池时不要只连一个节点尽量传入多个种子节点地址Lettuce/Jedis Cluster 会自动发现拓扑三是开启自动刷新拓扑Lettuce 里对应TOPOLOGY_REFRESH配置默认是 60 秒刷新一次故障转移后如果还是旧路由客户端会一直收到MOVED重定向刷新生效后才会稳定。注意Spring Boot 项目里如果只配了spring.redis.host和spring.redis.port客户端连的是普通 Redis 而不是 Cluster。要连集群请用spring.redis.cluster.nodes配置多个节点地址否则你看到的“连接不上 Redis”其实是没有走到集群模式。另外我遇到过一个蛋疼的情况Spring Boot Lettuce 在故障转移结束后一切正常但新的主节点刚晋升同一批热点 key 被并发打到其他从节点读取从节点默认只读且可能短暂返回旧数据。如果业务允许一定延迟可以接受如果不行尽量让客户端只走主节点读或者给这些从节点挂上replica-read-only no并承担数据不一致的代价。4.3 网络分区、锁丢失与容量覆盖策略上面说 Redis 故障转移是“大体一致”而非强一致。具体表现为主节点和从节点之间复制延迟超过阈值时主节点可能已经接受了写请求但从节点还没同步过来此时发生故障转移这条写请求就丢了。换到分布式锁场景就是最经典的“锁丢失”问题线程 A 在主节点上拿到锁并写入主节点未同步就宕机从节点晋升后锁不存在线程 B 也能拿到锁两个线程同时进入临界区。这个问题没有银弹业界比较常见的做法是 RedLock多节点独立锁或者在业务层引入 fencing token每次加锁分配递增编号旧编号的写请求直接拒绝。如果你不想引入额外复杂度至少要做到两点一是把cluster-require-full-coverage设为no防止某个分片故障时整个集群拒绝服务二是监控复制延迟延迟过高时提前告警把故障影响扼杀在引发问题之前。5. 关键配置参数与客户端适配速查这一节我整理一张配置速查表把上面分散提到的参数汇总一下方便你部署前直接对照检查。5.1 服务端参数速查表参数默认值我的建议说明cluster-enablednoyes必开集群模式开关cluster-config-filenodes.conf每个实例独立集群拓扑持久化文件cluster-node-timeout150005000~10000主客观下线判定基准太小误判、太大切换慢cluster-replica-validity-factor1010从节点允许的最大复制延迟倍数cluster-migration-barrier11~2一个主节点至少保留多少个从节点防止大量迁移cluster-require-full-coverageyesno槽位未全覆盖时是否拒绝服务生产强烈建议 nocluster-allow-reads-when-downno视业务集群部分故障时是否允许读replica-priority100按节点能力设置值越小从节点竞选优先级越高特别提醒cluster-require-full-coverage。Redis 以前默认是yes意味着只要有一个槽位不可用整个集群的所有读写都会被拒绝。这个设计保证了强一致性但可用性就差了。我们做过一次演练三主三从中坏了一个主节点故障转移尚未完成的那几秒CLUSTERDOWN错误铺天盖地连带其他分片的正常业务也全部被拒。改成no后故障分片内的请求依然会报错但其他分片能继续服务整体影响面小很多。5.2 客户端连接 Cluster 的配置要点服务端配置好了客户端也要跟着适配否则故障转移做了也白做。Java 生态里我用 Lettuce 和 Jedis 比较多先说 Lettuce。连接 Cluster 至少需要两个配置点spring: data: redis: cluster: nodes: - 10.0.0.1:7000 - 10.0.0.2:7001 - 10.0.0.3:7002 timeout: 3000ms lettuce: cluster: refresh: adaptive: true period: 10stimeout建议 3 到 5 秒不要设成 1 秒否则故障转移窗口期全是超时异常。adaptive: true是让客户端在收到 MOVED/ASK 时主动刷新拓扑而不是傻等固定周期。Jedis 对应的是JedisCluster构造时传入节点集合即可连接池和超时参数同理。Python 的redis-py要用RedisCluster而不是普通RedisGo 的go-redis用redis.NewClusterClient。这些客户端类库都实现了槽位缓存和自动重定向但前提是你传的种子节点地址要正确不能只传一个单机地址。还有一点容易被忽略多个客户端之间如果使用不同的序列化器同一个业务 key 在不同端看起来可能完全不一样这不仅影响缓存命中率还会让你在做故障转移演练时误判“数据丢了”。统一用 String 序列化 KeyValue 按业务选择 JSON 或二进制能少踩很多坑。5.3 中间件场景与数据类型的选择建议顺带聊一个我在团队里经常被问的问题Redis 在项目里除了做缓存还经常被当成中间件用比如分布式锁、延迟队列、限流器、幂等表。分片集群的自动故障转移对缓存场景是透明的利好缓存键丢了可以从数据库重建但对中间件场景就要谨慎得多。以分布式锁为例如果你用的是SET key value NX PX 10000这种常规锁一旦发生故障转移锁可能不可靠这点上面已经分析过如果你的延迟队列依赖 List 的BLPOP故障转移期间消费者会短暂阻塞需要用超时重试来兜底。不同数据类型在故障转移下的表现也不一样String 类型的缓存丢了大不了回源但 Hash/ZSet 里的会话状态、排行榜如果丢了恢复起来就要碰运气。所以我的建议是缓存数据可以大胆放 Cluster但存放“业务强一致状态”的 Redis 尽量单独部署别和缓存混用。实践里很多团队会把分布式锁单独放在一个小集群或者哨兵架构里容量不用大但稳定性优先这样即便主缓存集群发生故障转移核心锁服务也不受影响。6. 故障转移演练的落地经验与监控文章到这里技术链路和参数基本讲完了。最后分享我在实际项目里总结出来的经验以及一些可以量化故障转移效果的方法。6.1 演练环境越接近生产结果越可靠第一次演练故障转移一定要在真实网络环境里做不要用localhost。我最初在本地 Mac 上用127.0.0.1起 6 个节点故障转移 2 秒就完成了后来上到云服务器才发现跨机房网络延迟、防火墙端口、容器网络模式都会影响 Gossip 判定实际切换耗时比本地慢了好几倍。用 Docker 演练时建议至少跨两台宿主机模拟真实的网络分区而不是在一台机器上自嗨。另外演练要分成几个等级先手动 kill 进程再模拟拔网线再模拟整个机房的网络分区最后再叠加高写入压力。每个等级暴露出来的问题都不一样。我最深的一次教训是单纯 kill 进程时故障转移很顺但压着每秒 5 万次写入时再 kill从节点因为复制偏移量落后太多直接放弃了竞选等了 30 秒业务才恢复。所以演练一定要带流量不然只是走过场。6.2 参数调整要凭监控数据不是拍脑袋故障转移不是越频繁越好也不是越快越好。cluster-node-timeout设成 1 秒虽然能让主节点 1 秒后就被判定故障但节点 GC 停顿 1 秒在 Java 应用里很常见可能导致主节点明明活着却被摘掉。我见过有人把超时调到 1000ms结果每天凌晨 Full GC 时集群自动切换十几次数据还没丢先把运维吓坏了。合理的做法是先收集一段时间的监控数据看看节点的心跳延迟 P99、GC 暂停时长、网络抖动频率再反过来定cluster-node-timeout。如果网络本身很稳定5 秒足够如果跨机房部署可能 10 秒更稳妥。replica-priority也一样最好给从节点配置不同的优先级比如 SSD 机器上的从节点优先级调低数值小让它更容易在故障时被选中而不是两个从节点凭运气竞争。6.3 客户端超时与降级兜底是最后防线无论 Cluster 多高可用客户端和调用方永远要设置超时时间并且要有降级兜底。网上很多项目教程里同学们连不上 Redis 第一反应是换密码、清密码、重启 Redis但如果连接的是 Cluster更多时候是拓扑刷新慢导致的临时不可用。给调用方设置 3 秒超时超时后走本地缓存降级用户体验会好很多。我见过一个很典型的案例三主三从集群里一台主节点所在机器网络异常故障转移在 7 秒内完成但因为命令超时设成了 1 秒那 7 秒内的所有请求全部报错监控面板上一片红。后来把超时改成 3 秒并加了本地降级同样是 7 秒的故障转移窗口业务反馈几乎无感。6.4 用监控量化故障转移的效果最后说说怎么验证故障转移真的达标了。我一般关注四个指标cluster_state是否一直为 ok除了切换瞬间、cluster_slots_assigned是否等于 16384、从节点的master_link_status是否从 up 短暂变 down 后恢复、以及复制偏移量差值是否在阈值内。这些指标用redis_exporter配合 Prometheus 就能采到没有的话写个定时脚本轮询cluster nodes也行。演练完之后要记录一个关键数字从故障发生到新主节点开始接收写入的总耗时。把这个数字和你的客户端超时时间比一比只要超时时间大于故障转移耗时业务侧大概率只会闪断一次如果超时时间小于故障转移耗时就要考虑调大超时或者降低切换耗时。我在几个项目里都把这个数字写进了发布手册每次做架构变动后都要重新压测一轮确认自动故障转移的表现没有退化。我记得之前有一次生产故障三主三从集群里一台主节点所在机器网络异常故障转移在 7 秒内完成但因为我们把命令超时设成了 1 秒导致那 7 秒内的所有请求全部报错监控面板上一片红。后来把超时改成 3 秒并加了本地降级同样是 7 秒的故障转移窗口业务反馈几乎无感。这也是我推荐你在自己的项目里先做一轮故障注入测试的原因——把最坏情况想清楚比把参数调得完美更实际。