故障排查与最佳实践
先看什么
etcd 出问题时,第一反应不应该是翻日志,而是先确认「集群现在是什么状态」。按这个顺序过一遍,能快速定位到问题大类:
# 1. 每个端点还活着吗
etcdctl endpoint health --cluster
# 2. 谁有 leader、raft 进度差多少、db 多大
etcdctl endpoint status -w table
# 3. 有没有 NOSPACE / CORRUPT 之类的告警
etcdctl alarm list
# 4. 成员列表和 peer 地址
etcdctl member list -w table
再看日志里的关键词:
journalctl -u etcd --since "1 hour ago" | grep -iE \
'no leader|leader changed|timed out|slow|space|corrupt|fsync|heartbeat|handshake'
| 现象 | 优先怀疑 |
|---|---|
所有请求超时,endpoint health 全红 | 多数派丢失、网络分区 |
| 只有一台红,其余正常 | 该成员进程挂了或磁盘满 |
读正常、写报 database space exceeded | NOSPACE,db 超配额 |
| leader 反复切换,日志有 heartbeat 超时 | 慢盘、网络抖动、CPU 饱和 |
apiserver 报 too old resource version | watch 断流后重新 list,或压缩过猛 |
启动报 cluster ID mismatch | 数据目录和集群对不上 |
下面按最常见的四类故障展开:多数派丢失、NOSPACE、慢盘、watch 泄漏。
多数派丢失
现象
典型表现是所有写请求卡住或超时,etcdctl endpoint health 返回不健康,日志里反复出现:
{"level":"warn","msg":"failed to get leader"}
{"level":"error","msg":"etcdserver: no leader"}
{"level":"warn","msg":"lost the leadership"}
apiserver 会跟着报 context deadline exceeded、etcdserver: request timed out,kubectl get 也可能卡住。K8s 里已经运行的 Pod 不受影响,但任何需要读写 apiserver 的操作都会失败。
原因
多数派丢失,顾名思义就是「能正常工作的成员数不到一半以上」。常见触发:
- 两台机器同时宕机、断电、内核 panic。
- 交换机或安全组把 2380 端口封了,成员之间互相看不见。
- 某个成员磁盘满、fsync 卡死,虽然进程还在,但无法参与投票。
- 时钟漂移严重,证书校验失败导致 peer 连接断开。
- 误操作把成员的
initial-cluster改错,重启后自己起了一个新集群。
先排查「是不是能救回来」:机器还能不能启动、网络通不通、磁盘有没有满。只要能恢复多数派,优先恢复成员,不要急着做灾难恢复。
恢复策略
按优先级从高到低:
策略一:把挂掉的成员拉起来。 修好机器、清理磁盘、恢复网络,然后 systemctl start etcd。成员重启后会从 leader 同步落后的日志,追上后集群自动恢复。这是最安全、数据最完整的路径。
策略二:用快照重建集群。 如果挂掉的机器彻底没了(比如云主机释放、磁盘损坏),而手上有多数派成员的最后快照,可以参考部署篇的恢复流程,用同一份快照重建三个成员。恢复后 revision 会回到快照时刻,快照之后的数据会丢。
策略三:--force-new-cluster 单节点强启。 这是最后手段,只在「没有可用快照、又必须把集群拉起来」时用。3.5 还保留这个参数,3.6 起官方更推荐走快照恢复。
操作步骤:
# 1. 停掉所有 etcd 成员,先备份各自的数据目录
sudo systemctl stop etcd
sudo cp -a /var/lib/etcd /var/lib/etcd.bak-$(date +%F-%H%M)
# 2. 选数据最全的一个成员(raft index 最大),用原配置加 --force-new-cluster 启动
sudo -u etcd etcd --config-file=/etc/etcd/etcd.conf.yml --force-new-cluster
# 3. 确认它变成单节点集群,能读写
etcdctl endpoint status -w table
etcdctl member list -w table
# 4. 把其他成员的旧数据清掉,重新以 existing 状态加入
etcdctl member add etcd-02 --peer-urls=https://10.0.0.12:2380
它会丢弃其他成员上、还没同步到这个节点的日志;如果选错了成员,可能丢掉已经提交的数据。不要把多个成员同时用 --force-new-cluster 启动,那会得到几个互不相干的单节点集群,比原来更难收拾。操作前必须备份数据目录,并明确告诉业务方可能回退到哪个时间点。
预防
- 3 节点起步,跨机架或跨可用区部署,别把三台放在同一台宿主机上。
- 监控
etcd_server_has_leader和leader_changes_seen_total,多数派丢失前通常有选主抖动。 - 定期做快照,并验证恢复流程。多数派丢失时,快照是最后的底牌。
- 变更
initial-cluster、防火墙、证书前,一次只动一个成员。
NOSPACE
现象
写请求返回:
etcdserver: mvcc: database space exceeded
etcdctl alarm list 会看到:
memberID:8e9e05c52164694d alarm:NOSPACE
这时候读还能用,写全部失败。K8s 表现为新 Pod 创建不了、Deployment 扩缩容卡住、Service 更新不生效,但已有 Pod 继续运行。这是 etcd 的保护机制:db 超过配额后拒绝写入,避免把磁盘彻底写满导致数据损坏。
原因
- 没有开自动压缩,历史 revision 一直堆积。
- 删除的 key 只是逻辑删除,空间没释放,长期没有 defrag。
- K8s 里 Event、旧 ReplicaSet、已完成 Job 等对象太多。
- 有人往 etcd 里写了大量业务数据或大对象。
- 配额本身设得太小(默认只有 2 GiB)。
处理步骤
先看 db 大小和配额:
etcdctl endpoint status -w table
etcdctl endpoint status -w json | jq '.[0].Status | {dbSize, dbSizeInUse}'
etcdctl alarm list
# 配额只在 /metrics 里,HTTP 场景直接抓;HTTPS 要加 --cacert/--cert/--key
curl -s http://127.0.0.1:2379/metrics | grep '^etcd_server_quota_backend_bytes'
然后按「压缩 → 碎片整理 → 解除告警」的顺序处理:
# 1. 压缩到当前 revision,释放历史版本
REV=$(etcdctl endpoint status -w json | jq -r '.[0].Status.header.revision')
etcdctl compact $REV
# 2. 逐台 defrag,回收磁盘空间
etcdctl --endpoints=https://10.0.0.11:2379 \
--cacert=/etc/etcd/certs/ca.crt \
--cert=/etc/etcd/certs/client.crt \
--key=/etc/etcd/certs/client.key \
defrag --command-timeout=600s
# 3. 三台都整理完,解除 NOSPACE 告警
etcdctl alarm disarm
# 4. 验证写入恢复
etcdctl put /demo/recovered ok
etcdctl get /demo/recovered
几个关键点:
- compact 是集群级操作,对一个成员执行即可;defrag 是成员级操作,三个成员都要做,一台一台来。
- 删除 key 不会立刻释放空间。
del之后还要 compact + defrag,db 文件才会真正变小。 - defrag 需要额外磁盘空间。如果磁盘已经 100% 满,先扩容或清掉无关文件,否则 defrag 会失败。
- 解除告警前确认 db 已低于配额,否则写入很快又会触发 NOSPACE。
如果磁盘还有空间,但 compact/defrag 需要时间,可以把所有成员的 --quota-backend-bytes 临时调大并滚动重启,再执行 alarm disarm,先恢复写入,然后安排治理。调大配额是止血,不是治疗;db 继续涨,下一次还是同样的结局。
预防
- 开
--auto-compaction-mode=periodic --auto-compaction-retention=1h。 - 配额按 8 GiB 设置,db 到 80% 就告警。
- 每月检查一次碎片率,超过 50% 安排 defrag。
- 定期清理 K8s 里不用的对象,尤其是 Event 和 Completed 的 Job。
- 不要把 etcd 当业务 KV 用,大对象放对象存储或数据库。
慢盘导致 leader 抖动
现象
日志里高频出现:
{"level":"warn","msg":"failed to send out heartbeat on time (exceeded the 100ms timeout for 1s)"}
{"level":"warn","msg":"leader failed to send out heartbeat; took too long"}
{"level":"warn","msg":"apply request took too long"}
etcd_server_leader_changes_seen_total 持续增长,apiserver 请求间歇性超时,但进程没挂、端口也在。这种「看起来活着但一直抖」的问题,十有八九是磁盘。
排查
先看 etcd 自己的指标:
# WAL fsync P99,超过 10ms 就要警惕(按成员看)
histogram_quantile(0.99, sum by (instance, le) (rate(etcd_disk_wal_fsync_duration_seconds_bucket[5m])))
# backend commit P99
histogram_quantile(0.99, sum by (instance, le) (rate(etcd_disk_backend_commit_duration_seconds_bucket[5m])))
# 慢 apply 速率
rate(etcd_server_slow_apply_total[5m])
# 心跳发送失败速率
rate(etcd_server_heartbeat_send_failures_total[5m])
再上机器看 IO:
# 磁盘利用率和 await
iostat -x 1
# 看 etcd 进程的读写情况
pidstat -d 1 -p $(pidof etcd)
# 看是不是有别的进程在抢 IO
iotop -o
如果 iostat 的 await 长期高于 10ms、%util 接近 100%,基本可以确定磁盘扛不住。云盘还要看 IOPS 和吞吐是否被规格限制,突发额度用完后的性能会断崖式下跌。
处理
- 换盘:把数据目录迁到独立 SSD/NVMe,或者升级云盘规格。这是最有效的办法。
- 隔离 IO:别让日志采集、镜像拉取、业务写入和 etcd 共用一块盘。K8s 控制面节点尤其容易犯这个错。
- 降负载:减少不必要的 apiserver 写入,关掉高频的控制器重试,清理 Event 风暴。
- 调时间参数:跨可用区、网络 RTT 较大的环境,可以适当加大心跳和选举超时,例如:
heartbeat-interval: 250
election-timeout: 2500
这会降低误选主概率,但故障切换也会变慢。先确认磁盘和网络没问题,再考虑调参数;拿调参去掩盖慢盘,只是把问题推迟。
选举超时从 1000ms 调到 5000ms,故障切换时间也会变成 5 秒以上,apiserver 的请求超时可能先触发。调参要配合压测和监控,改完观察至少一周。
预防
- etcd 独占 SSD,容量留 20% 以上余量。
- 控制面节点不要跑重 IO 业务,容器运行时和镜像目录也尽量分盘。
- 监控 fsync P99,超过 10ms 就发告警,别等 leader 开始抖。
- 云环境选带稳定 IOPS 的盘,注意突发性能额度。
watch 泄漏与连接数
现象
- etcd 内存持续上涨,
etcd_debugging_mvcc_watcher_total只增不减。 etcd_debugging_mvcc_slow_watcher_total增长,日志出现watcher channel is full或slow watcher。- 宿主机 2379 的 ESTABLISHED 连接数远高于正常水平。
- apiserver 间歇性报
etcdserver: too many requests或 watch 断流。
原因
watch 是长连接,正常客户端的 watcher 数量应该跟「对象数量 × 控制器数量」大致成比例,并且随控制器退出而回收。持续上涨通常是:
- 客户端创建 watch 后没有
Cancel,进程重启也没释放。 - 自定义控制器在错误重试循环里反复建 watch,旧的不关。
- apiserver 的 watch 缓存失效,大量客户端同时重连、重新 list,形成风暴。
- 慢客户端消费不过来,etcd 端缓冲打满,watch 被取消后客户端立刻重连,循环往复。
排查
# 当前 watcher 数(HTTPS 场景加 --cacert/--cert/--key 并换成 https)
curl -s http://127.0.0.1:2379/metrics | grep -E 'etcd_debugging_mvcc_(watcher|slow_watcher)_total'
# 连接数
ss -tn state established '( sport = :2379 )' | tail -n +2 | wc -l
# 按来源 IP 排序,找出连接最多的客户端
ss -tn state established '( sport = :2379 )' | tail -n +2 \
| awk '{print $5}' | cut -d: -f1 | sort | uniq -c | sort -rn | head
如果是 apiserver 发起的连接,去查 apiserver 日志里的 watch chan error、too old resource version;如果是某个自定义控制器,查它的重启次数和 goroutine 数。
处理
- 先止血:重启泄漏的客户端(通常是 apiserver 或某个控制器),watcher 数会立刻回落。
- 再定位:对比重启前后的连接来源,找到持续建连的组件,修代码或升级版本。
- 处理 410 Gone:apiserver 报
too old resource version时,客户端应该重新 list 再 watch;如果压缩太激进,watch 起点频繁失效,可以适当放宽--auto-compaction-retention,给客户端留重连时间。 - 调 FD 上限:连接数确实大时,确认 systemd 的
LimitNOFILE=65536已生效,os_fd_used不要逼近os_fd_limit。
etcd 不会因为 watcher 太多直接拒绝,而是靠内存和事件缓冲扛着。等到开始 OOM 或大量慢 watcher,已经很危险了。把 watcher 数和连接数纳入监控,比设一个拍脑袋的上限更实用。
其他高频故障
| 报错 / 现象 | 常见原因 | 处理 |
|---|---|---|
x509: certificate has expired | 证书过期 | 续签并滚动重启成员,检查 SAN |
transport: authentication handshake failed | CA 不匹配、客户端没带证书 | 对齐 CA,检查 --client-cert-auth 和 etcdctl 证书 |
cluster ID mismatch | 数据目录属于另一个集群 | 找回原数据目录,或清空后以 existing 加入 |
request timed out | leader 过载、慢盘、提案积压 | 查 fsync、pending proposals、网络 |
snapshot apply in progress | 新成员或落后成员在同步快照 | 等同步完成,别急着重启 |
database space exceeded | NOSPACE | 按上文 compact + defrag + disarm |
no space left on device | 磁盘真的满了 | 清日志/扩容,检查 WAL 和 db 目录 |
| 时间跳变、选主异常 | NTP 不同步 | 配置 chrony,校准所有成员 |
最佳实践清单
把前面几篇的结论收成一份可以照着核对的清单:
- 规模选 3 或 5,奇数节点,跨机架/可用区部署,永远不要 2 或 4 节点。
- 磁盘用独立 SSD/NVMe,fsync P99 控制在 10ms 以内,容量留 20% 余量。
- 生产必须开 TLS,客户端和 peer 两套证书,
client-cert-auth和peer-client-cert-auth都打开。 - 证书纳入监控,剩余 30 天告警;kubeadm 集群记得定期
kubeadm certs renew。 - 配额设 8 GiB,db 到 80% 告警;不要把配额当成解决增长的手段。
- 开自动压缩,
--auto-compaction-mode=periodic --auto-compaction-retention=1h。 - 定期 defrag,逐台执行、低峰操作、leader 最后,碎片率超过 50% 就安排。
- 每小时快照 + 异地保存,每季度做一次恢复演练,没演练过的备份不算数。
- 监控四件套:leader 切换、fsync 延迟、db 配额、NOSPACE 告警;再加 watch 数和连接数。
- 变更一次只动一个成员,改配置、换证书、扩缩容都一样,动完确认健康再动下一个。
- 不要把 etcd 当业务库,不存大对象、不存日志、不存图片,单 value 控制在 KB 级。
- 控制面节点保持干净,别和重 IO 业务混部,NTP 必须开。
- 灾难恢复优先快照,
--force-new-cluster是最后手段,操作前备份数据目录并评估数据回退。 - 告警要演练,阈值先观察基线再收紧,避免误报导致整组告警被静音。
出事先做三件事:endpoint health --cluster 看谁活着,alarm list 看有没有 NOSPACE,journalctl -u etcd 搜 leader 和 timeout。这三步能覆盖八成以上的 etcd 故障,剩下的再按本篇场景往下查。
小结
- 排查顺序:健康检查 → 成员状态 → alarm → 日志关键词,先判断是可用性问题还是容量问题。
- 多数派丢失优先恢复成员,其次快照重建,
--force-new-cluster是最后手段且有数据丢失风险。 - NOSPACE 的解法是 compact → defrag → alarm disarm;删除 key 不会自动释放磁盘,预防靠自动压缩和配额告警。
- leader 抖动先查 fsync 和磁盘 IO,换盘、隔离 IO 比调大选举超时更根本。
- watch 泄漏看 watcher 数、慢 watcher、连接来源,先重启泄漏客户端止血,再修代码。
- 最佳实践的核心就三句话:磁盘要快、变更要慢、备份要能恢复。
到这里 etcd 系列的正篇就结束了。附录里整理了 etcdctl 命令速查和官方资源,日常排查时可以随手翻。