【博客683】k8s list请求优化以及合理使用list以维护集群稳定性

k8s list请求优化以及合理使用list以维护集群稳定性

apiserver/etcd List 示例分析

  • 1、LIST apis/cilium.io/v2/ciliumendpoints?limit=500&resourceVersion = 0

    这里同时传了两个参数,但 resourceVersion=0 会导致 apiserver 忽略 limit=500, 所以客户端拿到的是全量 ciliumendpoints 数据

    此时不会查etcd,因为有resourceVersion = 0,且resourceVersion = 0会忽略limit,因为limit一定要查etcd
    一种资源的全量数据可能是比较大的,需要考虑清楚是否真的需要全量数据

  • 2、LIST api/v1/pods?filedSelector=spec.nodeName%3Dnode1

    这个请求是获取 node1 上的所有 pods(%3D 是 = 的转义)。

    根据 nodename 做过滤,给人的感觉可能是数据量不太大,但其实背后要比看上去复杂:

    这种行为是要避免的,除非对数据准确性有极高要求,特意要绕过 apiserver 缓存。
    首先,这里没有指定 resourceVersion=0,导致 apiserver 跳过缓存,直接去etcd读数据;
    其次,etcd 只是 KV 存储,没有按 label/field 过滤功能(只处理 limit/continue),
    所以,apiserver 是从 etcd 拉全量数据,然后在内存做过滤,开销也是很大的,后文有代码分析。

  • 3、LISTapi/v1/pods?filedSelector=spec.nodeName%3Dnode1&resourceVersion = 0

    跟 2 的区别是加上了 resourceVersion=0,因此 apiserver 会从缓存读数据,性能会有量级的提升。

    但要注意,虽然实际上返回给客户端的可能只有几百 KB 到上百 MB(取决于 node 上 pod 的数量、pod 上 label 的多少等因素), 但 apiserver 需要处理的数据量可能是几个 GB。后面会有定量分析。

以上可以看到,不同的 LIST 操作产生的影响是不一样的,而客户端看到数据还有可能只 是 apiserver/etcd 处理数据的很小一部分。如果基础服务大规模启动或重启, 就极有可能把控制平面打爆。

判断是否必须从 etcd 读数据:shouldDelegateList()

func shouldDelegateList(opts storage.ListOptions) bool {
    resourceVersion := opts.ResourceVersion
    pred            := opts.Predicate
    pagingEnabled   := DefaultFeatureGate.Enabled(features.APIListChunking)      // 默认是启用的
    hasContinuation := pagingEnabled && len(pred.Continue) > 0                   // Continue 是个 token
    hasLimit        := pagingEnabled && pred.Limit > 0 && resourceVersion != "0" // 只有在 resourceVersion != "0" 的情况下,hasLimit 才有可能为 true

    // 1. 如果未指定 resourceVersion,从底层存储(etcd)拉去数据;
    // 2. 如果有 continuation,也从底层存储拉数据;
    // 3. 只有 resourceVersion != "0" 时,才会将 limit 传给底层存储(etcd),因为 watch cache 不支持 continuation
    return resourceVersion == "" || hasContinuation || hasLimit || opts.ResourceVersionMatch == metav1.ResourceVersionMatchExact
}
  • 问:客户端未设置 ListOption{} 中的 ResourceVersion 字段,是否对应到这里的 resourceVersion == ""?

    答:是的,所以第一节的 例子 会导致从 etcd 拉全量数据。

  • 问:客户端设置了 limit=500&resourceVersion=0 是否会导致下次 hasContinuation==true?

    答:不会,resourceVersion=0 将导致 limit 被忽略(hasLimit 那一行代码),也就是说, 虽然指定了 limit=500,但这个请求会返回全量数据。

  • 问:还有其他情况需要去etcd拉取吗?

    答:apiserver cache未建立完成的时候

apiserver/etcd List 开销分析

List 请求可以分为两种:

  • 1、List 全量数据:开销主要花在数据传输;

  • 2、指定用 label 或字段(field)过滤,只需要匹配的数据。

    大部分情况下,apiserver 会用自己的缓存做过滤,这个很快,因此耗时主要花在数据传输;

    需要将请求转给 etcd 的情况,

    前面已经提到,etcd 只是 KV 存储,并不理解 label/field 信息,因此它无法处理过滤请求。实际的过程是:apiserver 从 etcd 拉全量数据,然后在内存做过滤,再返回给客户端。

    因此除了数据传输开销(网络带宽),这种情况下还会占用大量apiserver CPU 和内存

