【K8S 运维实战】39-etcd脑裂故障复盘

案例: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                机房 B

3 个控制面节点分布在机房 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 应急挑战

  1. etcd 不可用,所有 kubectl 命令失败:常规 K8s 运维手段全部失效,必须直接操作 etcd。
  2. 脑裂状态下数据可能不一致:恢复时选哪个节点的数据?选错会丢数据。
  3. 操作风险高:etcdctl member remove 操作不当,可能把健康成员也踢出去,雪上加霜。
  4. 业务压力:每分钟影响约 4000 笔交易,老板每 5 分钟问一次"好了没"。

二、方案设计(应急流程)

2.1 etcd 脑裂应急决策树

#mermaid-svg-w9h0yicLaxxpFBYP{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-w9h0yicLaxxpFBYP .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-w9h0yicLaxxpFBYP .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-w9h0yicLaxxpFBYP .error-icon{fill:#552222;}#mermaid-svg-w9h0yicLaxxpFBYP .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-w9h0yicLaxxpFBYP .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-w9h0yicLaxxpFBYP .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-w9h0yicLaxxpFBYP .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-w9h0yicLaxxpFBYP .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-w9h0yicLaxxpFBYP .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-w9h0yicLaxxpFBYP .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-w9h0yicLaxxpFBYP .marker{fill:#333333;stroke:#333333;}#mermaid-svg-w9h0yicLaxxpFBYP .marker.cross{stroke:#333333;}#mermaid-svg-w9h0yicLaxxpFBYP svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-w9h0yicLaxxpFBYP p{margin:0;}#mermaid-svg-w9h0yicLaxxpFBYP .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-w9h0yicLaxxpFBYP .cluster-label text{fill:#333;}#mermaid-svg-w9h0yicLaxxpFBYP .cluster-label span{color:#333;}#mermaid-svg-w9h0yicLaxxpFBYP .cluster-label span p{background-color:transparent;}#mermaid-svg-w9h0yicLaxxpFBYP .label text,#mermaid-svg-w9h0yicLaxxpFBYP span{fill:#333;color:#333;}#mermaid-svg-w9h0yicLaxxpFBYP .node rect,#mermaid-svg-w9h0yicLaxxpFBYP .node circle,#mermaid-svg-w9h0yicLaxxpFBYP .node ellipse,#mermaid-svg-w9h0yicLaxxpFBYP .node polygon,#mermaid-svg-w9h0yicLaxxpFBYP .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-w9h0yicLaxxpFBYP .rough-node .label text,#mermaid-svg-w9h0yicLaxxpFBYP .node .label text,#mermaid-svg-w9h0yicLaxxpFBYP .image-shape .label,#mermaid-svg-w9h0yicLaxxpFBYP .icon-shape .label{text-anchor:middle;}#mermaid-svg-w9h0yicLaxxpFBYP .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-w9h0yicLaxxpFBYP .rough-node .label,#mermaid-svg-w9h0yicLaxxpFBYP .node .label,#mermaid-svg-w9h0yicLaxxpFBYP .image-shape .label,#mermaid-svg-w9h0yicLaxxpFBYP .icon-shape .label{text-align:center;}#mermaid-svg-w9h0yicLaxxpFBYP .node.clickable{cursor:pointer;}#mermaid-svg-w9h0yicLaxxpFBYP .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-w9h0yicLaxxpFBYP .arrowheadPath{fill:#333333;}#mermaid-svg-w9h0yicLaxxpFBYP .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-w9h0yicLaxxpFBYP .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-w9h0yicLaxxpFBYP .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-w9h0yicLaxxpFBYP .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-w9h0yicLaxxpFBYP .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-w9h0yicLaxxpFBYP .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-w9h0yicLaxxpFBYP .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-w9h0yicLaxxpFBYP .cluster text{fill:#333;}#mermaid-svg-w9h0yicLaxxpFBYP .cluster span{color:#333;}#mermaid-svg-w9h0yicLaxxpFBYP div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-w9h0yicLaxxpFBYP .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-w9h0yicLaxxpFBYP rect.text{fill:none;stroke-width:0;}#mermaid-svg-w9h0yicLaxxpFBYP .icon-shape,#mermaid-svg-w9h0yicLaxxpFBYP .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-w9h0yicLaxxpFBYP .icon-shape p,#mermaid-svg-w9h0yicLaxxpFBYP .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-w9h0yicLaxxpFBYP .icon-shape .label rect,#mermaid-svg-w9h0yicLaxxpFBYP .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-w9h0yicLaxxpFBYP .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-w9h0yicLaxxpFBYP .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-w9h0yicLaxxpFBYP :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 1 个
2 个


