k8s list請求優(yōu)化以及合理使用list以維護集群穩(wěn)定性
apiserver/etcd List 示例分析
-
1、LIST apis/cilium.io/v2/ciliumendpoints?limit=500&resourceVersion = 0
這里同時傳了兩個參數(shù),但 resourceVersion=0 會導(dǎo)致 apiserver 忽略 limit=500, 所以客戶端拿到的是全量 ciliumendpoints 數(shù)據(jù)
此時不會查etcd,因為有resourceVersion = 0,且resourceVersion = 0會忽略limit,因為limit一定要查etcd
一種資源的全量數(shù)據(jù)可能是比較大的,需要考慮清楚是否真的需要全量數(shù)據(jù) -
2、LIST api/v1/pods?filedSelector=spec.nodeName%3Dnode1
這個請求是獲取 node1 上的所有 pods(%3D 是 = 的轉(zhuǎn)義)。
根據(jù) nodename 做過濾,給人的感覺可能是數(shù)據(jù)量不太大,但其實背后要比看上去復(fù)雜:這種行為是要避免的,除非對數(shù)據(jù)準確性有極高要求,特意要繞過 apiserver 緩存。
首先,這里沒有指定 resourceVersion=0,導(dǎo)致 apiserver 跳過緩存,直接去etcd讀數(shù)據(jù);
其次,etcd 只是 KV 存儲,沒有按 label/field 過濾功能(只處理 limit/continue),
所以,apiserver 是從 etcd 拉全量數(shù)據(jù),然后在內(nèi)存做過濾,開銷也是很大的,后文有代碼分析。 -
3、LISTapi/v1/pods?filedSelector=spec.nodeName%3Dnode1&resourceVersion = 0
跟 2 的區(qū)別是加上了 resourceVersion=0,因此 apiserver 會從緩存讀數(shù)據(jù),性能會有量級的提升。
但要注意,雖然實際上返回給客戶端的可能只有幾百 KB 到上百 MB(取決于 node 上 pod 的數(shù)量、pod 上 label 的多少等因素), 但 apiserver 需要處理的數(shù)據(jù)量可能是幾個 GB。后面會有定量分析。
以上可以看到,不同的 LIST 操作產(chǎn)生的影響是不一樣的,而客戶端看到數(shù)據(jù)還有可能只 是 apiserver/etcd 處理數(shù)據(jù)的很小一部分。如果基礎(chǔ)服務(wù)大規(guī)模啟動或重啟, 就極有可能把控制平面打爆。
判斷是否必須從 etcd 讀數(shù)據(jù):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)拉去數(shù)據(jù);
// 2. 如果有 continuation,也從底層存儲拉數(shù)據(jù);
// 3. 只有 resourceVersion != "0" 時,才會將 limit 傳給底層存儲(etcd),因為 watch cache 不支持 continuation
return resourceVersion == "" || hasContinuation || hasLimit || opts.ResourceVersionMatch == metav1.ResourceVersionMatchExact
}
-
問:客戶端未設(shè)置 ListOption{} 中的 ResourceVersion 字段,是否對應(yīng)到這里的 resourceVersion == “”?
答:是的,所以第一節(jié)的 例子 會導(dǎo)致從 etcd 拉全量數(shù)據(jù)。
-
問:客戶端設(shè)置了 limit=500&resourceVersion=0 是否會導(dǎo)致下次 hasContinuation==true?
答:不會,resourceVersion=0 將導(dǎo)致 limit 被忽略(hasLimit 那一行代碼),也就是說, 雖然指定了 limit=500,但這個請求會返回全量數(shù)據(jù)。
-
問:還有其他情況需要去etcd拉取嗎?
答:apiserver cache未建立完成的時候
apiserver/etcd List 開銷分析
List 請求可以分為兩種:
-
1、List 全量數(shù)據(jù):開銷主要花在數(shù)據(jù)傳輸;
-
2、指定用 label 或字段(field)過濾,只需要匹配的數(shù)據(jù)。
大部分情況下,apiserver 會用自己的緩存做過濾,這個很快,因此耗時主要花在數(shù)據(jù)傳輸;
需要將請求轉(zhuǎn)給 etcd 的情況,
前面已經(jīng)提到,etcd 只是 KV 存儲,并不理解 label/field 信息,因此它無法處理過濾請求。實際的過程是:apiserver 從 etcd 拉全量數(shù)據(jù),然后在內(nèi)存做過濾,再返回給客戶端。
因此除了數(shù)據(jù)傳輸開銷(網(wǎng)絡(luò)帶寬),這種情況下還會占用大量apiserver CPU 和內(nèi)存
大規(guī)模部署時潛在的問題
用 k8s client-go 根據(jù) nodename 過濾 pod:
podList, err := Client().CoreV1().Pods(“”).List(ctx(), ListOptions{FieldSelector: “spec.nodeName=node1”})
來實際看一下它背后的數(shù)據(jù)量。以一個 4000 node,10w pod 的集群為例,全量 pod 數(shù)據(jù)量:
- etcd 中:緊湊的非結(jié)構(gòu)化 KV 存儲,在 1GB 量級;
- apiserver 緩存中:已經(jīng)是結(jié)構(gòu)化的 golang objects,在 2GB 量級
- apiserver 返回:client 一般選擇默認的 json 格式接收, 也已經(jīng)是結(jié)構(gòu)化數(shù)據(jù)。全量 pod 的 json 也在 2GB 量級。
可以看到,某些請求看起來很簡單,只是客戶端一行代碼的事情,但背后的數(shù)據(jù)量是驚人的。指定按 nodeName 過濾 pod 可能只返回了 500KB 數(shù)據(jù),但 apiserver 卻需要過濾 2GB 數(shù)據(jù) —— 最壞的情況,etcd 也要跟著處理 1GB 數(shù)據(jù) (以上參數(shù)配置確實命中了最壞情況,見下文代碼分析)。
集群規(guī)模比較小的時候,這個問題可能看不出來(etcd 在 LIST 響應(yīng)延遲超過某個閾值 后才開始打印 warning 日志);規(guī)模大了之后,如果這樣的請求比較多,apiserver/etcd 肯定是扛不住的。
LIST example
為了避免客戶端庫(例如 client-go)自動幫我們設(shè)置一些參數(shù),我們直接用 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 字段,客戶端下次帶上這個參數(shù),apiserver 將繼續(xù)返回剩下的內(nèi)容,直到 apiserver 不再返回 continue。
指定 limit=2&resourceVersion=0:limit=2 將被忽略,返回全量數(shù)據(jù):
$ ./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 測量以上兩種情況下的耗時,會發(fā)現(xiàn)對于大一些的集群,這兩種請求的響應(yīng)時間就會有明顯差異。
對于 4K nodes, 100K pods 規(guī)模的集群,以下數(shù)據(jù)供參考:
- 不帶 resourceVersion=0(讀 etcd 并在 apiserver 過濾): 耗時 10s
- 帶 resourceVersion=0(讀 apiserver 緩存): 耗時 0.05s
部署和調(diào)優(yōu)建議
1、List 請求默認設(shè)置 ResourceVersion=0
不設(shè)置這個參數(shù)將導(dǎo)致 apiserver 從 etcd 拉全量數(shù)據(jù)再過濾,導(dǎo)致很慢,規(guī)模大了 etcd 扛不住
因此,除非對數(shù)據(jù)準確性要求極高,必須從 etcd 拉數(shù)據(jù),否則應(yīng)該在 LIST 請求時設(shè)置 ResourceVersion=0 參數(shù), 讓 apiserver 用緩存提供服務(wù)。
如果你使用的是 client-go 的 ListWatch/informer 接口, 那它默認已經(jīng)設(shè)置了 ResourceVersion=0。
2、優(yōu)先使用 namespaced API
如果要 LIST 的資源在單個或少數(shù)幾個 namespace,考慮使用 namespaced API:
Namespaced API: /api/v1/namespaces//pods?query=xxx
Un-namespaced API: /api/v1/pods?query=xxx
3、Restart backoff
對于 per-node 部署的基礎(chǔ)服務(wù),例如 kubelet、cilium-agent、daemonsets,需要 通過有效的 restart backoff 降低大面積重啟時對控制平面的壓力。
例如,同時掛掉后,每分鐘重啟的 agent 數(shù)量不超過集群規(guī)模的 10%(可配置,或可自動計算)。
4、優(yōu)先通過 label/field selector 在服務(wù)端做過濾
如果需要緩存某些資源并監(jiān)聽變動,那需要使用 ListWatch 機制,將數(shù)據(jù)拉到本地,業(yè)務(wù)邏輯根據(jù)需要自己從 local cache 過濾。這是 client-go 的 ListWatch/informer 機制。
但如果只是一次性的 LIST 操作,并且有篩選條件,例如前面提到的根據(jù) nodename 過濾 pod 的例子, 那顯然應(yīng)該通過設(shè)置 label 或字段過濾器,讓 apiserver 幫我們把數(shù)據(jù)過濾出來。LIST 10w pods 需要幾十秒(大部分時間花在數(shù)據(jù)傳輸上,同時也占用 apiserver 大量 CPU/BW/IO), 而如果只需要本機上的 pod,那設(shè)置 nodeName=node1 之后,LIST 可能只需要 0.05s 就能返回結(jié)果。
另外非常重要的一點時,不要忘記在請求中同時帶上resourceVersion=0。
-
Label selector
在 apiserver 內(nèi)存過濾。 -
Field selector
在 apiserver 內(nèi)存過濾。 -
Namespace selector
etcd 中 namespace 是前綴的一部分,因此能指定 namespace 過濾資源,速度比不是前綴的 selector 快很多。
5、配套基礎(chǔ)設(shè)施(監(jiān)控、告警等)
以上分析可以看成,client 的單個請求可能只返回幾百 KB 的數(shù)據(jù),但 apiserver(更糟糕的情況,etcd)需要處理上 GB 的數(shù)據(jù)。因此,應(yīng)該極力避免基礎(chǔ)服務(wù)的大規(guī)模重啟,為此需要在監(jiān)控、告警上做的盡量完善。
-
使用獨立 ServiceAccount
每個基礎(chǔ)服務(wù)(例如 kubelet、cilium-agent 等),以及對 apiserver 有大量 LIST 操作的各種 operator, 都使用各自獨立的 SA, 這樣便于 apiserver 區(qū)分請求來源,對監(jiān)控、排障和服務(wù)端限流都非常有用。 -
Liveness 監(jiān)控告警
基礎(chǔ)服務(wù)必須覆蓋到 liveness 監(jiān)控。
6、Get 請求:GetOptions{}文章來源:http://www.zghlxwxcb.cn/news/detail-609632.html
基本原理與 ListOption{} 一樣,不設(shè)置 ResourceVersion=0 會導(dǎo)致 apiserver 去 etcd 拿數(shù)據(jù),應(yīng)該盡量避免。文章來源地址http://www.zghlxwxcb.cn/news/detail-609632.html
到了這里,關(guān)于【博客683】k8s list請求優(yōu)化以及合理使用list以維護集群穩(wěn)定性的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!