一、先从一次让我手心冒汗的故障说起

大概是下午三点多,线上一个Kubernetes集群突然开始“闹脾气”。新Pod调度不上去,旧Pod的日志也拉不出来,连监控面板上的指标都断断续续。我第一反应是某个服务挂了,但翻了一圈业务日志,发现所有问题的根源都指向同一个地方——Etcd集群。登录Etcd节点一看,日志里刷满了“No space”相关报错,系统明明还有大把磁盘空间,可Etcd就是拒绝服务,整个集群进入了只读保护模式。

这事儿搁谁身上都紧张。但紧张归紧张,处理流程是可以提前梳理清楚的。今天我就把这个场景下的应急恢复实操细节完整写出来,尽量用大白话讲清楚每一步为什么这么做、怎么做才能不丢数据。不管你是刚入门的小白还是老运维,跟着这套流程走一遍,下次再遇到就不会慌。

二、先说清楚“No space”到底是怎么来的

很多人第一次遇到这个报错都会懵,磁盘明明还剩几十个GB,凭什么提示空间不足?这里需要理解Etcd的特殊空间管理机制。

Etcd底层是一个名为“bolt db”的数据库文件,这个文件存了集群里所有数据以及这些数据的历史修改记录。每个key只要被修改过一次,旧版本的数据不会立刻消失,而是像叠罗汉一样层层保留。这些历史版本是Etcd实现Watch监听、事务回滚等高级特性的基础,但代价就是db文件会不断膨胀。

Etcd启动的时候有个参数叫quota-backend-bytes,它的含义是“允许db文件膨胀到的最大值”。默认情况下这个值可能是2GB,也可能更小。一旦db文件体积顶到这个上限,Etcd为了保护已有数据不被破坏,直接拒绝所有写操作,进入只读保护模式。这里的核心逻辑是:宁可牺牲可用性,也不能让canopy层面的数据损坏。

打个比方,db文件就像一个衣柜,历史版本就是一件件旧衣服。衣服越来越多,衣柜塞满了,新衣服(新写入)就放不进去。Etcd的选择是:先锁上柜门,谁也不许动,等主人来清理。

三、完整的应急恢复流程

3.1 第一步:先做备份,不看清楚不动手

很多人一看到“No space”第一反应是重启服务,这是最危险的操作之一。重启不会清除导致问题的根源,反而可能在异常状态下触发数据不一致。正确的做法是先做一次完整快照备份。

我习惯在任意一个Etcd节点上执行以下命令,把当前集群的所有数据镜像到一个文件里:

# 技术栈:Bash + Etcd v3 API(使用etcdctl命令行工具)

# 先声明使用v3版本的API,避免不同版本语法不一致的坑
export ETCDCTL_API=3

# 指定Etcd证书、密钥和CA,如果集群没有开启TLS则忽略下面三行
--cacert=/etc/etcd/ca.pem \
--cert=/etc/etcd/server.pem \
--key=/etc/etcd/server-key.pem \

# 执行快照备份,output指向一个带时间戳的文件,方便回溯
etcdctl --endpoints=https://127.0.0.1:2379 \
  snapshot save /backup/etcd-snapshot-$(date +%Y%m%d-%H%M%S).db

# 确认备份文件不是损坏的
etcdctl snapshot status /backup/etcd-snapshot-$(date +%Y%m%d-%H%M%S).db

执行完之后,我还会额外做一件事——把这份备份文件复制到一台跟当前集群毫无关系的机器上。因为我们接下来要做清理操作,万一过程中出了岔子,至少还有一份“后悔药”。

3.2 第二步:查看告警和安全指标

Etcd的故障状态是通过告警(alarm)机制上报的。这个机制类似报警器,一旦触发就会一直响,直到手动解除。用下面命令查看:

# 技术栈:Bash + Etcd v3 API

ETCDCTL_API=3 etcdctl \
  --endpoints=https://127.0.0.1:2379 \
  --cacert=/etc/etcd/ca.pem \
  --cert=/etc/etcd/server.pem \
  --key=/etc/etcd/server-key.pem \
  alarm list