告警:etcd 无 leader
确认网络分区范围
几个节点失联
多数派还在,集群自愈
多数派丢失,集群不可写
定位健康节点
健康节点数据是否最新
用健康节点重建集群
选数据最新的节点
member remove 失联节点
健康节点单点启动
apiserver 恢复
失联节点修复后逐个加入
数据一致性校验

2.2 关键决策点

决策 选项 风险 选择
用哪个节点恢复 etcd-0(健康) 数据可能不是最新 选(网络分区前是 leader)
是否直接删除失联节点 member remove 删错会永久丢成员 谨慎,先 backup
单节点能否撑业务 单点 etcd 再挂就全完 临时撑,立即扩回 3 节点

三、实施过程

3.1 第一步:确认故障范围(14:32 - 14:35)

bash 复制代码
# 1. 直接连 etcd(绕过 apiserver)
export ETCDCTL_API=3
ETCD_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 -c 3 10.0.1.11   #不通
ping -c 3 10.0.1.12   #不通
ping -c 3 10.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 单节点成集群":

bash 复制代码
# ⚠️ 这个操作后来证明是错的
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 正确做法:单节点强制启动
bash 复制代码
# 1. 先备份 etcd-0 数据(救命稻草)
etcdctl --endpoints=https://10.0.1.10:2379 \
  --cacert=... --cert=... --key=... \
  snapshot save /backup/etcd-snapshot-$(date +%s).db

# 2. 停止 etcd-0
systemctl stop etcd

# 3. 修改 etcd 配置:从集群模式改为单节点模式
# 关键:--force-new-cluster,用现有数据启动新集群
cat > /etc/etcd/etcd.conf.yml << 'EOF'
name: 'etcd-0'
data-dir: /var/lib/etcd
force-new-cluster: true              # ← 关键:用现有数据强制启动单节点集群
listen-peer-urls: https://10.0.1.10:2380
listen-client-urls: https://10.0.1.10:2379
initial-advertise-peer-urls: https://10.0.1.10:2380
advertise-client-urls: https://10.0.1.10:2379
initial-cluster: etcd-0=https://10.0.1.10:2380
initial-cluster-state: new
initial-cluster-token: 'etcd-cluster-prod'
client-transport-security:
  cert-file: /etc/etcd/etcd.pem
  key-file: /etc/etcd/etcd-key.pem
  trusted-ca-file: /etc/etcd/ca.pem
peer-client-transport-security:
  cert-file: /etc/etcd/etcd.pem
  key-file: /etc/etcd/etcd-key.pem
  trusted-ca-file: /etc/etcd/ca.pem
EOF

# 4. 启动 etcd-0(单节点)
systemctl start etcd
sleep 5
etcdctl --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 自动恢复:

bash 复制代码
# 验证 apiserver
kubectl get nodes
# 全部 Ready,apiserver 恢复

kubectl get pods -n prod | 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 不一致(分区期间各自有写入尝试),不能直接加入,必须清空数据重新同步。

bash 复制代码
# 在 etcd-1 上操作
systemctl stop etcd

# 清空旧数据(⚠️ 确认 etcd-0 数据正确后才做)
rm -rf /var/lib/etcd/member

# 修改配置:作为成员加入 etcd-0
cat > /etc/etcd/etcd.conf.yml << 'EOF'
name: 'etcd-1'
data-dir: /var/lib/etcd
listen-peer-urls: https://10.0.1.11:2380
listen-client-urls: https://10.0.1.11:2379
initial-advertise-peer-urls: https://10.0.1.11:2380
advertise-client-urls: https://10.0.1.11:2379
initial-cluster: etcd-0=https://10.0.1.10:2380,etcd-1=https://10.0.1.11:2380
initial-cluster-state: existing          # ← existing,不是 new
initial-cluster-token: 'etcd-cluster-prod'
# ... TLS 配置同上
EOF

