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

【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 编码的结构体,包含 apiVersion、resourceVersion(即 snap_rev 字符串)与 startKey(下一页起始 key,不含 pathPrefix)。解码路径在 staging/src/k8s.io/apiserver/pkg/storage/etcd3/store.go 的 decodeListContinue 函数(不发明行号;引用 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 .