分类: 故障排查与调优

  • 深入 Raft 陷阱排查:非对称网络分区引发的 Term 暴涨与 Leader 选举雪崩实战

    某次排查底层分布式 KV 存储集群的大面积超时告警,现象极其极端:读写 QPS 瞬间跌穿到零,P99 延时直接打满,整个集群处于完全瘫痪状态。最终结论并不复杂:机房一台边缘交换机故障,导致节点网卡出现非对称半连接(能发不能收)。由于该版本的 Raft 引擎实现未强制开启 Pre-Vote(预投票)机制,导致这个被隔离的孤岛节点疯狂发起选举,疯狂拉升全局的 Term(任期),合法 Leader 被这颗携带高 Term 的“毒丸”反复强制拉下马。解决思路:直接在引擎配置中强行开启 Pre-Vote 与 CheckQuorum,并在网络层剔除丢包源。

    案发现场:QPS 归零与疯狂跳动的 Term

    排查过程中,第一眼看 Grafana 面板,rate(raft_server_proposals_applied_total[1m]) 已经掉进冰窟窿,没有任何请求能被成功 Commit。与之形成鲜明对比的是,raft_term 指标像坐了火箭,几分钟内从 1024 飙升到了 85401 并在持续暴涨。

    切到原 Leader 节点 A 的日志,满屏的告警:

    [WARN] raft: Node A received RequestVote from Node C with higher term 85401. Stepping down.
    [INFO] raft: Node A entered state Follower
    [INFO] raft: Node A election timeout, starting election...
    [INFO] raft: Node A became candidate at term 85402
    [INFO] raft: Node A became leader at term 85402
    [WARN] raft: Node A received RequestVote from Node C with higher term 85403. Stepping down.
    

    集群并没有发生严重的硬件宕机,但这 5 个节点不仅没有稳定选出新 Leader,反而陷入了无休止的“选举-下台-再选举”的死循环。

    愚蠢的底层逻辑:教科书版 Raft 的致命缺陷

    Raft 协议的强 Leader 模型是把双刃剑。如果在生产环境中直接生搬硬套基础版本的 Raft 协议,而不引入 Diego Ongaro 在博士论文第 6 章提到的工程扩展,就是给自己埋雷。

    在这个案发现场,节点 C 遭遇了非对称网络分区:它收不到 Leader A 发来的 Heartbeat,导致自身的 election timer 必定超时。按照标准协议,节点 C 会做两件事:

    1. Term = Term + 1

    2. 转换为 Candidate 并向集群广播 RequestVote

    由于是非对称分区,C 的拉票请求成功发给了 A、B、D、E。尽管 C 的日志可能不是最新的,它绝对不可能赢得多数派选票当上 Leader。但是,Raft 协议里有一条铁律:任何节点(包括现任 Leader)只要收到比自己大的 Term,必须立刻无条件退化为 Follower。

    节点 C 携带的高 Term 就是一颗无解的“毒丸”。这导致了极其恶心的循环: 孤岛节点 C 增加 Term -> 广播给集群 -> 现任 Leader A 看到更高的 Term 被迫下台 -> 集群暂时无主,发起新一轮选举 -> A 或 B 凭借更新的日志重新当选 Leader -> 刚当选,又收到孤岛 C 发来的更高 Term 的拉票请求 -> 再次被迫下台。

    结果就是整个集群变成了一台只消耗 CPU 跑选举、不干任何业务逻辑的“Term 自增器”。

    破局与防御性配置:Pre-Vote 与 CheckQuorum

    要彻底阻断这种因为单点网络抖动引发的全局雪崩,必须在架构层面实现“防御性编程”。主流成熟的 Raft 实现(如 etcd/raft, TiKV)都已经内置了相关机制,但很多时候默认并未开启或配置不当。

    核心修复手段是开启 Pre-Vote(预投票)。

    Pre-Vote 的逻辑非常克制:节点在真正增加自己的 Term 并发起正式选举之前,必须先发起一轮预投票(PreVote)。只有当集群中大多数节点回复“我愿意投给你”(意味着这些正常节点也没有收到有效 Leader 的心跳)时,该节点才被允许自增 Term 并进入正式的 Candidate 状态。

    在我们的配置文件中,直接将以下参数打死:

    raft:
      # 强制开启预投票,阻断孤岛节点无脑推高 Term
      enable_pre_vote: true
    
      # 开启 CheckQuorum:Leader 如果连不上多数派会自动 StepDown,防止脑裂和幽灵复现
      check_quorum: true
    
      # 选举超时基准值与抖动值,防止多个候选人同时发起选举导致选票瓜分 (Split Vote)
      election_timeout: 1000ms
      election_timeout_jitter: 500ms
    

    开启 Pre-Vote 后,当孤岛节点 C 再次因为收不到心跳而发起预投票时,正常节点 A 和 B 仍在正常通信,确信当前 Leader A 存活,它们会直接拒绝 C 的预投票请求。C 拿不到多数派的预投票许可,它的 Term 就永远被按死在原地,无法再去毒害现任 Leader,系统瞬间恢复高可用。

    排查清单:同类问题速查

    1. 监控 raft_term 增速:正常集群的 Term 几个月都不会变。如果出现短时间内成百上千次的暴涨,100% 存在网络隔离或节点假死导致的频繁选举。

    2. 核对非对称网络连通性:不要只在 Leader 上 ping Follower。必须全节点交叉使用 ping 和 nc -vz 验证双向连通性与端口状态,特别是大包传输是否有 Drop。

    3. 确认 Pre-Vote 开启状态:检查你所使用的中间件/自研引擎的 Raft 配置,未开启 Pre-Vote 的引擎不具备抵抗网络分区的生产级能力。

    4. 排查 election_timeout 设置:如果底层磁盘 I/O 毛刺(如 Fsync 慢)导致的心跳处理延迟大于 election_timeout,同样会引发假选举。确保 election_timeout 至少是心跳间隔的 5~10 倍以上。