第 5 章 etcd:状态存储与一致性的代价

墨:老哥,这章进后厨。先问个扎心的:一个分布式系统,如果它的”状态存储”自己都不一致,上面再花哨的编排全是空中楼阁。 你认同吧?那”一致的状态存储”到底有多难?我们来把这层底裤扒了。


5.1 为什么 K8s 的状态非得存在 etcd 这种东西里?

回到第 2、3 章的设定:K8s 是”一堆控制器 + 一块共享状态板”。这块板必须满足几个硬条件:

  1. 一致(Consistent):所有控制器读到同一个值,不能 A 看到 3 个副本、B 看到 2 个;
  2. 持久(Durable):机器重启、进程崩,状态不丢;
  3. 可订阅(Watchable):状态一变,能推给所有监听者(第 3、4 章的 level-triggered 靠它);
  4. 高可用(Available):这块板自己不能动不动就挂。

墨:普通数据库(MySQL/PostgreSQL)能满足吗? 你先想,再往下看。

MySQL 主从复制是异步的,从库可能落后主库几秒——也就是说”一致”这条不满足(你读从库可能读到旧值)。而 K8s 的控制器们如果读到不一致的副本数,就会做出矛盾决策(一个去建、一个去删),集群就抽风。所以 K8s 要的是**强一致(线性一致 / linearizable)**的存储,且原生支持 watch。etcd 正是为这个场景生的:它用 Raft 保证强一致,用 MVCC + 事件流支持 watch。

一句话:etcd 是 K8s 选的”强一致 + 可 watch 的分布式 KV”。 它不是普通数据库,它是”系统的真相之源(source of truth)“。


5.2 一致性为什么这么贵:CAP 视角

分布式系统逃不掉 CAP 定理:网络分区(P)必然发生,你只能在**一致性(C)和可用性(A)**里选一个。

墨:分区发生时,你选 C 还是 A?K8s 为什么选 C?

etcd 选 C(一致性),牺牲部分 A。意思是:一旦 etcd 集群发生网络分区,少数派节点会停止对外服务(拒绝读写),宁可”不可用”,也不让你读到/写错旧数据。为什么 K8s 能接受这个?

因为 K8s 的”可用性”是分层的:etcd 短暂不可用,只是”新状态写不进、新调度暂停”,但已经跑起来的 Pod 还在跑(kubelet 是本地的,不依赖 etcd 实时在线)。也就是说,etcd 选 C 的代价是”控制面暂时僵住”,而不是”业务全灭”。这个代价 K8s 扛得起——比起”写入了矛盾状态导致集群精神分裂”,僵住几分钟可接受得多。

关键取舍:状态存储选 CP(一致优先),因为”错误的状态”比”暂时的静止”危害大得多。业务运行层不依赖存储实时在线,所以存储的 A 可以牺牲。

这又是一个”哪里该保守、哪里该激进”的分层设计典范。


5.3 Raft:etcd 怎么在不可靠的机器上达成一致

你要真懂 etcd,得懂一点 Raft——不用推导证明,但得懂它怎么干活。Raft 是 etcd(以及 Consul、TiKV 等)用的共识算法,目标是”让一群机器对’日志顺序’达成一致”。

5.3.1 三个角色

  • Leader(领导者):唯一能处理写请求、把日志复制给其他人的角色;
  • Follower(跟随者):平时啥也不决策,听 Leader 的,收心跳;
  • Candidate(候选者):Leader 挂了,Follower 变候选者,发起选举。

墨:为什么 Raft 要一个 Leader,而不是大家平权一起写?

因为”大家一起写”要对”谁先谁后”达成一致极难(冲突爆炸)。Raft 用”一切写都过 Leader”把复杂的并发共识,简化成”Leader 定序,其他人抄”。代价是 Leader 是热点(但 etcd 写吞吐本就不是为海量设计的,够用)。设计上”引入一个协调者”来化解分布式冲突,是极常见的套路——K8s 的调度也是类似思路(调度器是”分配协调者”)。

5.3.2 写流程:日志复制 + 提交

你(API Server)发一个写请求给 Leader:

  1. Leader 把这条写追加到自己的日志(log);
  2. Leader 并行发给所有 Follower:“把这条第 N 条日志记上”;
  3. 多数派(quorum) Follower 回”记好了”,Leader 就标记这条日志已提交(committed);
  4. Leader 应用到自己的状态机(真正改 KV),并通知 Follower 也应用;
  5. 返回你”写成功”。

注意第 3 步的”多数派”——这是 Raft 的灵魂,叫 quorum。

5.3.3 Quorum:为什么是”多数派”而不是”全部”

墨:Leader 要等所有人确认才提交,不行吗?

