Kubernetes apiserver/etcd 的 LIST 性能调优
Kubernetes LIST
操作通常都是非常重量级的,不仅占用大量的 磁盘 IO
、网络带宽
和 CPU
,而且会影响同时间段的其他请求(尤其是响应延迟要求极高的 选主
请求),是集群稳定性的一大杀手。
例如,一个 etcd
集群存储的数据量可能很小(几个 ~ 几十个 GB
),甚至足够缓存到内存中。一个 ~4000 nodes
的 k8s
集群的 etcd
。单个LIST
请求可能只需要 返回几十 MB 到上 GB
的流量,但并发请求一多,etcd 显然也扛不住,所以最好在前面有 一层缓存
,这就是 apiserver
的功能(之一)。K8s
的 LIST
请求大部分都应该被 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 数据量:
- etcd 中:紧凑的非结构化 KV 存储,在
1GB
量级; - apiserver 缓存中:已经是结构化的 golang objects,在
2GB
量级( TODO:需进一步确认); - apiserver 返回:client 一般选择默认的 json 格式接收, 也已经是结构化数据。全量 pod 的 json 也在
2GB
量级。
可以看到,某些请求看起来很简单,只是客户端一行代码的事情,但背后的数据量是惊人的。 指定按 nodeName
过滤 pod
可能只返回了 500KB
数据,但 apiserver
却需要过滤 2GB
数据 —— 最坏的情况,etcd 也要跟着处理 1GB
数据 (以上参数配置确实命中了最坏情况,见下文代码分析)。
集群规模比较小的时候,这个问题可能看不出来(etcd
在 LIST
响应延迟超过某个阈值 后才开始打印 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
「如果这篇文章对你有用,请随意打赏」
如果这篇文章对你有用,请随意打赏
使用微信扫描二维码完成支付