大规模部署时潜在的问题

用 k8s client-go 根据 nodename 过滤 pod:

podList, err := Client().CoreV1().Pods("").List(ctx(), ListOptions{FieldSelector: "spec.nodeName=node1"})

来实际看一下它背后的数据量。以一个 4000 node,10w pod 的集群为例,全量 pod 数据量:

  • etcd 中:紧凑的非结构化 KV 存储,在 1GB 量级;
  • apiserver 缓存中:已经是结构化的 golang objects,在 2GB 量级
  • apiserver 返回:client 一般选择默认的 json 格式接收, 也已经是结构化数据。全量 pod 的 json 也在 2GB 量级。

可以看到,某些请求看起来很简单,只是客户端一行代码的事情,但背后的数据量是惊人的。指定按 nodeName 过滤 pod 可能只返回了 500KB 数据,但 apiserver 却需要过滤 2GB 数据 ------ 最坏的情况,etcd 也要跟着处理 1GB 数据 (以上参数配置确实命中了最坏情况,见下文代码分析)。

集群规模比较小的时候,这个问题可能看不出来(etcd 在 LIST 响应延迟超过某个阈值 后才开始打印 warning 日志);规模大了之后,如果这样的请求比较多,apiserver/etcd 肯定是扛不住的。

LIST example

为了避免客户端库(例如 client-go)自动帮我们设置一些参数,我们直接用 curl 来测试,指定证书就行了:

$ cat curl-k8s-apiserver.sh
curl -s --cert /etc/kubernetes/pki/admin.crt --key /etc/kubernetes/pki/admin.key --cacert /etc/kubernetes/pki/ca.crt $@
使用方式:

$ ./curl-k8s-apiserver.sh "https://localhost:6443/api/v1/pods?limit=2"
{
  "kind": "PodList",
  "metadata": {
    "resourceVersion": "2127852936",
    "continue": "eyJ2IjoibWV0YS5rOHMuaW8vdjEiLCJ...",
  },
  "items": [ {pod1 data }, {pod2 data}]
}

指定 limit=2:response 将返回分页信息(continue)

$ ./curl-k8s-apiserver.sh "https://localhost:6443/api/v1/pods?limit=2"
{
  "kind": "PodList",
  "metadata": {
    "resourceVersion": "2127852936",
    "continue": "eyJ2IjoibWV0YS5rOHMuaW8vdjEiLCJ...",
  },
  "items": [ {pod1 data }, {pod2 data}]
}

可以看到:

  • 确实返回了两个 pod 信息,在 items[] 字段中;
  • 另外在 metadata 中返回了一个 continue 字段,客户端下次带上这个参数,apiserver 将继续返回剩下的内容,直到 apiserver 不再返回 continue。

指定 limit=2&resourceVersion=0:limit=2 将被忽略,返回全量数据:

$ ./curl-k8s-apiserver.sh "https://localhost:6443/api/v1/pods?limit=2&resourceVersion=0"
{
  "kind": "PodList",
  "metadata": {
    "resourceVersion": "2127852936",
    "continue": "eyJ2IjoibWV0YS5rOHMuaW8vdjEiLCJ...",
  },
  "items": [ {pod1 data }, {pod2 data}, ...]
}

items[] 里面是全量 pod 信息。

指定 spec.nodeName=node1&resourceVersion=0 vs. spec.nodeName=node1

$ ./curl-k8s-apiserver.sh "https://localhost:6443/api/v1/namespaces/default/pods?fieldSelector=spec.nodeName%3Dnode1" | jq '.items[].spec.nodeName'
"node1"
"node1"
"node1"
...

$ ./curl-k8s-apiserver.sh "https://localhost:6443/api/v1/namespaces/default/pods?fieldSelector=spec.nodeName%3Dnode1&resourceVersion=0" | jq '.items[].spec.nodeName'
"node1"
"node1"
"node1"
...

速度差异很大,用 time 测量以上两种情况下的耗时,会发现对于大一些的集群,这两种请求的响应时间就会有明显差异。

对于 4K nodes, 100K pods 规模的集群,以下数据供参考:

  • 不带 resourceVersion=0(读 etcd 并在 apiserver 过滤): 耗时 10s
  • 带 resourceVersion=0(读 apiserver 缓存): 耗时 0.05s

