「为什么 kubectl get pods -A
在大集群下卡很久?」——常见误判是 etcd 慢,实际上可能是
cacher 用 rv=0 从内存 store 服务,但
label selector 在 apiserver 侧全量过滤;或
rv="" 强制打穿 etcd,触发大
Range。分页的 continue token
也常被误认为是 etcd 游标,它实际上是 apiserver
内部的分页状态,编码了下一批起始 key 与
resourceVersion 快照。
本篇钉
staging/src/k8s.io/apiserver/pkg/storage/ 的
List 路径:continue token 结构、limit
的实际行为、一致性
List(rv="")与非一致性
List(rv="0")的 cacher vs etcd3
分工,以及 selector 放大 etcd Range 的成本模型。
本篇在系列中的位置
篇目 核心内容 第 5 篇 · Watch cache / cacher dispatch、bookmark、cache miss 第 6 篇 · List / Pagination continue token、一致性语义、Range 成本 第 7 篇 · Watch 路径(服务端) 长连接、410 Gone、timeout 系列目录 五轴、阅读路径
版本锚定:Kubernetes v1.30.3(源码 tag
v1.30.3)。机制钉staging/src/k8s.io/apiserver/pkg/storage/cacher/、pkg/storage/etcd3/、pkg/registry/generic/registry/@ kubernetes/kubernetes v1.30.3。无真实集群则不粘贴伪造kubectl get输出或 etcd Range 延迟数字。
一、List 请求的路由判据
apiserver 收到 GET /api/v1/pods 时,在
pkg/registry/generic/registry/store.go 的
List 方法决定走哪条路:
flowchart TD
listReq["LIST request\n(resourceVersion, limit, continue, selectors)"]
rvCheck{"resourceVersion?"}
fromCache["Cacher.List\n(watchCache.store)"]
fromEtcd["etcd3 store.List\n(Range)"]
listReq --> rvCheck
rvCheck -->|"rv='0' 或 rv=当前缓存已满足"| fromCache
rvCheck -->|"rv='' 或 rv=具体值"| fromEtcd
resourceVersion 值 |
语义 | 服务方 |
|---|---|---|
"" (默认,未指定) |
强一致性;读当前 etcd 状态 | etcd3(Range) |
"0" |
允许任何已知版本;可从缓存服务 | Cacher(watchCache.store) |
具体 RV(如 "4200") |
返回不早于该 RV 的结果 | 通常 Cacher,若 RV 超出 cache 则回退 etcd |
rv="" 在 K8s API Concepts 中定义为
Consistent List(类似 quorum
read);rv="0" 是允许 stale 的 Best
Effort
Cache。两者在集群规模下性能差异显著:rv=""
每次都需要一个 etcd Range(或 ReadIndex
保证新鲜度),rv="0" 直接读 Cacher 内存。
二、continue token:编码结构与语义
Kubernetes 分页的 continue 字段是
base64 编码的 protobuf 结构,对客户端完全
opaque(不可解释、不可构造)。其内部编码(pkg/storage/etcd3/etcd3_helper.go
附近)大致包含:
startKey:下一批扫描的 etcd key 前缀起点(Resource Group + Namespace + Name 编码);resourceVersion:首页 List 时的 RV 快照;分页过程中后续页使用同一 RV,保证多页结果来自同一 一致性快照(即所谓的 consistent pagination)。
// ListOptions 中:
type ListOptions struct {
Limit int64
Continue string // opaque,由 server 生成与消费
// ...
}关键约束:
continuetoken 有生命周期——其中的 RV 快照可能被 etcd compaction 裁掉;此时后续分页请求返回410 Gone(ErrCompacted映射到 HTTP 410)。客户端必须重新从头 List。continue不跨 apiserver 实例保持语义——负载均衡切换后,新实例必须能解析上一实例生成的 token。由于编码格式固定,同 tag 版本的 apiserver 之间一般兼容,但 token 不应被客户端缓存跨版本使用。- 指定
continue时不能同时指定resourceVersion(两者冲突);客户端若同时设置,apiserver 会返回错误。
三、limit + continue 的 etcd Range 路径
当 rv="" 时,分页 List 直接走 etcd3 store 的
Range:
sequenceDiagram
participant Client
participant apiserver
participant etcd
Client->>apiserver: GET /pods?limit=500
apiserver->>etcd: Range(prefix, limit=500)
etcd-->>apiserver: 500 objects, more=true
apiserver-->>Client: 500 items + continue token (RV=T, startKey=K500)
Client->>apiserver: GET /pods?limit=500&continue=<token>
apiserver->>etcd: Range(K500, limit=500, rev=T)
etcd-->>apiserver: 500 objects, more=true
apiserver-->>Client: 500 items + continue token (RV=T, startKey=K1000)
每一页都是独立的 etcd Range 请求(指定 rev=T
保证快照一致性)。
成本模型:etcd Range 的代价与
扫描 key 数量 成正比,与实际返回 key
数量无关(当有 selector 时)。若集群有 10 万个 Pod,带
label selector 的 LIST 请求:
- etcd 不理解 label selector——etcd 仅做 key 前缀过滤;
- apiserver 接收所有 10 万条(按 limit 分批),在内存中逐条匹配 label selector;
- 若命中率低(如 selector 只选出 100 个),apiserver
需要扫 10 万条才能凑满
limit=500的一页。
这是 selector 放大 etcd Range
的根本原因:etcd 侧看到的 key
扫描量远大于客户端实际消费量。在 cacher
路径(rv="0")下,selector 过滤在内存
watchCache.store 里完成,没有额外 etcd
Range,代价仅是 CPU 过滤全量 cache 对象。
四、Cacher 路径下的 List
rv="0" 时,List 由 Cacher 从
watchCache.store(内存全量快照)服务:
- 遍历
store中所有对象; - 在内存应用 label/field selector;
- 按 limit 分批返回;continue token 中 startKey 指向内存迭代器位置(或下一批对象 key)。
优点:无 etcd I/O;对 etcd 完全无压力。
限制:若 Cacher 还未 Ready(第 5
篇),请求会被阻塞;若 watchCache.store 的 RV
落后于 client 的期望 RV,Cacher 仍可能回退 etcd 路径。
一致性警告:rv="0"
的结果可能略早于当前时刻——不应在需要
read-your-own-write
语义的场景中使用。例如刚写入一个 Secret 后立即 List
Secrets(rv="0")可能还看不到刚写入的版本。需要
read-after-write 一致性的路径应使用 rv="" 或将
Create 返回的 RV 作为后续 List 的
resourceVersion。
五、field selector 与 etcd 存储索引
Field selector(如 spec.nodeName=node1)在
etcd 中没有专用索引(etcd 是 key-value
存储,value 不参与索引)。apiserver 在
pkg/storage/selection_predicate.go
中注册部分常用 field
selector(spec.nodeName、metadata.name、metadata.namespace
等)为 etcd key 可推导字段,apiserver
会将其转换为 etcd key 前缀过滤,减少全量扫描;但复合 field
selector 或不在注册列表中的字段,仍需 apiserver
侧全量扫描过滤。
label selector 在 etcd 侧完全无索引——所有 label 过滤都由 apiserver 内存完成。
六、与第 4、5、7 篇的分工
| 主题 | 篇目 |
|---|---|
resourceVersion 与 etcd mod revision
映射 |
第 4 篇 |
| List 用到的 watchCache.store 与 Ready 门 | 第 5 篇 |
| 本篇:List RV 语义、continue token、selector 成本 | 第 6 篇 |
| Watch 长连接的 RV 语义、410 Gone | 第 7 篇 |
参考资料
规范 / 源码(A)
- kubernetes/kubernetes
v1.30.3:staging/src/k8s.io/apiserver/pkg/storage/cacher/cacher.go;storage/etcd3/;registry/generic/registry/store.go;storage/selection_predicate.go - Kubernetes API Concepts · Pagination(线索;以 v1.30.3 源码为准)
- Kubernetes API Concepts · Resource Version Semantics
站内对照
- 第 4 篇 · resourceVersion 映射
- 第 5 篇 · Watch cache / cacher
- 第 7 篇 · Watch 路径(服务端)
- etcd/09 · Watch 机制
- etcd/12 · Compaction 与 SLO
上一篇:Watch cache / cacher:dispatch、bookmark 与穿透 etcd
下一篇:Watch 路径(服务端):长连接、410 Gone 与 timeout
读完这篇,下一步读什么
优先读同系列或同问题的下一篇,把单篇消费变成主题集群。
【kube-apiserver】resourceVersion 与 Revision 映射:mod revision、continue 与一致性读期望
钉 Kubernetes resourceVersion 字段与 etcd mod revision 的对应关系;分析 continue token 的分页语义与成本;说明不同 List 路径的一致性期望差异;以及 410 Gone 与 ErrCompacted 的分列。版本锚定 Kubernetes v1.30.3 / etcd v3.5.33。
【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 / Kubernetes 控制面内核:storage、cacher、Admission 与 APF
补齐 etcd 系列 K8s 耦合篇之上的 kube-apiserver 生产内核:storage.Interface、watch cache、resourceVersion、Admission/Webhook、APF,并以排障与相对 etcd 的分层收束。
【kube-apiserver】控制面全景:缺口、五轴坐标系与 16 篇路线
相对 etcd/13、distributed/50、k8s-network 补齐 kube-apiserver 生产内核缺口;以 Storage/Watch/Admission/Auth/APF 五轴为坐标系定义 16 篇阅读路线;版本锚定 Kubernetes v1.30.3。