You need to enable JavaScript to run this app.
导航

Kubernetes 开发者最佳实践

最近更新时间2023.11.24 17:37:05

首次发布时间2023.11.24 16:41:05

本文主要描述使用容器服务过程中关于访问控制面组件、优化客户端访问模式相关建议和最佳实践。

使用 list-watch 模式替代全量 list

背景信息

作为 Kubernetes 开发者,您的组件可能:

  • 需要定期查询一些 Kubernetes 对象的列表。
  • 需要在多个节点上运行(尤其 DaemonSet 方式部署的组件)。

通常情况下通过定期调用 list API 来查询对象状态,但该方法每次必须加载所有对象到 kube-apiserver 内存中进行序列化并传输。这类请求会占用控制面大量 CPU 和内存,使控制面(Control Plane)过载或请求超时,即使定期查询的 Kubernetes 对象状态未发生变化,也会在 kube-apiserver 上造成高负载。

推荐方案

强烈建议避免全量 list,替换为 list-watch 模式,即先通过调用 list API 加载所有对象后,后续使用 watch API 获取对象状态的增量变更。list-watch 模式与全量 list 相比,可有效缩短 kube-apiserver 处理请求的时间、减少请求流量,如果对象状态未发生更改,则不会产生额外的负载。

如果您使用 Golang 语言,请查看 SharedInformerSharedInformerFactory,了解实现 list-watch 模式的 Golang 包。

补充说明

无论是全量 list 还是 list-watch 模式,建议在 list 请求中设置 ResourceVersion 参数(ResourceVersion=0),从 kube-apiserver cache 中读取数据,减少 kube-apiserver 与 etcd 数据库的交互次数。

说明

  • ResourceVersion 参数相关说明,请参见 Kubernetes 官网文档
  • VKE 的 kube-apiserver 内置了限流规则,避免大量 list 请求导致 kube-apiserver 内存溢出(OOM),但 client-go 在收到限流响应后会自动重试,因此一般情况下不会导致您的 list 请求失败。
ns := "kube-system"
selector := "app.kubernetes.io/name=xxx"
labelSelector, err := labels.Parse(selector)

//client-go clienset 客户端
pods1, kerr := kubeCli.CoreV1().Pods(ns).List(ctx, metav1.ListOptions{
   LabelSelector:   selector,
   ResourceVersion: "0",
})

//controller-runtime 客户端
pods2 := &corev1.PodList{}
rerr := runtimeCli.List(ctx, pods2, &client.ListOptions{
   LabelSelector: labelSelector,
   Namespace:     ns,
   Raw: &metav1.ListOptions{
      ResourceVersion: "0",
   },
})

减少 watch 和 list 产生的不必要流量

Kubernetes 在内部使用 watch API 发送关于对象更新的通知。即便 watch 调用需要的资源比定期 list 调用要少得多,但在大规模集群中的 watch 调用也会占用大量集群资源,影响集群性能。最大的影响来自于创建从多个位置观察频繁更改对象的 watch 调用,例如对运行在多个节点上的组件对应的 Pod 数据进行 watch 调用。集群上安装的第三方代码或扩展程序,可能会在后台创建此类 watch 调用。

我们推荐以下最佳实践:

  • 减少 watch 和 list API 调用产生的不必要处理操作和流量。
  • 避免创建从多个节点观察频繁更改的对象的 watch(例如 DaemonSet)。
  • 建议在单个节点上创建一个中央控制器,实现对象数据的监听及处理。
  • 仅对必要的对象进行 watch 操作,避免对集群全量对象进行 watch 操作,例如每个节点上的 kubelet 只 watch 调度到同一节点上的 Pod。
    • 如果您使用 controller-runtime 库,可以参考 Kubernetes 官网文档 设置 watch 过滤器。代码片段如下:
      ctrl.Options.NewCache = cache.BuilderWithOptions(cache.Options{
                                  SelectorsByObject: cache.SelectorsByObject{
                                          &corev1.Pod{}: {
                                              Field: fields.SelectorFromSet(fields.Set{"spec.nodeName": "node01"}),
                                          }
                                      }
                                  }
                              )
      
    • 如果您直接使用 client-go 库,可以在构造 Informer 时设置 watch 过滤器。代码片段如下:
      lw := cache.NewListWatchFromClient(c.CoreV1().RESTClient(), "pods", metav1.NamespaceAll, fields.OneTermEqualSelector("spec.nodeName", string(nodeName)))
      informer := cache.NewSharedInformer(lw, &apiv1.Pod{}, resyncPeriod)
      
  • 避免部署需要执行大量 watch 或 list 调用的第三方组件或扩展程序,从而影响集群性能。

停用 API 访问凭证的自动挂载

在 Kubernetes 1.22 之前版本,如果您在创建 Pod 时未指定 ServiceAccount(服务账号),Kubernetes 会自动执行以下操作:

  • 将默认的 ServiceAccount 分配给 Pod。
  • 挂载 ServiceAccount 的 API 访问凭证作为 Pod 的 Secret。
  • 对于每个已挂载的 Secret,kubelet 会创建一个 watch,以观察每个节点上该 Secret 的更改。

在大规模集群中,上述操作意味着数千个不必要的 watch 调用,可能会在 kube-apiserver 上产生大量负载。

因此如果 Pod 中运行的逻辑不需要访问 Kubernetes API,建议您停用 ServiceAccount 的 API 访问凭证的自动挂载,以避免创建相关的 Secret 和 watch 调用。详细说明,请参见 Opt out of API credential automounting

API 序列化协议建议使用 Protobuf

Kubernetes API 序列化协议建议使用 Protobuf(Protocol Buffers),相比于 JSON 更节省内存和传输流量。更多信息,请参见 Alternate representations of resources

无论您使用 controller-runtime 库还是直接使用 client-go 库,都可以在 KubeConfig 中设置编码格式,代码样例如下:

kubeConfig.ContentType = "application/vnd.kubernetes.protobuf"
kubeConfig.AcceptContentTypes = "application/vnd.kubernetes.protobuf,application/json"
//controller-runtime
mgr, err := ctrl.NewManager(kubeConfig, ctrl.Options{})
//client-go
kubeClient, err := clientset.NewForConfig(kubeconfig)