性能调优之 Kubernetes apiserver/etcd 的 LIST 性能调优

Kubernetes apiserver/etcd 的 LIST 性能调优

Posted by 董江 on Wednesday, January 4, 2023

Kubernetes apiserver/etcd 的 LIST 性能调优

Kubernetes LIST 操作通常都是非常重量级的,不仅占用大量的 磁盘 IO网络带宽CPU,而且会影响同时间段的其他请求(尤其是响应延迟要求极高的 选主请求),是集群稳定性的一大杀手。

例如,一个 etcd 集群存储的数据量可能很小(几个 ~ 几十个 GB),甚至足够缓存到内存中。一个 ~4000 nodesk8s 集群的 etcd单个LIST 请求可能只需要 返回几十 MB 到上 GB 的流量,但并发请求一多,etcd 显然也扛不住,所以最好在前面有 一层缓存,这就是 apiserver的功能(之一)。K8sLIST 请求大部分都应该被 apiserver 挡住,从它的本地缓存提供服务,但如果使用不当,就会跳过缓存直接到达 etcd,有很大的稳定性风险。

kube-apiserver LIST 请求处理逻辑:

大规模部署时潜在的问题

再来看个例子,下面这行代码用 k8s client-go 根据 nodename 过滤 pod:

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

看起来非常简单的操作,我们来实际看一下它背后的数据量。 以一个 4000 node,10w pod 的集群为例,全量 pod 数据量:

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

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

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

apiserver List() 验证

为了避免客户端库(例如 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}]
}

kubectl 测试

调大 kubectl 的日志级别,也可以看到它背后用了 分批请求 continue 来获取全量 pods

$ kubectl get pods --all-namespaces --v=10
# 以下都是 log 输出,做了适当调整
# curl -k -v -XGET  -H "User-Agent: kubectl/v1.xx" -H "Accept: application/json;as=Table;v=v1;g=meta.k8s.io,application/json;as=Table;v=v1beta1;g=meta.k8s.io,application/json"
#   'http://localhost:8080/api/v1/pods?limit=500'
# GET http://localhost:8080/api/v1/pods?limit=500 200 OK in 202 milliseconds
# Response Body: {"kind":"Table","metadata":{"continue":"eyJ2Ijoib...","remainingItemCount":54},"columnDefinitions":[...],"rows":[...]}
# 
# curl -k -v -XGET  -H "Accept: application/json;as=Table;v=v1;g=meta.k8s.io,application/json;as=Table;v=v1beta1;g=meta.k8s.io,application/json" -H "User-Agent: kubectl/v1.xx"
#   'http://localhost:8080/api/v1/pods?continue=eyJ2Ijoib&limit=500'
# GET http://localhost:8080/api/v1/pods?continue=eyJ2Ijoib&limit=500 200 OK in 44 milliseconds
# Response Body: {"kind":"Table","metadata":{"resourceVersion":"2122644698"},"columnDefinitions":[],"rows":[...]}

指定 resourceVersion=0

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

$ time ./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"
...

对于 4K nodes, 100K pods 规模的集群,以下数据供参考: 1.不带 resourceVersion=0(读 etcd 并在 apiserver 过滤): 耗时 10s 2.带 resourceVersion=0(读 apiserver 缓存): 耗时 0.05s 差了 200 倍

全量 pod 的总大小按 2GB 计算,平均每个 20KB

「如果这篇文章对你有用,请随意打赏」

Kubeservice博客

如果这篇文章对你有用,请随意打赏

使用微信扫描二维码完成支付