正常情况下这个命令返回空列表,如果看到这种输出:

memberID: 1234567890abcdef alarm: NOSPACE 
memberID: 0987654321fedcba alarm: NOSPACE 

就说明多个Etcd节点都触发了空间告警,确定是我们要处理的场景。

3.3 第三步:搞清楚哪个版本号之后的数据可以清理

这里要提到一个概念叫revision,可以理解成Etcd的全局版本号。每次写入操作都会让这个数字加一,所以版本号越大代表数据越新。我们要做的“清理”并不是清掉当前值,而是清掉老版本留下的历史痕迹。

查看当前最新版本号的方法:

# 技术栈:Bash + Etcd v3 API

ETCDCTL_API=3 etcdctl \
  --endpoints=https://127.0.0.1:2379 \
  --cacert=/etc/etcd/ca.pem \
  --cert=/etc/etcd/server.pem \
  --key=/etc/etcd/server-key.pem \
  endpoint status --write-out=json | jq .

输出里有一个"revision"字段,那个数字就是我们关心的。假设输出显示"revision": 1024000,意味着当前共有1024000次写入记录。如果业务上你只需要保留最近1000次修改之前的历史,那么执行压缩时就可以用--rev=1019000之类的值。

这里特别提醒一句:压缩版本号不要选得太接近当前版本,留一点冗余量会保险得多。

3.4 第四步:压缩历史版本,为db文件瘦身

Etcd提供了内置的compact命令来处理历史版本。这个操作相当于把衣柜里的旧衣服整理打包丢掉,但注意——它只是逻辑上删除了旧数据,磁盘上的物理空间并不会马上还给操作系统。这一步的意义在于,后续执行空间整理时能够真正腾出地方。

看看完整的操作方式:

# 技术栈:Bash + Etcd v3 API

# 假设当前revision是1024000,我们保留最近4000次变更的历史
rev_to_keep=1020000

ETCDCTL_API=3 etcdctl \
  --endpoints=https://127.0.0.1:2379 \
  --cacert=/etc/etcd/ca.pem \
  --cert=/etc/etcd/server.pem \
  --key=/etc/etcd/server-key.pem \
  compact $rev_to_keep

# 压缩完成后再次查看db文件大小,发现几乎没有变化,这是正常的
# 因为物理空间还没释放,不要怀疑自己操作错了
du -sh /var/lib/etcd/member/snap/db

执行压缩时有几点讲究:

  • 必须在集群中多个节点都能访问的endpoint上执行,通常本地节点就行。
  • 压缩是异步过程吗?其实命令会阻塞直到完成,对于小型集群几分钟内就能结束。超大集群可能需要更久,这时候不要Ctrl+C中断,耐心等待。
  • 压缩只会影响历史操作级别,对key的当前值没有任何影响,所以不必担心业务数据丢失。

3.5 第五步:执行碎片整理,把空间真正还给操作系统

到了这一步,db文件逻辑上瘦了,但物理体积依然庞大得像一头撑大的河马。我们必须让它缩回原来的尺寸,这要靠defrag命令。它是真正的空间整理大师,把散落在文件各处的数据重新排列紧凑,然后让文件系统知道哪些块可以回收。

但这里有一条铁律:**只能在单个节点上执行,而且执行期间该节点的写请求会被短暂阻塞。**为了不中断业务,我的习惯是轮流处理——先处理一个节点,确认无异常后再处理下一个。

来看节点级别的完整清理脚本:

# 技术栈:Bash + Etcd v3 API(每个etcd节点独立执行)

# 停掉该节点上的etcd服务(使用systemd管理时)
sudo systemctl stop etcd

# 执行离线碎片整理
ETCDCTL_API=3 etcdctl \
  --endpoints=https://127.0.0.1:2379 \
  --cacert=/etc/etcd/ca.pem \
  --cert=/etc/etcd/server.pem \
  --key=/etc/etcd/server-key.pem \
  defrag

# 重新启动Etcd服务
sudo systemctl start etcd

# 等待几秒后检查节点健康状态
ETCDCTL_API=3 etcdctl \
  --endpoints=https://127.0.0.1:2379 \
  endpoint health

