Go clientSet Watch 运行后随机性失效

背景

List 和 Watch 机制是 kubernetes 中重要的机制之一。控制器通过 API Server 的 List API 来获取所有最新版本的 API 对象,通过 Watch API 来监听所有 API 对象的变化。 在程序设计过程中,往往也需要利用 List && Watch 机制,来观察 API 对象的状态,从而调用 EventHandler,做出响应。 基于此背景,Go 语言官方的 clientSet 中提供了相应的 API 接口供开发者使用。然而,笔者在使用 Watch 机制中踩到了不小坑。

问题

笔者在程序中创建 clientSet,并调用其 Watch 方法,监听某 Secret 资源变化,伪代码如下:

css 复制代码
secretWatch, err := clientSet.CoreV1().Secrets("命名空间").Watch(context.TODO(), 
	metav1.ListOptions{FieldSelector: fields.OneTermEqualSelector("metadata.name", "API 对象名").String()})
for {
	for range secretWatch.ResultChan() {
		// 响应操作	
	}
}

笔者在启动后,经过几番调试确实可以监听到信息,安心提交。 然而,经过一段运行时间后,Watch 机制突然失灵,而且无法恢复。本地 DeBug 也始终找不到异常点。

解决方案

问题分析

clientSet 的 Watch 接口缺失能够监听了需要的信息,然而其难以处理各类异常。如下源码所示,因此当异常发生时,Watch 会自动关闭。

scss 复制代码
// Interface can be implemented by anything that knows how to watch and report changes.
type Interface interface {
	// Stops watching. Will close the channel returned by ResultChan(). Releases
	// any resources used by the watch.
	Stop()

	// Returns a chan which will receive all the events. If an error occurs
	// or Stop() is called, this channel will be closed, in which case the
	// watch should be completely cleaned up.  !!!明确说了在出现错误或者被调用Stop时,通道会自动关闭的
	ResultChan() <-chan Event
}

解决方案

使用

使用 RetryWatcher 来替换传统 Watcher,简单来说,此类 RetryWatcher 能够保证 Watch 自动关闭后,重新拉起一个 Watcher,使得程序继续正常运行。 使用方法如下:

scss 复制代码
secretWatch, err := watchtools.NewRetryWatcher("资源版本", &cache.ListWatch{
	WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) {
		return clientSet.CoreV1().Secrets("命名空间").Watch(context.TODO(), metav1.ListOptions{
			FieldSelector: fields.OneTermEqualSelector("metadata.name", "API 对象名称").String(),
		})
	},
})
for {
	for range secretWatch.ResultChan() {
		// 响应操作	
	}
}

代码解读

了解使用后,笔者带着大家来深究一下源码: 首先来看看 newRetryWatcher 函数(NewRetryWatcher 接口会调用该内部函数),笔者发现其中启动了一个 rw.receive() 的协程。receive() 的官方解释是"reads the result from a watcher, restarting it if necessary.",表示除了正常 Watch 之外,在必要时重启该 Watch。

go 复制代码
func newRetryWatcher(initialResourceVersion string, watcherClient cache.Watcher, minRestartDelay time.Duration) (*RetryWatcher, error) {
	switch initialResourceVersion {
	case "", "0":
		// TODO: revisit this if we ever get WATCH v2 where it means start "now"
		//       without doing the synthetic list of objects at the beginning (see #74022)
		return nil, fmt.Errorf("initial RV %q is not supported due to issues with underlying WATCH", initialResourceVersion)
	default:
		break
	}

	rw := &RetryWatcher{
		lastResourceVersion: initialResourceVersion,
		watcherClient:       watcherClient,
		stopChan:            make(chan struct{}),
		doneChan:            make(chan struct{}),
		resultChan:          make(chan watch.Event, 0),
		minRestartDelay:     minRestartDelay,
	}

	go rw.receive()
	return rw, nil
}

更深层次的代码需要了解 doReceive() 函数(如下代码)。若正常结束,doReceive() 返回 true,上层 receive 函数会调用 cancel 退出程序。若不正常结束,则返回 false,receive 会调用 NonSlidingUntilWithContext 重建 Watcher 继续监听。

scss 复制代码
func (rw *RetryWatcher) receive() {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	go func() {
		select {
		case <-rw.stopChan:
			cancel()
			return
		case <-ctx.Done():
			return
		}
	}()

	// We use non sliding until so we don't introduce delays on happy path when WATCH call
	// timeouts or gets closed and we need to reestablish it while also avoiding hot loops.
	wait.NonSlidingUntilWithContext(ctx, func(ctx context.Context) {
		done, retryAfter := rw.doReceive()
		if done {
			cancel()
			return
		}

		time.Sleep(retryAfter)

		klog.V(4).Infof("Restarting RetryWatcher at RV=%q", rw.lastResourceVersion)
	}, rw.minRestartDelay)
}

func (rw *RetryWatcher) doReceive() (bool, time.Duration) {
    // ...
}

为了降低阅读难度,笔者将 doReceive() 重要的部分拆解。

  • 首先:因为 RetryWatch 最终还是调用的传统 Watch 函数,因此,先捕捉获取 Watch 中出现的 error,若发现则返回 false;
go 复制代码
watcher, err := rw.watcherClient.Watch(metav1.ListOptions{
		ResourceVersion:     rw.lastResourceVersion,
		AllowWatchBookmarks: true,
	})
	switch err {
	case nil:
		break

	case io.EOF:
		// ...
	// ...
	}
  • 其次:在获取消息时,同样可能会出现异常,返回 false。
csharp 复制代码
for {
		select {
		case <-rw.stopChan:
			// ...
		case event, ok := <-ch:
			if !ok {
				// ...
			}
			switch event.Type {
			case watch.Added, watch.Modified, watch.Deleted, watch.Bookmark:
				// ...
			case watch.Error:
				// ...
			default:
				// ...
			}
		}
	}
相关推荐
Ares-Wang12 分钟前
kubernetes》》k8s》》Heml
云原生·容器·kubernetes
阿里云大数据AI技术16 分钟前
千万级数据秒级响应!碧桂园基于 EMR Serverless StarRocks 升级存算分离架构实践
大数据·云原生·serverless
容器魔方1 小时前
Bilibili、中电信人工智能科技、商汤科技、联通云等正式加入Volcano社区用户组
云原生·容器·云计算
阿里云云原生2 小时前
MCP云托管最优解,揭秘国内最大MCP中文社区背后的运行时
云原生
数字化综合解决方案提供商3 小时前
云原生时代的双轮驱动
云原生
小马爱打代码3 小时前
云原生 - Service Mesh
云原生·service_mesh
weisian1514 小时前
云原生--核心组件-容器篇-2-认识下Docker(三大核心之镜像,容器,仓库)
docker·云原生·容器
陈奕昆5 小时前
6.1腾讯技术岗2025面试趋势前瞻:大模型、云原生与安全隐私新动向
算法·安全·云原生·面试·腾讯
孔令飞6 小时前
Go 1.24 中的弱指针包 weak 使用介绍
人工智能·云原生·go
weisian1516 小时前
云原生--核心组件-容器篇-3-Docker核心之-镜像
docker·云原生·容器