etcd 是 Kubernetes 的唯一持久化存储——集群里所有的 Pod、Service、Secret、CRD 实例、RBAC 规则、Lease 对象都存在这里。它一旦不可用,API Server 只能读缓存、无法写入,整个控制面等于瘫痪;它一旦数据损坏且没有备份,集群只能重建。然而 etcd 的运维门槛恰恰在于:它不是一个普通数据库,它的性能瓶颈几乎全在磁盘 fsync 延迟上,而它的容量问题表现为一个容易被忽略的 NOSPACE 告警。本文覆盖从拓扑设计、备份恢复、碎片整理到配额与调优的完整运维面。
1. etcd 的角色与数据模型
1.1 唯一数据存储
kubectl / 控制器 / kubelet
↓ (只与 API Server 通信)
kube-apiserver
↓ (唯一直接读写 etcd 的组件)
etcd 集群(3 或 5 节点)
这条链路决定了两个结论:① 没有任何组件能绕过 API Server 直接改 etcd(否则会破坏 watch cache 一致性);② etcd 的延迟会 1:1 放大成整个集群的写入延迟。
1.2 key 布局
etcd 里的 key 是扁平的字节串,Kubernetes 按资源类型分组:
# 常用查询(在 etcd 容器内用 etcdctl)
etcdctl get /registry/pods/default/nginx --keys-only
etcdctl get /registry/ --prefix --keys-only | head
/registry/pods/<namespace>/<name>
/registry/services/specs/<namespace>/<name>
/registry/secrets/<namespace>/<name>
/registry/events/<namespace>/<name>.<uid>
/registry/apiextensions.k8s.io/customresourcedefinitions/<name>
/kubernetes.io/leases/kube-node-lease/<node> # 节点心跳 Lease
Event 与 Lease 是量最大的两类 key:Event 默认保留 1 小时(--event-ttl),Lease 每个节点每秒刷新一次。大集群的 etcd 增长,80% 来自这两者。
1.3 MVCC 与 revision
etcd 使用 MVCC(多版本并发控制),每次写都会让全局 revision 单调递增,旧版本保留直到被压实(compaction):
etcdctl endpoint status --write-out=table
# 输出含:DB SIZE、IS LEADER、RAFT INDEX、RAFT TERM、RAFT APPLIED INDEX、REVISION
DB SIZE 包含历史版本,所以**「删了对象但 DB 没变小」是正常的**——需要 compaction + defrag 才会真正回收空间。
一句话:etcd 的空间由「历史版本 + Event/Lease」共同撑大,只看对象数量无法判断容量风险,要看
DB SIZE与REVISION。
2. 集群拓扑与选主
2.1 为什么必须是奇数节点
etcd 基于 Raft 共识算法,写操作需要**多数派(quorum = N/2 + 1)**确认:
| 节点数 | 多数派 | 容忍故障数 |
|---|---|---|
| 1 | 1 | 0 |
| 3 | 2 | 1 |
| 5 | 3 | 2 |
| 7 | 4 | 3 |
偶数节点(如 4)的多数派是 3,容忍度与 3 节点相同(1),但成本更高——所以永远用奇数。生产推荐 3(中小集群)或 5(大规模 / 跨 AZ)。
2.2 Raft 写入流程
客户端 → Leader
Leader 追加日志(本地 WAL 落盘 + fsync)
Leader 并行发 AppendEntries 给 Follower
Follower 落盘后 ACK
多数派 ACK → Leader 提交(apply 到 KV 存储)→ 返回客户端
每一次写入至少两次 fsync(Leader 一次、多数 Follower 各一次)。所以 etcd 的延迟下限由最慢的那个节点的磁盘 fsync 决定——这就是为什么 etcd 必须用 SSD/NVMe,且不能与高 IO 负载混部。
2.3 learner 与成员变更
# 查看成员与健康
etcdctl member list -w table
etcdctl endpoint health --cluster
etcdctl endpoint status --cluster -w table
新增成员时用 learner 模式先同步数据、再提升为投票成员,避免新节点加入瞬间拖慢集群:
etcdctl member add etcd-4 --learner --peer-urls=https://10.0.1.14:2380
# 等 learner 追上后
etcdctl member promote <member-id>
3. 备份与恢复
3.1 快照备份
# 在任一 etcd 节点上执行(etcdctl v3 API)
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 \
snapshot save /backup/etcd-$(date +%Y%m%d%H%M).db
# 校验快照完整性(重要!)
etcdctl snapshot status /backup/etcd-20261007.db -w table
# 输出:hash、revision、total keys、total size
要点:snapshot save 是在线操作,对集群影响很小(它会读一份一致性的快照),但要确保备份期间 Leader 没有发生切换(切换会导致快照失败)。建议对每个 etcd 节点都跑一次或至少固定一台。
3.2 恢复流程
恢复不是把文件拷回去,而是一个正式流程:
# 1) 停止所有 etcd 与 apiserver(否则会写入不一致数据)
systemctl stop kubelet
# 用静态 Pod 部署时,移动 manifest 即可
mv /etc/kubernetes/manifests/etcd.yaml /tmp/
# 2) 在每个节点上执行 restore(注意 initial-cluster 参数)
ETCDCTL_API=3 etcdctl snapshot restore /backup/etcd-20261007.db \
--name=etcd-1 \
--initial-cluster=etcd-1=https://10.0.1.11:2380,etcd-2=https://10.0.1.12:2380,etcd-3=https://10.0.1.13:2380 \
--initial-cluster-token=etcd-cluster-1 \
--initial-advertise-peer-urls=https://10.0.1.11:2380 \
--data-dir=/var/lib/etcd-restore
# 3) 替换数据目录并重启
rm -rf /var/lib/etcd && mv /var/lib/etcd-restore /var/lib/etcd
mv /tmp/etcd.yaml /etc/kubernetes/manifests/
最容易踩的坑:
| 坑 | 后果 | 对策 |
|---|---|---|
restore 时 --name/--initial-advertise-peer-urls 写错 | 成员身份错乱 | 每个节点用自己的一组值 |
忘了 --initial-cluster 包含全部成员 | 集群起不来 | 三个节点用同一份 initial-cluster |
| 数据目录权限不对 | etcd 启动失败 | chown -R etcd:etcd |
| 只恢复一个节点就启动全部 | 数据分叉 | 三个节点都 restore 后再一起启动 |
3.3 定期备份方案
# 用 CronJob 或集群外的定时任务做备份(推荐放在控制面外)
apiVersion: batch/v1
kind: CronJob
metadata:
name: etcd-backup
namespace: kube-system
spec:
schedule: "0 */2 * * *" # 每 2 小时
jobTemplate:
spec:
template:
spec:
hostNetwork: true
nodeSelector:
node-role.kubernetes.io/control-plane: ""
tolerations:
- operator: Exists
containers:
- name: backup
image: registry.k8s.io/etcd:3.5.9-0
command:
- /bin/sh
- -c
- |
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 \
snapshot save /backup/etcd-$(date +%s).db
restartPolicy: OnFailure
备份必须异地存储并定期演练恢复——没有演练过的备份等于没有备份。集群级的灾难恢复策略可参考 Kubernetes 备份与灾难恢复 。
一句话:etcd 备份是「快照 + 异地 + 演练」三件套,恢复是「全停 → 逐节点 restore → 一起启动」的正式流程,任何一步偷懒都会让集群回不来。
4. 碎片整理与压缩
4.1 compaction 与 defrag 的区别
这是 etcd 运维里最常混淆的一对概念:
| 操作 | 作用 | 是否释放磁盘 | 是否影响服务 |
|---|---|---|---|
| compaction | 删除某个 revision 之前的历史版本 | 否(逻辑删除) | 几乎无感 |
| defrag | 重整 boltdb 文件,回收空洞 | 是 | 阻塞该节点读写 |
顺序必须是先 compaction、再 defrag——直接 defrag 无法回收仍被引用的历史版本。
4.2 手动执行
# 1) 查看当前 revision
etcdctl endpoint status -w table
# 2) 压缩到某个 revision 之前(保留最近 1000 个版本)
etcdctl compact $(($(etcdctl endpoint status --write-out=json | jq -r '.[0].Status.header.revision') - 1000))
# 3) 碎片整理(逐个节点做,不要同时)
etcdctl defrag --endpoints=https://10.0.1.11:2379
etcdctl defrag --endpoints=https://10.0.1.12:2379
etcdctl defrag --endpoints=https://10.0.1.13:2379
# 4) 观察 DB SIZE 是否下降
etcdctl endpoint status --cluster -w table
4.3 自动压缩
etcd 支持自动压缩,避免历史版本无限堆积:
# etcd 启动参数
--auto-compaction-mode=periodic
--auto-compaction-retention=1h # 保留 1 小时内的历史版本
| 模式 | 参数含义 | 适用 |
|---|---|---|
| periodic | 按时间保留(如 1h、30m) | 通用推荐 |
| revision | 按版本数保留(如 10000) | 写入量稳定 |
注意:自动压缩只做 compaction,不做 defrag。所以 DB 文件仍会缓慢增长(空洞不回收),需要定期(如每周)做一次 defrag 或滚动重启 etcd 节点。许多发行版在 etcd 启动时自动执行 defrag(--defrag-on-start 之类),这也是「重启 etcd 后 DB 变小」的原因。
一句话:compaction 删逻辑版本,defrag 回收物理空间——自动压缩解决 90% 的增长问题,剩下的空洞靠定期 defrag 或滚动重启释放。
5. 配额与 NOSPACE 告警
5.1 quota-backend-bytes
etcd 默认配额是 2GiB,超过后整个集群变成只读:
--quota-backend-bytes=8589934592 # 8GiB
| 配额 | 说明 |
|---|---|
| 2 GiB(默认) | 小集群够用,大集群危险 |
| 4~8 GiB | 生产推荐 |
| > 8 GiB | 谨慎:etcd 恢复(restore/replay WAL)时间会显著变长 |
配额不是越大越好:DB 越大,etcd 启动时重放 WAL 的时间越长,故障恢复越慢。业界经验是控制在 8GiB 以内,超过说明该做 compaction/defrag,或该清理 Event。
5.2 NOSPACE 告警的处理
# 症状:API Server 报 etcdserver: mvcc: database space exceeded
etcdctl alarm list
# 输出:memberID:xxx alarm:NOSPACE
# 处理三步:
# 1) 压缩到最新 revision
rev=$(etcdctl endpoint status --write-out=json | jq -r '.[0].Status.header.revision')
etcdctl compact $rev
# 2) 逐节点 defrag
etcdctl defrag --endpoints=<每个节点>
# 3) 解除告警
etcdctl alarm disarm
只做 alarm disarm 而不压缩/defrag 是无效的——告警会立刻回来,因为空间并没有释放。
5.3 容量监控
关键指标(Prometheus):
etcd_mvcc_db_total_size_in_bytes # DB 物理大小
etcd_mvcc_db_total_size_in_use_in_bytes # 逻辑使用量(两者差距=碎片)
etcd_server_quota_backend_bytes # 配额上限
etcd_debugging_mvcc_db_compaction_pause_duration_milliseconds
告警阈值:db_total_size_in_bytes / quota_backend_bytes > 70% 就应预警,> 85% 必须处理。另外监控 in_use / total 的比值,若长期 < 0.5 说明碎片严重,该 defrag 了。
6. 性能调优
6.1 磁盘是第一瓶颈
etcd 对磁盘的要求几乎苛刻:
| 指标 | 推荐值 | 说明 |
|---|---|---|
| WAL fsync 延迟(p99) | < 10ms | 超过 25ms 集群开始不稳 |
| 磁盘类型 | 本地 SSD/NVMe | 禁止网络盘、禁止与日志盘共用 |
| 顺序写吞吐 | > 50MB/s | Raft 日志写入 |
| IOPS | > 500 | 峰值场景 |
# 用 fio 实测 fsync 延迟(etcd 官方推荐方法)
fio --rw=write --ioengine=sync --fdatasync=1 --directory=/var/lib/etcd \
--size=22m --bs=2300 --name=etcd-perf
# 关注输出中的 fsync 部分(p99)
最常见的性能事故:把 etcd 数据目录放在网络存储(EBS gp2、NFS)上,或与高写入的日志服务混部。前者 fsync 延迟随负载抖动,后者直接抢 IO。
6.2 核心参数
--heartbeat-interval=100 # 心跳间隔(ms),默认 100
--election-timeout=1000 # 选举超时(ms),默认 1000
--snapshot-count=10000 # 触发快照的已提交事务数
--max-request-bytes=1572864 # 单请求最大字节(默认 1.5MiB)
--max-snapshots=5 # 保留的快照数
--max-wals=5 # 保留的 WAL 数
| 场景 | 调整 |
|---|---|
| 跨 AZ 高延迟 | heartbeat-interval=250、election-timeout=2500 |
| 大对象(如大 CRD) | 调大 --max-request-bytes(同时要调 apiserver 的 --max-request-bytes) |
| 磁盘慢 | 优先换盘,不要靠调大 timeout 掩盖 |
6.3 网络与资源
- etcd 节点间必须低延迟(同 AZ 内 < 1ms,跨 AZ < 10ms);
- 单独网卡/独立带宽,避免与业务流量争抢;
- 不要与 apiserver 抢 CPU(etcd 对 CPU 敏感度中等,但 fsync 受 CPU 调度影响);
- 建议为 etcd 进程设置 CPU 亲和或独占核。
7. 监控与故障定位
7.1 核心监控项
| 指标 | 含义 | 告警线 |
|---|---|---|
| etcd_server_has_leader | 是否有 Leader | = 0 立即告警 |
| etcd_server_leader_changes_seen_total | Leader 切换次数 | 持续增长需排查 |
| etcd_disk_wal_fsync_duration_seconds | WAL fsync 延迟 | p99 > 25ms |
| etcd_disk_backend_commit_duration_seconds | 后端提交延迟 | p99 > 25ms |
| etcd_network_peer_round_trip_time_seconds | 节点间 RTT | p99 > 50ms |
| etcd_mvcc_db_total_size_in_bytes | DB 大小 | > 70% 配额 |
| etcd_server_proposals_failed_total | 提案失败 | 持续增长 |
7.2 常见故障对照
| 症状 | 根因 | 处理 |
|---|---|---|
集群只读 + database space exceeded | 达到配额 | compact → defrag → disarm |
| Leader 频繁切换 | 磁盘慢 / 网络抖动 | 查 fsync 延迟与 RTT |
| API Server 写入超时 | etcd 提交慢 | etcd_disk_backend_commit_duration_seconds |
| 某节点一直落后 | 磁盘差 / 网络分区 | 换盘,或用 learner 重建 |
| 恢复后数据「回退」 | 用了旧快照 | 确认快照 revision |
| 启动极慢 | WAL 重放 / DB 过大 | defrag、调小配额 |
# 集群健康一键检查
etcdctl endpoint health --cluster -w table
etcdctl endpoint status --cluster -w table
etcdctl alarm list
7.3 与集群运维的联动
etcd 的健康直接影响 Kubernetes 集群升级
:升级前必须做一次快照,升级中要观察 leader_changes,升级后要确认所有成员版本一致。当集群出现「API 变慢、kubectl 超时」时,第一站应该是 etcd 的 fsync 指标而不是 apiserver。更完整的控制面排障流程见 Kubernetes 集群排障与诊断
。
8. 小结
| 主题 | 关键结论 |
|---|---|
| 拓扑 | 奇数节点,3 或 5;跨 AZ 用 learner 加节点 |
| 备份 | snapshot save 在线快照 + 异地存储 + 定期演练 |
| 恢复 | 全停 → 逐节点 restore → 一起启动,参数别写错 |
| 空间 | compaction 删版本,defrag 回收空间,顺序不能反 |
| 配额 | 默认 2GiB 太小,建议 4~8GiB,>70% 预警 |
| 性能 | 磁盘 fsync 是唯一真瓶颈,用 fio 实测 |
| 监控 | has_leader、fsync p99、DB size 三件套 |
一句话记住:etcd 是 Kubernetes 的「心脏」,它的健康不取决于对象数量,而取决于磁盘 fsync 延迟与 DB 大小。把备份做成习惯、把 compaction/defrag 做成周期任务、把配额与 fsync 做成告警,集群的控制面才有真正的下限保障——因为 etcd 出问题,不是「某个服务挂了」,而是「整个集群失去了记忆」。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。