文章目录
-
- [为什么 etcd 是 Kubernetes 的生死线](#为什么 etcd 是 Kubernetes 的生死线)
- [Raft 一致性协议:etcd 的共识机制](#Raft 一致性协议:etcd 的共识机制)
-
- [为什么 etcd 需要共识协议](#为什么 etcd 需要共识协议)
- [Leader Election:心跳触发的选举机制](#Leader Election:心跳触发的选举机制)
- 日志复制:写入如何成为共识
- [两层存储架构:WAL 与 bbolt](#两层存储架构:WAL 与 bbolt)
-
- [WAL:预写日志,Raft 的执行记录](#WAL:预写日志,Raft 的执行记录)
- bbolt:嵌入式键值数据库,持久化存储
- [WAL 与 bbolt 的协作关系](#WAL 与 bbolt 的协作关系)
- [备份与恢复:etcd 数据安全的完整流程](#备份与恢复:etcd 数据安全的完整流程)
-
- 快照备份:获取数据的时间点副本
- [灾难恢复:--bump-revision 的关键作用](#灾难恢复:--bump-revision 的关键作用)
- [etcd v2 与 v3 API 存储后端的本质区别](#etcd v2 与 v3 API 存储后端的本质区别)
- [监控体系:etcd 健康状态的判断依据](#监控体系:etcd 健康状态的判断依据)
-
- 必须监控的关键指标
- [etcd 监控仪表盘的核心视图](#etcd 监控仪表盘的核心视图)
- [性能调优:磁盘 I/O 与内存配置](#性能调优:磁盘 I/O 与内存配置)
-
- [磁盘 I/O 是 etcd 性能的生死门](#磁盘 I/O 是 etcd 性能的生死门)
- 碎片整理:删除数据后磁盘不释放的根因
- [空间配额:NOSPACE 告警与恢复](#空间配额:NOSPACE 告警与恢复)
- 运维检查清单
- 总结
前置知识:本篇文章需要读者对 Kubernetes 的基本架构有初步了解,包括 API Server、Controller Manager、Scheduler 等核心组件的作用。若对 K8s 基础概念尚不熟悉,建议先阅读本系列前面的文章。
为什么 etcd 是 Kubernetes 的生死线
Kubernetes 是一个状态机驱动的系统------API Server 是状态机的输入端,etcd 是状态机的存储端。整个集群的所有资源对象(Deployment、Pod、Service、ConfigMap、Secret、Ingress、PV......)最终都持久化在 etcd 中。没有 etcd,Kubernetes 控制器无法确认当前集群的真实状态;没有 etcd,调度器无法知道有哪些节点可用;没有 etcd,kubelet 无法知道本节点应该运行哪些 Pod。
一旦 etcd 不可用,Kubernetes 集群会在几秒内停止处理任何变更操作:新的 Deployment 无法创建、HPA 无法调整副本数、Deployment 滚动更新会卡在半途。即使 Kubernetes 控制平面的其他组件运行正常,etcd 故障等同于集群全面瘫痪。
这才是 etcd 最需要被理解的地方:它不是"一个存储服务",它是 Kubernetes 的唯一真实数据来源。这也是为什么 etcd 官方文档明确建议生产环境使用至少 3 节点集群,并分配专用磁盘。
Raft 一致性协议:etcd 的共识机制
为什么 etcd 需要共识协议
etcd 是一个分布式键值存储,这意味着数据同时存在于多个节点上。在多副本场景下,有一个根本性的问题需要回答:如果网络分区发生了,部分节点看到的值和另一部分节点看到的值不一致,应该以哪个为准?
Raft 协议回答了这个问题。Raft 将分布式共识拆解为三个子问题:领导者选举(Leader Election) 、日志复制(Log Replication) 、安全性(Safety)。etcd 正是基于 Raft 协议实现多副本数据一致性的。
Leader Election:心跳触发的选举机制
在 Raft 协议中,每个 etcd 节点处于三种状态之一:Follower(跟随者) 、Candidate(候选人) 、Leader(领导者)。正常运行时,集群只有一个 Leader,所有写入请求必须经过 Leader 处理。
选举的触发条件是心跳超时。每个 Follower 节点都有一个随机化的选举超时时间(默认 100~500ms),如果 Follower 在这个时间内没有收到 Leader 的心跳(包含 Leader 心跳的 RPC 请求),就会认定当前 Leader 已失效,发起新一轮选举。
选举过程如下:
Follower-3 Follower-2 Leader Follower-1 Follower-3 Follower-2 Leader Follower-1 正常运行:Leader 定期发送心跳 心跳超时,切换为 Candidate 获得多数票,成为新 Leader AppendEntries RPC (心跳) AppendEntries RPC (心跳) AppendEntries RPC (心跳) 增加当前任期号 给自己投票 RequestVote RPC RequestVote RPC 同意投票(term 更大,尚未投给其他人) 同意投票(term 更大,尚未投给其他人) 我现在是 Leader,开始同步日志 我现在是 Leader,开始同步日志
选举成功需要满足两个条件:获得超过半数节点的投票 (即 Raft 的"多数派"原则),且任期号(term)是所有节点中最大的。这两个条件缺一不可,确保 Raft 只能产生唯一的 Leader,且新的 Leader 一定包含所有已提交的日志条目。
日志复制:写入如何成为共识
当客户端向 Leader 写入一个键值对时,Leader 并不会立即返回成功。它首先将这个操作追加到本地 WAL(Write-Ahead Log),然后并行地向所有 Follower 发送 AppendEntries RPC 请求,要求 Follower 也将这条日志追加到各自的 WAL 中。
是
否
客户端写入
key: /registry/pods/default/nginx
Leader 追加到
本地 WAL
并行发送 AppendEntries RPC
到所有 Follower
收到多数派
ACK?
Leader 提交
(写入 bbolt)
写入失败
返回客户端错误
Leader 回复客户端
写入成功
后续心跳通知
Follower 同步提交
为什么需要 WAL? Raft 协议要求在将数据应用到状态机之前,先将操作记录到持久化日志中。这是因为在 Leader 将数据应用到状态机之后,Follower 可能还没有收到这条数据------如果此时 Leader 崩溃,Follower 成为新 Leader 后必须能够完整恢复之前所有的操作。WAL 就是这个持久化日志。
为什么需要多数派确认? 假设集群有 5 个节点,如果只有 2 个节点确认了写入,此时 Leader 崩溃,剩余 3 个节点中没有任何一个包含这条数据,新 Leader 必然不会包含这条日志------那么这条数据就丢失了。多数派确认确保了数据在 Leader 崩溃后仍然可以被新的 Leader 恢复。
两层存储架构:WAL 与 bbolt
这是 etcd 运维中最容易被忽视的知识点。大多数文章把 etcd 的存储一笔带过,但理解 WAL 和 bbolt 的关系,是理解 etcd 备份、压缩、碎片整理行为的根本前提。
WAL:预写日志,Raft 的执行记录
WAL(Write-Ahead Log)目录中存储的是 Raft 协议的原始操作日志。每一条写入请求在提交之前都会先写入 WAL,格式为 wal 文件(序列化的 proto 消息)。WAL 的作用是记录所有尚未被快照保存的操作,以便节点重启时能够重放日志恢复状态。
WAL 文件达到一定大小后会生成新的文件,etcd 后台会定期将 WAL 中的已提交日志打包成快照快照文件,释放 WAL 空间。这个过程由 etcd 自动完成,不需要人工干预,但监控 WAL 目录的大小变化是运维的一个重要关注点。
bbolt:嵌入式键值数据库,持久化存储
bbolt(formerly BoltDB)是 etcd v3 的持久化存储引擎。它是一个嵌入式、只读的 B+ 树数据库,所有已提交的 Raft 日志最终会被应用到 bbolt 数据库中。每个 etcd 节点都有且只有一个 db 文件,路径在 $DATA_DIR/member/snap/db。
bbolt 的核心特性:删除操作不会立即回收磁盘空间 。bbolt 使用 B+ 树存储数据,当一个 key 被删除时,bbolt 只是将对应的 B+ 树节点标记为可复用,并不会将空间归还给操作系统。这导致 etcd 的数据库文件会越来越大,即使大量数据被删除,磁盘使用量也不会下降。这个现象叫做存储碎片(fragmentation)。
理解这一点,才能理解为什么 etcd 运维中有两个看似矛盾的操作:压缩(Compaction)和碎片整理(Defragmentation)------前者清理历史版本数据,后者整理存储碎片归还磁盘空间。
WAL 与 bbolt 的协作关系
bbolt 层(持久化 KV 存储)
快照层(WAL 打包快照)
WAL 层(原始操作日志)
定期打包
重启时加载
重放 WAL 恢复
已快照,可删除
wal-00001
wal-00002
wal-00003
snap-00001.db
snap-00002.db
member/snap/db
节点启动时,etcd 按以下顺序恢复状态:
- 加载最新的快照文件(
snap-*.db),恢复最近一次快照时刻的 bbolt 数据库状态。 - 从快照对应的 WAL index 开始,重放后续 WAL 文件中的操作,将数据库恢复到崩溃前的最新状态。
这个两层设计保证了即使节点在写入过程中崩溃,重启后也能通过 WAL 重放恢复完整数据------这是 Raft 协议"已提交日志不丢失"承诺的技术基础。
备份与恢复:etcd 数据安全的完整流程
警告 :以下操作均通过
etcdctl或etcdutl工具执行。请根据实际安装的 etcd 版本确认使用 v2 还是 v3 版本 API。Kubernetes 集群通常使用 etcd v3 API ,因此所有命令均使用ETCDCTL_API=3前缀。
快照备份:获取数据的时间点副本
etcd 支持两种快照获取方式,从运行中的节点获取快照是生产环境的推荐方式。
方式一:从运行中的 etcd 节点获取快照(推荐)
bash
# 在任意一个可连通 etcd 节点的机器上执行
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-snapshot-$(date +%Y%m%d).db
方式二:从 etcd 数据目录直接复制(风险较高)
bash
# 直接复制 bbolt 数据库文件(可能丢失 WAL 中尚未持久化的数据)
cp /var/lib/etcd/member/snap/db /backup/etcd-db-$(date +%Y%m%d)
直接复制数据目录的方式可能丢失尚未写入 bbolt 的 WAL 数据,因此仅在 etcd 完全不可用时才考虑使用。
验证快照状态
bash
ETCDCTL_API=3 etcdutl snapshot status /backup/etcd-snapshot-20260520.db -w table
输出示例:
+----------+----------+------------+------------+
| HASH | REVISION | TOTAL KEYS | TOTAL SIZE |
+----------+----------+------------+------------+
| 7ef846e | 485261 | 11642 | 94 MB |
+----------+----------+------------+------------+
这里有两个关键指标需要关注:REVISION (快照对应的修订版本号)和 TOTAL KEYS(快照包含的键数量)。REVISION 决定了恢复后 Kubernetes Informer 的缓存行为。
灾难恢复:--bump-revision 的关键作用
如果 etcd 集群彻底不可用(例如多节点同时磁盘损坏),需要基于快照重建集群。恢复流程中最容易被忽略的一步是修订版本回退问题。
当快照恢复到新集群后,etcd 的修订版本会倒退到快照时刻的值。在 Kubernetes 环境中,这会导致一个严重问题:所有使用 Watch API 的组件(API Server、Controller Manager、Scheduler、 kubelet 中的 Informer)维护了一个本地缓存(Informer Index),这个缓存依赖修订版本号来判断数据是否更新。一旦修订版本号倒退,Informer 的本地缓存会认为"数据没有变化",从而拒绝刷新,导致集群状态与实际状态永久不一致。
解决方案:使用 --bump-revision 参数跳过修订版本回退
bash
# 为三个节点分别恢复数据目录
# 节点 1
etcdutl snapshot restore snapshot.db \
--name m1 \
--data-dir /var/lib/etcd/m1.etcd \
--initial-cluster m1=https://host1:2380,m2=https://host2:2380,m3=https://host3:2380 \
--initial-cluster-token etcd-cluster-1 \
--initial-advertise-peer-urls https://host1:2380 \
--bump-revision 1000000000 \
--mark-compacted
# 节点 2、节点 3 同样操作,仅修改 --name 和 --initial-advertise-peer-urls
--bump-revision 1000000000 将修订版本号增加 10 亿,确保恢复后的修订版本永远不会低于原集群的历史最大值。--mark-compacted 参数将 bump 后的所有修订版本标记为已压缩状态,强制终止所有 watch 连接,迫使所有 Informer 重建本地缓存。
关于 --force-new-cluster :部分运维文档提到
--force-new-cluster参数可以在保留数据的同时用单个节点启动集群。强烈不建议在生产环境中使用此参数 。如果原集群的其他节点仍然存活,使用--force-new-cluster启动的新节点会与原节点产生脑裂,导致数据不一致甚至 etcd 进程 panic。
etcd v2 与 v3 API 存储后端的本质区别
Kubernetes 从 v1.11 开始默认使用 etcd v3 API。v2 和 v3 的存储后端完全不同:
| 维度 | etcd v2 | etcd v3 |
|---|---|---|
| 存储后端 | 内存树(in-memory tree) | bbolt(持久化 B+ 树) |
| 数据持久化 | 快照文件(v2 store) | bbolt + WAL |
| 历史版本 | 不保留 | 保留完整历史(通过 revision) |
| API 版本 | v2 API | v3 API(K8s 1.11+ 默认) |
| 压缩机制 | 无内置压缩 | 支持 revision-based compaction |
| K8s 支持 | K8s 1.10 及之前版本 | K8s 1.11+ |
关键影响:如果管理着较老版本的 Kubernetes 集群,需要注意 etcd v2 没有内置的 compaction 和 defragment 机制,数据量增长到一定程度后只能通过重启释放内存。升级到 K8s 1.11+ 并迁移到 etcd v3 是解决此问题的唯一路径。
监控体系:etcd 健康状态的判断依据
监控 etcd 的核心原则是:etcd 的问题最终表现为 API Server 的延迟问题。等到 API Server 超时再去查 etcd,往往已经错过了最佳干预时机。
必须监控的关键指标
etcd_server_leader_changes_seen_total(领导者变更次数)
每次领导者变更都意味着一次 Raft 重新选举,在此期间集群无法处理写入请求。正常运行的集群不应该频繁出现领导者变更。如果这个计数器的增长速度异常(例如每小时超过 1 次),需要立即检查:网络是否稳定、节点负载是否过高、WAL 写入延迟是否异常。
etcd_server_has_leader(是否有领导者)
这是 etcd 最关键的告警指标。当值为 0 时,说明当前节点没有 Leader,集群已经停止处理写入。这个指标应该配置为立即告警(P0 级)。
etcd_server_proposals_pending(待处理提议数)
提议(Proposal)是 Raft 协议中一次写入请求的抽象。当 Leader 收到客户端请求后,会生成一个提议,等待多数派节点确认。如果 proposals_pending 持续增长,说明 Leader 无法在合理时间内获得多数派确认------通常是网络延迟或磁盘 I/O 延迟导致 WAL 写入变慢。这个指标的增长是 etcd 性能退化的最早期信号。
etcd_disk_wal_fsync_duration_seconds( WAL fsync 延迟)
每次写入 WAL 时,etcd 必须调用 fsync 将数据刷到磁盘。fsync 是阻塞操作,其延迟直接决定了 Raft 协议能否按时完成心跳和日志复制。etcd 官方建议 WAL fsync P99 延迟不超过 100ms。超过这个阈值会导致 Leader 心跳超时,触发不必要的重新选举。
这个指标对磁盘类型极为敏感。etcd 官方明确建议:WAL 目录必须使用专用 SSD,且不建议使用任何形式的网络存储(NFS、云厂商的网络块存储)------因为 fsync 的延迟在这些存储类型上不可控。
etcd_disk_backend_commit_duration_seconds(bbolt 提交延迟)
与 WAL fsync 不同,这个指标反映的是 bbolt 数据库层面的提交延迟。bbolt 提交通常比 WAL 写入更快,但如果数据库碎片化严重(B+ 树深度增加),提交延迟会显著上升。这个指标是碎片整理需求的重要判断依据。
dbSize 和 wal_size(存储空间监控)
监控 /var/lib/etcd/$DATA_DIR/member/snap/db 文件大小和 WAL 目录大小。WAL 目录持续增长通常是快照打包不正常的信号;数据库文件持续增长且删除数据后不下降,是碎片积累的表现。
etcd 监控仪表盘的核心视图
etcd 监控仪表盘核心视图
是
否
是
否
是
否
has_leader == 0
⚠️ 紧急告警 P0
检查集群成员数
是否满足多数派
网络是否正常?
检查 WAL fsync 延迟
检查网络连通性
etcd_peer_latency
wal_fsync P99 > 100ms?
检查磁盘 I/O
是否使用 SSD
检查 proposals_pending
重新挂载 SSD
或升级磁盘规格
proposals_pending 持续增长?
etcd 性能退化
考虑重新构建成员
正常波动
性能调优:磁盘 I/O 与内存配置
磁盘 I/O 是 etcd 性能的生死门
etcd 对磁盘延迟极为敏感,原因是 Raft 协议中的 WAL 写入和快照保存都需要同步 I/O。如果磁盘延迟超过阈值,不仅写入性能下降,还会触发 Leader 心跳超时,导致集群频繁重新选举。
生产环境 etcd 磁盘配置原则:
| 配置项 | 推荐值 | 说明 |
|---|---|---|
| WAL 磁盘 | 专用 SSD(最低 NVMe) | WAL fsync 直接影响 Raft 协议性能 |
| 数据磁盘 | 可与 WAL 共用 SSD,或独立 SSD | 分离部署可减少读写竞争 |
| 文件系统 | xfs 或 ext4 | 生产环境不建议使用 btrfs |
| RAID 配置 | RAID 0(性能优先)或无 RAID | RAID 5/6 的写入放大效应会显著拖慢 fsync |
| 网络存储 | 禁止用于 WAL | NFS/NAS 的 fsync 延迟不可控,会破坏 Raft 时序 |
碎片整理:删除数据后磁盘不释放的根因
如前所述,bbolt 删除 key 后不会归还磁盘空间。这个行为在 Kubernetes 环境中尤为重要------当一个 Deployment 滚动更新时,旧的 ReplicaSet 资源对象会被删除;当你删除大量资源对象时,这些数据在 bbolt 中被标记删除,但磁盘空间不会自动释放。
碎片整理操作:
bash
# 针对单个节点
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 \
defrag
# 针对集群所有节点
ETCDCTL_API=3 etcdctl --endpoints=https://127.0.0.1:2379,https://127.0.0.1:2379,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 \
defrag --cluster
注意:碎片整理会阻塞当前节点的 I/O 操作。如果对所有节点同时执行碎片整理,会造成集群暂时性写入延迟升高。建议逐个节点操作,且在业务低峰期进行。
空间配额:NOSPACE 告警与恢复
etcd 有内置的空间配额保护机制,防止数据库无限增长。当 bbolt 数据库文件超过 --quota-backend-bytes(默认约 2GB,实际值与 --auto-compaction-retention 相关)时,etcd 会在集群范围内触发 NOSPACE 告警,并进入只读维护模式------集群将无法处理任何写入操作。
空间配额告警的完整恢复流程:
bash
# 第一步:获取当前修订版本
rev=$(ETCDCTL_API=3 etcdctl endpoint status \
--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 \
-w json | jq -r '.[0].Status.header.revision')
# 第二步:压缩旧修订版
ETCDCTL_API=3 etcdctl compact $rev \
--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
# 第三步:碎片整理释放空间
ETCDCTL_API=3 etcdctl defrag \
--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 \
--cluster
# 第四步:解除 NOSPACE 告警
ETCDCTL_API=3 etcdctl alarm disarm \
--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
# 第五步:验证写入恢复
ETCDCTL_API=3 etcdctl put test-key test-value \
--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
预防胜于治疗:在生产环境中,应该配置自动压缩策略,防止数据库无限增长:
yaml
# kubeadm 部署的 etcd Pod 启动参数(修改 Pod spec 或 static Pod manifest)
spec:
containers:
- name: etcd
command:
- /usr/local/bin/etcd
- --auto-compaction-mode=revision
- --auto-compaction-retention=1000
- --quota-backend-bytes=4294967296 # 4GB,根据实际集群规模调整
--auto-compaction-mode=revision --auto-compaction-retention=1000 表示保留最近 1000 个修订版本,自动丢弃更早的历史数据。建议根据集群写入频率调整保留窗口------高频写入集群建议 retention=10000 或更大。
运维检查清单
最后给出一个 etcd 日常运维的检查清单,按重要程度排序:
日常预防
每日快照备份
验证快照完整性
监控磁盘使用率趋势
WAL 目录大小监控
P2 - 计划处理
dbSize 接近配额 80%
调整自动压缩策略
安排碎片整理窗口
验证压缩效果
执行碎片整理
逐节点低峰期操作
P1 - 24小时内处理
proposals_pending 持续增长
检查 WAL fsync 延迟
leader_changes 增速异常
检查节点负载和网络
P0 - 立即处理
否
是
has_leader == 0
集群不可用
立即介入
检查多数派是否满足
网络是否正常?
修复网络问题
检查故障节点磁盘
准备恢复
总结
etcd 运维的核心可以归纳为三句话:理解 WAL + bbolt 的两层存储架构 ,才能理解备份、压缩、碎片整理的真正含义;把磁盘 I/O 当作 etcd 的生命线 ,SSD 是最低要求而不是锦上添花;把快照备份当作 Kubernetes 集群的生命保险,没有备份的 etcd 等于没有数据保护。
Kubernetes 集群的稳定性上限由 etcd 决定。API Server 再快、调度器再智能、控制器再多,如果 etcd 出现性能退化或数据损坏,整个控制平面都会陷入停顿。理解 etcd 的内部机制,不是为了成为 etcd 专家,而是为了让 Kubernetes 集群真正稳定地运行在可预期的基础上。
如果这篇文章对你有帮助,欢迎点赞、收藏。如果还想了解更多 Kubernetes 运维相关的实战内容,可以关注作者的后续更新。