K8s API-Server Query 接口使用最佳实践

引言

最近在做 CRD Operatior 开发时一直有个疑问:k8s 中各种 Controller 在List-Watch或GEt索引对象时,广泛使用了 Selector 机制,如 Label、Node、Field Selector 等。他们是如何做索引?查询逻辑是怎么实现的?查询过滤是在 Api-Server 还是在 etcd 中?

粗略翻看了下 etcd 接口文档,也没有提供类似的检索能力。合理推测,相关逻辑应该在 Api-Server 中处理的。那么,在中大规模的集群中,有数以千计的 kubelets 和各种资源的 controller, 作为 client 向 API-Server 发起海量请求。面对这么多 QPS 的带 Selector 条件查询,k8s 是如何实现的呢?

带着这些疑问,打开了 Github 中的 k8s Api-Server 源码。

下面是正文:

APIServer 如何处理 List Objects 请求?

Api-Server 会在 Etcd 和 Client 之间建立起一道缓存。大部分情况可以直接从缓存中获取,但有些情况下,会穿透缓存直接到达 etcd。 因此对于几千节点的 k8s 集群,不合理的 List / Get Objects 可能会压垮 API-Server 或者 Etcd

Api-Server 会缓存全量的 etcd 数据。一般情况下,对一致性要求”非极致一致性”的场景下,从 Api-Server 中获取即可。但特定情况下,也会绕过缓存直接触达 etcd。

具体,分如下几个情况可能会直接从 etcd 中获取数据:

  1. 判断请求中是否有 ResourceVersion 参数。如果没有,则从 etcd 中直接获取。
  2. 当 Api-Server 的缓存还没有建立好时,直接从 etcd 中获取。
  3. 当 ResourceVersion 不为 0 且有 limit 参数,从 etcd 中获取。

API-Server 如何处理 selector 请求?

无论对象是从 etcd 中获取还是直接使用缓存,对于 selector 相关计算, 实际上是执行 filter 操作,均在 Api-Server 中进行。

因此不合适请求参数,可能导致全量的 objects 需要过滤,最后只返回少数一些 objects 的情况,这时对 Api-Server CPU 和内存的压力就比较大。

在 Client 开发时有哪些优化建议?

  1. 使用 namespaced api,减少需要处理的总对象数;
  2. 使用 Resourcevision = 0 的 client-go 默认配置,避免穿透缓存;
  3. 不同的 clients 使用不同的 service account token 便于区分,后面可以根据不同的优先级,对不同来源的异常流量区分降级, 限流策略;
  4. Daemonset 等部署量大的基础组件。在运维更新或重启时,设置节奏,避免全量同时重启,同时发起 List-watch 重连的请求,一下子打爆 Api-Server;

关于 ResourceVerison 值如何设置?

resoureVersion 的语义

  1. resourceVersion unset: Most Recent
  2. resourceVersion=”0”: Any
  3. resourceVersion=”{value other than 0}”: Not older than

对应的处理逻辑:

  1. Most Recent:去 etcd 拿数据;
  2. Any:优先用最新的,但不保证一定是最新的;
  3. Not older than:不低于某个版本号。

说明

  1. 本文的内容很多是参考了这个文章 K8s 集群稳定性:LIST 请求源码分析、性能评估与大规模基础服务部署调优。这篇文章作者写的很详尽,如有需要可以直接看原文
  2. 本文中对实现细节的描述,基于的是 k8s v1.24.0 版本。