etcd Revision 爆炸外呼瘫痪复盘:4000 万条历史版本撑爆 8GB 存储

etcd分布式存储事故复盘Linux

etcd Revision 爆炸外呼瘫痪复盘:4000 万条历史版本撑爆 8GB 存储

TL;DR 一个下午,外呼服务突然全线瘫痪,602 个坐席打不了电话。追了 24 分钟才发现,原因不是什么代码 bug 或网络故障,而是 etcd 的 lease 续约没开自动压缩,4000 万条历史版本撑爆了 8GB 存储。更绝望的是,等我们发现时已经恢复不了了——扩容没用、压缩拒绝执行、备份也恢复失败。最后只能把用户切回老系统。这篇文章是我对整件事的复盘。


📌 本文要点

  • 一个 --auto-compaction-retention 参数没配,如何让 etcd 无声地走向死亡
  • etcd 存储写满后为什么扩容都救不回来(Raft 一致性的一个常见误解)
  • 为什么”做了备份”不等于”灾难时能恢复”
  • 我们对 lease 续约频率的反思——有些”最佳实践”其实需要重新审视

🏗️ 背景与环境

项目信息
etcd 版本3.x
etcd 存储限制8GB(默认 quota-backend-bytes)
拨打节点数10 个
lease 续约频率1 次/秒,TTL = 5 秒
自动压缩未开启
影响范围602 个灰度坐席无法拨打电话

重点标记 未开启自动压缩1次/秒续约频率——这两个条件叠加,就是 revision 爆炸的根因。


☎️ 一个坐席打不出电话的下午

下午两点五分,告警群突然炸了。

“外呼异常!灰度用户无法拨打!”

我快速看了一眼监控——外呼服务的所有拨打节点全部失联。没有断网,没有宕机,但 etcd 里可用的节点列表就是空的。

外呼服务的逻辑很简单:拨打节点启动时向 etcd 注册自己,然后每隔 1 秒续约一次(lease TTL=5 秒)。外呼服务收到拨打请求时,从 etcd 拉取可用节点列表,选一个来执行拨打。

节点列表为空,意味着所有拨打请求都会失败。 602 个灰度用户在那一刻打不出任何电话。

当时的第一反应是:etcd 挂了?先排查看看。


🔍 第一眼:etcd 不可写

查 etcd 集群状态——unhealthy。再细看,etcd 的 8GB 存储满了

不是被什么大文件撑满的,而是被 4000 多万条 revision 信息填满的。

这里需要先解释一个 etcd 的特性。


💡 先搞清楚:etcd 的 revision 是什么

etcd 每次数据变化,都会生成一个全局递增的版本号叫 revision。这个版本号不是”覆盖旧值”,而是追加一个新的历史版本——类似 Git 每次 commit 都会生成一个新 hash,而不是覆盖上一个 commit。

我们的拨打节点每 1 秒续约一次 lease,每次续约就是一次写入,每次写入就产生一条新 revision。

做个算术:

1 个节点 × 1 次/秒 × 3600 秒 × 24 小时 = 每天 86,400 条 revision
如果有 10 个节点同时续约 → 每天 86.4 万条
运行几个月 → 4000 万条 revision → 8GB 撑爆

etcd 不自动清理旧 revision,它们就像日志一样只增不减。 而我们的集群没有开启自动压缩(auto-compaction)。

一条配置就能防住的事:

--auto-compaction-retention=1h

只保留最近 1 小时的 revision,其他定期清理。但我们没配。


🔗 连锁反应:存储满了 → 全部崩盘

存储满了不只是”写不进去”这么简单。对 etcd 这种分布式共识系统来说,存储满意味着:

各节点数据不一致 → Raft 多数派被破坏 → 选不出 leader → 集群 unhealthy → lease 无法续约 → 5 秒 TTL 到期,节点列表清空 → 所有外呼请求失败

从”存储快满了”到”全线瘫痪”,中间没有任何人能感知到——没有告警,因为没有监控磁盘使用率。


🔧 艰难(且失败)的恢复尝试

我们开始尝试恢复。每一步都让人更绝望:

第一步:重启 etcd 节点。 不行。重启不会清理历史 revision,空间依然满,集群依然 unhealthy。

第二步:执行数据压缩。 等于是说”etcd,请把旧的 revision 删掉”。但 etcd 回复:“抱歉,我现在 unhealthy,没法执行管理操作。“——已经 unhealthy 了才想压缩,晚了。

第三步:扩容磁盘,8G → 16G。 空间够了吧?重启节点。节点进程起来了,显示”available”……过一会儿又变成”unavailable”。

这里有个很多人的误解。我们以为扩容 = 加空间 = 恢复。但对 etcd 来说,空间够了不等于数据对得上

三个节点的 revision 在存储打满的那段时间里已经变得不一致了。扩容给了更大的存储空间,但三本账本的数据还是对不上。Raft 拒绝接受:你不能拿一本跟别人不一样的账本当 leader。所以节点虽然能启动,但在 Raft 层面它无法加入集群,永远 unavailable。