不行。如果要求”全部节点都确认”,那只要有一个节点慢或挂,写就永远卡住——可用性太差。Raft 要的是”超过半数(N/2+1)确认即可”:

  • 3 节点集群:quorum = 2,允许 1 个挂;
  • 5 节点集群:quorum = 3,允许 2 个挂;
  • 一般规律:N 个节点容忍 (N-1)/2 个故障,要求 N 是奇数。

为什么奇数?因为”多数派”在偶数时边界模糊:4 节点 quorum=3,容忍 1 个挂,和 3 节点一样容错,却多花一台机器——所以偶数节点是浪费。这就是**etcd 集群一律奇数(3/5/7)**的原因。

📦 案例(脑裂防呆): 5 节点 etcd,网络分成 2 和 3 两半。3 那半有 quorum(3≥3),继续服务;2 那半没有 quorum(2<3),拒绝所有写。这样绝对不会出现”两边各写各的、数据分裂”。等网络恢复,2 那半发现自己的日志落后,自动从 3 那半同步补齐。这就是 quorum 如何防止”脑裂导致双写”。

5.3.4 Term(任期):避免”过期 Leader”捣乱

每次选举,任期号(term)+1。任何节点收到”旧 term 的消息”直接忽略。这保证了同一个 term 里最多一个合法 Leader,过期的 Leader 想发号施令会被无视。相当于给”谁是老大”加了版本号,旧老大说话不算数。


5.4 MVCC 与 Watch:etcd 为什么能”订阅变化”

etcd v3 用 MVCC(多版本并发控制):每次写都留一个带版本号(revision)的快照,旧版本不立刻删。这带来两个能力:

  1. 无锁读:读历史版本不用加锁,读和写不互相阻塞;
  2. watch 基于 revision:客户端说”从 revision=100 开始看”,etcd 就把 100 之后的所有变更事件推给它。即使客户端短暂掉线,重连时带上”我从 100 看”,就能续上,不丢事件。

墨:这跟第 4 章讲的 List-Watch 对上了没?

完美对上。API Server 的 watch,底层就是 etcd 的 watch。etcd 的 MVCC revision 就是”状态变更的全局流水号”。API Server 在它之上做扇出缓存(第 4.7),etcd 提供”可靠的、可断点续传的变更流”。整个 K8s 的 level-triggered 自愈能力,根上依赖 etcd 这个”带版本的变更流”。 你品,一层套一层。


5.5 性能边界:etcd 不是万能仓库

墨:既然 etcd 这么强,我把业务的订单数据也存进去行不行?

绝对不行。etcd 有硬边界,K8s 设计时刻意”只用它存元数据”:

  • 写吞吐有限:Raft 每次写要多数派落盘 + 复制,单 etcd 集群写 QPS 通常在几千到一两万量级,远不能当业务数据库;
  • 单 key 大小限制:默认单 key-value 上限约 1.5MB(旧版甚至 1MB 出头),大对象塞不进;
  • 总数据量建议上限:官方建议不超过 8GB(甚至更小),因为快照恢复、defrag 都随数据量变慢;
  • 延迟敏感:每次写要网络 round-trip + 磁盘 fsync,P99 延迟在毫秒到十几毫秒。

所以 K8s 的铁律:etcd 只存”集群元数据”(Pod/Service/Deployment 这些对象的定义和状态),绝不存业务数据(用户上传的文件、订单记录)。 你的应用数据去数据库/对象存储,别碰 etcd。很多 K8s 事故就是有人往 ConfigMap/Secret 里塞了几 MB 的配置、或副本数开太大导致对象爆炸,把 etcd 写慢甚至写满。

⚠️ 坑: etcd 磁盘满了(quorum 全写不进)是集群级灾难——API Server 拒绝所有写,调度停止,但已运行 Pod 不受影响。监控 etcd 的 db size 和 fsync 延迟是生产必做项。


5.6 压缩与碎片:etcd 的”家务”

MVCC 一直留旧版本,磁盘会爆。etcd 有两套机制:

  • 压缩(Compact):定期删掉旧 revision 的历史(保留最近 N 个或最近一段时间)。压缩后旧版本不能 watch 了,但磁盘空间标记为空闲;
  • 碎片整理(Defrag):压缩只是”逻辑标记”,真实磁盘块没还操作系统。defrag 才真正把空闲块归还。defrag 时该 etcd 节点会暂停服务(短暂),所以要对每个节点轮流做。

墨:这像不像你熟悉的什么? 像不像 Go 的 GC?MVCC 历史像”还没回收的对象”,compact 像标记清除,defrag 像把内存真正还给 OS(Go 的 debug.FreeOSMemory)。分布式系统和单机 runtime 的家务活,道理相通。