执行完defrag之后你再去看db文件大小,往往会吃惊——体积可能从2GB左右一下子掉到几十MB甚至更小。这里顺带提一嘴:defrag这个操作本质上是把数据整个重写一遍,相当于把衣柜拆了重装,所以请确定备份已经做完再执行。

3.6 第六步:调大quota参数,给未来留出余量

历史版本清理干净之后,如果quota上限还是原来的默认值,过不了多长时间又会触发同样的报警。所以调整quota-backend-bytes参数是必须走的一步。

那么这个值调到多少合适?我通常建议设置成“未来半年到一年业务写入量的预估峰值”,同时要结合机器内存和磁盘综合评估。因为Etcd在内存里也会维护一份数据索引,db文件太大时内存使用会水涨船高。

修改方式取决于Etcd的部署形态。如果是二进制方式启动,直接改启动脚本的参数;如果是Kubernetes静态Pod,要修改manifests文件并让kubelet重新拉起。我这里以一个标准的systemd管理方式为例,调整参数后重启服务:

# 技术栈:Bash + systemd + etcd

# 编辑etcd服务配置文件
sudo vim /etc/systemd/system/etcd.service

# 找到ExecStart那行,在末尾加上或修改如下参数
# --quota-backend-bytes=8589934592 即8GB,根据业务量自定义
# --auto-compaction-retention=1   这个后面单独讲

# 修改完成后重新加载服务配置并重启
sudo systemctl daemon-reload
sudo systemctl restart etcd

注意,调整quota参数生效需要重启Etcd,所以务必按“一个节点一个节点来”的方式操作,确保其他节点正常运行,集群本身不中断。

3.7 第七步:解除告警,验证读写正常

前面告警还在挂着,我们需要手动让它闭嘴。在任何一个节点上执行:

# 技术栈:Bash + Etcd v3 API

ETCDCTL_API=3 etcdctl \
  --endpoints=https://127.0.0.1:2379 \
  --cacert=/etc/etcd/ca.pem \
  --cert=/etc/etcd/server.pem \
  --key=/etc/etcd/server-key.pem \
  alarm disarm

执行后再查一次告警列表,确认没有输出。接下来做一次写读验证,用最小代价确认集群恢复正常:

# 技术栈:Bash + Etcd v3 API

# 写入一个测试键
ETCDCTL_API=3 etcdctl \
  --endpoints=https://127.0.0.1:2379 \
  put /test/write-check "ok"

# 读回这个键,看到输出"ok"说明读写链路正常
ETCDCTL_API=3 etcdctl get /test/write-check

# 验证完立即删除,不留垃圾数据
ETCDCTL_API=3 etcdctl del /test/write-check

这一步非常重要,因为光看告警解除并不代表写入真的通了,必须实际写一个key确认。

3.8 第八步:观察一段时间再放业务流量

恢复流程走完之后,不要急着把业务立刻全量切回。我的习惯是让集群在低负载状态下跑15到30分钟,观察以下指标:

  • etcd的is_leader状态是否稳定
  • db文件大小是否还在快速上涨
  • 告警列表有没有重新冒出NOSPACE
  • 业务写入错误日志是否清零

确认这些都没有异常后,再逐步放开业务流量,恢复完整服务。

四、避免重蹈覆辙的自动压缩配置

应急恢复完成后,最怕的就是同一个坑再次踩进去。Etcd提供了自动压缩机制,相当于给衣柜装了一个智能整理的装置,定期自动丢掉老旧的历史版本。推荐在配置里加上这两个参数。

--auto-compaction-retention有两个模式:一个是按版本号保留,另一个是按时间保留。我强烈推荐用时间模式,因为它更符合人类的理解习惯,也不用关心当前revision是多少。

举例来说,如果你希望Etcd只保留最近1小时内的历史版本数据(注意,是历史版本,不是当前值),可以这样配:

# 技术栈:Bash + systemd等等cd服务

sudo vim /etc/systemd/system/etcd.service

# 在ExecStart行里加入如下两行参数
# --auto-compaction-mode=periodic
# --auto-compaction-retention=1

