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 的元数据中存有:
create_revision:key 首次创建时的全局 Revision。mod_revision:key 最后一次被修改(Put/Delete)时的全局 Revision。version:该 key 的自身写入计数(单调递增,与全局 Revision 无关)。
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 名)。
分页的成本与局限
- 单次 etcd Range 成本:etcd Range 操作在
Leader 处理,返回字节量与
limit个对象的序列化大小成正比。大 limit 值(如500)在对象较大时(如包含大 annotation 的 ConfigMap)仍可能产生高网络与内存压力。 - continue token 不可跨版本使用:若 apiserver 重启或 etcd 发生 compaction 清理了 snap_rev,持有旧 continue token 的客户端再请求时会收到 410 Gone(snap_rev 已被 compact)。此时客户端必须重新从头 List。
- watch cache 路径的差异:当 List 请求被
cacher 服务(不穿透 etcd)时,cacher
内部有自己的分页实现,
continue的语义由 cacher 控制,不完全等同于 etcd Range 的分页语义。cacher 的详细行为见 第 5 篇与 第 6 篇。
四、一致性读期望:哪些路径保证线性一致,哪些不保证
四种读路径
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)
- Kubernetes · API Concepts · Resource Versions(v1.30 对应语义)
- etcd v3.5 · Data model(mod_revision 定义)
- Kubernetes v1.30.3(源码 tag
v1.30.3)
源码(A)
staging/src/k8s.io/apiserver/pkg/storage/etcd3/store.go:decodeListContinue、encodeListContinue、rv 填充路径staging/src/k8s.io/apiserver/pkg/storage/etcd3/watcher.go:ErrCompacted → 410 映射staging/src/k8s.io/apiserver/pkg/storage/cacher/:rv=0 / rv=具体值的路由逻辑
论文(A)
- Li, X., et al. (2017). etcd: A Distributed Key-Value Store for Inspiring Reliable Distributed Systems. USENIX ;login:.(Revision MVCC 形式定义)
站内对照(A/B)
- etcd/04 · MVCC 模型(mod_revision 全局语义)
- etcd/08 · 读一致性(ReadIndex 机制)
- etcd/12 · Compaction 与容量(ErrCompacted 来源)
- etcd/13 · K8s 控制面耦合(resourceVersion 首次定义)
- 第 5 篇 · Watch cache / cacher(cacher 侧 410 与分页)
- 第 6 篇 · List / Pagination(continue token 完整分析)
实验台账
- 本篇无集群实测;无伪造
kubectl输出。分页与 410 行为来自 v1.30.3 源码逻辑与 Kubernetes API 规范,不附伪造 trace。
读完这篇,下一步读什么
优先读同系列或同问题的下一篇,把单篇消费变成主题集群。
【kube-apiserver】List、Pagination 与一致性 List:continue token 与 etcd Range 成本
钉 Kubernetes v1.30.3 List 分页的 continue token 编码语义、limit+continue 多轮 etcd Range 行为、resourceVersion 对一致性语义的影响,以及 label/field selector 在 cacher 与 etcd3 路径上的不同成本。
【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】Watch 路径(服务端):长连接、410 Gone 与 timeout
钉 Kubernetes v1.30.3 服务端 Watch 路径:从 REST handler 到 cacher/etcd3 watcher 的建立过程、resourceVersion=0 与具体 RV 的语义差异、410 Gone 与 etcd ErrCompacted 的映射、bookmark 推送与连接生命周期,以及与 client-go Informer 的边界。
【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 矩阵指针。