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 事件的三个作用

  1. 保持连接活跃:防止代理或负载均衡器因连接超时而关闭
  2. 更新 ResourceVersion:Reflector 会更新 lastSyncResourceVersion,用于 Watch 断线重连
  3. 无对象数据: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 源码分析

posted @ 2026-06-13 18:18  左扬  阅读(5)  评论(0)    收藏  举报