第四步:用备份数据单节点启动。 我们做过备份,定期跑 etcdctl snapshot save 导出的文件。拿出来恢复——也失败了。

“做了备份”和”备份能恢复”是两回事。 备份文件可能已经损坏了,也可能太旧了恢复出来节点列表是空的。关键在于——我们从来没验证过备份能不能恢复。它在磁盘上躺着,大小正常,看起来一切良好,但到用的时候才发现不行。

第五步(无奈之举):切回老系统。 我们放弃了恢复,把所有灰度部门切回旧的外呼系统。坐席重新扫码,恢复正常。恢复耗时 24 分钟。


🛠️ 怎么修的

定位到问题后,修复和预防分几步走:

1. 开启自动压缩(治本)

--auto-compaction-retention=1h

只保留最近 1 小时的 revision,定期清理。一条配置,防住整类问题。

2. 加磁盘使用率告警(预警)

etcd 磁盘使用率 > 75% 立刻告警,到了就人工介入压缩。别等 unhealthy 了再救。

3. 降低 lease 续约频率(减负)

从 1 秒一次改为 10 秒一次,TTL 从 5 秒改为 30 秒。对外呼系统来说,30 秒知道一个节点挂了和 5 秒知道,没有本质区别。但 etcd 写入压力降了 90%。

方案写入频率etcd 压力发现节点挂的时间
1s 续约 / 5s TTL1 次/秒/节点最多 5 秒
10s 续约 / 30s TTL0.1 次/秒/节点低 90%最多 30 秒

4. 定期验证备份可恢复(兜底)

每个月选一台测试机器,跑一遍完整的 restore + 启动 + 数据校验 流程。如果 restore 脚本在演练中挂掉了,那不是”浪费时间”,而是避了一次灾。


💭 事后复盘:我学到的东西

1. 一行配置没配,4000 万 revision 就来了

回头看,整件事最憋屈的地方在于:一条配置就能防住。

--auto-compaction-retention=1h

就这么一行。而且 etcd 的官方文档里写得清清楚楚。但它不像 MySQL 的 auto_increment 那样人尽皆知,revision 在 etcd 里是个隐形的磁盘消耗者——你看不见它在涨,它也不报错,直到某一天空间满了。

我现在看任何一个新接入的系统,第一件事就是问:哪些数据是”只增不减”的?它们的清理策略是什么?

2. 存储满了的 etcd,比挂了更可怕

分布式系统和单机系统最大的区别之一是:单机系统挂了就是挂了,你知道该修;分布式系统可能会进入一种”半死不活”的状态,让人不知道该怎么下手。

MySQL 磁盘满:
  → 抛错 "disk full"
  → 清理空间 → 重启 → 恢复 ✅

etcd 磁盘满:
  → 部分写入成功、部分失败
  → 各节点数据不一致
  → Raft 多数派破坏
  → 集群 unhealthy
  → 此时想压缩 → 拒绝执行
  → 扩容 → 数据还对不上
  → 备份 → 没验证过恢复不了
  → ❌ 等你发现的时候,自救已经晚了

3. 验证备份能不能恢复,比做备份更重要

“我们每天凌晨跑 snapshot save,备份文件都在。”

这话我以前也信。但这次事故之后,我的感受是完全不一样的:

备份流程:
  crontab 每天执行 snapshot save
  → 文件写入 /backup/ 目录
  → 磁盘空间监控显示文件存在、大小正常
  → 所有人都觉得"我们有备份了"

实际上没验证过:
  这个文件真的能用 etcdctl restore 恢复吗?
  恢复出来的数据是不是完整的?
  恢复后服务能不能正常启动?

4. 高频不一定是好事

这次事故的触发源头之一是 lease 续约频率——1 秒一次。最初可能觉得”越快越好,节点挂了能尽快感知”。但后果呢?每次续约都是 etcd 的一次写入,每次写入都是 revision 的一次增长。

高频不一定是好事,觉得”越快越好”时,算算它对下游的影响。


🔚 写在最后

事故过去之后,我一直觉得这件事挺讽刺的。

一个运行了几个月都没问题的系统,因为一行配置没配,在某个下午突然崩了。崩了之后还救不回来,因为 etcd 的设计让它陷入了一致性死锁。连备份都不管用,因为从没人试过。

一行配置、一个磁盘告警、一次恢复演练——任何一个环节做了,这次事故都不会发生。但三个环节都没做,它就精确地踩中了。


✅ 复盘 Checklist:搭建 etcd 集群前过一遍

  • --auto-compaction-retention=1h 配好了吗? 一行配置防住整类问题
  • 磁盘使用率 > 75% 有告警吗? 别等 unhealthy 再救
  • lease 续约频率合理吗? 不是越快越好,算算对 etcd 的写入压力
  • 备份能恢复吗? 定期验证 restore 流程,而不是只做 snapshot save
  • 哪些数据是”只增不减”的? 它们的清理策略是什么

都是小事。但有时候,崩掉一个大系统的,就是这些小事。