土法炼钢 · 系统与基础设施

【kube-apiserver】storage.Interface 与 etcd3:codec、prefix 与 CRUD 路径

文章导航

分类入口
kubernetesdistributed
标签入口
#kubernetes#apiserver#storage#etcd3#codec#guaranteed-update#optimistic-concurrency#encryption-at-rest#v1.30.3

目录

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.Interfacek8s.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 路径

接口约定

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}

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),负责:

版本转换在解码时完成:etcd 里存的可能是 apps/v1 对象,GetList 返回时已经过 ConvertToVersion 转换——这是 apiserver 向前兼容旧 etcd 数据的关键路径,但也是「etcd 有数据但 apiserver 解码报错」类故障的发生点。

value.Transformer

value.Transformerk8s.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:乐观并发写

GuaranteedUpdatek8s.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 的关系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

Createetcd3/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

Deleteetcd3/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

Watchetcd3/store.go)返回一个 watch.Interface,其实现(etcd3/watcher.go,tag v1.30.3):

  1. 调用 etcd3 gRPC Watch.Watch,携带 startRevision(来自 ListOptions.ResourceVersion → 转换后的 etcd revision,详见 第 4 篇)。
  2. 从 gRPC stream 接收 WatchResponse,每个 Event 包含 Type(PUT/DELETE)和 Kv(含 mod revision、value)。
  3. Kv.Value 调用 value.Transformer.TransformFromStorage(解密/解压)。
  4. 用 codec 解码为 Go 对象,产生 watch.Event{Type, Object}
  5. 推入 watch.Interface 的 channel,供 cacher(第 5 篇)或直接 Watch 路径消费。

etcd3 gRPC Watch 的特性:etcd3 Watch API 支持 startRevision,即从历史 revision 开始接收事件;若该 revision 已被 compact,etcd 返回 mvcc: required revision has been compactedErrCompacted),apiserver 侧 Watch 会关闭并向上传播错误——这是「410 Gone 可能来自 etcd 侧」的根源(另一个来源是 cacher 侧,见 第 5 篇)。

etcd Raft/MVCC/Watch 机制见 etcd/04etcd/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_secondsoperation=list 分类,再查 第 5 篇 watch cache 的缓存命中率。

etcd 本身的 etcd_server_proposals_committed_totaletcd_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。

工程间隙

  1. codec 版本转换的脆弱性:etcd 里的对象格式版本(如 apps/v1beta1)在 API 废弃周期内必须维持可解码。若同时存在多个 apiserver 版本在写同一 etcd 集群,旧格式字节可能被新 apiserver 无法解码——这是 HA 升级时的已知风险,见 第 14 篇

  2. GuaranteedUpdate 重试放大:在高并发场景下(如大量 controller 同时 Update 同一对象),GuaranteedUpdate 的内部重试会放大 etcd 读写次数。这不是 etcd Raft 的问题,而是乐观并发在竞争写时的代价——可以通过 server-side apply(PATCH with application/apply-patch)减少冲突概率,但不消除。

  3. Watch channel 背压:etcd3 gRPC Watch stream 的事件速率受限于 etcd 侧推送速率;apiserver 侧若消费慢(cacher channel 积压),会反压到 etcd Watch 连接。v1.30.3 对此有 sendInitialEvents 与 watch progress notification 机制,但不提供完整的背压 SLO。


参考资料

规范 / 源码(A)

官方文档(A/B)

站内对照(A/B)

实验台账


上一篇apiserver 进程与请求路径

下一篇resourceVersion 与 Revision 映射

读完这篇,下一步读什么

优先读同系列或同问题的下一篇,把单篇消费变成主题集群。

2026-08-28 · kubernetes / distributed

【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 轴。


By .