大部分人接触 etcd 的路径是:安装 Kubernetes → 看到控制面里有个 etcd → "哦,就是存 YAML 的地方"。然后把它当成一个"特殊一点的 Redis"或者"分布式版的 SQLite",存进去、读出来、定期备份,万事大吉。
但这种认知会在某个凌晨两点用极其惨烈的方式纠正你。
etcd 不是你熟悉的任何一类数据库。它不是关系型数据库(没有 SQL、没有 JOIN、没有事务 ACID 里剩下那三个),不是文档数据库(没有索引、没有查询语言),也不是缓存(没有 TTL、没有过期策略)。它本质上是一个共识引擎------存的不仅是数据,而是所有控制面组件对"集群现在是什么样子"的共识。
这个差异导致了一系列反直觉的行为。下面逐个拆开。
一、etcd 真正的核心不是"存",而是"共识"
先看一个最简单的场景:你执行 kubectl create deployment nginx --image=nginx,这条命令经过了 API Server → etcd。99% 的人理解的流程是"API Server 把 Deployment 对象存进了 etcd"。
实际上,真正发生的事情是:
yaml
kubectl → API Server → etcd Leader 收到写请求 →
Leader 把这条日志发给所有 Follower →
超过半数(quorum)回复"收到" →
Leader 把日志持久化,回复 API Server "写入成功"
这跟"写一条 SQL INSERT"有本质区别。SQL INSERT 只需要数据库实例自己确认就返回了。etcd 的写操作需要集群里大多数节点一致同意------这是 Raft 共识协议的要求。
这就是为什么 etcd 集群的节点数必须是奇数,而且通常选 3 或 5:
yaml
3 节点:容忍 1 台挂掉(quorum = 2,挂 1 台还能达成共识)
4 节点:容忍 1 台挂掉(quorum = 3,跟 3 节点一样,还多花了一台机器的钱)
5 节点:容忍 2 台挂掉(quorum = 3)
4 节点是个陷阱。你多花了一台机器的成本,却没有提高容错能力------因为 quorum 从 2 提到了 3,跟 3 节点比,4 节点允许挂掉的节点数还是 1 台。没有任何理由用 4 节点的 etcd 集群。
更重要的一组数字:
yaml
3 节点 etcd 集群:
- 1 台挂了 → 正常读写 ✅(还有 2 台,满足 quorum)
- 2 台挂了 → 完全不可用 ❌(只剩 1 台,不满足 quorum,连读都不行)
很多人都以为"3 台机器,挂了 2 台剩 1 台,至少有台机器还活着,是不是能读?"------不能。etcd 的读操作(线性一致性读)也需要 quorum。剩 1 台节点既不能写也不能读。整个 Kubernetes 集群的控制面跟着瘫痪------你没法创建 Pod、没法更新 Deployment、kubectl 基本废了。
这就是 etcd 跟普通数据库在心态上最大的区别:普通数据库你担心数据丢了,etcd 你担心集群达成不了共识。
二、etcd 不是"永远可用的 KV 存储"------它的容量上限会直接杀死你的集群
etcd 有一个默认参数:--quota-backend-bytes,默认值 2GB。
这不是建议值,是硬上限。一旦 etcd 的存储超过 2GB,etcd 进程不会报错、不会警告、不会自动清理------它直接拒绝所有写操作。所有的。包括你创建 Pod、更新 Deployment、kubectl apply 任何东西。
你的 Kubernetes 集群会变成只读状态。已有服务继续跑(Pod 在节点上运行不依赖 etcd),但你不能部署、不能扩缩容、不能更新配置------你的集群成了博物馆。
为什么会超过 2GB?三个最常见的原因:
第一,没开自动压缩(compaction)。 etcd 保留了每个 key 的所有历史版本(revision)。你改了一次 Deployment 的 replicas 从 3 到 5,etcd 里存了两个版本。改 1000 次就存 1000 个版本。这些历史版本在 compact 之前一直占着空间。
Kubernetes 自带了一个 compact 机制(--auto-compaction-mode=periodic --auto-compaction-retention=30m),但很多人用外部 etcd 部署时忘了配。结果是 etcd 空间缓慢增长,某天突然超 2GB,集群变砖。
第二,ConfigMap/Secret 当成"配置中心"用。 有些团队把大文件(几百 KB 甚至几 MB)塞进 ConfigMap,或者把 CI/CD 的日志输出存进去。etcd 不是对象存储。每个写入都要复制到 quorum 节点,一个 1MB 的 ConfigMap 在 3 节点集群里实际占用 3MB。多存 10 个就是 30MB。
第三,CRD 对象泛滥。 如果一个 Controller 有 bug,每秒钟创建一个 CRD 实例并且旧的不清理------etcd 被几十万个小对象撑爆的案例在我经历过的生产事故里排前五。
解药只有三个动作:compact(清理历史版本)→ defrag(回收磁盘空间)→ 调大 quota(如果你确实需要更多空间):
bash
# 1. 查看当前存储使用量
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
# 2. 压缩旧版本(保留最近 1 小时)
ETCDCTL_API=3 etcdctl compact $(etcdctl endpoint status --write-out="json" | jq .[0].Status.header.revision -r)
# 3. 碎片整理(回收磁盘空间,每个节点单独执行)
ETCDCTL_API=3 etcdctl defrag --endpoints=https://127.0.0.1:2379 ...
defrag 期间该节点不服务请求,所以在 3 节点集群里逐个执行------每台做完等它重新加入集群再做下一台。
三、etcd 备份不是"dump 一个文件",还原也不是"把文件拷回去"
这件事在 etcd 生产事故中占了最大的比例------备份存在但还原不了。
etcd 有两个你必须要知道的东西:
WAL(Write-Ahead Log): 每个写操作先记录到 WAL,再应用到数据文件。如果进程崩了,重启后从 WAL 重放丢失的写入。WAL 是连续写入的,文件会不断增长,etcd 会自动切割。
Snapshot(快照): 某个时间点的全量数据快照。用 etcdctl snapshot save 创建。
关键问题出在这:WAL 和数据文件是绑定在一起的。 你不能只拷数据文件到另一台机器上指望它能启动。正确的备份是打快照:
bash
ETCDCTL_API=3 etcdctl snapshot save /backup/etcd-snapshot-$(date +%Y%m%d%H%M).db
这个 .db 文件就是你的完整备份,包含了创建快照那一刻的所有数据。
还原时最大翻车点:etcd 版本不兼容。 用 etcd 3.4 创建的快照,在 etcd 3.5 的集群上还原时你会发现------还原不了。etcd 的存储格式在不同大版本间不保证兼容。快照文件名、扩展名可以随便叫,但版本号决定了它是可读的还是废的。
还原的正确姿势:
bash
# 先把快照还原到一个新的数据目录
ETCDCTL_API=3 etcdctl snapshot restore /backup/etcd-snapshot-20240715.db \
--data-dir=/var/lib/etcd-restored \
--name=etcd-0 \
--initial-cluster=etcd-0=https://127.0.0.1:2380 \
--initial-advertise-peer-urls=https://127.0.0.1:2380
# 然后修改 etcd 的 data-dir 指向还原后的目录并重启
注意:snapshot restore 会生成一个新的 etcd 数据目录,你要用这个新目录替换现有 etcd 的数据目录,再启动 etcd。不是把快照文件放到数据目录里就完事------那个文件只会被原封不动地无视。
另一个致命的认知偏差:你的 etcd 备份里只有 Kubernetes 资源对象,没有持久卷里的数据。 备份了 etcd ≠ 备份了数据库。如果一个 StatefulSet 里的 MySQL Pod 管理的 PVC 在 Ceph RBD 上,你的 etcd 快照只存了 "有个 StatefulSet 叫 mysql,它关联的 PVC 叫 mysql-data-0"------不存数据本身。数据库的备份你还要用 Velero、VolumeSnapshot 或应用层 mysqldump 单独做。
四、太多对象 = etcd 变慢 = 全集群变慢
Kubernetes 的几乎每一次操作都要经过 etcd。kubectl get pods 时,API Server 从 etcd 读取 Pod 列表。Scheduler 调度 Pod 时,从 etcd 读节点列表。Controller 做 reconcile 时,从 etcd 读它管理的对象。
这就是为什么"对象多了 etcd 就慢"这句话的真正意思是"全集群一起慢"。
etcd 并不擅长处理"全量扫描"。如果你有 10 万个 Pod、5 万个 ConfigMap、3 万个 Secret------那是一个很大的 etcd。当某个控制器想做一次全量 List 时,它需要 etcd 把所有匹配前缀的 key-value 打包返回,这个过程随着对象数呈线性增长。
这就是 Informer + Watch 机制存在的核心原因。Informer 在启动时做一次全量 List 建立本地缓存,之后通过 Watch 只接收增量更新------不需要反复全量扫描 etcd。但 Informer 只解决"反复 List"的问题,不解决"etc 里东西太多"的问题。
生产上的几条硬经验:
-
不要往 K8s 里塞大量小对象。 如果你的业务需要管理百万级配置项,用专门的配置中心,不要用 ConfigMap 堆。
-
CRD 控制器的调和周期要合理。 如果每个 CRD 实例每 10 秒触发一次全量 List,你的集群在有 1000 个实例时很快就会感觉"不对劲"。
-
定期审查集群里有多少资源对象。 很多集群跑了一两年,ConfigMap 累积了上千个、Secret 累积了几百个------很多是废弃的 Deployment 留下的垃圾。
kubectl get all --all-namespaces并不能"get all",它不列出 ConfigMap 和 Secret。
五、etcd 的选举超时,网络一抖就脑裂
etcd 用 Raft 协议在集群里选出一个 Leader。Leader 处理所有写请求,Follower 从 Leader 同步日志。选举超时(election timeout)默认是 1000ms,心跳间隔(heartbeat interval)默认是 100ms。
这套参数在稳定的网络环境里工作得完美。但在网络偶尔抖动的环境下------比如云环境的随机网络延迟峰值------会发生什么:
yaml
1. Follower 在 election timeout 内没收到 Leader 的心跳
2. Follower 认为 Leader 挂了,自己发起选举
3. 原 Leader 其实没挂,只是网络卡了一下
4. 集群进入一段时间的双 Leader 混乱状态 → 实际上两个都不能提供线性一致性保证
Kubernetes 控制面是 etcd 的客户端。当 etcd 内部在频繁选举时,API Server 看到的是一系列的 "etcdserver: request timed out" 或者 "context deadline exceeded"。结果是你的 kubectl 命令时快时慢、创建 Pod 偶尔超时------但节点上的 Pod 还是正常跑的。
这就是"Kubernetes 控制面卡了但业务没问题"的经典场景------控制面卡是 etcd 慢了,不是你的服务出了问题。
生产环境里,如果你的集群规模大(几千个 Pod、几百个节点),或者 etcd 磁盘是慢速卷(云厂商某些默认存储类型 IOPS 极低),election timeout 和 heartbeat 值得改成更宽松的值:
bash
# etcd 启动参数
--heartbeat-interval=250 # 从 100ms 调大到 250ms
--election-timeout=3000 # 从 1000ms 调大到 3000ms
这不是"越高越好"------太高了 Leader 真挂掉时恢复时间更长。这是在容忍网络抖动和加速故障恢复之间做取舍。
六、生产环境等保命的五条规则
如果只能记五条,就记这些:
1. 3 节点就够了,别用 4 节点,更别用单节点。
单节点 etcd 挂了 = 集群控制面完全不可用。3 节点是生产最低配置,不是推荐配置。
2. 配好自动压缩,别让它撑到 2GB。
如果你用的是 kubeadm 部署的 Kubernetes,--auto-compaction-mode=periodic --auto-compaction-retention=30m 一般已经配好了。如果是外部 etcd,一定确认这个参数存在。
3. 定期打快照,并确认快照能还原。
备份不验证就是没备份。每个月至少做一次还原演练------在一台隔离的机器上把快照还原到一个新的 etcd 进程,确认它能启动、能读写。
4. etcd 的磁盘不能用慢速卷。
云厂商的默认 HDD 或低 IOPS 卷是 etcd 的天敌。使用 SSD,至少 3000 IOPS。etcd 对磁盘延迟极其敏感------fsync 延迟高意味着每个写操作都慢,写操作多了集群就堆成队。
5. 定期做 defrag,别等到空间告警才动手。
defrag 可以做成 CronJob,在凌晨低峰期逐个节点跑。不要等到 etcd_mvcc_db_total_size_in_bytes 指标到了 1.8GB 才想起来。
etcd 是那种"平时你感受不到它的存在,一旦出事就是全集群级别事故"的组件。它不像你的业务服务那样天天触发告警,但每次出现在告警里------都是大事。理解它的共识机制、容量模型和备份还原的正确方式,不是"顺便学一下"的事,是运行 Kubernetes 集群的基本功。
一个集群你可以不用 Scheduler(静态 Pod),你可以不用 Controller Manager(手动管理资源),你可以关掉 kube-proxy 换 eBPF------但没有 etcd,Kubernetes 就不是 Kubernetes。它就是这个系统的根。