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

【kube-apiserver】List、Pagination 与一致性 List:continue token 与 etcd Range 成本

文章导航

分类入口
kubernetesdistributed
标签入口
#kubernetes#apiserver#list#pagination#continue-token#resourceVersion#etcd-range#v1.30.3

目录

「为什么 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 的实际行为、一致性 Listrv="")与非一致性 Listrv="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.goList 方法决定走哪条路:

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 附近)大致包含:

// ListOptions 中:
type ListOptions struct {
    Limit    int64
    Continue string   // opaque,由 server 生成与消费
    // ...
}

关键约束

  1. continue token 有生命周期——其中的 RV 快照可能被 etcd compaction 裁掉;此时后续分页请求返回 410 GoneErrCompacted 映射到 HTTP 410)。客户端必须重新从头 List。
  2. continue 不跨 apiserver 实例保持语义——负载均衡切换后,新实例必须能解析上一实例生成的 token。由于编码格式固定,同 tag 版本的 apiserver 之间一般兼容,但 token 不应被客户端缓存跨版本使用。
  3. 指定 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 selectorLIST 请求:

这是 selector 放大 etcd Range 的根本原因:etcd 侧看到的 key 扫描量远大于客户端实际消费量。在 cacher 路径(rv="0")下,selector 过滤在内存 watchCache.store 里完成,没有额外 etcd Range,代价仅是 CPU 过滤全量 cache 对象。


四、Cacher 路径下的 List

rv="0" 时,List 由 Cacher 从 watchCache.store(内存全量快照)服务:

  1. 遍历 store 中所有对象;
  2. 在内存应用 label/field selector;
  3. 按 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.nodeNamemetadata.namemetadata.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)

站内对照


上一篇Watch cache / cacher:dispatch、bookmark 与穿透 etcd

下一篇Watch 路径(服务端):长连接、410 Gone 与 timeout

读完这篇,下一步读什么

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


By .