bigywater.tech
Kubernetes · 95

client-go informer:本地缓存和 resync

控制器为什么都用 informer 而不是轮询 API Server?本地缓存和 WATCH 是怎么配合的?lister 和直接 GET 差多少?resync 到底是什么,会不会增加 API Server 的压力?这篇写一个 60 行的 Go 程序,在客户端数每一个 HTTP 请求。

60µs vs 18.5 s
lister 读 1000 次与直接向 API Server GET 100 次的耗时
LIST=0, WATCH=1
启动后客户端只发出一个带 sendInitialEvents=true 的 WATCH
12 次
25 秒里 resync 重放的 UpdateFunc 次数(4 个对象 × 约 3 轮),0 个 API 请求

启动:一个 WATCH 灌满缓存

实测 · 本机执行输出
  +  0.0s ADD    pre-0
  +  0.0s ADD    pre-1
  +  0.0s ADD    pre-2
  +  0.1s HasSynced=true;客户端发出的请求: LIST=0 WATCH=1 GET单个=0 写=3
  +  0.1s 第一个 WATCH 请求的查询参数:allowWatchBookmarks=true&resourceVersionMatch=NotOlderThan&sendInitialEvents=true&timeout=7m15s&timeoutSeconds=435&watch=true
  +  0.1s 从本地缓存(lister)读 1000 次:用时 60µs;请求数 LIST=0 WATCH=1 GET单个=0(没有增加)
  + 18.6s 直接向 API Server GET 100 次:用时 18.491s;GET单个=100

informer 启动后 0.1 秒 HasSynced=true:客户端只发出了 1 个 WATCH、0 个 LIST。这个 WATCH 的查询参数是 allowWatchBookmarks=true&resourceVersionMatch=NotOlderThan&sendInitialEvents=true&…&watch=true——新版本 client-go 用“带初始事件的 watch”(WatchList)把缓存灌满,先收到 3 个 ADD(pre-0、pre-1、pre-2)。之后从本地缓存(lister)读 1000 次只用了 60µs,请求数不变;直接向 API Server GET 100 次却用了 18.5 秒——client-go 默认限流约 5 QPS,量一大就成了瓶颈。控制器几乎都读缓存,原因就在这里。

事件

实测 · 本机执行输出
  + 18.8s ADD    live
  + 19.1s UPDATE live rv 142544 -> 142547
  + 19.4s lister 里已经能读到 live:true
  + 19.4s DELETE live
  + 19.7s 事件回调累计:ADD=5 UPDATE(真实变化)=1 DELETE=1;resync 触发的 UpdateFunc(新旧 resourceVersion 相同)=4

创建、更新、删除一个 ConfigMap live:WATCH 把变化推给客户端,回调依次收到 ADD live、UPDATE live rv 142544 -> 142547(旧值和新值都带着 resourceVersion)、DELETE live;约 300 毫秒后 lister 里已经能读到 live(缓存先于回调更新,回调里读缓存就是新值)。回调累计 ADD=5(3 个 pre、live、命名空间自带的 kube-root-ca.crt)、真实 UPDATE 1 次、DELETE 1 次。

resync

实测 · 本机执行输出
  + 44.7s 又过 25 秒(resync 间隔 10 秒,缓存里有 4 个对象):resync 触发的 UpdateFunc 累计 16 次(增加 12)
  + 44.7s 全程请求数:LIST=0 WATCH=1 GET单个=100 写=6

resync 间隔 10 秒:缓存里 4 个对象,即使没有变化,每 10 秒也会对每个对象调一次 UpdateFunc,且新旧 resourceVersion 相同——25 秒里增加 12 次(累计 16 次,正好 4 个对象 × 4 轮)。这不是 API Server 在推送,而是 informer 用本地缓存重放。整个过程请求总数:LIST=0、WATCH=1、GET=100(我们自己发的)、写 6 次,resync 没有产生任何请求。回调里用“新旧 resourceVersion 是否相同”区分 resync 与真实变化。

分步演示

informer
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / kind(Docker 里的单节点或多节点真实集群,节点是 Docker Desktop 虚拟机里的容器)/ kubectl;用空的 kubeconfig 之外的独立 kubeconfig,不碰用户自己的集群;计时用 date +%s%N / kubectl 事件时间戳,数字每次运行会有小幅抖动。三节点 kind 集群 kx-main(Kubernetes v1.37.0);Go 1.27.1 + k8s.io/client-go v0.36.1(程序源码在 tools/articles/ktmp/informer/main.go,用 RoundTripper 包装统计 HTTP 请求);informer 工厂限定在一个命名空间、resync 10 秒;QPS 为 client-go 默认值;单次运行(各次的数字会略有差别,如 GET 100 次用时 18.5 秒前后);每种情形各一次
    • 没有验证:watch 连接断开后的重新 LIST / relist(resourceVersion 过期,410 Gone,见 k6、k7);WatchListClient 在旧版本 API Server 上的回退;SharedInformer 多个 handler 共享一份缓存;indexer 自定义索引;DeltaFIFO 内部队列与 workqueue(下一篇);resync 间隔为 0 的含义;大对象数量下的内存占用
    京ICP备2026067502号-1京公网安备11010502063487号