演化注脚: etcd v2 用类文件系统的树形 key(/registry/pods/…),v3 改成扁平 KV + MVCC + 租约,性能和 watch 能力质的飞跃。K8s 从 1.几 版本起强制 etcd v3。这是”存储引擎换代解决老瓶颈”的典型——v2 的 watch 在大集群下事件丢失严重,v3 的 MVCC watch 才稳。又见”演化以重写/换引擎的形式发生”。


5.7 租约(Lease):短期对象的命门

etcd 有 Lease(租约) 机制:一个 key 绑定一个租约(带 TTL),租约过期 key 自动删。这被用来做存活检测——比如节点心跳、SA token 有效期,都靠 lease 自动失效,不用人工清理死对象。

墨:你想想,要是没有 lease,节点宕机了它的”心跳 key”谁去删? 没人。状态板会一直以为它还活着。lease 让”失效”自动化——宕机 = 心跳不续 = 租约到期 = 自动移除。又是”声明期望 + 系统自动收敛”的思想,在存储层的落地。


5.8 与 ZooKeeper / Consul 的对比(演化视角)

墨:你听过 ZooKeeper 吧?它和 etcd 干的事好像一样,K8s 为啥不用 ZK?

三者都能做”一致 KV + 协调”,但历史和取舍不同:

  • ZooKeeper:用 ZAB 协议,强一致,但 API 是树形 znode + 会话,且客户端逻辑重(要维护会话、要自己处理各种 watch 一次性失效)。K8s 早期(Borg 系)其实更熟 Chubby/ZK 思路,但 etcd 提供了更简洁的 HTTP/gRPC API + 原生 MVCC watch,更贴合云原生;
  • Consul:功能更全(服务发现 + KV + 健康检查一体),但 K8s 只需要”一致 KV + watch”,Consul 的额外功能用不上,且 Raft 实现和生态不如 etcd 与 K8s 绑定深;
  • etcd:Go 写(和 K8s 同语言,好维护)、Raft 实现清晰、gRPC 接口、MVCC watch 稳、社区和 K8s 同步演进。

选 etcd 不是因为它”最强”,是因为它”正好契合 K8s 要的那点:强一致 KV + 稳定 watch + Go 生态”。 系统设计里,“够用且契合”常胜过”功能最多”。


5.9 灾难恢复:etcd 是唯一不能丢的东西

墨:如果整个集群炸了,你恢复 K8s 靠什么? 靠 etcd 的备份快照(snapshot)。

etcd 可以 etcdctl snapshot save 导出全量快照。恢复时 snapshot restore 到一个新集群,再把 API Server 指向它。因为 etcd 是”真相之源”,只要快照在,集群状态就能完整回来;而 Pod 实际跑在哪、网络规则是什么,kubelet 和控制器会根据恢复后的状态重新调和出来(第 3 章的 level-triggered 又立功了)。所以备份 etcd = 备份整个 K8s。

⚠️ 坑: 很多人忘了备份 etcd,集群 etcd 数据损坏后只能重建——等于所有 Deployment/Service 配置全没,得从 Git 重新 apply(所以 GitOps 的价值在这:声明都在 Git,etcd 只是运行态缓存)。结论:etcd 要定期快照备份;同时,所有声明应存 Git,双保险。

演化看点: 托管 K8s(GKE/EKS/AKS)把 etcd 备份和恢复完全托管了——用户连 etcd 进程都摸不到。这是”把最不能出错的核心,交给云厂商专业运维”的趋势(第 16 章高可用会展开)。但代价是:你失去了对 etcd 的掌控,出问题只能找云厂商。又是”便利 vs 控制”的权衡。


5.10 本章演化线小结

  • 存储选型:一致 KV 必须 CP,etcd 用 Raft 实现线性一致;
  • 引擎换代:etcd v2(树形、watch 易丢)→ v3(扁平 MVCC、watch 稳),K8s 强制 v3;
  • 可用性策略:选 C 牺牲 A,因业务运行不依赖存储实时在线;
  • 家务机制:compact + defrag 管理 MVCC 历史,类比 GC;
  • lease 让”失效”自动化,是 level-triggered 在存储层的落地;
  • 恢复:etcd 快照 = 集群备份,Git 存声明 = 双保险;
  • 趋势:托管化,把 etcd 交给云厂商(便利换控制)。

5.11 本章思考题

🔧 思考题 1

5 节点 etcd,quorum=3。如果同时挂了 3 个节点(剩 2 个),会发生什么?此时 API Server 还能写吗?已运行的 Pod 受影响吗?网络恢复后怎么自行修复?

