kube-apiserver 返回 504 Gateway Timeout
时,值班笔记里常见两种归因:「etcd 挂了」和「apiserver
负载高」。两者都可能对,但修复动作完全不同——前者要查
Leader、WAL fsync、quota;后者可能是 watch cache
未命中、准入链路过长,或 controller 从过旧
resourceVersion 追赶触发 List 风暴。若把
apiserver 当成 etcd 的透明代理,会把 Watch 背压误判成 Raft
选举问题。
前 12 篇已把 etcd 服务器内核拆到五轴精度(Raft
单组、WAL、MVCC、Watch、Lease、Compaction/quota)。本文只钉
Kubernetes 控制面在链路上的落点:谁直接连
etcd、resourceVersion 与 Revision
的对应关系、Node Lease 如何占用 Lease 轴,以及 apiserver
分层缓存如何改变 etcd 负载形态。
本篇在系列中的位置
篇目 核心内容 第 12 篇 · Compaction 与容量 auto-compaction、defrag、quota 第 13 篇 · K8s 控制面耦合 apiserver、resourceVersion、Node Lease 第 14 篇 · 运维与升级 member change、backup/restore、升级门 系列目录 全部篇目
版本锚定:etcd v3.5.33(源码 tag
v3.5.33)。Kubernetes 控制面常见嵌入 3.5.x;apiserver 存储接口行为以你集群的 K8s 小版本为准,本文机制不超前写未钉 tag 的 etcd 能力。Watch/MVCC 百科速览见 distributed/50;apiserver 实现全书不在此展开。
一、分层:谁直接碰 etcd
Kubernetes 集群状态的唯一持久化后端是 etcd(原生栈;Kine 等兼容层见 第 16 篇 边界一句)。但几乎没有任何业务组件直接连 etcd——scheduler、controller-manager、kubelet、kubectl 都走 kube-apiserver 的 REST API。
flowchart LR
kubectl["kubectl / controllers / kubelet"]
apiserver["kube-apiserver"]
wcache["apiserver watch cache"]
etcd["etcd cluster"]
kubectl --> apiserver
apiserver --> wcache
wcache -->|"ListAndWatch"| etcd
apiserver -->|"direct read/write on miss"| etcd
| 层级 | 组件 | 与 etcd 的关系 |
|---|---|---|
| 客户端 | controller、operator、kubelet | 经 apiserver;不持有 etcd 证书 |
| 聚合层 | kube-apiserver | 唯一直连 etcd 的控制面组件;认证/授权/准入在此 |
| 缓存层 | apiserver watch cache(按资源类型) | 合并对 etcd 的 Watch;List 可走缓存 |
| 持久层 | etcd v3 | 扁平 KV;不理解 Pod/Deployment 语义 |
对 etcd 而言,apiserver 写入的是 protobuf
序列化后的 opaque bytes,key 形如
/registry/pods/default/nginx(Kubernetes
文档 · Configure and upgrade etcd,A
级)。排障时要把「对象语义」和「KV 字节」分开:etcd 轴看
Revision、quota、Watch;apiserver 轴看 List 分页、watch
cache 一致性、准入超时。
工程间隙:apiserver 默认会对 etcd 读做
Serializable 一致性(走本地缓存或 follower
转发),对写与部分读走 线性一致路径。K8s
文档与 etcd
文档对「读是否最新」的表述层级不同——故障时应分别查 apiserver
日志与
etcdctl endpoint status,不要混成一条「etcd
慢」。
二、resourceVersion:从 API 字段到 Revision
metadata.resourceVersion 是 Kubernetes API
的乐观并发控制与 Watch
起点的核心字段。在 etcd v3 栈上,它对应对象在 etcd
中的 mod revision(该 key
最后一次变更时的全局 Revision),而不是 create
revision。
etcd 的 Revision 是全局单调递增的 64
位整数(main + sub
用于同一事务内排序,见 第 4
篇)。每次成功的 Put/Txn 提交都会产生新
Revision。apiserver 在解码 etcd 中的对象后,把 mod revision
写入 resourceVersion 字符串返回给客户端。
| 概念 | Kubernetes API | etcd v3 |
|---|---|---|
| 全局变更序 | 间接:各对象 resourceVersion 可比较同 key
新旧 |
全局 Revision |
| Watch 起点 | ?watch=true&resourceVersion=<rv> |
Watch 的 start_revision |
| 冲突检测 | Update 带
resourceVersion;不匹配则 409 |
Txn compare on mod revision |
| 全量同步 | resourceVersion="" 或 0 的
List |
Range + 当前 rev |
List-Watch 控制循环(声明式 API 的基础)在 etcd 侧依赖两件事(distributed/50 §一):
- Watch 持久、有序——同一 stream 上事件按 Revision 递增,不丢不重(在 compaction 窗口内)。
ErrCompacted时必须重 List——若 controller 保存的resourceVersion已被 auto-compaction 清掉,Watch 无法从历史 rev 追赶,只能全量 List 重建本地缓存。
因此大集群里 compaction 保留窗口 与
controller 断连时长 共同决定 catch-up
成本——这是 Watch 轴问题,不是 Raft 选举问题。apiserver watch
cache 会缓冲 etcd 事件,但 cache 本身仍从 etcd ListAndWatch
种子化;etcd compaction 过 aggressive 时,cache 与 client
都会收到 ErrCompacted 语义。
常见误区:把
resourceVersion
当成「集群全局逻辑时钟」去比较不同对象的新旧——跨对象比较
resourceVersion
大小没有线性一致语义;只有同一资源类型、同一对象的
Update 冲突检测才有意义。
三、apiserver watch cache 与 etcd 负载
若每个 controller 都直接 Watch etcd,大型集群会出现 Watch 连接数 × 资源类型 的放大。apiserver 的 watch cache(按 GroupVersionResource 分片的存储缓存)设计意图是:
- 合并 Watch:多个 client 对同一资源的 Watch 由 apiserver 统一服务;
- List 走缓存:满足一致性条件时,List 不必每次穿透到 etcd;
- SharedInformer 模式:controller 进程内 List 全量 + Watch 增量,读本地 store,写仍经 apiserver。
flowchart TD
c1["controller A"]
c2["controller B"]
inf["SharedInformer local store"]
wc["apiserver watch cache"]
etcd["etcd"]
c1 --> inf
c2 --> inf
inf -->|"ListAndWatch"| wc
wc --> etcd
对 etcd 五轴的含义:
| 现象 | 更可能轴 | 说明 |
|---|---|---|
| apiserver CPU 高、etcd QPS 正常 | apiserver / cache | 准入、序列化、cache 索引 |
etcd etcd_server_proposals_failed_total
涨 |
Raft | Leader 或磁盘 fsync |
| 大量 Watch 断开重连 | Watch | ErrCompacted 或网络;触发 List 风暴 |
mvcc: database space exceeded |
MVCC / quota | 与 apiserver 超时不一定同时 |
apiserver
存储接口实现(storage.Interface、cacher
层)不在本系列重写——排障时若 etcd
指标健康而 apiserver LIST 超时,应查 apiserver
的 etcd_request_duration_seconds 与 watch cache
是否 stale,而不是先 defrag。
Events 分片:Kubernetes 1.22+ 支持
--etcd-servers-overrides,将
/events 前缀路由到独立 etcd 集群(K8s
文档)。Events 写入量远大于 Pod/Deployment;分片后
主 etcd 的 MVCC/quota 轴与 events etcd
分列运维——第 14 篇 backup 应对每个后端分别 snapshot。
四、Node Lease:Lease 轴上的 K8s 心跳
Kubernetes 1.14+ 起,节点心跳默认通过
coordination.k8s.io/v1 Lease
实现,替代频繁更新 Node.status(减少 etcd
写入放大)。kubelet 周期性 renew
Lease;Lease 过期后节点被标记为 NotReady。
在 etcd 层,每个 Node Lease 对应:
- 一个 Lease 对象(TTL、KeepAlive)——走 Lease 轴;
- 绑定 Lease ID 的 Lease key(存放 Lease 资源 JSON)——走 MVCC 写入路径。
| 环节 | 组件 | etcd 机制 |
|---|---|---|
| 创建/续约 | kubelet → apiserver → etcd | Lease grant / KeepAlive |
| TTL 到期 | etcd lessor | key 删除 → Watch 事件 |
| 控制面反应 | node-controller Watch Lease/Node | Watch 轴 |
Leader 切换与 TTL:KeepAlive 不经 Raft 提交(第 10 篇);Leader 故障时新 Leader 需加载 Lease 状态。etcd 3.4+ 的 Lease checkpoint(v3.5.33 已稳定)缓解 Leader 切换后 TTL 重置——K8s 生产栈应确认 checkpoint 已启用(集群级 etcd 参数,非 kubelet 配置)。
规模压力:\(N\) 个节点 ≈ \(N\) 个持续续约的 Lease,加上 Pod/Endpoint 等对象的 Watch churn。5000 节点集群的 Lease 续约是 写入 QPS 的底噪——这不是「调 kubelet 心跳间隔」 alone 能消掉的,quota 与 auto-compaction 必须按 第 12 篇 预留。
与 Jepsen 边界:基于 Lease 的分布式锁在特定故障序下不安全(distributed/53);K8s Node Lease 语义由控制面定义,不等同于通用 etcd Lock API。排障 NotReady 漂移时先查 Lease 轴(etcd Lease 是否存在、TTL)与 网络,不要把 Node 问题默认等同 Raft 无 Leader。
五、症状分列:apiserver 超时 vs etcd 轴
值班口令:先判 apiserver 是否穿透 etcd,再点名 etcd 五轴(完整表见 第 15 篇)。
| 入口症状 | 先查 | 若异常则轴 |
|---|---|---|
kubectl get 慢 |
apiserver 日志、etcd 延迟指标 | MVCC 读 / Raft ReadIndex |
kubectl write 409 |
预期冲突 | 正常 optimistic concurrency |
| controller 不再 reconcile | controller --v 日志、Watch 断开 |
Watch → ErrCompacted? |
| 全集群 NotReady | Node Lease、etcd Leader | Lease + Raft |
| apiserver 504 | etcd_request_duration_seconds |
分列 etcd vs apiserver |
| 仅 Events 异常 | events etcd 端点 | 分集群 MVCC |
flowchart TD
slow["apiserver slow or 504"]
slow --> pierce{"etcd request slow\nin apiserver metrics?"}
pierce -->|"No"| api["apiserver admission cache auth"]
pierce -->|"Yes"| axis["Name etcd axis"]
axis --> r["Raft leader WAL"]
axis --> m["MVCC quota compaction"]
axis --> w["Watch ErrCompacted"]
axis --> l["Lease Node heartbeat"]
restore 与 resourceVersion 回退:从 etcd
snapshot 恢复若未做 revision bump,controller 本地
resourceVersion 可能高于 etcd
当前 Revision,导致 Watch 行为异常。Kubernetes 语境下
restore 应使用 etcdutl snapshot restore 的
--bump-revision 与
--mark-compacted(etcd
v3.5 Disaster recovery,A 级)——详见 第 14
篇。
六、本篇边界
| 题目 | 归属 | 本篇 |
|---|---|---|
| etcd Revision / treeIndex / bbolt | 第 4–7 篇 | 只引用 |
| Watch synced/unsynced / victims | 第 9 篇 | 只引用 |
| apiserver 存储源码 / cacher 实现 | K8s 源码 | 指针,不重写 |
| scheduler 调度算法 | K8s 组件 | 不写 |
| Helm 安装 etcd | 运维菜谱 | 不写 |
| 跨方案 QPS 对比 | — | 禁止 |
参考资料
规范 / 官方文档(A)
- etcd v3.5.33;Data model
- Kubernetes · Configure and upgrade etcd
- Kubernetes · Using Lease
- etcd
v3.5 · Disaster recovery(
--bump-revision/--mark-compacted)
源码(A)
etcd-io/etcdtagv3.5.33:server/storage/mvcc/revision.go;server/lease/kubernetes/kubernetes:staging/src/k8s.io/apiserver/pkg/storage/etcd3/(存储适配层,读具体 release 分支)
站内
- distributed/50 · etcd 深度解剖 §6 Kubernetes
- 第 9 篇 · Watch;第 10 篇 · Lease
- 第 15 篇 · 排障五轴
论文 / 报告(B)
- Li, X., et al. (2017). etcd: A Distributed Key-Value Store for Inspiring Reliable Distributed Systems. USENIX ;login:.
实验台账
- 本篇无粘贴
kubectl/etcdctl输出;命令语义来自官方文档。有集群时应分列 apiserver 与 etcd 指标后再下结论。
下一篇:运维与升级
读完这篇,下一步读什么
优先读同系列或同问题的下一篇,把单篇消费变成主题集群。
【etcd】生产全景:缺口、五轴坐标系与 16 篇路线
相对 distributed/50、39、13 钉清 etcd 生产内核缺口;定义 Raft/WAL/MVCC/Watch/Lease 五轴排障坐标系,给出 16 篇阅读路线与 K8s 控制面耦合指针。版本锚定 v3.5.33。
etcd / 生产内核:Raft、WAL、MVCC 与 Kubernetes 控制面底座
补齐 distributed 百科单篇之上的 etcd 生产内核:单 Raft 组、WAL/快照、Revision MVCC、bbolt 单写者、Watch/Lease、K8s apiserver 耦合,并以排障与相对 TiKV/FoundationDB 的选型收束。
【etcd】Watch 机制:watchableStore、synced/unsynced 与 ErrCompacted
钉 etcd v3.5.33 watchableStore 的 synced/unsynced/victims 三分法、Apply 后 notify 与历史追赶 syncWatchersLoop,以及 CompactRevision 触发 ErrCompacted 时的客户端重同步边界。
【etcd】排障五轴:Raft/WAL/MVCC/Watch/Lease 口令表
按 Raft、WAL、MVCC、Watch、Lease 五轴做症状否证;解释 etcdctl endpoint status/health 字段语义;并对照 K8s 控制面 apiserver 超时与 Node Lease 漂移。