# 在 etcd-0 上把 etcd-1 加为成员(先加 member 再启动)
etcdctl --endpoints=https://10.0.1.10:2379 \
  --cacert=... --cert=... --key=... \
  member add etcd-1 \
  --peer-urls=https://10.0.1.11:2380

# 启动 etcd-1
systemctl 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(同样流程)
bash 复制代码
# etcd-2 上
systemctl stop etcd
rm -rf /var/lib/etcd/member

# etcd-0 上加成员
etcdctl --endpoints=https://10.0.1.10:2379 \
  --cacert=... --cert=... --key=... \
  member add etcd-2 \
  --peer-urls=https://10.0.1.12:2380

# etcd-2 上改配置并启动(同 etcd-1 流程)
systemctl start etcd

14:55:17,3 节点全部 online,leader 选举完成,集群恢复。

3.5 第五步:数据一致性校验(15:00 - 15:30)

脑裂期间,etcd-1/etcd-2 可能接受过少量写请求(虽然无法 commit,但 WAL 日志可能有脏数据)。必须校验数据一致性。

3.5.1 集群哈希对比
bash 复制代码
# 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 关键资源完整性校验
bash 复制代码
# 对比关键资源数量
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 文件。

修复:

bash 复制代码
# 彻底清空,包括 wal
systemctl stop etcd
rm -rf /var/lib/etcd/*
systemctl start etcd

4.3 踩坑3:apiserver 缓存导致业务偶发异常

现象:etcd 恢复后,部分 apiserver 请求返回旧数据。

定位:apiserver 本地有缓存,etcd 恢复后缓存没刷新。

修复:

bash 复制代码
# 重启所有 apiserver,强制刷新缓存
kubectl -n kube-system rollout restart deploy kube-apiserver
# 或直接 ssh 到控制面节点
systemctl restart kube-apiserver

4.4 踩坑4:监控告警风暴影响判断

现象:故障期间收到 200+ 条告警,真正的根因告警被淹没。

改进:后续做了告警收敛,etcd/apiserver 故障时只推一条聚合告警,其他依赖告警静默。

五、复盘与改进

5.1 故障影响总结

指标 数据
故障时长 23 分钟
业务影响 约 8 万笔交易延迟,无数据丢失
RTO 23 分钟(目标 < 15 分钟,未达标)
RPO 0(无数据丢失)
根因 机房交换机故障 + etcd 拓扑不合理

5.2 经验教训

  1. etcd 拓扑必须跨故障域:3 节点不能有 2 个在同一交换机,这次是拓扑设计失误。
  2. 脑裂下 member remove 无效 :必须 --force-new-cluster 单节点启动,这是核心知识点。
  3. 数据备份是底线:恢复前必须 snapshot,操作失败还能回滚。
  4. 清数据要彻底 :/var/lib/etcd/member 和 wal 都要清,残留会导致加入失败。
  5. apiserver 缓存要刷新:etcd 恢复后必须重启 apiserver。
  6. 告警必须收敛:故障时告警风暴严重影响判断,聚合告警是刚需。
  7. 应急流程要演练:我们这次操作有犹豫,因为没演练过,后续每月演练一次。

5.3 长期改进

5.3.1 etcd 拓扑优化

把 etcd 节点分散到不同交换机域,甚至跨机房:

复制代码
改造后拓扑:
etcd-0 → 机房A-交换机1
etcd-1 → 机房A-交换机2
etcd-2 → 机房B-交换机1(异地容灾)
5.3.2 etcd 监控告警增强
yaml 复制代码
# Prometheus 告警规则
groups:
  - name: etcd
    rules:
      - alert: EtcdClusterHasNoLeader
        expr: etcd_server_has_leader == 0
        for: 1m
        labels:
          severity: critical
        annotations:
          summary: "etcd 集群无 leader,可能脑裂"
          runbook: "https://wiki.example.com/etcd-split-brain"

      - alert: EtcdMembersUnhealthy
        expr: count(etcd_server_health_failures) by (cluster) > 0
        for: 2m
        labels:
          severity: critical
        annotations:
          summary: "etcd 有成员不健康"

      - alert: EtcdClusterNoQuorum
        expr: |
          count(up{job="etcd"} == 1) by (cluster) < 2
        for: 1m
        labels:
          severity: critical
        annotations:
          summary: "etcd 多数派丢失,集群不可写"

      - alert: EtcdFsyncDurationHigh
        expr: |
          histogram_quantile(0.99, rate(etcd_disk_wal_fsync_duration_seconds_bucket[5m])) > 0.1
        for: 3m
        labels:
          severity: warning
        annotations:
          summary: "etcd WAL fsync 延迟高,磁盘可能瓶颈"
5.3.3 etcd 健康巡检脚本
bash 复制代码
#!/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 $CERTS endpoint status --write-out=table

# 2. 成员列表
echo "--- 成员列表 ---"
etcdctl --endpoints=$ENDPOINTS $CERTS member list --write-out=table

# 3. 数据哈希一致性
echo "--- 数据哈希 ---"
etcdctl --endpoints=$ENDPOINTS $CERTS endpoint hashkv --cluster

# 4. 告警检查
echo "--- 无 leader 检查 ---"
LEADER=$(etcdctl --endpoints=$ENDPOINTS $CERTS endpoint status -w json | jq -r '.[0].Status.header.member_id')
if [ -z "$LEADER" ]; then
  echo "WARN: 无 leader!"
  exit 1
fi

# 5. 磁盘使用
echo "--- 数据目录大小 ---"
for host in 10.0.1.10 10.0.1.11 10.0.1.12; do
  echo "$host: $(ssh $host du -sh /var/lib/etcd | awk '{print $1}')"
done

# 6. 备份验证
echo "--- 最近备份 ---"
ls -lht /backup/etcd-snapshot-*.db | head -3

echo "===== 巡检完成 ====="
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 复盘报告模板

markdown 复制代码
# etcd 故障复盘报告
## 1. 故障概述
- 故障时间:
- 影响时长:
- 业务影响:
- 根因:
## 2. 时间线(分钟级)
| 时间 | 事件 | 操作人 |
## 3. 根因分析
- 直接原因:
- 深层原因:
- 架构问题:
## 4. 处置过程
- 应急动作:
- 踩坑记录:
## 5. 数据一致性校验
- 哈希对比:
- 资源数量对比:
- 业务侧确认:
## 6. 改进项
| 改进项 | 负责人 | 截止时间 | 状态 |
## 7. 经验沉淀
- 可复用 SOP:
- 监控告警优化:
- 演练计划:

思考题

  1. 如果 3 节点 etcd 中有 1 个数据损坏(非网络问题),你会如何处置?和脑裂处置有什么区别?
  2. --force-new-cluster 操作的风险点在哪?如何保证选用的节点数据是最新的?
  3. etcd 集群规模是 3 节点还是 5 节点更合理?在成本和可用性之间如何权衡?

延伸阅读

  • etcd 官方灾难恢复文档:https://etcd.io/docs/v3.5/op-guide/recovery/
  • Raft 论文:In Search of an Understandable Consensus Algorithm
  • etcd 脑裂与多数派机制解析
  • K8s 控制面高可用最佳实践
  • 《分布式系统:概念与设计》第 5 章
相关推荐
Ai拆代码的曹操1 小时前
K8s 实战:CrashLoopBackOff 状态下 kubectl logs 拿不到日志的三层排查方案
linux·docker·kubernetes
tedcloud1232 小时前
Kimi-K3 部署指南:大模型应用开发环境搭建实践
linux·运维·服务器·开源·音视频
爱吃蔬菜09132 小时前
一键部署K8s v1.30.14全离线解决方案
云原生·容器·kubernetes
大模型丫丫2 小时前
GitHub Actions 自动化运维实战:从 CI/CD 到云端部署
运维·自动化·github
山海鲸可视化3 小时前
数字孪生技术:创新性破解城市高温难题
大数据·运维·数字孪生·数据可视化·可视化大屏
XR风云3 小时前
docker buildx 忽略容器镜像服务的证书校验的方法
运维·docker·容器
去伪存真20253 小时前
数字工厂与产线孪生的平台能力深度拆解
大数据·运维·人工智能·机器人
闲云自留地3 小时前
课后作业-20260806-Linux iSCSI服务器
linux·运维·服务器
风雨_833 小时前
轻量级 Nginx 日志分析和可视化平台 NginxPulse安装
运维·nginx