案例:etcd 脑裂故障复盘
一句话定位:3 节点 etcd 集群因机房网络分区导致 2 节点失联,集群瞬间不可写,核心业务全挂 23 分钟——一次典型 Raft 多数派失效故障的完整应急复盘。
写在前面
etcd 是 K8s 的"心脏",所有集群状态都存在它里面。平时我们聊 etcd 高可用,大多停留在"3 节点能挂 1 个"的理论层面,真遇到网络分区导致多数派失效,很多团队的第一反应是慌。2024 年 6 月,我们生产集群经历了一次 etcd 脑裂:3 节点中 2 节点因机房网络设备故障失联,Raft 协议要求多数派(2/3)在线才能写入,瞬间整个集群 apiserver 全部 5xx,核心业务全挂。
这篇文章复盘这次故障的全过程:从告警触发、影响评估、紧急恢复,到数据一致性校验和长期改进。etcd 脑裂不是"重启就好"的故障,恢复过程中如果操作不当,可能导致数据丢失或脑裂后双主。我把我们踩过的坑、查过的资料、最终的操作步骤都记录下来,希望对大家有帮助。
etcd 脑裂的本质是 Raft 协议的"多数派失效",这是设计上的保护机制,不是 bug。理解这一点,是正确处置这类故障的前提。
案例概览
| 维度 | 内容 |
|---|---|
| 集群规模 | 生产 K8s 1.30,1200 节点,3 控制面 |
| etcd 版本 | v3.5.13,3 节点,部署在控制面节点 |
| 故障时间 | 2024/06/21 14:32:08 - 14:55:17(共 23 分钟) |
| 故障现象 | etcd 无主,apiserver 全部 5xx,业务 Pod 无法调度/重启 |
| 影响范围 | 全集群,新建/调度/扩容全部失败,运行中 Pod 不受影响 |
| 根因 | 机房核心交换机故障,etcd-1/etcd-2 之间网络分区 |
| 恢复方式 | 隔离 etcd-2,member remove 后重新加入,数据校验 |
| 业务影响 | 约 8 万笔交易延迟,无数据丢失 |
故障时间线:
14:32:08 告警:etcd cluster has no leader 14:32:15 告警:apiserver 5xx 率 100% 14:32:20 值班 SRE 确认,拉应急群 14:35:00 初判:etcd 脑裂,2 节点失联 14:38:00 决策:隔离 etcd-2,单节点恢复(错误决策,后修正) 14:42:00 尝试单节点启动失败(数据不一致) 14:45:00 改策略:用 etcd-0(健康节点)数据恢复 14:48:30 etcd-0 单节点成集群,apiserver 恢复 14:52:00 etcd-1 重新加入 14:55:17 etcd-2 重新加入,集群 3 节点正常,业务全恢复 15:30:00 数据一致性校验完成,无丢失一、背景与挑战
1.1 集群架构
┌──────────────────────────────────────────────────────┐ │ K8s 控制面 │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ cp-node-0│ │ cp-node-1│ │ cp-node-2│ │ │ │ apiserver│ │ apiserver│ │ apiserver│ │ │ │ etcd-0 │ │ etcd-1 │ │ etcd-2 │ │ │ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ │ └──────交换机A─┴──交换机B─┘ │ └──────────────────────────────────────────────────────┘ 机房 A 机房 B3 个控制面节点分布在机房 A 的两个交换机域:cp-node-0/etcd-0 在交换机 A,cp-node-1/etcd-1 + cp-node-2/etcd-2 在交换机 B。这个拓扑是后来出事的伏笔——两个节点在同一个交换机域,交换机 B 故障直接带走 2 个 etcd 成员。
1.2 故障现象
6/21 14:32,值班手机被告警刷屏:
[FIRING:1] EtcdClusterHasNoLeader (critical) etcd_cluster_has_leader{job="etcd"} = 0 etcd-0, etcd-1, etcd-2 全部无 leader [FIRING:1] ApiserverDown (critical) apiserver_request_total{code=~"5.."} rate = 1.0 所有 apiserver 5xx 100% [FIRING:1] PodScheduleFail (critical) kube_pod_status_unschedulable 持续增长业务侧反馈:新订单创建失败、Pod 扩容失败、kubectl 全部超时,但已运行的 Pod 业务正常(因为 kubelet 不依赖 etcd)。
1.3 应急挑战
- etcd 不可用,所有 kubectl 命令失败:常规 K8s 运维手段全部失效,必须直接操作 etcd。
- 脑裂状态下数据可能不一致:恢复时选哪个节点的数据?选错会丢数据。
- 操作风险高:etcdctl member remove 操作不当,可能把健康成员也踢出去,雪上加霜。
- 业务压力:每分钟影响约 4000 笔交易,老板每 5 分钟问一次"好了没"。
二、方案设计(应急流程)
2.1 etcd 脑裂应急决策树
2.2 关键决策点
| 决策 | 选项 | 风险 | 选择 |
|---|---|---|---|
| 用哪个节点恢复 | etcd-0(健康) | 数据可能不是最新 | 选(网络分区前是 leader) |
| 是否直接删除失联节点 | member remove | 删错会永久丢成员 | 谨慎,先 backup |
| 单节点能否撑业务 | 单点 etcd | 再挂就全完 | 临时撑,立即扩回 3 节点 |
三、实施过程
3.1 第一步:确认故障范围(14:32 - 14:35)
# 1. 直接连 etcd(绕过 apiserver)exportETCDCTL_API=3ETCD_ENDPOINTS="https://10.0.1.10:2379,https://10.0.1.11:2379,https://10.0.1.12:2379"etcdctl--endpoints=$ETCD_ENDPOINTS\--cacert=/etc/etcd/ca.pem\--cert=/etc/etcd/etcd.pem\--key=/etc/etcd/etcd-key.pem\endpoint status --write-out=table# 输出:# +------------------------+------------------+---------+---------+-----------+# | ENDPOINT | ID | VERSION | DB SIZE | IS LEADER |# +------------------------+------------------+---------+---------+-----------+# | https://10.0.1.10:2379 | 8e9e05c52164694d | 3.5.13 | 4.3 GB | false |# | https://10.0.1.11:2379 | 91bc3c398fb3c146 | 3.5.13 | 4.3 GB | false | ← 超时# | https://10.0.1.12:2379 | fd422379fda50e85 | 3.5.13 | 4.3 GB | false | ← 超时# +------------------------+------------------+---------+---------+-----------+# 三个节点都 false,无 leader,确认脑裂# 2. 查网络连通性ping-c310.0.1.11#不通ping-c310.0.1.12#不通ping-c310.0.1.10#通(本机)# 3. 确认 etcd-0 本地服务状态systemctl status etcd# active (running),但日志疯狂报:# "failed to connect to peer fd422379fda50e85"结论:etcd-0 健康,etcd-1/etcd-2 因网络分区失联,脑裂确认。
3.2 第二步:关键决策与错误尝试(14:36 - 14:45)
3.2.1 错误决策:直接 member remove
我们第一反应是"把失联的两个节点踢了,让 etcd-0 单节点成集群":
# ⚠️ 这个操作后来证明是错的etcdctl--endpoints=https://10.0.1.10:2379\--cacert=...--cert=...--key=...\member remove 91bc3c398fb3c146# 报错:# Error: etcdserver: request timed out# 原因:Raft 协议要求多数派同意才能执行 member remove# 当前只有 1/3,无法达成多数派,命令失败教训:脑裂状态下,member remove 也需要多数派,直接 etcdctl 删不掉。必须先让健康节点单节点启动(脱离原集群配置),再操作。
3.2.2 正确做法:单节点强制启动
# 1. 先备份 etcd-0 数据(救命稻草)etcdctl--endpoints=https://10.0.1.10:2379\--cacert=...--cert=...--key=...\snapshot save /backup/etcd-snapshot-$(date+%s).db# 2. 停止 etcd-0systemctl stop etcd# 3. 修改 etcd 配置:从集群模式改为单节点模式# 关键:--force-new-cluster,用现有数据启动新集群cat>/etc/etcd/etcd.conf.yml<<'EOF' name: 'etcd-0'># 4. 启动 etcd-0(单节点)systemctl start etcdsleep5etcdctl--endpoints=https://10.0.1.10:2379\--cacert=...--cert=...--key=...\endpoint status --write-out=table# 输出:IS LEADER = true,集群恢复(单节点)关键点:--force-new-cluster用本地数据创建新集群,member list 里只有自己。这一步必须确认数据是正确的,因为后续都基于这份数据。
3.3 第三步:恢复 apiserver(14:48)
etcd-0 单节点起来后,apiserver 自动恢复:
# 验证 apiserverkubectl get nodes# 全部 Ready,apiserver 恢复kubectl get pods-nprod|head# 业务 Pod 正常列出# 业务侧确认:新订单创建恢复14:48:30,apiserver 恢复,业务新建/调度恢复,但 etcd 还是单点,风险极高,必须立即扩回 3 节点。
3.4 第四步:修复失联节点并重新加入(14:50 - 14:55)
3.4.1 修复 etcd-1
网络分区原因是交换机 B 故障,网络团队 14:45 修复了交换机,etcd-1/etcd-2 网络恢复。但它们的数据可能和 etcd-0 不一致(分区期间各自有写入尝试),不能直接加入,必须清空数据重新同步。
# 在 etcd-1 上操作systemctl stop etcd# 清空旧数据(⚠️ 确认 etcd-0 数据正确后才做)rm-rf/var/lib/etcd/member# 修改配置:作为成员加入 etcd-0cat>/etc/etcd/etcd.conf.yml<<'EOF' name: 'etcd-1'># 在 etcd-0 上把 etcd-1 加为成员(先加 member 再启动)etcdctl--endpoints=https://10.0.1.10:2379\--cacert=...--cert=...--key=...\memberaddetcd-1\--peer-urls=https://10.0.1.11:2380# 启动 etcd-1systemctl start etcd# 验证:etcd-1 自动从 etcd-0 同步数据etcdctl--endpoints=https://10.0.1.10:2379\--cacert=...--cert=...--key=...\endpoint status --write-out=table# etcd-0 = leader,etcd-1 = follower,数据同步中3.4.2 修复 etcd-2(同样流程)
# etcd-2 上systemctl stop etcdrm-rf/var/lib/etcd/member# etcd-0 上加成员etcdctl--endpoints=https://10.0.1.10:2379\--cacert=...--cert=...--key=...\memberaddetcd-2\--peer-urls=https://10.0.1.12:2380# etcd-2 上改配置并启动(同 etcd-1 流程)systemctl start etcd14:55:17,3 节点全部 online,leader 选举完成,集群恢复。
3.5 第五步:数据一致性校验(15:00 - 15:30)
脑裂期间,etcd-1/etcd-2 可能接受过少量写请求(虽然无法 commit,但 WAL 日志可能有脏数据)。必须校验数据一致性。
3.5.1 集群哈希对比
# etcd 提供了 hash 命令,对比各节点数据哈希etcdctl--endpoints=https://10.0.1.10:2379\--cacert=...--cert=...--key=...\endpoint hashkv--cluster# 输出:# +------------------------+------------------+------------+# | ENDPOINT | ID | HASHKV |# +------------------------+------------------+------------+# | https://10.0.1.10:2379 | 8e9e05c52164694d | 2834567891 |# | https://10.0.1.11:2379 | 91bc3c398fb3c146 | 2834567891 | ← 一致# | https://10.0.1.12:2379 | fd422379fda50e85 | 2834567891 | ← 一致# +------------------------+------------------+------------+# 三个节点哈希一致,数据一致3.5.2 关键资源完整性校验
# 对比关键资源数量etcdctl--endpoints=https://10.0.1.10:2379\--cacert=...--cert=...--key=...\get /registry/pods--prefix--keys-only|wc-l# 28456 条(与故障前监控记录一致)etcdctl--endpoints=https://10.0.1.10:2379\--cacert=...--cert=...--key=...\get /registry/services--prefix--keys-only|wc-l# 1842 条# 业务侧校验:抽样确认订单、支付关键数据# DB 层面数据未受影响(业务数据不在 etcd)校验结论:数据零丢失,一致性正常。
四、踩坑与应急
4.1 踩坑1:member remove 在脑裂下失败
(见 3.2.1)
教训:脑裂状态下任何需要多数派的操作都会失败,必须先--force-new-cluster单节点启动。
4.2 踩坑2:etcd-1 重新加入报 “database schema incompatible”
现象:etcd-1 启动后报错,无法同步。
定位:etcd-1 旧数据没清干净,/var/lib/etcd/member下还有 wal 文件。
修复:
# 彻底清空,包括 walsystemctl stop etcdrm-rf/var/lib/etcd/* systemctl start etcd4.3 踩坑3:apiserver 缓存导致业务偶发异常
现象:etcd 恢复后,部分 apiserver 请求返回旧数据。
定位:apiserver 本地有缓存,etcd 恢复后缓存没刷新。
修复:
# 重启所有 apiserver,强制刷新缓存kubectl-nkube-system rollout restart deploy kube-apiserver# 或直接 ssh 到控制面节点systemctl restart kube-apiserver4.4 踩坑4:监控告警风暴影响判断
现象:故障期间收到 200+ 条告警,真正的根因告警被淹没。
改进:后续做了告警收敛,etcd/apiserver 故障时只推一条聚合告警,其他依赖告警静默。
五、复盘与改进
5.1 故障影响总结
| 指标 | 数据 |
|---|---|
| 故障时长 | 23 分钟 |
| 业务影响 | 约 8 万笔交易延迟,无数据丢失 |
| RTO | 23 分钟(目标 < 15 分钟,未达标) |
| RPO | 0(无数据丢失) |
| 根因 | 机房交换机故障 + etcd 拓扑不合理 |
5.2 经验教训
- etcd 拓扑必须跨故障域:3 节点不能有 2 个在同一交换机,这次是拓扑设计失误。
- 脑裂下 member remove 无效:必须
--force-new-cluster单节点启动,这是核心知识点。 - 数据备份是底线:恢复前必须 snapshot,操作失败还能回滚。
- 清数据要彻底:
/var/lib/etcd/member和 wal 都要清,残留会导致加入失败。 - apiserver 缓存要刷新:etcd 恢复后必须重启 apiserver。
- 告警必须收敛:故障时告警风暴严重影响判断,聚合告警是刚需。
- 应急流程要演练:我们这次操作有犹豫,因为没演练过,后续每月演练一次。
5.3 长期改进
5.3.1 etcd 拓扑优化
把 etcd 节点分散到不同交换机域,甚至跨机房:
改造后拓扑: etcd-0 → 机房A-交换机1 etcd-1 → 机房A-交换机2 etcd-2 → 机房B-交换机1(异地容灾)5.3.2 etcd 监控告警增强
# Prometheus 告警规则groups:-name:etcdrules:-alert:EtcdClusterHasNoLeaderexpr:etcd_server_has_leader == 0for:1mlabels:severity:criticalannotations:summary:"etcd 集群无 leader,可能脑裂"runbook:"https://wiki.example.com/etcd-split-brain"-alert:EtcdMembersUnhealthyexpr:count(etcd_server_health_failures) by (cluster)>0for:2mlabels:severity:criticalannotations:summary:"etcd 有成员不健康"-alert:EtcdClusterNoQuorumexpr:|count(up{job="etcd"} == 1) by (cluster) < 2for:1mlabels:severity:criticalannotations:summary:"etcd 多数派丢失,集群不可写"-alert:EtcdFsyncDurationHighexpr:|histogram_quantile(0.99, rate(etcd_disk_wal_fsync_duration_seconds_bucket[5m])) > 0.1for:3mlabels:severity:warningannotations:summary:"etcd WAL fsync 延迟高,磁盘可能瓶颈"5.3.3 etcd 健康巡检脚本
#!/bin/bash# etcd_health_check.sh - 每日巡检ENDPOINTS="https://10.0.1.10:2379,https://10.0.1.11:2379,https://10.0.1.12:2379"CERTS="--cacert=/etc/etcd/ca.pem --cert=/etc/etcd/etcd.pem --key=/etc/etcd/etcd-key.pem"echo"===== etcd 集群健康巡检$(date)====="# 1. 集群状态echo"--- 节点状态 ---"etcdctl--endpoints=$ENDPOINTS$CERTSendpoint status --write-out=table# 2. 成员列表echo"--- 成员列表 ---"etcdctl--endpoints=$ENDPOINTS$CERTSmember list --write-out=table# 3. 数据哈希一致性echo"--- 数据哈希 ---"etcdctl--endpoints=$ENDPOINTS$CERTSendpoint hashkv--cluster# 4. 告警检查echo"--- 无 leader 检查 ---"LEADER=$(etcdctl--endpoints=$ENDPOINTS $CERTS endpoint status-wjson|jq-r'.[0].Status.header.member_id')if[-z"$LEADER"];thenecho"WARN: 无 leader!"exit1fi# 5. 磁盘使用echo"--- 数据目录大小 ---"forhostin10.0.1.1010.0.1.1110.0.1.12;doecho"$host:$(ssh$hostdu-sh/var/lib/etcd|awk'{print $1}')"done# 6. 备份验证echo"--- 最近备份 ---"ls-lht/backup/etcd-snapshot-*.db|head-3echo"===== 巡检完成 ====="5.3.4 定期容灾演练
每月一次 etcd 故障注入演练:
- 模拟单节点宕机(验证集群自愈)
- 模拟双节点宕机(验证应急恢复流程)
- 模拟网络分区(验证脑裂处置)
- 模拟数据损坏(验证备份恢复)
六、可复用产出
6.1 etcd 故障应急预案(分级响应)
| 级别 | 现象 | 响应时间 | 处置 |
|---|---|---|---|
| P0 | 集群无 leader/脑裂 | 1 分钟内响应 | 启动应急流程,force-new-cluster |
| P0 | 多数派丢失 | 1 分钟内响应 | 同上 |
| P1 | 单节点宕机 | 5 分钟内响应 | 集群自愈,观察是否扩缩 |
| P1 | 磁盘使用 > 80% | 15 分钟内响应 | compact + defrag + 扩容 |
| P2 | fsync 延迟高 | 30 分钟内响应 | 排查磁盘 IO |
| P2 | leader 切换频繁 | 30 分钟内响应 | 排查网络抖动 |
6.2 脑裂检测告警规则
(见 5.3.2)
6.3 etcd 健康巡检脚本
(见 5.3.3)
6.4 复盘报告模板
# etcd 故障复盘报告 ## 1. 故障概述 - 故障时间: - 影响时长: - 业务影响: - 根因: ## 2. 时间线(分钟级) | 时间 | 事件 | 操作人 | ## 3. 根因分析 - 直接原因: - 深层原因: - 架构问题: ## 4. 处置过程 - 应急动作: - 踩坑记录: ## 5. 数据一致性校验 - 哈希对比: - 资源数量对比: - 业务侧确认: ## 6. 改进项 | 改进项 | 负责人 | 截止时间 | 状态 | ## 7. 经验沉淀 - 可复用 SOP: - 监控告警优化: - 演练计划:思考题
- 如果 3 节点 etcd 中有 1 个数据损坏(非网络问题),你会如何处置?和脑裂处置有什么区别?
--force-new-cluster操作的风险点在哪?如何保证选用的节点数据是最新的?- etcd 集群规模是 3 节点还是 5 节点更合理?在成本和可用性之间如何权衡?
延伸阅读
- etcd 官方灾难恢复文档:https://etcd.io/docs/v3.5/op-guide/recovery/
- Raft 论文:In Search of an Understandable Consensus Algorithm
- etcd 脑裂与多数派机制解析
- K8s 控制面高可用最佳实践
- 《分布式系统:概念与设计》第 5 章