新聞中心
kube-apiserver 又 OOM 了?
作者:Kaku 2023-10-30 22:23:12
云計(jì)算
云原生 我們經(jīng)常看到各種源碼分析,原理解析的文章,容易輕信其內(nèi)容,但隨著版本迭代,以及一些細(xì)節(jié)的處理,可能會(huì)導(dǎo)致我們理解不到位,或者并不能真正的掌握。

網(wǎng)站建設(shè)哪家好,找創(chuàng)新互聯(lián)!專注于網(wǎng)頁(yè)設(shè)計(jì)、網(wǎng)站建設(shè)、微信開發(fā)、成都小程序開發(fā)、集團(tuán)企業(yè)網(wǎng)站建設(shè)等服務(wù)項(xiàng)目。為回饋新老客戶創(chuàng)新互聯(lián)還提供了霸州免費(fèi)建站歡迎大家使用!
由來(lái)
前一篇已經(jīng)介紹了 Informer 的實(shí)現(xiàn),Informer 對(duì) kube-apiserver 發(fā)起了 list 和 watch 請(qǐng)求。我們知道大規(guī)模集群下,kube-apiserver 會(huì)成為瓶頸,尤其在內(nèi)存方面,相信很多人也遇到過 kube-apiserver OOM 等問題(碰巧的是最近線上連續(xù)出現(xiàn)兩次 kube-apiserver OOM 的問題)。本篇主要講 kube-apiserver 中 Informer 需要用到的兩個(gè)接口 list 和 watch 的實(shí)現(xiàn)。
網(wǎng)上搜索的話,可以找到大量相關(guān)的源碼解析的文章,這里我并不會(huì)去過多涉及代碼,主要還是以講原理、流程為主,最后簡(jiǎn)單介紹下當(dāng)前存在的問題,理論實(shí)踐相結(jié)合。本篇主要講當(dāng)前實(shí)現(xiàn),只有了解了當(dāng)前實(shí)現(xiàn),明白了為什么會(huì)有問題,才知道如何去解決問題,接下來(lái)的一篇會(huì)詳細(xì)分析如何解決這些問題。
原理
Cacher 加載
圖片
核心組件:Cacher,watchCache,cacheWatcher,reflector。其中 watchCache 作為 reflector 的 store,Etcd 作為 listerWatcher 的 storage,store 和 listerWatcher 作為參數(shù)用來(lái)構(gòu)造 reflector。數(shù)據(jù)流大致如下:
- kube-apiserver 啟動(dòng),針對(duì)每種資源類型,調(diào)用其對(duì)應(yīng) cacher 的 startCaching,進(jìn)而調(diào)用 reflector.ListAndWatch,觸發(fā) listerWatcher 的 list 和 watch,對(duì)應(yīng) Etcd list 之后再 watch,watch 時(shí)會(huì)創(chuàng)建 watchChan,從 Etcd 讀到的結(jié)果會(huì)先進(jìn)入到 watchChan 的 incomingEventChan 中,經(jīng)過 transform 處理后發(fā)送到 watchChan 的 resultChan 中,供 reflector 消費(fèi);
- reflector 會(huì)消費(fèi)上述 resultChan 的數(shù)據(jù),即 watch.Event 對(duì)象,并根據(jù)事件類型調(diào)用 store 的增刪改方法,此處 store 即 watchCache,經(jīng)過 watchCache.processEvent 處理,組裝 watchCacheEvent 對(duì)象,更新 watchCache 的 cache(大小自適應(yīng)的喚醒緩沖區(qū),保留歷史 event)和 store(全量數(shù)據(jù)),并最終通過 eventHandler 將其發(fā)送到 cacher 的 incoming chan 中;
- cacher.dispatchEvents 消費(fèi) incoming chan 的數(shù)據(jù),經(jīng)過處理后發(fā)送給每個(gè) cacheWatcher 的 input chan;
- 外部調(diào)用 kube-apiserver watch 請(qǐng)求后會(huì)創(chuàng)建一個(gè)對(duì)應(yīng)的 cacheWachter 對(duì)象,最終到 cacheWatcher 的 Watch 處理機(jī)中,消費(fèi) input chan,調(diào)用 watchCacheEvent 進(jìn)行事件分發(fā);
圖片
Cacher 數(shù)據(jù)流
用來(lái)緩存數(shù)據(jù)的核心結(jié)構(gòu)是 watchCache,其內(nèi)部又兩個(gè)關(guān)鍵結(jié)構(gòu):cache(cyclic buffer),store(thread safe store),分別用來(lái)存儲(chǔ)歷史的 watchCacheEvent 和真實(shí)的資源對(duì)象,其中 store 里面存儲(chǔ)的是全量對(duì)象,而 cache 雖然是自適應(yīng)大小的,但還是有最大容量限制的,所以他存儲(chǔ)的 watchCacheEvent 所代表的對(duì)象集合并不一定能覆蓋 store 的全部數(shù)據(jù)。
歷史問題
kube-apiserver 在優(yōu)化自身內(nèi)存使用方面做了很多優(yōu)化了,不過至今仍然存在一些尚未完全解決的問題。
kube-apiserver OOM
內(nèi)存消耗來(lái)源
kube-apiserver 的內(nèi)存消耗,主要兩個(gè)來(lái)源:
- 一部分來(lái)自于他緩存了集群所有數(shù)據(jù)(Event 除外,此 Event 為 k8s 的資源類型),并且為每種資源緩存了歷史 watchCacheEvent,以及一些內(nèi)部的數(shù)據(jù)結(jié)構(gòu)和 chan 等,這部分是不可避免的,雖然可以適當(dāng)優(yōu)化,但作用并不大;
- 另一部分來(lái)自于客戶端請(qǐng)求,尤其是 list 請(qǐng)求,kube-apiserver 需要在內(nèi)存中進(jìn)行數(shù)據(jù)深拷貝,序列化等操作,所需內(nèi)存量和數(shù)據(jù)量、請(qǐng)求量正相關(guān),隨著數(shù)據(jù)量的增加,請(qǐng)求量的增加,所需要的內(nèi)存也越大,而且這部分的內(nèi)存通過 golang GC 是沒有辦法完全回收的,而 list 請(qǐng)求的主要來(lái)源就是 Informer;
list 請(qǐng)求占用內(nèi)存多的原因如下:
- 默認(rèn)情況下(沒有指定 resourceversion 的情況下),直接從etcd獲取數(shù)據(jù)可能需要大量?jī)?nèi)存,超過數(shù)據(jù)存儲(chǔ)的完整響應(yīng)大小數(shù)倍;
- 請(qǐng)求明確指定 ResourceVersion 參數(shù)來(lái)從緩存中獲取數(shù)據(jù)(例如,ResourceVersinotallow="0"),這實(shí)際上是大多數(shù)基于 client-go 的控制器因性能原因而使用的方法。內(nèi)存使用量將比第一種情況低得多。但這并不是完美的,因?yàn)槲覀內(nèi)匀恍枰臻g來(lái)存儲(chǔ)序列化對(duì)象并保存完整響應(yīng),直到發(fā)送。
常見場(chǎng)景
有兩個(gè)常見的容易引起 kube-apiserver OOM 的場(chǎng)景:
- 一些 DaemonSet 之類的程序里面用到了 Informer,在進(jìn)行變更,或者故障重啟的時(shí)候,隨著集群規(guī)模的增加,請(qǐng)求量也隨之增加,對(duì) kube-apiserver 內(nèi)存的壓力也增加,在沒有任何防護(hù)措施(限流)的情況下,很容易造成 kube-apiserver 的 OOM,而且在 OOM 之后,異常連接轉(zhuǎn)移到其他 master 節(jié)點(diǎn),引起雪崩。理論上也屬于一種容量問題,應(yīng)對(duì)措施擴(kuò)容(加 master 節(jié)點(diǎn))、限流(服務(wù)端限流:APF、MaxInflightRequest等,及客戶端限流)。
- 某種類型資源的數(shù)據(jù)量很大,kube-apiserver 配置的 timeout 參數(shù)太小,不足以支持完成一次 list 請(qǐng)求的情況下,Informer 會(huì)一直不斷地嘗試進(jìn)行 list 操作,這種情況多發(fā)生在控制面組件,因?yàn)樗麄兺枰@取全量數(shù)據(jù)。
too old resource version
原理
嚴(yán)格說,這并不能算是一個(gè)問題,機(jī)制如此,理論上單機(jī)資源無(wú)限的情況下是可以避免這個(gè)現(xiàn)象的。為了方便描述,用 RV 代指 resourceversion。
其本質(zhì)是客戶端在調(diào)用 watch api 時(shí)攜帶非 0 的 RV,服務(wù)端在走到 cacher 的 watch 實(shí)現(xiàn)邏輯時(shí)需要根據(jù)傳入的 RV 去 cyclic buffer 中二分查找大于 RV 的所有 watchCacheEvent 作為初始 event 返回給客戶端。當(dāng) cyclic buffer 的最小 RV 還要比傳入的 RV 大時(shí),也就是說服務(wù)端緩存的事件的最小 RV 都要比客戶端傳過來(lái)的大,意味著緩存的歷史事件不全,可能是因?yàn)槭录^多,緩存大小有限,較老的 watchCacheEvent 已經(jīng)被覆蓋了。
常見場(chǎng)景
- 這種情況多發(fā)生在 kubelet 連接 apiserver 的場(chǎng)景下,或者說 watch 帶了 labelselector 或 fieldselector 的情況下。因?yàn)槊總€(gè) kubelet 只關(guān)心自己節(jié)點(diǎn)的 Pod,如果自身節(jié)點(diǎn) Pod 一直沒有變化,而其他節(jié)點(diǎn)上的 Pod 變化頻繁,則可能 kubelet 本地 Informer 記錄的 last RV 就會(huì)比 cyclic buffer 中的最小的 RV 還要小,這時(shí)如果發(fā)生重連(網(wǎng)絡(luò)閃斷,或者 Informer 自身 timeout 重連),則可以在 kube-apiserver 的日志中看到 "too old resoure version" 的字樣。
- kube-apiserver 重啟的場(chǎng)景,如果集群中部分類型資源變更頻繁,部分變更不頻繁,則對(duì)于去 watch 變更不頻繁的資源類型的 Informer 來(lái)說起本地的 last RV 是要比最新的 RV 小甚至小很多的,在 kube-apiserver 發(fā)生重啟時(shí),他以本地這個(gè)很小的 RV 去 watch,還是有可能會(huì)觸發(fā)這個(gè)問題;
客戶端 Informer 遇到這個(gè)報(bào)錯(cuò)的話會(huì)退出 ListAndWatch,重新開始執(zhí)行 LIstAndWatch,進(jìn)而造成 kube-apiserver 內(nèi)存增加甚至 OOM。問題本質(zhì)原因:RV 是全局的。場(chǎng)景的景本質(zhì)區(qū)別在于場(chǎng)景 1 是在一種資源中做了篩選導(dǎo)致的,場(chǎng)景 2 是多種資源類型之間的 RV 差異較大導(dǎo)致的。
優(yōu)化
經(jīng)過上述分析,造成這個(gè)問題的原因有兩個(gè):
- cyclic buffer 長(zhǎng)度有限;
- 客戶端 Informer 持有的 last RV 過于陳舊;
社區(qū)也已經(jīng)在多個(gè)版本之前進(jìn)行了優(yōu)化來(lái)降低這個(gè)問題出現(xiàn)的概率。
針對(duì)問題一,采用了自適應(yīng)窗口大小,雖然還是會(huì)有問題,但相比之前寫死一個(gè)值出現(xiàn)問題的概率要小,同時(shí)在不必要的時(shí)候縮小長(zhǎng)度,避免內(nèi)存資源的浪費(fèi)。
針對(duì)問題二,有兩個(gè)優(yōu)化,引入了 BOOKMARK 機(jī)制來(lái)優(yōu)化同一種資源不同篩選條件導(dǎo)致的問題,BOOKMARK 是一種 event 類型,定期將最新的 RV 返回客戶端;引入 ProgressNotify 解決多種資源類型 RV 差異較大,在 kube-apiserver 重啟后,Informer resume 時(shí)導(dǎo)致的問題,本質(zhì)是利用了 Etcd 的 clientv3 ProgressNotify 的機(jī)制,kube-apiserver 在 Watch Etcd 的時(shí)候攜帶了特定的 Options 開啟此功能。ProgressNotify 參考 Etcd 官方文檔[1]:
WithProgressNotify makes watch server send periodic progress updates every 10 minutes when there is no incoming events. Progress updates have zero events in WatchResponse.
詳情可以參考如下 KEP 956-watch-bookmark[2] 和 1904-efficient-watch-resumption[3]
stale read
這更是一個(gè)歷史悠久的問題了,自從有了 watchCache 之后就有了這個(gè)問題,本質(zhì)是將之前直接訪問 Etcd 時(shí)的線性一致性讀(Etcd 提供的能力),降級(jí)成了讀 kube-apiserver cache 的順序一致性。
場(chǎng)景
- T1: StatefulSet controller creates pod-0 (uid 1) which is scheduled to node-1
- T2: pod-0 is deleted as part of a rolling upgrade
- node-1 sees that pod-0 is deleted and cleans it up, then deletes the pod in the api
- The StatefulSet controller creates a second pod pod-0 (uid 2) which is assigned to node-2
- node-2 sees that pod-0 has been scheduled to it and starts pod-0
- The kubelet on node-1 crashes and restarts, then performs an initial list of pods scheduled to it against an API server in an HA setup (more than one API server) that is partitioned from the master (watch cache is arbitrarily delayed). The watch cache returns a list of pods from before T2
- node-1 fills its local cache with a list of pods from before T2
- node-1 starts pod-0 (uid 1) and node-2 is already running pod-0 (uid 2).
詳情可以參考 issue 59848[4]。
思考
我們經(jīng)常看到各種源碼分析,原理解析的文章,容易輕信其內(nèi)容,但隨著版本迭代,以及一些細(xì)節(jié)的處理,可能會(huì)導(dǎo)致我們理解不到位,或者并不能真正的掌握。例如是否在 list 請(qǐng)求時(shí)傳 RV=0 就一定會(huì)走 kube-apiserver 的緩存?網(wǎng)上搜的話,應(yīng)該都是說會(huì)走,但看代碼你會(huì)發(fā)現(xiàn)并不是這樣,例如當(dāng) kube-apiserver 重啟后數(shù)據(jù)還沒有完全加載好的時(shí)候,遇到 list 帶了 RV=0 的請(qǐng)求會(huì)直接去訪問 Etcd 獲取數(shù)據(jù)??此撇黄鹧鄣募?xì)節(jié),可能會(huì)影響我們處理問題的思路,比如 Etcd 負(fù)載較高要查原因,如果你知道這個(gè)細(xì)節(jié)的話,就會(huì)有意識(shí)的去看所有的 list 請(qǐng)求,而不只是那些 RV != 0 的請(qǐng)求。
最后留一個(gè)思考,kube-apiserver 的內(nèi)存壓力主要來(lái)自 list 請(qǐng)求,那么我們是否可以不使用 list 請(qǐng)求而是使用一種流式處理來(lái)實(shí)現(xiàn) list 的功能呢?這樣是不是就可以把內(nèi)存消耗限制在一個(gè)常數(shù)的空間復(fù)雜度范圍內(nèi)了?下一篇將會(huì)專門分析使用流式 api 解決 list 導(dǎo)致的內(nèi)存暴漲的問題,敬請(qǐng)期待~
參考資料
[1]Etcd: https://pkg.go.dev/github.com/coreos/etcd/clientv3#WithProgressNotify
[2]bookmark: https://github.com/kubernetes/enhancements/blob/master/keps/sig-api-machinery/956-watch-bookmark/README.md
[3]watch-resumption: https://github.com/kubernetes/enhancements/blob/c63ac8e05de716370d1e03e298d226dd12ffc3c2/keps/sig-api-machinery/1904-efficient-watch-resumption/README.md
[4]stale read: https://github.com/kubernetes/kubernetes/issues/59848
分享名稱:Kube-apiserver又OOM了?
轉(zhuǎn)載源于:http://www.dlmjj.cn/article/cdgpoic.html


咨詢
建站咨詢