【参考答案】 剩 2 个 < quorum(3),etcd 停止接受写(少数派拒绝服务),API Server 写请求失败(创建/更新 Pod、调度结果都写不进)。但已运行的 Pod 不受影响——kubelet 本地管理,不依赖 etcd 实时在线;Service 的 iptables 规则也已生效在节点上。读(get)可能部分成功(看打到哪个节点)。网络恢复后:2 节点那半发现 term 落后、日志落后,自动向存活的(其实都恢复了,重新选举出 Leader),落后的节点通过 Raft 日志复制从 Leader 补齐,集群自愈。关键结论:etcd 选 C 的代价是”控制面僵住”,不是”业务死”——这正是 K8s 能接受 CP 的原因(呼应 5.2)。

🔧 思考题 2

etcd 的 MVCC watch 和”每秒轮询一次全量”比,优势在哪?如果客户端 watch 连接断了 10 秒,etcd 怎么保证它不丢事件?

【参考答案】 轮询劣势:(1) 延迟高(最坏等一个轮询周期);(2) 浪费带宽(每次拉全量,海量重复);(3) 服务端压力大(每次全量扫描)。MVCC watch:客户端带 revision 订阅增量,etcd 只推变化事件,低延迟低开销。断线 10 秒不丢事件,靠 revision 断点续传:etcd 保留历史 revision(未被 compact 的部分),客户端重连时带上”我从 revision=X 看”,etcd 把 X 之后的事件补推。前提是断线时长 < compact 保留窗口;若超过(旧版本已被 compact),etcd 返回”too old”,客户端退化成一次 List 重新全量同步,再继续 watch。这体现了”增量订阅 + 版本游标 + 过期回退全量”的通用可靠订阅三件套。

🔧 思考题 3

书里说”etcd 只存元数据,不存业务数据”。如果某团队把用户上传的头像(几 MB 一张)存进 ConfigMap 给 Pod 挂载,会有什么连锁灾难?从 etcd 边界倒推。

【参考答案】 (1) 单 key 超限:ConfigMap 默认上限 1MB(某些版本),几 MB 直接创建被拒;(2) 就算拆小,海量头像使 etcd 总数据量暴涨,超过 8GB 建议值,defrag/snapshot 恢复变慢;(3) 写放大:头像更新频繁 → etcd 写 QPS 飙升(etcd 写很贵,Raft 复制+fsync),挤占集群元数据写入,调度变慢;(4) watch 风暴:头像变 → ConfigMap 变 → 所有 watch 它的 Pod 的 kubelet 收到事件、可能触发 Pod 重建或重新挂载;(5) 备份膨胀:etcd 快照巨大,恢复慢,RTO 飙升。正确做法:头像存对象存储(S3/OSS),ConfigMap/Secret 只存”引用路径或挂载配置”。这题考的是”状态存储的边界意识”——K8s 的 etcd 是集群神经,不是你的硬盘。

🔧 思考题 4(进阶)

Raft 的 Leader 是写热点。如果 K8s 集群极大(比如百万 Pod),etcd 写压力集中在 Leader,怎么破?K8s 自身和 etcd 社区分别用了什么手段缓解(而不是加机器)?

【参考答案】 加 etcd 节点不能提升写吞吐(写要 quorum 复制,节点越多复制越慢),所以破法在”减少写”和”拆分”:(1) K8s 侧减少写:status 子资源分离——Pod 的 spec(用户期望)和 status(kubelet 上报)分开,kubelet 频繁更新 status 不污染 spec 的 watch;且只上报变化字段。(2) 拆分 etcd(多租户/多存储):超大集群把不同资源组放不同 etcd 实例(event 单独存一个 etcd,因为事件量巨大且可丢,不影响核心);(3) etcd 自身优化:更高效的 boltdb 批处理、gRPC 流式、调大 max-request-batch;(4) 降低 watch 压力:API Server 层 Informer 缓存扇出(第 4.7),避免几千客户端直击 etcd。本质思路:“热点不能靠堆副本解决,要靠减少经过热点的流量 + 把热点按类型拆分”。这是所有中心化写入系统的通用解法(如 MySQL 写分库分表思路一致)。


5.12 小结

  • etcd 是 K8s 的”真相之源”:强一致、持久、可 watch、高可用;
  • Raft 用 Leader 定序 + 多数派(quorum)提交,奇数节点容 (N-1)/2 故障;
  • 选 CP 牺牲 A,因业务运行不依赖存储实时在线,僵住比精神分裂好;
  • MVCC + revision 支撑可靠的、可断点续传的 watch,是 level-triggered 的根;
  • etcd 有吞吐/单key/总量边界,K8s 铁律:只存元数据,不存业务数据;
  • compact+defrag 管历史,lease 自动化失效,快照=集群备份;
  • 托管化趋势把 etcd 交云厂商,便利换控制。

下一章,我们出后厨,看”分配”——调度器。期望状态里写着”要 3 个 Pod”,但具体落哪台机器?这决策比你想的复杂得多。