Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:控制器与 APIServer 完整交互流程:从 Watch 到缓存同步
Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:控制器与 APIServer 完整交互流程:从 Watch 到缓存同步
当我们深入理解 Informer、Reflector、DeltaFIFO 的工作原理后,还有一个关键问题没有解答:Controller 和 APIServer 之间的交互细节是什么?Watch 是如何工作的?ResourceVersion 有什么用?为什么有时候会收到过期的对象?Bookmark 事件又是什么?
这一篇文章,我们从源码级别深入理解 Controller 与 APIServer 的完整交互流程。
Kubernetes Watch ResourceVersion Bookmark v1.36.1 🔓 学习重点提示 — 建议先通读全文,再重点回顾标注内容
★ 重点掌握(必须)
• Watch 分页机制:limit 和 continue 参数如何实现大数据量分页
• ResourceVersion 传递:为什么 Watch 需要从特定的 ResourceVersion 开始
• Bookmark 事件:定时心跳的作用和对 Controller 的影响
☆ 次重点(了解即可)
• 早期版本兼容性问题
一、从 List 到 Watch:完整的同步流程
当我们启动一个 Informer 时,Reflector 会执行完整的 List-Watch 流程。这个流程分为两个阶段:
阶段一:List(全量同步)
List 是 Informer 启动时的第一步,目的是把 APIServer 上的所有资源一次性拉到本地。对于大数据量的资源,List 可能会分多个请求完成(分页)。
┌──────────────────────────────────────────────────────────────────────────┐ │ List 全量同步流程 │ ├──────────────────────────────────────────────────────────────────────────┤ │ │ │ Reflector APIServer │ │ │ │ │ │ │ ──── GET /api/v1/pods?limit=500 ──→ │ │ │ │ │ │ │ ←─── 500 pods + continue token ─── │ │ │ │ │ │ │ ──── GET /api/v1/pods? ──→ │ │ │ continue=xxx&limit=500 │ │ │ │ │ │ │ ←─── 更多 pods + 新 continue ──── │ │ │ │ │ │ │ ──── GET /api/v1/pods?... ──→ │ │ │ │ │ │ │ ←─── 最后一批 pods(无 continue) ── │ │ │ │ │ │ │ ✓ 所有数据已拉取,记录最后的 ResourceVersion │ │ │ │ │ │ ▼ 开始 Watch 阶段 │ └──────────────────────────────────────────────────────────────────────────┘
阶段二:Watch(增量同步)
Watch 是基于 HTTP long-polling 的长连接。Reflector 会从 List 结束时的 ResourceVersion 开始 Watch,实时接收集群的变化事件。
┌──────────────────────────────────────────────────────────────────────────┐ │ Watch 增量同步流程 │ ├──────────────────────────────────────────────────────────────────────────┤ │ │ │ Reflector APIServer │ │ │ │ │ │ │ ──── GET /api/v1/pods? ──→ │ │ │ watch=true& │ │ │ resourceVersion=12345 │ │ │ │ │ │ │ │ (长连接打开,等待事件) │ │ │ │ │ │ │ ←─── ADDED event ──────────────── │ │ │ ←─── MODIFIED event ───────────── │ │ │ ←─── Bookmark (心跳) ───────────── │ │ │ ←─── DELETED event ────────────── │ │ │ │ │ │ │ 处理事件,更新 DeltaFIFO... │ │ │ │ │ │ │ ←─── 更多事件 ───────────────────── │ │ │ │ │ └──────────────────────────────────────────────────────────────────────────┘
二、Watch 分页:limit 和 continue 参数
当集群中的资源数量非常大时(比如有成千上万个 Pod),一次 List 请求可能返回不了所有数据。Kubernetes 提供了分页机制解决这个问题。
List 请求的响应中会包含一个 continue token,用于获取下一页数据:
# 第一次请求:获取前 500 个 Pod
GET /api/v1/pods?limit=500
# 响应
{
"apiVersion": "v1",
"kind": "PodList",
"metadata": {
"resourceVersion": "12345",
"continue": "eyJza2luX3ZlcnNpb24iOiJza2luX3ZlcnNpb24iLCJkaXJlY3RvcnlLZXkiOiJwb2RzI2RlZmF1bHQiLCJjb250aW51ZVRva2VuIjoiZXlKMlpXWnBjR1ZoY21Wc1BqeGphWFhwY3lnME1qQXhZakV6TURFME1USTJNZz09In0ifQ==" // Base64 编码的游标
},
"items": [ /* 500 个 Pod */ ]
}
# 第二次请求:使用 continue token 获取下一页
GET /api/v1/pods?limit=500&continue=eyJza2luX3ZlcnNpb24iOiJza2luX3ZlcnNpb24iLCJkaXJlY3RvcnlLZXkiOiJwb2RzI2RlZmF1bHQiLCJjb250aW51ZVRva2VuIjoiZXlKMlpXWnBjR1ZoY21Wc1BqeGphWFhwY3lnME1qQXhZakV6TURFME1USTJNZz09In0ifQ==
Reflector 中的 List 循环会一直执行,直到没有 continue token 为止:
// Reflector 中的 List 循环伪代码
func (r *Reflector) ListAndWatch() error {
// ... 准备 List options ...
var resourceVersion string
for {
// 执行 List 请求
listObj, err := r.listerWatcher.List(options)
listMetaInterface, _ := meta.ListAccessor(listObj)
resourceVersion = listMetaInterface.GetResourceVersion()
// 将每个对象放入 DeltaFIFO
for _, obj := range listMetaInterface.GetItems() {
r.watchListWatermark.Record(int64(len(listMetaInterface.GetItems())))
r.expectedGVK = r.groupVersionKind(obj)
if err := r.store.Add(obj); err != nil {
// 处理错误
}
}
// 检查是否有更多页(continue token)
continueToken := listMetaInterface.GetContinue()
if continueToken == "" {
// 没有更多数据,退出 List 阶段
break
}
// 更新 options,准备下一次请求
options.Continue = continueToken
// 继续循环获取下一页
}
// List 完成,使用最后一个 ResourceVersion 开始 Watch
r.watch(resourceVersion)
}
三、ResourceVersion:理解版本号的核心作用
ResourceVersion 是 Kubernetes 实现乐观并发的核心机制。每个资源对象都有一个 resourceVersion 字段,每次更新时这个字段都会递增。
ResourceVersion 的三个作用
- 一致性保证:Watch 从指定的 ResourceVersion 开始,保证不会漏掉任何事件
- 冲突检测:更新资源时携带 ResourceVersion,APIServer 会检测是否冲突
- 分页定位:List 请求可以用 ResourceVersion 确定数据的时间点
为什么 Watch 必须指定 ResourceVersion?如果不指定,APIServer 会从"现在"开始推送事件,可能漏掉 List 和 Watch 之间发生的变更。通过从 List 结束时的 ResourceVersion 开始 Watch,可以确保不漏掉任何变更。
⚠️ 警告
ResourceVersion 不是单调递增的全局序列。不同 namespace、不同类型的资源有独立的 ResourceVersion 计数器。跨资源类型的操作(如同时 Watch Pod 和 Deployment)需要注意这一点。
四、Bookmark 事件:被忽视的心跳机制
Bookmark 是 Watch 事件中容易被忽视的一种类型。它的作用是作为心跳,确保长连接不会因为空闲超时而被代理或负载均衡器关闭。
// Bookmark 事件的示例
{
"type": "BOOKMARK",
"object": {
"kind": "Pod",
"apiVersion": "v1",
"metadata": {
"resourceVersion": "13000" // 当前 APIServer 的最新 ResourceVersion
}
}
}
Bookmark 事件的三个作用:
- 保持连接活跃:防止代理或负载均衡器因连接超时而关闭
- 更新 ResourceVersion:Reflector 会更新 lastSyncResourceVersion,用于 Watch 断线重连
- 无对象数据:Bookmark 的 object 字段只包含 metadata,没有实际资源数据
在 Reflector 中处理 Bookmark 事件:
// Reflector 处理 Watch 事件的部分代码
func (r *Reflector) watchHandler(...) error {
for {
select {
case
五、Watch 断线重连机制
当 Watch 连接断开时(比如网络抖动),Reflector 需要自动重连。关键问题是:从哪个 ResourceVersion 开始重连?
┌──────────────────────────────────────────────────────────────────────────┐ │ Watch 断线重连流程 │ ├──────────────────────────────────────────────────────────────────────────┤ │ │ │ Reflector APIServer │ │ │ │ │ │ │ ✓ 从 ResourceVersion=12345 开始 Watch │ │ │ ←─── 事件流 ────────────────────── │ │ │ │ │ │ │ ✗ 连接断开!(网络问题) │ │ │ │ │ │ │ ✓ 使用 lastSyncResourceVersion=12400 重连 │ │ │ ←─── 从 12400 之后的事件 ────── │ │ │ │ │ │ │ ✓ 不会漏事件,也不会重复收到 │ │ │ │ │ └──────────────────────────────────────────────────────────────────────────┘
Reflector 维护了 lastSyncResourceVersion,每次收到 Bookmark 事件或完成 List 后都会更新它。断线重连时,就从这个版本开始。
六、Watch 请求参数详解
Watch 请求支持多种参数来控制行为:
| 参数 | 说明 | 示例 |
|---|---|---|
| watch=true | 启用 Watch 模式 | 必须 |
| resourceVersion | 从指定版本开始 Watch | resourceVersion=12345 |
| resourceVersionMatch | 匹配模式(NotOlderThan/Exact) | NotOlderThan=12345 |
| timeoutSeconds | Watch 超时时间 | timeoutSeconds=600 |
| sendInitialEvents | 是否发送初始事件(BOOKMARK 特性) | sendInitialEvents=true |
七、常见问题与排查
问题 1:Too large resource version
这个错误表示请求的 ResourceVersion 太旧了。APIServer 默认只保留最近 5 分钟的资源版本数据。如果 Watch 断开太久,lastSyncResourceVersion 可能已经过期。
解决方案:Reflector 会自动检测到这个错误并重新执行 List,重新获取最新的数据。
问题 2:Event 顺序不一致
Kubernetes 不保证 Watch 事件的顺序。对于同一对象的事件(如 ADDED + MODIFIED),DeltaFIFO 会处理乱序问题:它会根据对象的 ResourceVersion 做判断,只保留最新版本。
八、总结
这一节我们深入理解了 Controller 与 APIServer 的完整交互流程:
- List-Watch 两阶段:List 拉取全量数据,Watch 接收增量变更
- 分页机制:通过 limit 和 continue token 实现大数据量分页
- ResourceVersion:保证一致性、检测冲突、定位分页
- Bookmark 事件:保持连接活跃,更新 lastSyncResourceVersion
- 断线重连:从 lastSyncResourceVersion 重新开始 Watch
下一节我们将学习 DynamicClient 操作 CRD,了解如何动态操作未编译进代码的自定义资源。敬请期待!
Kubernetes 编程 / Operator 专题【左扬精讲】—— 控制器与 APIServer 完整交互流程 · 来源:Kubernetes v1.36.1 client-go 源码分析

浙公网安备 33010602011771号