这两个参数的含义是:每过一段时间,Etcd会自动计算出一个压缩点,把1小时之前的旧版本全部清理掉。这样db文件的增长速率就有了一个上限,不会再无限制膨胀下去。

不过有一点要明白:自动压缩解决的是逻辑空间的增长问题,它不会自动帮我们执行物理空间的碎片整理。所以我的建议是设置一个周期性的定时任务,比如每月执行一次defrag,双管齐下。

五、这个方案的适用场景、优势和不足

这套应急恢复方案目前来看最适合中小规模、业务对Etcd依赖非常核心的集群。所谓中小规模,大致是单节点数据量不超过10GB、revision增长相对平缓。在这种场景下,快照备份和执行compact/defrag都非常快,整个恢复流程可以在半小时内完成,业务影响可控。

优点很明显:第一,完整保留了当前最新数据的安全,不会出现任何数据丢失;第二,操作步骤透明,每一步都可以验证,适合严谨的生产变更流程;第三,一次性把历史版本、空间配额、自动压缩都调整到位,未来很长一段时间不需要再折腾。

缺点也同样明显。如果集群数据量非常庞大,达到几十GB甚至上百GB,每次defrag都是对IO性能的巨大考验,耗时可能是小时级别。同时,Etcd在这种规模下本身也会出现其他问题,比如fragmentation导致的内存暴增、watch事件积压等,单靠清理历史版本和调quota治标不治本,那个时候就要考虑Etcd数据重新迁移、或者换用其他分布式存储方案了。

另外还有一种情况是Etcd版本过旧,比如3.3甚至更早。那些版本本身在空间管理上就有缺陷,即使调整参数也无法彻底解决——比如旧版本没有自动碎片整理能力。这种情况建议优先规划节点滚动升级,先把版本拉新,再应用上面的策略才有效。

六、整个流程中的注意事项清单

把这些注意事项一条一条列在下面,都是我踩过坑后的经验,值得认真看一遍:

  • 备份优先于一切,哪怕你觉得集群已经凉了也要先备份,因为备份是唯一不依赖任何其他条件的兜底手段。
  • compact和defrag不是一回事,只做compact不做defrag,db文件物理大小不会变化,NoSpace告警很可能不会消失。
  • defrag必须逐节点进行,同时间只能处理一个节点,并且要在该节点上暂停etcd服务,否则极大概率导致数据竞争或写入失败。
  • quota参数不是越大越好,设置得过大意味着失控时风险也大。务必结合业务增长速度设定合理的上限,而不是拍脑袋给个超大数字。
  • quota调整后要确认所有节点配置一致,如果某个节点忘改了,后续这个节点可能成为整个集群的短板,被其他节点持续踢出集群。
  • 重启的顺序很关键,不允许所有Etcd节点同时重启,那是自杀式操作。正确做法是一个一个轮流重启,随时观察集群状态。
  • 注意API版本,旧版本习惯用ETCDCTL_API=2,新版本必须显式设为3,否则很多v3专属命令会报“unknown command”。
  • 压缩点编号要留余量,compact时别选当前revision,稍微往前挪一些,给未来可能出现的索引查询留点空间。
  • 验证写入测试时用不敏感key,比如/test/write-check这种前缀,千万别用跟业务相关的真实key做测试,万一忘记删除可能引出后续麻烦。
  • 监控告警的持久化配置,清理完一定要检查监控平台的告警阈值,看告警是否真的被触发过,并确认告警通知渠道畅通,不要出事半小时后才发现监控根本没推送。

七、写在最后

Etcd的NoSpace故障并不是什么罕见问题,尤其是那些频繁更新配置、服务注册信息变化快的集群,几乎每天都在跟历史版本做斗争。只要理解了它背后的空间管理机制,遵循“先备份、再compact、再defrag、再调参”的路线,就能把影响控制在最小范围,并且保证业务数据完整无损。

最关键的还是那句话——不要在故障发生时凭感觉操作,提前把方案烂熟于心,才能真正在紧急时刻稳住手脚。希望这篇文章能帮你提前把心态和技术都准备好。