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

【kube-apiserver】resourceVersion 与 Revision 映射:mod revision、continue 与一致性读期望

文章导航

分类入口
kubernetesdistributed
标签入口
#kubernetes#apiserver#resourceversion#etcd#revision#pagination#consistency#410-gone#v1.30.3

目录

metadata.resourceVersion 是 Kubernetes API 里最容易被误读的字段之一。常见误用:把两个不同对象的 resourceVersion 相减判断「哪个更新」;从 resourceVersion=0 发起 Watch 并期望得到完整历史;把 continue token 当成页码参数。这三个误用都会在生产中引发静默错误——List 返回的不是期望时间点的数据,Watch 从意外位置开始推送,分页迭代中间有对象被跳过。

本文钉 resourceVersion 字段的来源(etcd mod revision)、语义边界(只对同一对象有单调语义,跨对象不保证),以及 continue token 的分页路径与成本。同时说明不同 List 调用路径的一致性期望差异:哪些路径走 watch cache(可能读到略旧的版本),哪些路径穿透到 etcd(保证线性一致)。

本篇在系列中的位置

篇目 核心内容
第 3 篇 · storage.Interface 与 etcd3 codec、prefix、GuaranteedUpdate
第 4 篇 · resourceVersion 与 Revision 映射 mod revision、continue、一致性读期望
第 5 篇 · Watch cache / cacher 架构 dispatch、bookmark、cache miss
系列目录 全部篇目

版本锚定:Kubernetes v1.30.3(源码 tag v1.30.3);etcd 后端 v3.5.33。resourceVersion 语义以 Kubernetes API Concepts 文档(A 级官方规范)与 staging/src/k8s.io/apiserver/pkg/storage/ 源码为准,不以非版本化行为描述为准。


一、mod revision → resourceVersion 字符串

etcd3 的 MVCC 模型为每个写操作维护一个全局单调递增的 64 位整数 Revision(见 etcd/04 · MVCC 模型)。每个 key 的元数据中存有:

apiserver 把 mod_revision 直接转换为字符串,写入 metadata.resourceVersion。转换在 etcd3 store 的解码路径中完成(staging/src/k8s.io/apiserver/pkg/storage/etcd3/store.go,tag v1.30.3):

// 伪代码示意,不对应行号
obj.SetResourceVersion(strconv.FormatInt(kv.ModRevision, 10))

语义边界

场景 是否有意义
同一对象两个 rv:rv_new > rv_old 有意义:对象确实更新了
不同对象 rv 相减 无意义:两个不同 key 的 mod_revision 没有顺序关系可以跨对象推断
rv 相等的两个对象 不可能(不同 key,mod_revision 相同意味着同一个 etcd 事务写入了两个 key)
List 响应的 metadata.resourceVersion 是该 List 所在时间点的全局 Revision 快照,不是 max(item.rv)

List 响应 rv 的特殊性GetList 的响应在 ListMeta.ResourceVersion 字段存放的是 List 操作所读取的 etcd revision 快照值(来自 etcd Range 响应的 Header.Revision),而不是列表中各对象 mod_revision 的最大值。这个值可以作为后续 Watch 的起点,表示「从这个时间点开始推送新事件」。


二、Watch 起点与 resourceVersion 语义

Kubernetes API 规范(API Concepts · Resource Versions,A 级)定义了 Watch resourceVersion 参数的三种取值的语义:

