workqueue 是 client-go 控制器的调度骨架,非通用工具包;它通过限速、去重、done/forget 机制保障高可靠事件处理,裸 channel 或 slice 队列无法应对 kubernetes 的更新风暴、失败重试与 key 合并需求。

Workqueue 不是拿来“用”的工具包,而是 client-go 里控制器必须套用的调度骨架——它不处理业务逻辑,只决定“谁在什么时候拿什么去处理”。直接往里面塞函数或改结构体,八成会卡死、漏事件、爆内存。
为什么 processNextWorkItem 必须配 workqueue.RateLimitingInterface
裸 channel 或自建 slice 队列无法应对 Kubernetes 的真实压力:对象反复更新、etcd 限速、处理失败需重试、key 重复提交要合并。workqueue 提供三重保障:
-
Get()返回前自动限速,防打爆 apiserver(否则429 Too Many Requests会让队列积压雪崩) -
Add(key)自动去重,同一对象多次变更只留最后一次 -
Done(key)触发Forget(key),避免失败 key 持续重试占满内存
漏掉 Done(key) 是最常见 panic 根源:日志出现 processNextWorkItem: failed to get key from queue: queue is shutting down,基本就是某个 key 卡在 processing 集合里没被标记完成。
并发数设 2 还是 20?看单次耗时和容忍积压量
官方示例写 for i := 0; i ,只是保底值,不是推荐值。真实并发数得反推:
- 测出平均单次 reconcile 耗时,比如
80ms - 定好你允许的最大积压数,比如峰值能忍
100个未处理 key - 理论并发 ≈
1000ms / 80ms ≈ 12,再留余量,设8–10更稳 - 超过
20后,queue.Get()内部 mutex 锁竞争 + goroutine 调度开销反而拖慢整体
别忘了同步调大 rest.Config.QPS 和 Burst,否则限速器还没起作用,client 就先被 apiserver 拒绝了。
限速器选错,高频变更就卡死
DefaultControllerRateLimiter() 是指数退避 + 每秒 10 次突发,适合故障恢复,但 ConfigMap 每秒更新 5 次时,失败项会越压越慢。按场景换:
- 短周期高频同步(如监控配置热更):
NewMaxOfRateLimiter( newItemExponentialFailureRateLimiter(5*time.Millisecond, 1000*time.Second), newTickRateLimiter(10*time.Second, 10) ),保证失败 key 不霸占队列,且每 10 秒最多重试 10 次 - 纯吞吐优先(如批量修复旧资源):
NewBucketRateLimiter(100, 100),桶容量 100、每秒补 100 令牌,几乎无延迟 - 所有限速器只约束
Get()返回速度,不影响Add();上游事件爆炸(如 node 故障触发 500+ pod 删除),得靠Forget()+Done()及时清理已处理 key,否则内存泄漏
informer 事件进队前必须过滤,否则等于全集群扫描
没加 fieldSelector 或 namespace 的 informer,等价于对整个集群做全表扫描。哪怕你只关心 default 命名空间的 Deployment,默认也会拉下全部 10 万+ 条记录再本地过滤。
- 用
cache.NewSharedIndexInformer构建 informer,显式传入cache.ListWatchOptions{FieldSelector: "metadata.namespace=default"} - 加
cache.Indexers按 namespace 索引,避免遍历全量缓存查对象 -
ResyncPeriod: 0会关闭周期性重同步,防止无意义全量 reload
真正难的不是写对 Add() 和 Get(),而是理解 dirty 和 processing 两个集合怎么交互——key 进 dirty 才会被 Get() 拿走,拿走后进 processing,Done() 后才从 processing 移出。中间任何一步断掉,key 就永远卡住。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











