[{"content":"0. 介绍 client-go list\u0026amp;watch 精讲 介绍过 Reflector 通过 list\u0026amp;watch 实时获取 Kubernetes 资源的信息。并且作为生产者将资源信息存到 DeltaFIFO 中。\n那么消费者是在哪里定义的呢？本文围绕 DeltaFIFO 继续介绍是哪个模块消费了 DeltaFIFO 中的资源。\n1. controller 在 controller.Run 方法中\nfunc (c *controller) Run(stopCh \u0026lt;-chan struct{}) { ... // 创建 Reflector 对象 r := NewReflectorWithOptions( ... ) ... var wg wait.Group // 开启协程运行 Reflector wg.StartWithChannel(stopCh, r.Run) // 运行 controller.processLoop wait.Until(c.processLoop, time.Second, stopCh) wg.Wait() } controller.processLoop 将作为消费者处理 DeltaFIFO 中的资源：\nfunc (c *controller) processLoop() { // 正常状态下 processLoop 是一个永不退出的函数 for { // 从 DeltaFIFO 中 Pop 资源 // 将资源送入 PopProcessFunc 中处理 obj, err := c.config.Queue.Pop(PopProcessFunc(c.config.Process)) if err != nil { // 如果接收到 ErrFIFOClosed：DeltaFIFO: manipulating with closed queue 则退出 if err == ErrFIFOClosed { return } // 如果接收到 RetryOnError 则将资源重新加入到 DeltaFIFO 中等待下一次重新处理 if c.config.RetryOnError { // This is the safe way to re-enqueue. c.config.Queue.AddIfNotPresent(obj) } } } } func (f *DeltaFIFO) Pop(process PopProcessFunc) (interface{}, error) { f.lock.Lock() defer f.lock.Unlock() for { for len(f.queue) == 0 { // When the queue is empty, invocation of Pop() is blocked until new item is enqueued. // When Close() is called, the f.closed is set and the condition is broadcasted. // Which causes this loop to continue and return from the Pop(). if f.closed { return nil, ErrFIFOClosed } // 如果 DeltaFIFO 中无数据可消费，则调用 f.cond.Wait() 陷入阻塞，等待被唤醒 // 前面的调用 f.cond.Brondcast 就是唤醒这里的协程 f.cond.Wait() } // 取出 DeltaFIFO 的首资源 key，保证资源处理的顺序性 id := f.queue[0] f.queue = f.queue[1:] depth := len(f.queue) // 拿到资源信息 item, ok := f.items[id] if !ok { // This should never happen klog.Errorf(\u0026#34;Inconceivable! %q was in f.queue but not f.items; ignoring.\u0026#34;, id) continue } // 删除 DeltaFIFO 中的资源，注意这里删除的是资源，不是资源对应的 key，key 还保留在 DeltaFIFO 中 delete(f.items, id) // 处理资源 item err := process(item, isInInitialList) // 如果资源处理出错，需要将资源重新放回 DeltaFIFO 中 if e, ok := err.(ErrRequeue); ok { f.addIfNotPresent(id, item) err = e.Err } // Don\u0026#39;t need to copyDeltas here, because we\u0026#39;re transferring // ownership to the caller. return item, err } } 调用 process(item, isInInitialList) 实际调用的是 sharedIndexInformer.HandleDeltas：\nfunc (s *sharedIndexInformer) HandleDeltas(obj interface{}, isInInitialList bool) error { s.blockDeltas.Lock() defer s.blockDeltas.Unlock() if deltas, ok := obj.(Deltas); ok { // 获取资源的 Delta 结构，并交由 processDeltas 处理 return processDeltas(s, s.indexer, deltas, isInInitialList) } return errors.New(\u0026#34;object given as Process argument is not Deltas\u0026#34;) } func processDeltas(...) error { // from oldest to newest // deltas 记录的是对象的顺序更新状态 for _, d := range deltas { obj := d.Object switch d.Type { // 对于 Sync, Replaced, Added, Updated 状态进入一样的逻辑处理 case Sync, Replaced, Added, Updated: if old, exists, err := clientState.Get(obj); err == nil \u0026amp;\u0026amp; exists { if err := clientState.Update(obj); err != nil { return err } handler.OnUpdate(old, obj) } else { if err := clientState.Add(obj); err != nil { return err } handler.OnAdd(obj, isInInitialList) } // 处理 Deleted 资源变更状态 case Deleted: if err := clientState.Delete(obj); err != nil { return err } handler.OnDelete(obj) } } return nil } processDeltas 主要是调用 clientState 和 handler 处理资源。分别看 clientState 和 handler 做了什么。\n1.1 clientState 以 Add 类型为例，进入 clientState.Add:\n// 调用 clientState.Add 实际调用的是 cache.Add 方法 // cache 是 client-go 的缓存实现 func (c *cache) Add(obj interface{}) error { // 获取对象对应的 key 信息 key, err := c.keyFunc(obj) if err != nil { return KeyError{obj, err} } // 调用 c.cacheStorage.Add 方法 c.cacheStorage.Add(key, obj) return nil } // c.cacheStorage.Add 实际调用的是 threadSafeMap.Add 方法 func (c *threadSafeMap) Add(key string, obj interface{}) { c.Update(key, obj) } func (c *threadSafeMap) Update(key string, obj interface{}) { c.lock.Lock() defer c.lock.Unlock() // 获取缓存中 key 对应的资源 oldObject := c.items[key] // 将新资源赋给 缓存中的 items c.items[key] = obj // 更新 indexer 中资源的信息，indexer 包括缓存 threadSafeMap c.index.updateIndices(oldObject, obj, key) } clientState 根据资源的状态类型到缓存中处理，资源存储在缓存 threadSafeMap 的 items 中，threadSafeMap 是一个并发 安全的 map，内部通过加锁实现并发(不像 Go 自带的并发安全 map，通过两个 map 实现)。\n这里有个组件需要注意的是 indexer，我们先不过多介绍发散，在下一篇文章中会重点介绍 indexer 组件。\n1.2 handler 继续查看 handler.OnAdd 实现：\nfunc (s *sharedIndexInformer) OnAdd(obj interface{}, isInInitialList bool) { ... s.processor.distribute(addNotification{newObj: obj, isInInitialList: isInInitialList}, false) } func (p *sharedProcessor) distribute(obj interface{}, sync bool) { p.listenersLock.RLock() defer p.listenersLock.RUnlock() for listener, isSyncing := range p.listeners { switch { case !sync: // non-sync messages are delivered to every listener // 调用 listener.add 处理资源 listener.add(obj) case isSyncing: // sync messages are delivered to every syncing listener listener.add(obj) default: // skipping a sync obj for a non-syncing listener } } } func (p *processorListener) add(notification interface{}) { if a, ok := notification.(addNotification); ok \u0026amp;\u0026amp; a.isInInitialList { p.syncTracker.Start() } // 将包装资源的 notification 写到 processorListener.addCh 通道 p.addCh \u0026lt;- notification } handler.OnAdd 将资源写入到 processorListener 的 addCh 通道。\n这里不继续探究哪里接收 processorListener.addCh 通道的 notification，在后面的文章中会重点介绍。\n3. 小结 本文重点介绍了 controller 作为消费者是如何消费 DeltaFIFO 中的资源的。同时也可以看出在缓存中是不带事件变更类型的， 缓存中存储的只是资源的 item 信息。\ncontroller 根据 DeltaFIFO 中不同的资源类型做相应的处理，也侧面说明了 DeltaFIFO 为什么要 Delta 结构而不用普通队列。\n从 controller 可以看出更新到缓存中的永远是最新的资源，保证了处理资源的一致性。\n回头看，这里的逻辑是 client-go 架构图 的第四步和第六步。\n","permalink":"https://xhyyx.pages.dev/posts/deepdive-client-go-deltafifo/","summary":"client-go DeltaFIFO","title":"client-go DeltaFIFO 精讲"},{"content":"1. 介绍 client-go DeltaFIFO 介绍了从 DeltaFIFO 中取出的资源将被放到 indexer ，接着交由 processorListener 处理。\n本文继续看 processorListener 是如何处理 DeltaFIFO pop 的资源的。\n2. 启动 handler processorListener 的启动在 sharedIndexInformer.Run() 方法定义：\nfunc (s *sharedIndexInformer) Run(stopCh \u0026lt;-chan struct{}) { ... // 开启协程运行 sharedIndexInformer.processor.run 方法 wg.StartWithChannel(processorStopCh, s.processor.run) ... } func (p *sharedProcessor) run(stopCh \u0026lt;-chan struct{}) { func() { p.listenersLock.RLock() defer p.listenersLock.RUnlock() for listener := range p.listeners { // 开启协程运行 processorListener.run p.wg.Start(listener.run) // 开启协程运行 processorListener.pop p.wg.Start(listener.pop) } p.listenersStarted = true }() \u0026lt;-stopCh ... } sharedProcessor.run 的重点在 processorListener.pop 和 processorListener.run，分别介绍如下。\n2.1 processorListener.pop func (p *processorListener) pop() { defer utilruntime.HandleCrash() defer close(p.nextCh) // Tell .run() to stop var nextCh chan\u0026lt;- interface{} var notification interface{} for { select { case nextCh \u0026lt;- notification: var ok bool // 读取 processorListener.pendingNotifications notification, ok = p.pendingNotifications.ReadOne() if !ok { // Nothing to pop // 表示 processorListener.pendingNotifications 没有资源 nextCh = nil // Disable this select case } // DeltaFIFO 将资源信息存入 processorListener.addCh case notificationToAdd, ok := \u0026lt;-p.addCh: if !ok { return } if notification == nil { // No notification to pop (and pendingNotifications is empty) // 如果 notification 为 nil，表示目前没有资源处理 // 将 processorListener.addCh 的资源赋给 notification，下一次处理该资源 notification = notificationToAdd // 绑定 nextCh 和 processorListener.nextCh nextCh = p.nextCh } else { // There is already a notification waiting to be dispatched // 如果 notification 不为空，表示当前有资源在处理 // 将 notificationToAdd 写入 processorListener.pendingNotifications p.pendingNotifications.WriteOne(notificationToAdd) } } } } 这里的 processorListener.pop() 是两个逻辑耦合在一起的。一个是从通道 addCh 中将资源转入 nextCh 通道。另一个是如果 nextCh 的资源未能被及时处理，则将资源写入 processorListener.pendingNotifications，以等待 nextCh 空闲时处理。\n为什么这样处理是 nextCh 和 addCh 的处理速度不一致。nextCh 的处理要慢于 addCh，所以需要将资源写入 pendingNotifications 做为缓冲。\nprocessorListener.pendingNotifications 是一个 ringBuffer 循环队列，它不是并发安全的，不同于普通循环队列，它的长度是可扩容的。这是相比于通道的好处，通道的长度是固定的（但是通道是并发安全的）。\n2.2 processorListener.run 继续看 processorListener.run 是如何处理资源的。\nfunc (p *processorListener) run() { stopCh := make(chan struct{}) wait.Until(func() { // 从 processorListener.nextCh 通道中获取资源 for next := range p.nextCh { // 获取资源类型 switch notification := next.(type) { case updateNotification: // 处理 updateNotification 类型资源 p.handler.OnUpdate(notification.oldObj, notification.newObj) case addNotification: // 处理 addNotification 类型资源 p.handler.OnAdd(notification.newObj, notification.isInInitialList) if notification.isInInitialList { p.syncTracker.Finished() } case deleteNotification: // 处理 deleteNotification 类型资源 p.handler.OnDelete(notification.oldObj) default: utilruntime.HandleError(fmt.Errorf(\u0026#34;unrecognized notification: %T\u0026#34;, next)) } } // the only way to get here is if the p.nextCh is empty and closed close(stopCh) }, 1*time.Second, stopCh) } processorListener.run 根据不同资源类型调用 processorListener.handler 做相应的处理。\n这里以 processorListener.handler.OnAdd() 方法为例看 processorListener.handler 对资源做了什么。\n// processorListener.handler.OnAdd() 实际调用的是 ResourceEventHandlerFuncs.OnAdd() 方法 func (r ResourceEventHandlerFuncs) OnAdd(obj interface{}, isInInitialList bool) { if r.AddFunc != nil { r.AddFunc(obj) } } ResourceEventHandlerFuncs.OnAdd() 实际执行的是 ResourceEventHandlerFuncs.AddFunc() 该方法是我们在创建 informer 时注入的：\nfunc main() { // 解析 kubeconfig config, err := clientcmd.BuildConfigFromFlags(\u0026#34;\u0026#34;, \u0026#34;/Users/hxia/.kube/config\u0026#34;) if err != nil { panic(err) } // 创建 ClientSet 客户端对象 clientset, err := kubernetes.NewForConfig(config) if err != nil { panic(err) } stopCh := make(chan struct{}) defer close(stopCh) // 创建 sharedInformers sharedInformers := informers.NewSharedInformerFactory(clientset, time.Minute) // 创建 informer informer := sharedInformers.Core().V1().Pods().Informer() // 创建 Event 回调 handler informer.AddEventHandler(cache.ResourceEventHandlerFuncs{ // 注入 AddFunc 到 informer // ResourceEventHandlerFuncs.AddFunc() 调用的 AddFunc 实际执行的是这里 AddFunc: func(obj interface{}) { mObj := obj.(v1.Object) log.Printf(\u0026#34;New Pod Added to Store: %s\u0026#34;, mObj.GetName()) }, }) // 运行 informer informer.Run(stopCh) } 到此我们可以看到 nextCh 的资源将根据资源类型执行不同的 event handler 事件。通过该事件将资源交由业务逻辑处理。\n3. 小结 通过两个通道实现资源的传递，这一结构有点类似于 Reflector 和 controller。\nReflector 负责 list\u0026amp;watch APIServer 的资源，并将资源放入 DeltaFIFO 中，controller 负责将 DeltaFIFO 中的资源拿出来存入 indexer 和 processorListener.addCh。\n类似的，processorListener.pop 负责将 processorListener.addCh 中的资源写入到 processorListener.nextCh，如果processorListener.nextCh 来不及处理，则将资源写入到 processorListener.pendingNotifications 队列。processorListener.run 负责将 processorListener.nextCh 中的 资源交由不同的 event handler 处理。\n通过 processorListener.addCh 和 processorListener.nextCh 将资源的处理过程解耦，使得协程的职责更清晰。\n我们可以看到模块之间可以通过通道/队列解耦，实现不同模块各司其职，较少耦合性。\n除了用通道/队列接偶，模块之间解耦还有什么方式吗？比如：\n定义接口和实现，模块之间依赖接口而不是实现。 通过消息总线/队列，模块之间只需要接/发消息到消息总线/队列，而不用关心对端的逻辑。 通过依赖注入，只需实现自己的逻辑，依赖可以通过注入解决，可用于业务逻辑和控制逻辑解耦。 通过代理模式或中间层解耦两个模块，模块只需要和代理或中间层通信，不需要管对方的处理逻辑，减少了耦合。 通过分层架构，在架构层面解耦。不同层负责处理不同事情，减少依赖。 ","permalink":"https://xhyyx.pages.dev/posts/deepdive-client-go-event-handler/","summary":"client-go event handler","title":"client-go event handler 精讲"},{"content":"1. 介绍 indexer 是 client-go 中的缓存，不同于普通缓存，indexer 是带索引的缓存。\n在 client-go DeltaFIFO 精讲 中介绍到 DeltaFIFO 中的元素被加入到 threadSafeMap 中，这个 map 实际是 indexer 的一部分。\nindexer 在 client-go 定义为接口，实现该 indexer 接口的是 cache 对象：\ntype Indexer interface { Store Index(indexName string, obj interface{}) ([]interface{}, error) IndexKeys(indexName, indexedValue string) ([]string, error) ListIndexFuncValues(indexName string) []string ByIndex(indexName, indexedValue string) ([]interface{}, error) GetIndexers() Indexers AddIndexers(newIndexers Indexers) error } cache 对象定义：\ntype cache struct { cacheStorage ThreadSafeStore keyFunc KeyFunc } 其中，ThreadSafeStore 是线程安全存储的接口，KeyFunc 是返回对象 key 的函数，函数声明为 type KeyFunc func(obj interface{}) (string, error)\n直接去看 indexer 做了什么比较绕，这里通过一个示例去熟悉。示例如下：\nfunc main() { // 创建 index index := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{ \u0026#34;namespace\u0026#34;: cache.MetaNamespaceIndexFunc, }) // 定义对象 pod1 := \u0026amp;v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: \u0026#34;one\u0026#34;, Namespace: \u0026#34;default\u0026#34;, Annotations: map[string]string{\u0026#34;users\u0026#34;: \u0026#34;ernie,bert\u0026#34;}}} pod2 := \u0026amp;v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: \u0026#34;two\u0026#34;, Namespace: \u0026#34;default\u0026#34;, Annotations: map[string]string{\u0026#34;users\u0026#34;: \u0026#34;bert,oscar\u0026#34;}}} pod3 := \u0026amp;v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: \u0026#34;three\u0026#34;, Namespace: \u0026#34;default\u0026#34;, Annotations: map[string]string{\u0026#34;users\u0026#34;: \u0026#34;ernie,elmo\u0026#34;}}} // 将对象添加到 index 中 index.Add(pod1) index.Add(pod2) index.Add(pod3) // 索引对象 pods, err := index.ByIndex(\u0026#34;namespace\u0026#34;, \u0026#34;default\u0026#34;) if err != nil { panic(err) } for _, pod := range pods { fmt.Println(pod.(*v1.Pod).Name) } } 结合着示例看 indexer 做了什么会更加清晰。\n1.1 cache.NewIndexer 调用 cache.NewIndexer 方法创建 indexer：\nfunc NewIndexer(keyFunc KeyFunc, indexers Indexers) Indexer { return \u0026amp;cache{ cacheStorage: NewThreadSafeStore(indexers, Indices{}), keyFunc: keyFunc, } } // NewThreadSafeStore 创建的是 threadSafeMap func NewThreadSafeStore(indexers Indexers, indices Indices) ThreadSafeStore { return \u0026amp;threadSafeMap{ items: map[string]interface{}{}, index: \u0026amp;storeIndex{ indexers: indexers, indices: indices, }, } } 不同于普通的 threadSafeMap，这里的 threadSafeMap 还包括一个 index 结构，其中包括 indexers 和 indices 属性。\n这两个属性和索引相关，试想如果想从缓存中获取 namespace 下的资源或者某个 label 的资源该怎么做呢？ 这里的 index 的存在就是为了解决索引的问题，方便快速索引到缓存中的资源。\n1.2 cache.Add 调用 cache.Add 将对象添加到缓存，并且建立索引。\nfunc (c *cache) Add(obj interface{}) error { // cache.keyFunc 获取对象的 key // 这里的 keyFunc 是 cache.MetaNamespaceKeyFunc key, err := c.keyFunc(obj) if err != nil { return KeyError{obj, err} } // 将 key 和资源添加到缓存 c.cacheStorage.Add(key, obj) return nil } cache.cacheStorage.Add 不仅添加资源到缓存还建立了索引：\nfunc (c *threadSafeMap) Add(key string, obj interface{}) { c.Update(key, obj) } func (c *threadSafeMap) Update(key string, obj interface{}) { c.lock.Lock() defer c.lock.Unlock() // 获取缓存中的旧资源 oldObject := c.items[key] // 将新资源存到缓存中 c.items[key] = obj // 更新索引 c.index.updateIndices(oldObject, obj, key) } 这里的重点在于 cache.index.updateIndices() 方法。这个方法比较绕，我们不去探究其中的实现细节，只给出经过这个方法之后索引和缓存中的资源是如何对应的。对应关系如下图所示：\n1.3 cache.ByIndex 建立索引之后就可以通过 cache.ByIndex 获取对应索引下的资源了。\nfunc (c *cache) ByIndex(indexName, indexedValue string) ([]interface{}, error) { return c.cacheStorage.ByIndex(indexName, indexedValue) } func (c *threadSafeMap) ByIndex(indexName, indexedValue string) ([]interface{}, error) { c.lock.RLock() defer c.lock.RUnlock() // 获取索引 indexName/indexedValue 对应的资源 set, err := c.index.getKeysByIndex(indexName, indexedValue) if err != nil { return nil, err } list := make([]interface{}, 0, set.Len()) for key := range set { list = append(list, c.items[key]) } return list, nil } threadSafeMap.ByIndex 的重点在 threadSafeMap.index.getKeysByIndex():\nfunc (i *storeIndex) getKeysByIndex(indexName, indexedValue string) (sets.String, error) { // 获取 indexFunc，这里主要判断索引是否建立 indexFunc := i.indexers[indexName] if indexFunc == nil { return nil, fmt.Errorf(\u0026#34;Index with name %s does not exist\u0026#34;, indexName) } index := i.indices[indexName] return index[indexedValue], nil } threadSafeMap.index.getKeysByIndex() 的逻辑并不复杂，结合索引的示意图来看应该是比较好理解的。\n最后拿着索引找到的结果从缓存中拿资源，实现根据指定索引查找缓存中的资源。\n2. 小结 通过上述示例可以看出 indexer 是如何工作的。indexer 实现了缓存，又结合索引实现根据索引快速查找缓存功能。\n这里的缓存通过 map 实现，没有用别的数据结构。可以看出这里的结构是为了业务服务的，并且可自定义索引函数，定制化较高，设计的真不错。\n同时，DeltaFIFO 将根据不同资源类型对缓存做操作，保证了缓存中的数据和 etcd 数据是一致的。\n如果缓存中的数据过时了，可能是因为 list\u0026amp;watch 未和 apiserver 建立连接，这时候 Reflector 通过 retry 或者指数退避直到建立 连接，根据获取到的资源信息在更新本地缓存以保证资源是最新的。\n","permalink":"https://xhyyx.pages.dev/posts/client-go-indexer/","summary":"client-go indexer","title":"client-go indexer 精讲"},{"content":"1. 介绍 client-go 架构\n通过 client-go 可以实现访问 kubernetes 资源的实时性，可靠性和顺序性。那么 client-go 是 如何实现的呢？\n2. list \u0026amp; watch client-go 通过 list\u0026amp;watch 机制实现实时访问 kubernetes 资源，满足实时性。接下来从源码层面解析 client-go 的 list\u0026amp; watch 机制。\n2.1 list 机制 client-go 的 list\u0026amp; watch 实现在 tools/cache/reflect.go 中定义:\nfunc (r *Reflector) ListAndWatch(stopCh \u0026lt;-chan struct{}) error { ... if useWatchList { ... } // 首次运行 client-go fallbackToList 为 true if fallbackToList { // 调用 Refelector.list 方法实现 list kubernetes 资源 err = r.list(stopCh) if err != nil { return err } } // list 之后接着执行 watch 同步资源信息 return r.watchWithResync(w, stopCh) } Reflector.list 方法实现如下：\nfunc (r *Reflector) list(stopCh \u0026lt;-chan struct{}) error { // 设置 resourceVersion var resourceVersion string options := metav1.ListOptions{ResourceVersion: r.relistResourceVersion()} ... go func() { ... // Attempt to gather list in chunks, if supported by listerWatcher, if not, the first // list request will return the full response. pager := pager.New(pager.SimplePageFunc(func(opts metav1.ListOptions) (runtime.Object, error) { // 调用 Reflector.listerWatcher 的 List 方法 list 资源 return r.listerWatcher.List(opts) })) ... // 通过分页器 list 资源 list, paginatedResult, err = pager.ListWithAlloc(context.Background(), options) // 处理出错的情况： if isExpiredError(err) || isTooLargeResourceVersionError(err) { ... } close(listCh) }() select { case \u0026lt;-stopCh: return nil case r := \u0026lt;-panicCh: panic(r) case \u0026lt;-listCh: } r.setIsLastSyncResourceVersionUnavailable(false) // list was successful listMetaInterface, err := meta.ListAccessor(list) if err != nil { return fmt.Errorf(\u0026#34;unable to understand list result %#v: %v\u0026#34;, list, err) } // 获取 list 资源的 resource version // 该 resouce version 为资源最新的 resource version resourceVersion = listMetaInterface.GetResourceVersion() // 提取 list 资源信息到 items 中 items, err := meta.ExtractListWithAlloc(list) if err != nil { return fmt.Errorf(\u0026#34;unable to understand list result %#v (%v)\u0026#34;, list, err) } // 同步资源信息到 DeltaFIFO 中 if err := r.syncWith(items, resourceVersion); err != nil { return fmt.Errorf(\u0026#34;unable to sync list result: %v\u0026#34;, err) } initTrace.Step(\u0026#34;SyncWith done\u0026#34;) // 更新 Reflector 的 resource version r.setLastSyncResourceVersion(resourceVersion) initTrace.Step(\u0026#34;Resource version updated\u0026#34;) return nil } Reflector.list 的实现是比较清晰的，主要包括：\nresourceVersion：设置 resourceVersion，满足实时性和顺序性的要求。 分页器访问资源：通过 pager 分页器访问资源，实际调用的是 Reflector.listerWatcher.List(opts) 方法。 同步到 DeltaFIFO: Reflect 作为生产者将资源同步到 DeltaFIFO 队列中。 这里为了介绍更详实，分别重点介绍上述主要实现。\n2.1.1 resource version Reflector.relistResourceVersion 初始化 resource version:\n// relistResourceVersion determines the resource version the reflector should list or relist from. // Returns either the lastSyncResourceVersion so that this reflector will relist with a resource // versions no older than has already been observed in relist results or watch events, or, if the last relist resulted // in an HTTP 410 (Gone) status code, returns \u0026#34;\u0026#34; so that the relist will use the latest resource version available in // etcd via a quorum read. func (r *Reflector) relistResourceVersion() string { r.lastSyncResourceVersionMutex.RLock() defer r.lastSyncResourceVersionMutex.RUnlock() // 如果 sync 资源版本不可用，则将 resource version 设置成 \u0026#34;\u0026#34; if r.isLastSyncResourceVersionUnavailable { // Since this reflector makes paginated list requests, and all paginated list requests skip the watch cache // if the lastSyncResourceVersion is unavailable, we set ResourceVersion=\u0026#34;\u0026#34; and list again to re-establish reflector // to the latest available ResourceVersion, using a consistent read from etcd. return \u0026#34;\u0026#34; } // 如果上一次同步的资源版本为 \u0026#34;\u0026#34;，则将 resource version 设置成 \u0026#34;0\u0026#34; // 上一次同步的资源版本为 \u0026#34;\u0026#34; 表示首次 list，否则 list 的上一次同步资源版本应该是资源的 resource version if r.lastSyncResourceVersion == \u0026#34;\u0026#34; { // For performance reasons, initial list performed by reflector uses \u0026#34;0\u0026#34; as resource version to allow it to // be served from the watch cache if it is enabled. return \u0026#34;0\u0026#34; } // 根据上一次的资源版本继续 list，设置 resource version 的资源版本为上一次的资源版本 return r.lastSyncResourceVersion } 设置 resource version 为 \u0026quot;\u0026quot; 将从 etcd 中获取资源信息，如果为 \u0026ldquo;0\u0026rdquo; 则从 APIServer 的缓存中获取资源的缓存信息，减轻 etcd 的访问压力。\n设置 resource version 有点类似于“断点续传”，如果在 list 时没办法全部 list 完则记录这次最新的资源 resource version 在下一次的时候拿着这个 resource version 在去 list 剩下的。\n2.1.2 分页器 分页器的实现在 ListPager.list：\nfunc (p *ListPager) list(ctx context.Context, options metav1.ListOptions, allocNew bool) (runtime.Object, bool, error) { // 设置 Limit // Limit 是单次 list 所能获取的最多资源条目，默认 500 if options.Limit == 0 { options.Limit = p.PageSize } requestedResourceVersion := options.ResourceVersion requestedResourceVersionMatch := options.ResourceVersionMatch var list *metainternalversion.List paginatedResult := false for { // 实际调用的是 Reflector.listerWatcher.List 方法 obj, err := p.PageFn(ctx, options) if err != nil { // 处理 list 出错的情况： ... } // 如果资源全部 list 完，不需要在分页，返回 // if we have no more items, return the list if len(m.GetContinue()) == 0 { return list, paginatedResult, nil } // 如果资源没有 list 完，获取下一次 list 的 Continue // set the next loop up options.Continue = m.GetContinue() // Clear the ResourceVersion(Match) on the subsequent List calls to avoid the // `specifying resource version is not allowed when using continue` error. // See https://github.com/kubernetes/kubernetes/issues/85221#issuecomment-553748143. options.ResourceVersion = \u0026#34;\u0026#34; options.ResourceVersionMatch = \u0026#34;\u0026#34; // At this point, result is already paginated. paginatedResult = true } } 分页器的重点在于 options 的 Limit 和 Continue 这两个属性，具体的信息可参考 kubernetes api reference\n在访问 APIServer 时带上这两个属性，流程如下：\n1. 首先 `pager.list` 调用 `Reflector.ListerWatcher.list` 方法 func (lw *ListWatch) List(options metav1.ListOptions) (runtime.Object, error) { // 实际调用的是 ListFunc 方法 return lw.ListFunc(options) } 2. ListFunc 实现 func NewFilteredPodInformer(client kubernetes.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { return cache.NewSharedIndexInformer( \u0026amp;cache.ListWatch{ // ListFunc 是提前注入的 ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { // 实际通过客户端 client 访问 apiserver return client.CoreV1().Pods(namespace).List(context.TODO(), options) }, ... }, \u0026amp;apicorev1.Pod{}, resyncPeriod, indexers, ) } 构造的 URL 如下图所示： 可以通过一个简单的示例程序 查看 limit 和 continue 是如何协同，实现资源分页的。\n2.1.3 同步 DeltaFIFO 拿到资源之后需要同步该资源到 DeltaFIFO。同步的实现在 Reflector.syncWith 方法：\nfunc (r *Reflector) syncWith(items []runtime.Object, resourceVersion string) error { found := make([]interface{}, 0, len(items)) for _, item := range items { found = append(found, item) } // 将资源同步到 Reflector 的 DeltaFIFO 中 return r.store.Replace(found, resourceVersion) } // 实际调用的是 DeltaFIFO.replace 方法，具体实现略 func (f *DeltaFIFO) Replace(list []interface{}, _ string) error { ... } DeltaFIFO 的结构如下图所示： 其中，queue 表示按顺序进入队列的资源 key，items 记录的是 key 所对应的资源和行为（Delta）。\n为什么需要这样的队列，直接用普通队列将资源 push 到普通队列中不行吗？主要原因在于上层处理逻辑（controller）需要根据不同的 资源变化执行相应的动作。如对于 Added 变化，controller 增加资源到缓存，对于 Updated 变化，更新资源到缓存，对于 Deleted 变化， 删除缓存中的资源。引入 Delta 可以实现，本地 client-go 缓存和 ETCD 的实时一致。保持实时性，避免资源重复。\n2.2 watch 机制 list，更新 resource version 之后将进入 watch 的处理。源码在 Reflector.watch 方法：\nfunc (r *Reflector) watch(w watch.Interface, stopCh \u0026lt;-chan struct{}, resyncerrc chan error) error { ... for { ... // start the clock before sending the request, since some proxies won\u0026#39;t flush headers until after the first watch event is sent start := r.clock.Now() if w == nil { ... // 调用 client 的 watch 方法 // APIServer 将更新的资源回写到 w 中 w, err = r.listerWatcher.Watch(options) if err != nil { ... } } // 处理 watch 事件 err = handleWatch(start, w, r.store, r.expectedType, r.expectedGVK, r.name, r.typeDescription, r.setLastSyncResourceVersion, r.clock, resyncerrc, stopCh) ... } } } 经过层层调用，最终将 watch 到的事件交由 handleAnyWatch 函数处理：\nfunc handleAnyWatch(...) (bool, error) { ... loop: for { select { // stop 通道，负责处理 stop 信号 case \u0026lt;-stopCh: return watchListBookmarkReceived, errorStopRequested // err 通道，负责处理 watch 的 err case err := \u0026lt;-errCh: return watchListBookmarkReceived, err // apiserver 将资源变更写到 w.ResultChan 通道 case event, ok := \u0026lt;-w.ResultChan(): ... // 获取变更资源的 resource version resourceVersion := meta.GetResourceVersion() // 获取变更资源的更新类型 switch event.Type { // 如果是新增资源 case watch.Added: // 将资源添加到 store 中，store 实际是 DeltaFIFO err := store.Add(event.Object) if err != nil { utilruntime.HandleError(fmt.Errorf(\u0026#34;%s: unable to add watch event object (%#v) to store: %v\u0026#34;, name, event.Object, err)) } case watch.Modified: // 更新 store 的资源 err := store.Update(event.Object) if err != nil { utilruntime.HandleError(fmt.Errorf(\u0026#34;%s: unable to update watch event object (%#v) to store: %v\u0026#34;, name, event.Object, err)) } case watch.Deleted: // TODO: Will any consumers need access to the \u0026#34;last known // state\u0026#34;, which is passed in event.Object? If so, may need // to change this. err := store.Delete(event.Object) if err != nil { utilruntime.HandleError(fmt.Errorf(\u0026#34;%s: unable to delete watch event object (%#v) from store: %v\u0026#34;, name, event.Object, err)) } ... } // 更新 Reflector 的 resource version 为 watch 的资源 resource version setLastSyncResourceVersion(resourceVersion) ... } } } watch 调用 client 的 watch 方法，和 APIServer 建立长链接。APIServer 将资源的变更信息（包括变更类型和资源）写到 w.ResultChan 通道。在 Reflector 的 handleAnyWatch 中对通道中的变更信息做相应的处理。\n以 watch.Deleted 删除变更类型看是如何处理的：\n// 实际 store.Delete 调用的是 DeltaFIFO.Delete 方法 func (f *DeltaFIFO) Delete(obj interface{}) error { // 获取对象的 key，key 是 namespace/name 形式 id, err := f.KeyOf(obj) if err != nil { return KeyError{obj, err} } ... // 重点在 DeltaFIFO.queueActionLocked 方法 // exist in items and/or KnownObjects return f.queueActionLocked(Deleted, obj) } func (f *DeltaFIFO) queueActionLocked(actionType DeltaType, obj interface{}) error { // 调用 DeltaFIFO.queueActionInternalLocked 方法 return f.queueActionInternalLocked(actionType, actionType, obj) } func (f *DeltaFIFO) queueActionInternalLocked(actionType, internalActionType DeltaType, obj interface{}) error { // 获取资源对象的 key id, err := f.KeyOf(obj) if err != nil { return KeyError{obj, err} } ... oldDeltas := f.items[id] newDeltas := append(oldDeltas, Delta{actionType, obj}) newDeltas = dedupDeltas(newDeltas) if len(newDeltas) \u0026gt; 0 { // 判断 DeltaFIFO.items 中是否存在资源 if _, exists := f.items[id]; !exists { // 如果不存在则将资源的 key 存入 Delta.queue 中 f.queue = append(f.queue, id) } // 不管资源存不存在，更新 DeltaFIFO.items f.items[id] = newDeltas // Reflector 作为生产者通知下游的消费者退出阻塞 f.cond.Broadcast() } else { ... } return nil } watch 主要是将变更的资源根据类型记录到 DeltaFIFO 中，并且通知下游的消费者开始处理资源。\n3. 异常处理 Reflector 的 list\u0026amp;watch 机制到此就差不多介绍完了。这是 list\u0026amp;watch 的正常逻辑，如果出现异常，比如执行 list\u0026amp;watch 时 apiserver 不可用， 此时 client-go（Reflector） 会如何处理呢？\n接下来，假设 watch 资源时 apiserver 不可用。查看 watch 源码分析异常处理逻辑。\n3.1 retry watch 的异常处理在 Reflector.watch\nfunc (r *Reflector) watch(w watch.Interface, stopCh \u0026lt;-chan struct{}, resyncerrc chan error) error { var err error ... // handleWatch 正常状态下是不会退返回的 err = handleWatch(start, w, r.store, r.expectedType, r.expectedGVK, r.name, r.typeDescription, r.setLastSyncResourceVersion, r.clock, resyncerrc, stopCh) // 如果返回，停止 watch w.Stop() w = nil // 设置下次重试时间 retry.After(err) if err != nil { // 判断是否是 stop request 错误 // 如果是，退出 watch。否则，进入 switch 判断错误类型 if !errors.Is(err, errorStopRequested) { switch { // 如果是资源过期错误，则重新进入循环 case isExpiredError(err): // Don\u0026#39;t set LastSyncResourceVersionUnavailable - LIST call with ResourceVersion=RV already // has a semantic that it returns data at least as fresh as provided RV. // So first try to LIST with setting RV to resource version of last observed object. klog.V(4).Infof(\u0026#34;%s: watch of %v closed with: %v\u0026#34;, r.name, r.typeDescription, err) // 处理 too many requests 错误 case apierrors.IsTooManyRequests(err): klog.V(2).Infof(\u0026#34;%s: watch of %v returned 429 - backing off\u0026#34;, r.name, r.typeDescription) select { // 如果 stopCh 有消息，则退出 watch case \u0026lt;-stopCh: return nil // 如果 backoff 通道有消息，则重新运行 watch // 这个应该是和 retry.After(err) 有关系的 case \u0026lt;-r.backoffManager.Backoff().C(): continue } // 如果 internal error 和 should retry 为 true 的话，重新运行 watch case apierrors.IsInternalError(err) \u0026amp;\u0026amp; retry.ShouldRetry(): klog.V(2).Infof(\u0026#34;%s: retrying watch of %v internal error: %v\u0026#34;, r.name, r.typeDescription, err) continue // 默认打印 Warning 并退出 watch default: klog.Warningf(\u0026#34;%s: watch of %v ended with: %v\u0026#34;, r.name, r.typeDescription, err) } } return nil } } watch 如果出错的话，会根据错误类型判断是重新运行 watch 还是退出 watch。如果退出 watch 下次会重新运行吗？\n继续看 client-go 的指数退避实现。\n3.2 指数退避 client-go 的指数退避在 Reflector.Run 方法中实现：\nfunc (r *Reflector) Run(stopCh \u0026lt;-chan struct{}) { klog.V(3).Infof(\u0026#34;Starting reflector %s (%s) from %s\u0026#34;, r.typeDescription, r.resyncPeriod, r.name) // 调用 wait.BackoffUntil 在 func() 退出时重试 wait.BackoffUntil(func() { if err := r.ListAndWatch(stopCh); err != nil { r.watchErrorHandler(r, err) } }, r.backoffManager, true, stopCh) klog.V(3).Infof(\u0026#34;Stopping reflector %s (%s) from %s\u0026#34;, r.typeDescription, r.resyncPeriod, r.name) } 具体的指数退避策略定义在 Reflector.backoffManager 中，定义如下：\nfunc NewReflectorWithOptions(lw ListerWatcher, expectedType interface{}, store Store, options ReflectorOptions) *Reflector { ... r := \u0026amp;Reflector{ ... // We used to make the call every 1sec (1 QPS), the goal here is to achieve ~98% traffic reduction when // API server is not healthy. With these parameters, backoff will stop at [30,60) sec interval which is // 0.22 QPS. If we don\u0026#39;t backoff for 2min, assume API server is healthy and we reset the backoff. backoffManager: wait.NewExponentialBackoffManager(800*time.Millisecond, 30*time.Second, 2*time.Minute, 2.0, 1.0, reflectorClock), } ... } wait.NewExponentialBackoffManager 创建 backoffManager。其中定义初始重试时间为 800ms，最大重试时间 30s，每 2m 重置一次重试时间 重试的指数因子为 2.0，干扰因子 jitter 1.0。\n采用指数退避算法，Reflector 的 list\u0026amp;watch 机制是不会退出的。\n4. 思考 为什么 list\u0026amp;watch 可以实现实时性？ 根据上文分析，list\u0026amp;watch 通过 list 获取资源，watch 更新资源变化的方式，实现资源的实时更新。\n并且，watch 采用的是 http/2 流式传输实现数据的实时传输，对于 http/1.1 APIServer 采用 http trunked 传输来保证 数据传输的实时性。\n这一点并未在本文体现，有兴趣的读者可以查看 watch 的返回包中的字段深入了解。相应的代码示例 可以参考 watch。\n不用 DeltaFIFO 直接拿 Reflector list\u0026amp;watch 的资源信息处理行不行？ 不行。\n首先业务逻辑处理相对 list\u0026amp;watch 的处理速度是比较慢的，如果直接处理可能导致业务逻辑无法及时响应 造成资源信息丢失或者拥塞。\n直接处理没有缓存，如果业务逻辑需要拿资源信息还得通过 list\u0026amp;watch 拿，或者通过 APIServer 拿数据，造成资源浪费。\n耦合性太强，DeltaFIFO 是和 APIServer 打交道的组件，如果既和 APIServer 交互，又和业务逻辑打交道，会使得耦合性太强， 不够专注，逻辑混乱。\n无法合并重复变化，处理最新的资源变化，资源可能在短时间内有大量变更状态，如果直接将资源丢给业务逻辑，业务逻辑 无法判断资源是什么状态，会对每次变更做处理，反而没时间处理最新的应该处理的变更，而导致做了很多无用功，造成资源浪费。\n从 list\u0026amp;watch 设计可以学到什么？ 1）wait 包是一个好东西，通过 wait 包结合指数退避可以实现程序的重试，并且不会造成重试拥堵问题。\n2）wg.StartWithChannel 开启一个协程运行程序的方式比较优雅，这个也可以用起来。 3） 指数退避是最后的屏障，如果在启用前，可以先判断是否可以 retry。\n4）队列不止只有普通队列，DeltaFIFO 就是围绕业务场景设计的队列，很优雅。 5） Reflector 通过 list\u0026amp;watch 实现实时性的要求，通过 retry 和指数退避实现可靠性的要求。这在构造类似应用时可以用上。\n5. 参考文章 client-go中watch机制的一些陷阱 2022年最新k8s编程operator篇 Kubernetes源码剖析 Kubernetes Operator 开发进阶 ","permalink":"https://xhyyx.pages.dev/posts/client-go-list-watch/","summary":"client-go list \u0026amp; watch","title":"client-go list \u0026 watch 精讲"},{"content":"1. 介绍 传递到 event handler 的资源一般不会立即处理，而是先放到 workqueue 中。这是因为业务的处理速度要比资源的更新速度慢。 通过 workqueue 可解耦业务的处理逻辑和资源更新逻辑。\n具体的说，processor 负责将待处理的资源加入到 workqueue 中，业务逻辑负责从 workqueue 中取资源并且处理。\nworkqueue 底层采用延迟队列实现。在 client-go/util/workqueue 中定义了 client-go 的 workqueue 实现。\nclient-go/util/workqueue 有 FIFO 队列，延迟 workqueue 和 限速 workqueue 实现。\n2. FIFO 队列 FIFO workqueue 在普通队列基础上实现了标记，去重。其结构如下：\ntype Typed[t comparable] struct { // 实际存储元素的队列，保证元素处理的有序性 queue Queue[t] // 用于去重，保证并发唯一性 dirty set[t] // 用于标记元素是否正在处理，具有唯一性 processing set[t] } queue, dirty 和 processing 是非常重要的结构，queue 实现为队列，用于存储资源（通常是资源的 key）， dirty 用于去重，如果 dirty 中已经有数据了则不添加数据到 queue,processing 用于标记当前正在处理的资源。\ndirty 和 processing 为 hash map 实现，具有唯一性。\n如下图所示为一个正常的资源处理流程：\n假设在并发场景下，如果业务逻辑在处理资源 1(key: 1)，此时 1 资源做了更新，那么业务逻辑正在处理的资源实际是旧资源。\n这时候 FIFO workqueue 的处理流程如下：\n3. 延迟队列 延迟队列基于 FIFO 队列，通过引入延迟时间和堆实现队列的延迟。\n首先，将资源的延迟时间和 key 通过通道传递到 waitingForAddCh 通道。接着 waitingLoop 将从 waitingForAddCh 中取数据。\n如果资源未到延迟时间，则将资源存入最小堆 waitForPriorityQueue，最小堆按照资源的延迟时间排序，实现如下：\nfunc (pq waitForPriorityQueue[T]) Len() int { return len(pq) } func (pq waitForPriorityQueue[T]) Less(i, j int) bool { return pq[i].readyAt.Before(pq[j].readyAt) } func (pq waitForPriorityQueue[T]) Swap(i, j int) { pq[i], pq[j] = pq[j], pq[i] pq[i].index = i pq[j].index = j } 如果资源达到延迟时间，则将资源存入 FIFO 队列。\n示意图如下： 4. 限速队列 限速队列是在延迟队列基础上，通过引入限速算法确定延迟时间的方式实现限速。\nclient-go 使用四种限速算法实现限速，分别是排队指数算法，计数器算法，令牌桶算法和混合模式。\n4.1 排队指数算法 排队指数算法是对同一元素实现限速延迟的算法。如果同一元素在限速周期内被重复加入，则会触发排队。\n其核心实现在：\nfunc (r *TypedItemExponentialFailureRateLimiter[T]) When(item T) time.Duration { r.failuresLock.Lock() defer r.failuresLock.Unlock() // 查看元素是否在限速周期内 exp := r.failures[item] // 如果在将元素的 failures + 1 r.failures[item] = r.failures[item] + 1 // 指数算法确定元素的延迟入队时间 backoff := float64(r.baseDelay.Nanoseconds()) * math.Pow(2, float64(exp)) // 如果该时间大于 math.MaxInt64，则设置延迟入队时间为 maxDelay if backoff \u0026gt; math.MaxInt64 { return r.maxDelay } // 如果计算的时间大于 maxDelay 则设置延迟入队时间为 maxDelay calculated := time.Duration(backoff) if calculated \u0026gt; r.maxDelay { return r.maxDelay } return calculated } 可以看到这里排队指数使用属性 failures 记录元素的排队时间，这是因为排队指数算法常用在失败重试的场景下。 通过指数计算下次处理的时间，避免频繁重试对系统造成冲击。\n4.2 计数器算法 计数器算法限制一段时间内允许通过的元素数量。\n计数器算法实现如下：\nfunc (r *TypedItemFastSlowRateLimiter[T]) When(item T) time.Duration { r.failuresLock.Lock() defer r.failuresLock.Unlock() r.failures[item] = r.failures[item] + 1 if r.failures[item] \u0026lt;= r.maxFastAttempts { return r.fastDelay } return r.slowDelay } 假设 fastDelay 是 5ms，slowDelay 是 10s， maxFastAttempts 是 3。在一个限速周期内通过 AddRateLimited 方法插入 4 个相同的元素， 那么前 3 个元素使用 fastDelay 定义的 fast 速率，当触发 maxFastAttempts 字段时，第 4 个元素使用 slowDelay 定义的 slow 速率。\nclient-go 实现的计数器算法对普通计算器算法做了改造。普通计数器算法不针对同一元素，在元素数量 达到阈值时，则切换延迟时间来控制请求速率，防止请求过多占用过多资源。\n4.3 令牌桶算法 令牌桶算法的实现在 golang.org/x/time/rate 库中定义。\n令牌桶算法内部实现了一个存放 token（令牌）的“桶”，初始时“桶”是空的，token 会以固定速率往“桶”里填充，直到将其填满为止， 多余的 token 会被丢弃。每个元素都会从令牌桶得到一个 token，令牌桶算法内部实现了一个存放 token（令牌）的“桶”，初始时“桶”是空的， token 会以固定速率往“桶”里填充，直到将其填满为止，多余的 token 会被丢弃。每个元素都会从令牌桶得到一个 token，\n在实例化 rate.NewLimiter 后，传入 r 和 b 两个参数，其中 r 参数表示每秒往“桶”里填充的 token 数量，b 参数表示令牌桶的大小 （即令牌桶最多存放的 token 数量）。\n假定 r 为 10，b 为 100。假设在一个限速周期内插入了 1000 个元素，通过 r.Limiter.Reserve().Delay 函数返回指定元素应该等待的时间， 那么前 b（即 100）个元素会被立刻处理，而后面元素的延迟时间分别为 item100/100ms、item101/200ms、item102/300ms、item103/400ms，以此类推。\n4.4 混合模式 混合模式是将多种限速算法混合使用，比较获取最大延迟时间来限速的模式。\n示例如下：\nfunc TestMaxOfRateLimiter(t *testing.T) { // 混合排队指数算法和计数器算法 limiter := NewMaxOfRateLimiter( NewItemFastSlowRateLimiter(5*time.Millisecond, 3*time.Second, 3), NewItemExponentialFailureRateLimiter(1*time.Millisecond, 1*time.Second), ) // 确定延迟时间 if e, a := 5*time.Millisecond, limiter.When(\u0026#34;one\u0026#34;); e != a { t.Errorf(\u0026#34;expected %v, got %v\u0026#34;, e, a) } ... } 5. 小结 资源采用限速队列的方式加入到 workqueue 中，最后由上层业务逻辑进行处理。在实际开发中，使用哪种限速队列对我们也是有意义的。\n在实际应用中，选择限速队列算法需综合考虑多方面因素： 流量特征 - 突发流量：若流量具有明显的突发性质，如短时间内有大量请求集中到达，令牌桶算法是较好的选择，它能在限制平均流量的同时，允许一定程度的突发处理。 - 平稳流量：对于流量相对平稳、速率变化不大的场景，计数算法较为合适，可精确控制请求速率，实现简单且能有效防止系统过载。 任务性质 - 重试策略：当任务需要根据重试次数动态调整等待时间，如一些涉及资源竞争或可能导致系统不稳定的操作，排队指数算法能通过指数增长的等待时间，避免频繁重试对系统造成冲击。 - 时效性要求：若任务对时效性要求极高，不允许有较大的延迟，令牌桶算法或计数算法更优，因为排队指数算法可能因指数增长的等待时间而导致任务处理延迟较大。 系统资源 - 资源有限：在系统资源紧张，如 CPU、内存有限的情况下，计数算法由于实现简单，对资源的消耗相对较小，是一个可行的选择。令牌桶算法虽然实现稍复杂，但能有效利用资源应对突发流量，在资源允许的情况下也可考虑。 - 资源充足：如果系统资源较为充足，可更多地考虑算法对流量控制的精确性和灵活性。此时，令牌桶算法是不错的选择，能在保证系统稳定的同时，充分利用资源处理突发流量。 业务需求 - 精度要求：某些业务场景对请求速率的控制精度要求极高，如金融交易系统，此时计数算法可精确限制每秒请求数量，满足高精度控制需求。 - 业务逻辑复杂：若业务逻辑复杂，涉及多种不同类型的任务和处理流程，可能需要综合考虑多种算法，或者根据不同的任务类型选择不同的算法。例如，对于一些关键且不允许丢失的任务，可以使用令牌桶算法保证其在突发情况下也能得到及时处理；对于一些可重试的非关键任务，可以采用排队指数算法来控制重试频率。 实际的场景案例如下：\n以下以 Kubernetes 集群中节点与 API Server 的通信场景为例，说明如何选择合适的算法： 场景描述 - Kubernetes 集群中有多个节点，这些节点会定期向 API Server 发送心跳信息以汇报自身状态，同时会在资源发生变化时向 API Server 发送更新请求。 流量特征分析 - 心跳信息的发送频率相对稳定，属于平稳流量。而资源更新请求可能会因为某些操作（如大规模应用部署）而出现突发流量。 任务性质分析 - 心跳信息要求时效性高，不能有较大延迟，以保证 API Server 能及时了解节点状态。资源更新请求如果失败，需要有合理的重试策略，避免对系统造成过大压力。 系统资源分析 - 节点的 CPU 和内存资源有限，需要考虑算法对资源的消耗。 业务需求分析 - 对于心跳信息，要求对发送速率有严格控制，以避免过度占用网络带宽和 API Server 资源。对于资源更新请求，要能在保证系统稳定的前提下，尽可能快速地处理突发的更新任务。 算法选择 - 对于心跳信息的限速，由于流量平稳且对速率控制精度要求高，选择计数算法。可以设置每秒允许发送一定数量的心跳请求，精确控制请求速率，防止因心跳请求过多占用过多资源。 - 对于资源更新请求，考虑到存在突发流量和任务的重试需求，选择令牌桶算法结合排队指数算法。令牌桶算法用于处理突发的资源更新请求，允许在一定时间内有较多的请求被处理，以应对大规模应用部署等情况。当更新请求失败时，采用排队指数算法进行重试，随着重试次数增加，等待时间逐渐变长，避免频繁重试对系统造成冲击。 ","permalink":"https://xhyyx.pages.dev/posts/deepdive-client-go-workqueue/","summary":"client-go indexer","title":"client-go workqueue 精讲"},{"content":"这里就可以写一些关于的相关信息了。\n","permalink":"https://xhyyx.pages.dev/about/","summary":"about","title":"关于"},{"content":"这里就可以写一些关于阅读的相关信息了。\n","permalink":"https://xhyyx.pages.dev/read/","summary":"read","title":"阅读"}]