kube-apiserver 在 etcd 里存的
/registry/pods/default/nginx,并不是
kubectl get pod nginx -o json 的 JSON。它是
protobuf 序列化后的字节流,key 由 pathPrefix +
resourcePrefix 拼接,value 经过
value.Transformer(可能是加密或压缩)处理。读路径反向解码,写路径用
etcd3 的 Txn compare-and-swap
做乐观并发控制。若不理解这一层,就无法区分「etcd
有写入但对象内容损坏」与「对象从未进入
etcd」这两种失败。
本文拆解 storage.Interface 的合约、etcd3
store
实现(k8s.io/apiserver/pkg/storage/etcd3/,tag
v1.30.3)的关键路径,并说明
GuaranteedUpdate
如何实现乐观并发、value.Transformer
处于哪个边界、Watch 如何从 etcd3 gRPC stream 映射到
apiserver 事件。这是 Storage/etcd 耦合轴的核心;etcd 侧的
Raft/MVCC/bbolt 不在本文重写,见 etcd 系列。
本篇在系列中的位置
篇目 核心内容 第 2 篇 · 进程与请求路径 HandlerChain、REST 路由、失败落点 第 3 篇 · storage.Interface 与 etcd3 codec、prefix、CRUD/Watch 路径 第 4 篇 · resourceVersion 与 Revision 映射 mod revision、continue、一致性读期望 系列目录 全部篇目
版本锚定:Kubernetes v1.30.3(源码 tag
v1.30.3);etcd 后端 v3.5.33。storage 路径引用staging/src/k8s.io/apiserver/pkg/storage/;etcd3 实现在staging/src/k8s.io/apiserver/pkg/storage/etcd3/。不以 live 文档版本冒充 v1.30.3;不粘贴伪造 etcdctl 输出。
一、storage.Interface 契约
storage.Interface(k8s.io/apiserver/pkg/storage/interfaces.go,tag
v1.30.3)定义了 apiserver
与后端的完整合约。主要方法:
| 方法 | 语义 | 备注 |
|---|---|---|
Create(ctx, key, obj, out, ttl) |
当 key 不存在时写入对象 | 存在则返回 AlreadyExists |
Delete(ctx, key, out, preconditions, validateDeletion, cachedExistingObject) |
删除 key;preconditions 含 rv 检查 | 不存在返回 NotFound |
Get(ctx, key, opts, objPtr) |
读取单个 key | opts 包含 resourceVersion 要求 |
GetList(ctx, key, opts, listObj) |
带前缀的范围读取 | List 操作底层入口 |
GuaranteedUpdate(ctx, key, ptrToType, ignoreNotFound, preconditions, tryUpdate, cachedExistingObject) |
乐观并发写,含重试循环 | Update/Patch 的底层实现 |
Watch(ctx, key, opts) |
返回 watch.Interface;从 startRevision
推送事件 |
Watch 轴入口 |
Count(ctx, key) |
统计前缀下 key 数量 | 用于指标 |
RequestWatchProgress(ctx) |
请求 progress notify(etcd3 bookmark 对应层) | v1.30.3 Watch 路径 |
接口约定:
key已包含 pathPrefix;实现不再添加额外前缀(具体见下节)。- 返回的对象必须经过 codec 解码为 Go 结构体;写入前必须编码。
ResourceVersion字段在返回前由实现层填充(对应 etcd mod revision)。
storage.Interface 的存在使 apiserver
在理论上可以替换后端(测试用的内存实现
k8s.io/apiserver/pkg/storage/etcd3/testing
即利用此接口)。但在 v1.30.3 的生产栈,唯一的正式 etcd
实现是 etcd3/store。
二、etcd3 store:key 结构、codec 与 value.Transformer
key 结构
etcd3 store 的 key 由两部分拼接:
/{pathPrefix}/{resourcePrefix}/{namespace}/{name}
- pathPrefix:全局前缀,默认
/registry(由--etcd-prefix标志控制)。 - resourcePrefix:资源类型前缀,例如
pods、deployments、secrets。最终如/registry/pods/default/nginx、/registry/deployments/kube-system/coredns。
在 etcd3/store 的实现中,key
参数传入时已由上层 REST
storage(k8s.io/apiserver/pkg/registry/generic/registry/store.go)拼好
pathPrefix + resourcePrefix + namespace + name;etcd3 store
直接使用该 key,不再追加。
Events 可以通过 --etcd-servers-overrides
路由到独立 etcd 集群(见 etcd/13);前缀分离不影响
key 格式,只影响路由目标。
codec
storage.Interface 实例持有
runtime.Codec(接口,k8s.io/apimachinery/pkg/runtime),负责:
- 编码:Go 结构体 → protobuf(或
JSON,取决于媒体类型协商)。
kube-apiserver对 etcd 的持久化默认使用 protobuf(application/vnd.kubernetes.protobuf),节省存储空间并减少序列化 CPU。 - 解码:etcd bytes → Go
结构体,再由版本转换层(
k8s.io/apimachinery/pkg/runtime/serializer)升级到内部版本(__internal),最后转到 API 响应版本。
版本转换在解码时完成:etcd 里存的可能是
apps/v1 对象,GetList 返回时已经过
ConvertToVersion 转换——这是 apiserver
向前兼容旧 etcd 数据的关键路径,但也是「etcd 有数据但
apiserver 解码报错」类故障的发生点。
value.Transformer
value.Transformer(k8s.io/apiserver/pkg/storage/value/,tag
v1.30.3)是 加密静态数据(encryption at
rest)的边界:
etcd bytes <-- value.Transformer.TransformFromStorage --> plaintext codec bytes
plaintext codec bytes --> value.Transformer.TransformToStorage --> etcd bytes
默认配置(无
EncryptionConfiguration)时,Transformer 是
identity(直传)。启用 encryption at
rest(--encryption-provider-config)后,Transformer
实现 AES-GCM 或 KMS envelope 加密;etcd 存储的是密文,etcd
侧完全不知道对象内容。
排障含义:若 apiserver 更换了加密
provider 但 etcd
里仍有旧格式密文,TransformFromStorage
会解密失败,表现为 apiserver 读取对象 500 错误,而 etcd
指标完全正常——这是典型的「etcd 健康但 apiserver
报错」场景,需要查 apiserver 的
storage_transformation_failures_total
指标(v1.30.3 metrics)。
三、GuaranteedUpdate:乐观并发写
GuaranteedUpdate(k8s.io/apiserver/pkg/storage/etcd3/store.go,tag
v1.30.3)是 Update/Patch 操作的底层实现,封装了
etcd3 的 compare-and-swap(Txn)与重试循环:
loop:
1. 从 etcd 读取当前值(或使用 cachedExistingObject)
2. 调用 tryUpdate(current) 得到新对象
3. 检查 preconditions(ResourceVersion、UID 等)
4. 构造 etcd3 Txn:
If mod_revision == current_revision
Then Put new_value
Else 重新读取并重试
5. 若 Txn 失败(mod_revision 不匹配)→ 回到步骤 1,最多重试 defaultMaxRetryAttempts 次
6. 若成功 → 填充 out 对象(含新 mod_revision → resourceVersion)
关键点:
- etcd 的 Txn 是原子的:compare
mod_revision与 thenPut之间没有竞争窗口。 tryUpdate可以是 apiserver 内部的 update 函数(如字段合并、generation 递增),也可以是 strategic merge patch 的应用结果。- 若调用方传入了
cachedExistingObject(来自 cacher 的缓存,见 第 5 篇),第一次读 etcd 可以省略;但 Txn 失败后仍需从 etcd 重新读取真实值。
与 etcd Txn
的关系:GuaranteedUpdate 的 Txn
最终调用 etcd3 gRPC KV.Txn,etcd 在 Leader
处理后经 Raft 提交(见 etcd/07 ·
写路径)。mod_revision 由 etcd MVCC
层维护,每次成功的 Put 都会产生新的全局 Revision(见 第 4
篇)。
409 Conflict 的来源:若
preconditions.ResourceVersion
不匹配(客户端传了旧 rv),GuaranteedUpdate
在步骤 3 即返回 Conflict,不进入 Txn 重试;若没有 rv
约束,Txn 失败会触发内部重试,外部调用者看到的是最终成功或
500。
四、Create 与 Delete 路径
Create
Create(etcd3/store.go,tag
v1.30.3)使用 etcd3 Txn:
If key 不存在(version == 0)
Then Put(key, encoded_value)
Else 返回 AlreadyExists
编码(codec + Transformer)在 Txn 构造前完成。Put 携带
TTL 时,etcd3 store 会先 grant 一个 Lease 并把 key 绑定到该
Lease(对应 --etcd-prefix 下的 TTL
资源)。注意:这里的 Lease 是 etcd 原生 Lease,不是
Kubernetes
coordination.k8s.io/v1 Lease(后者是上层应用语义,见
etcd/10)。
Delete
Delete(etcd3/store.go,tag
v1.30.3)先读取当前对象以检查 preconditions(如
--cascade=background 触发的 finalizer
检查由上层 REST handler 完成),再调用 etcd3
KV.Delete(或 Txn)。v1.30.3 中
Delete 的实现包含一次 Get + 一次
Txn delete,保证 preconditions 不在两次操作之间漂移。
五、Watch 路径:从 etcd3 gRPC stream 到 storage.Interface
Watch(etcd3/store.go)返回一个
watch.Interface,其实现(etcd3/watcher.go,tag
v1.30.3):
- 调用 etcd3 gRPC
Watch.Watch,携带startRevision(来自ListOptions.ResourceVersion→ 转换后的 etcd revision,详见 第 4 篇)。 - 从 gRPC stream 接收
WatchResponse,每个Event包含Type(PUT/DELETE)和Kv(含 mod revision、value)。 - 对
Kv.Value调用value.Transformer.TransformFromStorage(解密/解压)。 - 用 codec 解码为 Go 对象,产生
watch.Event{Type, Object}。 - 推入
watch.Interface的 channel,供 cacher(第 5 篇)或直接 Watch 路径消费。
etcd3 gRPC Watch 的特性:etcd3 Watch API
支持 startRevision,即从历史 revision
开始接收事件;若该 revision 已被 compact,etcd 返回
mvcc: required revision has been compacted(ErrCompacted),apiserver
侧 Watch 会关闭并向上传播错误——这是「410 Gone 可能来自 etcd
侧」的根源(另一个来源是 cacher 侧,见 第 5
篇)。
etcd Raft/MVCC/Watch 机制见 etcd/04、etcd/09;本篇只钉 apiserver 适配层。
六、指标语义与排障锚点
v1.30.3 apiserver 暴露以下与 storage 相关的指标(名称来自
k8s.io/apiserver 源码
pkg/storage/,具体注册见
pkg/storage/etcd3/metrics/,不伪造数值):
| 指标 | 含义 | 排障用法 |
|---|---|---|
etcd_request_duration_seconds |
apiserver 调用 etcd3 gRPC 的延迟直方图 | 区分 apiserver 慢 vs etcd 慢 |
etcd_requests_total |
etcd3 操作计数(按 operation、resource) | 确认操作类型与频率 |
storage_transformation_duration_seconds |
value.Transformer 的加密/解密延迟 | 加密慢导致写超时 |
storage_transformation_failures_total |
Transformer 失败次数 | 密钥更换或 KMS 不可用 |
排障口令: -
etcd_request_duration_seconds 高 + etcd 侧
etcd_server_proposals_failed_total 正常 →
查网络延迟、etcd 节点负载。 -
storage_transformation_failures_total 非零 → 查
encryption at rest 配置,优先于换 etcd。 - etcd 健康但
apiserver List 慢 → 先查
etcd_request_duration_seconds 的
operation=list 分类,再查 第 5 篇 watch
cache 的缓存命中率。
etcd 本身的
etcd_server_proposals_committed_total、etcd_mvcc_db_total_size_in_bytes
等指标在 etcd 侧查,不混入 apiserver
指标台账——两组指标必须分列,不能把「apiserver
etcd_request_duration_seconds 高」等同于「etcd 集群内 Raft
有问题」。
七、设计谱系与工程间隙
storage.Interface 的设计根植于「apiserver
是唯一写入方」的架构原则(Saltzer et al. end-to-end
argument,见 第
1 篇)。具体的 etcd3 适配层在 2016–2017 年从 etcd v2
HTTP API 迁移到 etcd v3 gRPC API 时成形(Kubernetes
v1.5–v1.6),引入了 MVCC Revision 语义和持久 Watch。
工程间隙:
codec 版本转换的脆弱性:etcd 里的对象格式版本(如
apps/v1beta1)在 API 废弃周期内必须维持可解码。若同时存在多个 apiserver 版本在写同一 etcd 集群,旧格式字节可能被新 apiserver 无法解码——这是 HA 升级时的已知风险,见 第 14 篇。GuaranteedUpdate 重试放大:在高并发场景下(如大量 controller 同时 Update 同一对象),
GuaranteedUpdate的内部重试会放大 etcd 读写次数。这不是 etcd Raft 的问题,而是乐观并发在竞争写时的代价——可以通过 server-side apply(PATCHwithapplication/apply-patch)减少冲突概率,但不消除。Watch channel 背压:etcd3 gRPC Watch stream 的事件速率受限于 etcd 侧推送速率;apiserver 侧若消费慢(cacher channel 积压),会反压到 etcd Watch 连接。v1.30.3 对此有
sendInitialEvents与 watch progress notification 机制,但不提供完整的背压 SLO。
参考资料
规范 / 源码(A)
- Kubernetes v1.30.3(源码 tag
v1.30.3) staging/src/k8s.io/apiserver/pkg/storage/interfaces.go:storage.Interface契约staging/src/k8s.io/apiserver/pkg/storage/etcd3/store.go:GuaranteedUpdate、Create、Delete、Watchstaging/src/k8s.io/apiserver/pkg/storage/etcd3/watcher.go:Watch gRPC stream 适配staging/src/k8s.io/apiserver/pkg/storage/value/:value.Transformer接口与 encrypt 实现staging/src/k8s.io/apiserver/pkg/storage/etcd3/metrics/:指标注册- etcd v3.5.33:gRPC
KV.Txn、Watch.WatchAPI(存储后端对照)
官方文档(A/B)
- Kubernetes
· Encrypting Secret Data at
Rest(
EncryptionConfiguration语义) - etcd v3.5 · Data model(mod revision 语义)
站内对照(A/B)
- 第 4 篇 · resourceVersion 与 Revision 映射
- 第 5 篇 · Watch cache / cacher 架构
- etcd/04 · MVCC 模型
- etcd/09 · Watch
- etcd/13 · K8s 控制面耦合
实验台账
- 本篇无集群实测;无伪造
etcdctl/kubectl输出。etcd_request_duration_seconds等指标语义来自 v1.30.3 源码注册,不附伪造数值。
下一篇:resourceVersion 与 Revision 映射
读完这篇,下一步读什么
优先读同系列或同问题的下一篇,把单篇消费变成主题集群。
【kube-apiserver】控制面全景:缺口、五轴坐标系与 16 篇路线
相对 etcd/13、distributed/50、k8s-network 补齐 kube-apiserver 生产内核缺口;以 Storage/Watch/Admission/Auth/APF 五轴为坐标系定义 16 篇阅读路线;版本锚定 Kubernetes v1.30.3。
【kube-apiserver】Watch cache / cacher:dispatch、bookmark 与穿透 etcd
钉 Kubernetes v1.30.3 cacher 对 storage.Interface 的包装:watchCache 滑动窗口与 Ready 门、bookmark 推送路径、cache miss 打穿 etcd 的触发条件,以及 List 风暴成因与 watch cache SLO 与 compaction 间隔之间的开放问题。
【kube-apiserver】排障五轴:Storage、Watch、Admission、Auth、APF
按 Storage/Watch/Admission/Auth/APF 五轴做症状否证;给出完整五轴命令表与症状→轴映射(504、410、401、403、webhook 超时、List 风暴、OOM);说明 apiserver_request_duration_seconds 等核心 metrics 语义;并提供决策树:何时穿透到 etcd/15,何时留在 apiserver 轴。
kube-apiserver / Kubernetes 控制面内核:storage、cacher、Admission 与 APF
补齐 etcd 系列 K8s 耦合篇之上的 kube-apiserver 生产内核:storage.Interface、watch cache、resourceVersion、Admission/Webhook、APF,并以排障与相对 etcd 的分层收束。