一、背景与现象
一个自建 K8s 集群,某天上午开发同学反馈:新建 Pod 一直 Pending,kubectl apply 报 etcdserver: mvcc: database space exceeded。同时现有 Pod 正常运行,但任何写操作(创建 / 更新 / 删除)都失败,apiserver 日志猛刷 quota exceeded。
问题不在节点,而是 etcd 后端数据库配额被历史版本撑爆。最坑的是:集群“看起来一切正常”,只是写不进去,所以告警往往比业务反馈来得晚。这类问题在开了高频 reconcile 的集群里尤其常见。
二、排查过程
先确认 etcd 是否真的空间告急:
# 查看每个 etcd 成员 db 大小与配额
ETCDCTL_API=3 etcdctl \
--endpoints=https://127.0.0.1:2379 \
--cacert=/etc/kubernetes/pki/etcd/ca.crt \
--cert=/etc/kubernetes/pki/etcd/server.crt \
--key=/etc/kubernetes/pki/etcd/server.key \
endpoint status --write-out=table
# 看 dbSize 与 quota(默认 2GB),dbSize 接近 quota 即危险
再看有没有报警被触发:
# 列出当前 etcd alarm
etcdctl alarm list
# 典型输出:
# memberID:xxxx alarm:NOSPACE
NOSPACE 报警一旦挂上,etcd 会拒绝一切写请求,哪怕只是新建一个空 ConfigMap。确认 alarm 后,查看历史版本堆积:
# 看当前修订号,判断堆积量
etcdctl endpoint status --write-out=json | python -m json.tool
# 关注 header.revision 与 dbSize 的增长趋势
# 顺便确认成员健康,排除 leader 切换导致的写入抖动
etcdctl endpoint health --write-out=table
如果想知道是谁在疯狂写,可以用 etcdctl watch 临时抓一下写入热点,或者翻 apiserver 审计日志里 verb=create/update 最频繁的 resource。另一个定位写入源的实用办法是:在 apiserver 审计日志里按 resource 和 verb 统计写操作频次,往往能直接指认 culprit,比如某个 Operator 的 reconcile 循环在空转刷写。
三、根因分析
etcd 是 MVCC 模型,每次写都会生成一个新版本,旧版本不会立即删除,靠压缩(compact)+ 碎片整理(defrag)回收。如果集群开着高频写(如每几秒更新的 Endpoint、kubelet 心跳、Prometheus Operator 频繁 reconcile),而 auto-compaction 没开或配得太松,历史版本会无限堆积,直到触及 quota-backend-bytes(默认 2GB)触发 NOSPACE。
这里有两个容易踩的点:一是用 periodic 模式按时间压缩(如保留 1h),不如 revision 模式按版本数压缩(如保留最近 1000 个修订)来得稳,因为 revisions 的增长速度和写频率直接挂钩;二是只 compact 不 defrag,db 文件大小不会降,空间依旧占着。根因:只写不压(或压得太慢)+ 配额不设防。
落到数字上,etcd 每个 revision 大约几十到几百字节,集群里成千上万个对象、每几秒一次 Endpoint 刷新,一天就能产生几百万个 revision。2GB 配额在高频写入下可能几周就被吃满。这也解释了为什么问题常在集群稳定运行一段时间后才爆发:不是忽然写的变多,而是旧版本一直没人收。对于特别大的 etcd,还可以把 defrag 排进每月一次的运维窗口,作为预防性维护,而不是等 NOSPACE 才救火。
四、解决方案
分三步:解 alarm → 压缩历史 → 碎片整理释放空间。
# 1) 解除 NOSPACE 报警(不解报警,写请求仍被拒)
etcdctl alarm disarm
# 2) 压缩到当前最新修订号(先取 revision)
REV=$(etcdctl endpoint status --write-out=json | \
python -c "import sys,json;print(json.load(sys.stdin)['status']['header']['revision'])")
etcdctl compact $REV --physical
# 3) 逐个成员做离线碎片整理,真正回收 db 文件空间
etcdctl defrag --cluster
# 验证:dbSize 应明显下降,写操作恢复
etcdctl endpoint status --write-out=table
defrag 会短暂阻塞该成员写,生产建议逐个成员滚动执行,避免 etcd 整体不可用。配额也可临时上调应急:
# 临时调大到 8GB(治标,配合压缩后尽快回落)
# 修改 etcd 静态 Pod 的 --quota-backend-bytes=8589934592 后滚动重启
五、复盘与预防
-
开启自动压缩,别等手动:--auto-compaction-mode=revision --auto-compaction-retention=1000(保留最近 1000 个修订)。
-
配额显式设置并监控:--quota-backend-bytes=4294967296(4GB),用 etcd_mvcc_db_total_size_in_bytes 接 Prometheus,超 70% 即告警。下面这条告警规则可直接抄:
# prometheus alert rule
- alert: EtcdDBSizeNearQuota
expr: etcd_mvcc_db_total_size_in_bytes / etcd_server_quota_backend_bytes > 0.7
for: 10m
labels: {severity: warning}
annotations: {summary: "etcd db 已超配额 70%,请尽快 compact + defrag"}
-
告警联动:把 etcd_server_has_leader、alarm:NOSPACE、defrag 失败都接进夜莺 / Alertmanager。
-
规范:高频写的控制器(Prometheus Operator、Cert-Manager)单独评估,避免无意义的频繁 reconcile 把 etcd 写爆。
六、小结
mvcc: database space exceeded 是 etcd 的“磁盘配额爆了”信号,根子在历史版本没压缩。口诀:先 alarm disarm 解报警,再 compact + defrag 回收,最后把 auto-compaction 和 quota 固化进集群配置,别让下次重演。apiserver 写不动时,第一反应就该查 etcd 而不是节点。