部署和调优建议

1、List 请求默认设置 ResourceVersion=0

不设置这个参数将导致 apiserver 从 etcd 拉全量数据再过滤,导致很慢,规模大了 etcd 扛不住

因此,除非对数据准确性要求极高,必须从 etcd 拉数据,否则应该在 LIST 请求时设置 ResourceVersion=0 参数, 让 apiserver 用缓存提供服务。

如果你使用的是 client-go 的 ListWatch/informer 接口, 那它默认已经设置了 ResourceVersion=0。

2、优先使用 namespaced API

如果要 LIST 的资源在单个或少数几个 namespace,考虑使用 namespaced API:

Namespaced API: /api/v1/namespaces//pods?query=xxx

Un-namespaced API: /api/v1/pods?query=xxx

3、Restart backoff

对于 per-node 部署的基础服务,例如 kubelet、cilium-agent、daemonsets,需要 通过有效的 restart backoff 降低大面积重启时对控制平面的压力。

例如,同时挂掉后,每分钟重启的 agent 数量不超过集群规模的 10%(可配置,或可自动计算)。

4、优先通过 label/field selector 在服务端做过滤

如果需要缓存某些资源并监听变动,那需要使用 ListWatch 机制,将数据拉到本地,业务逻辑根据需要自己从 local cache 过滤。这是 client-go 的 ListWatch/informer 机制。

但如果只是一次性的 LIST 操作,并且有筛选条件,例如前面提到的根据 nodename 过滤 pod 的例子, 那显然应该通过设置 label 或字段过滤器,让 apiserver 帮我们把数据过滤出来。LIST 10w pods 需要几十秒(大部分时间花在数据传输上,同时也占用 apiserver 大量 CPU/BW/IO), 而如果只需要本机上的 pod,那设置 nodeName=node1 之后,LIST 可能只需要 0.05s 就能返回结果。

另外非常重要的一点时,不要忘记在请求中同时带上resourceVersion=0。

  • Label selector

    在 apiserver 内存过滤。

  • Field selector

    在 apiserver 内存过滤。

  • Namespace selector

    etcd 中 namespace 是前缀的一部分,因此能指定 namespace 过滤资源,速度比不是前缀的 selector 快很多。

5、配套基础设施(监控、告警等)

以上分析可以看成,client 的单个请求可能只返回几百 KB 的数据,但 apiserver(更糟糕的情况,etcd)需要处理上 GB 的数据。因此,应该极力避免基础服务的大规模重启,为此需要在监控、告警上做的尽量完善。

  • 使用独立 ServiceAccount

    每个基础服务(例如 kubelet、cilium-agent 等),以及对 apiserver 有大量 LIST 操作的各种 operator, 都使用各自独立的 SA, 这样便于 apiserver 区分请求来源,对监控、排障和服务端限流都非常有用。

  • Liveness 监控告警

    基础服务必须覆盖到 liveness 监控。

6、Get 请求:GetOptions{}

基本原理与 ListOption{} 一样,不设置 ResourceVersion=0 会导致 apiserver 去 etcd 拿数据,应该尽量避免。

相关推荐
胖毁青春,瘦解百病5 分钟前
Docker镜像源设置不生效问题排查
docker·容器
fnd_LN5 分钟前
Linux文件目录 --- mkdir命令,创建目录,多级目录,设置目录权限
linux·运维·服务器
CloudPilotAI7 分钟前
15条 Karpenter 最佳实践,轻松掌握弹性伸缩
kubernetes·弹性伸缩·karpenter
会飞的土拨鼠呀15 分钟前
Flannel是什么,如何安装Flannel
运维·云原生·kubernetes
达帮主23 分钟前
7.C语言 宏(Macro) 宏定义,宏函数
linux·c语言·算法
行思理34 分钟前
Linux 下SVN新手操作手册
linux·运维·svn
初学者丶一起加油1 小时前
C语言基础:指针(数组指针与指针数组)
linux·c语言·开发语言·数据结构·c++·算法·visual studio
ether-lin1 小时前
DevOps实战:用Kubernetes和Argo打造自动化CI/CD流程(1)
ci/cd·kubernetes·devops
一只搬砖的猹1 小时前
cJson系列——常用cJson库函数
linux·前端·javascript·python·物联网·mysql·json
IPdodo全球网络服务2 小时前
如何通过TikTok引流到私域流量池
运维·服务器·网络