resourceVersion 取值 含义 一致性等级
不传(empty) 从当前 etcd 版本开始,不保证初始同步完整性 最新(但不包含历史)
"0" 任意最新版本(可能来自 cache) 可能略旧
具体值(如 "12345" 从该 revision 之后开始推送事件 精确起点

rv=0 的生产含义:client-go Informer 的 ListWatch 在重新 List 时通常传 rv=0,意图是「接受 cache 可能略旧」以减少 etcd 负载。apiserver cacher(第 5 篇)会服务这类请求,不穿透 etcd。若 cacher 尚未启动或 cache 未初始化,才会穿透到 etcd。

从具体 rv Watch 的约束:若 client 传入的 rv 早于 etcd 当前的 compact revision,etcd3 Watch 会立即返回 ErrCompacted,apiserver 侧映射为 HTTP 410 Gone(详见第四节)。这是「controller 断连后 rv 过旧」触发全量 resync 的根因。


三、continue token:List 分页的实现与成本

分页机制

大型 List 请求(如 kubectl get pods --all-namespaces,集群有数万 Pod)若不分页,etcd Range 操作会一次性返回全部字节,产生大 response、高 etcd 内存压力与网络带宽峰值。v1.30.3 apiserver 支持 limit/continue 分页。

分页流程:

客户端: GET /api/v1/pods?limit=500
            ↓
apiserver: 调用 storage.GetList(key, {Limit: 500})
            ↓
etcd3:    Range(key, rangeEnd, Limit=500, Revision=snap_rev)
            ↓
返回: items[0..499] + nextKey(下一页起始 key)
            ↓
apiserver: 把 nextKey + snap_rev 打包为 continue token(base64 编码)
            写入 ListMeta.Continue
客户端: GET /api/v1/pods?limit=500&continue=<token>
            ↓
apiserver: 解码 continue token,得到 nextKey + snap_rev
            调用 etcd3 Range(nextKey, rangeEnd, Limit=500, Revision=snap_rev)

关键snap_rev 在第一次 List 时固定,后续所有分页请求都在同一 revision 快照上读取,保证分页结果的一致性(不会因中间有对象创建/删除导致漏读或重读)。

continue token 格式

v1.30.3 中 continue token 是 JSON 序列化后 base64 编码的结构体,包含 apiVersionresourceVersion(即 snap_rev 字符串)与 startKey(下一页起始 key,不含 pathPrefix)。解码路径在 staging/src/k8s.io/apiserver/pkg/storage/etcd3/store.godecodeListContinue 函数(不发明行号;引用 package + function 名)。

分页的成本与局限


四、一致性读期望:哪些路径保证线性一致,哪些不保证

四种读路径

v1.30.3 的 List/Get 有四种路径,一致性级别不同:

路径 resourceVersion 参数 一致性 是否穿透 etcd
强一致读 不传或传当前 rv Linearizable(经 etcd ReadIndex)
走 cacher 的 rv=0 "0" 可能略旧(cacher 内部 bookmarks)
走 cacher 的 rv=具体值 具体 rv 不早于指定 rv(cacher 至少更新到该 rv) 否或是(不足时穿透)
走 cacher 的 rv=“0” List(完全) "0" 返回 cacher 内存快照

「可能略旧」的含义:etcd3 的 ReadIndex(线性一致读)保证 Read 返回最近提交的写(见 etcd/08 · 读一致性)。cacher 内存快照的新鲜度取决于 cacher 从 etcd Watch 接收事件的延迟——通常在毫秒级,但在网络抖动或 etcd 压力下可能更大。Kubernetes API 规范接受这种「not older than」语义,而不要求每次 List 都走 etcd ReadIndex。

etcd ReadIndex 与 apiserver List 的关系:并非每个 apiserver List 都触发 etcd ReadIndex RPC。走 cacher 的路径完全在 apiserver 内存完成;只有「强一致读」路径(通常是携带当前 rv 且 cacher 未命中,或明确要求不走 cache 的路径)才会穿透到 etcd 并触发 etcd 侧的读一致性机制。

Kubernetes API 规范的保证

Kubernetes · API Concepts · Resource Versions(A 级官方文档,v1.30 对应)明确:

Any resource version value returned by the API server may be passed to a Watch as a starting point. The server guarantees that it will start watching from a version not older than the specified one.

这意味着 Watch 从具体 rv 开始时,apiserver 保证不漏发该 rv 之后的事件,但不承诺返回的事件严格从该 rv+1 开始(cacher 可能已有更新的起点)。这与 etcd 自身 Watch 的「精确 startRevision」语义有细微差异——etcd 从 startRevision 开始推送,apiserver 的保证是「不早于」。


五、410 Gone 与 ErrCompacted 的分列

410 Gone 在 Kubernetes Watch 语义中表示「请求的 startRevision 已不可用,客户端必须全量重新 List」。它可以来自两处:

来源 1:etcd ErrCompacted

etcd MVCC compaction(etcd/12)清理了指定 revision 之前的历史版本。若 Watch startRevision 早于 compact revision,etcd3 Watch 立即返回 CompactRevision 错误(gRPC code),apiserver 侧 etcd3 watcher 收到后关闭 Watch channel,并向上传递 410 事件。

证据包:etcd 侧 etcd_mvcc_db_compaction_revision(已 compact 的最大 revision);apiserver etcd_request_errors_total{operation="watch"}

来源 2:cacher 侧 channel 溢出或 watch 过期

cacher 维护的 watch channel 有容量上限;若 channel 积压(消费端慢),cacher 可能强制关闭该 Watch 并发送 410。此时 etcd 侧完全正常,但 apiserver 侧的 cacher 队列已满。

证据包:apiserver watch_cache_capacity(cacher 容量配置);apiserver metrics apiserver_watch_events_dropped_total(v1.30.3,如有)。

客户端处理

无论哪种来源,410 Gone 的正确处理是:丢弃本地状态,从 rv=""rv="0" 重新 List,再用新 List 的 ListMeta.ResourceVersion 作为 Watch 起点。这是 client-go Informer 内置的重连逻辑(ListWatch.Resync)。

410 来源 排障动作
etcd ErrCompacted 查 etcd compaction 配置;拉长 --auto-compaction-retention(评估 quota 影响);见 etcd/12
cacher channel 溢出 查 apiserver watch cache 容量;查 controller 消费速率;见 第 5 篇

六、开放问题与争论:resourceVersion 的形式语义

开放问题:Kubernetes API 规范对 resourceVersion 的保证是工程约定,而非形式验证的不变量。Li et al. (2017) 的 etcd 论文给出了 MVCC Revision 的形式定义,但 apiserver cacher 的「not older than」语义与 etcd ReadIndex 语义的精确关系,目前没有发表的形式证明(截至 2026 年本站知识范围内)。

工程间隙:etcd/13 已指出「apiserver 默认对 etcd 读做 Serializable(走本地缓存或 follower 转发),对写与部分读走线性一致」(见 etcd/13 §一)。这条描述中「Serializable」指 K8s/etcd 语境下的「可能略旧但单调」,不等同于数据库事务隔离级别的 Serializable——混用这两个词是生产文档的常见错误来源。排障时应区分「读到的对象是否到达了预期写入 rv」,而不是问「是否 Linearizable」,因为后者依赖具体读路径。

与 etcd ReadIndex 的关系etcd/08 · 读一致性 给出了 etcd ReadIndex 机制的完整路径;本篇只说明 apiserver 侧 List 不一定触发 ReadIndex 的条件。若需要精确的线性一致读,应使用 resourceVersion 为空(不传)且不走 cacher 的强一致路径——但代价是每次都穿透 etcd,放大 etcd 读负载。


参考资料

规范 / 官方文档(A)

源码(A)

论文(A)

站内对照(A/B)

实验台账


上一篇storage.Interface 与 etcd3

下一篇Watch cache / cacher 架构

读完这篇,下一步读什么

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

2026-08-28 · kubernetes / distributed

【kube-apiserver】运维与升级:HA、flags、graceful shutdown 与 etcd 联检

kube-apiserver 高可用模式:多实例共享 etcd、无需内部 leader election;核心 flag 语义(--etcd-servers、--etcd-servers-overrides、--shutdown-delay-duration、encryption-provider-config);与 etcd/14 的联合升级检查单;Kubernetes 版本偏差策略与 etcd 矩阵指针。


By .