kubernetes中node心跳處理邏輯分析
最近在查看一個kubernetes集群中node not ready的奇怪現(xiàn)象,順便閱讀了一下kubernetes kube-controller-manager中管理node健康狀態(tài)的組件node lifecycle controller。我們知道kubernetes是典型的master-slave架構(gòu),master node負(fù)責(zé)整個集群元數(shù)據(jù)的管理,然后將具體的啟動執(zhí)行pod的任務(wù)分發(fā)給各個salve node執(zhí)行,各個salve node會定期與master通過心跳信息來告知自己的存活狀態(tài)。其中slave node上負(fù)責(zé)心跳的是kubelet程序, 他會定期更新apiserver中node lease或者node status數(shù)據(jù),然后kube-controller-manager會監(jiān)聽這些信息變化,如果一個node很長時間都沒有進(jìn)行狀態(tài)更新,那么我們就可以認(rèn)為該node發(fā)生了異常,需要進(jìn)行一些容錯處理,將該node上面的pod進(jìn)行安全的驅(qū)逐,使這些pod到其他node上面進(jìn)行重建。這部分工作是由node lifecycel controller模塊負(fù)責(zé)。
在目前的版本(v1.16)中,默認(rèn)開啟了TaintBasedEvictions, TaintNodesByCondition這兩個feature gate,則所有node生命周期管理都是通過condition + taint的方式進(jìn)行管理。其主要邏輯由三部分組成:
- 不斷地檢查所有node狀態(tài),設(shè)置對應(yīng)的condition
- 不斷地根據(jù)node condition 設(shè)置對應(yīng)的taint
- 不斷地根據(jù)taint驅(qū)逐node上面的pod
一. 檢查node狀態(tài)
檢查node狀態(tài)其實就是循環(huán)調(diào)用monitorNodeHealth函數(shù),該函數(shù)首先調(diào)用tryUpdateNodeHealth檢查每個node是否還有心跳,然后判斷如果沒有心跳則設(shè)置對應(yīng)的condtion。
node lifecycle controller內(nèi)部會維護(hù)一個nodeHealthMap 數(shù)據(jù)結(jié)構(gòu)來保存所有node的心跳信息,每次心跳之后都會更新這個結(jié)構(gòu)體,其中最重要的信息就是每個node上次心跳時間probeTimestamp, 如果該timestamp很長時間都沒有更新(超過--node-monitor-grace-period參數(shù)指定的值),則認(rèn)為該node可能已經(jīng)掛了,設(shè)置node的所有condition為unknown狀態(tài)。
gracePeriod, observedReadyCondition, currentReadyCondition, err = nc.tryUpdateNodeHealth(node)
tryUpdateNodeHealth傳入的參數(shù)為每個要檢查的node, 返回值中observedReadyCondition為當(dāng)前從apiserver中獲取到的數(shù)據(jù),也就是kubelet上報上來的最新的node信息, currentReadyCondition為修正過的數(shù)據(jù)。舉個例子,如果node很長時間沒有心跳的話,observedReadyCondition中nodeReadyCondion為true, 但是currentReadyCondion中所有的conditon已經(jīng)被修正的實際狀態(tài)unknown了。
如果observedReadyCondition 狀態(tài)為true, 而currentReadyCondition狀態(tài)不為true, 則說明node狀態(tài)狀態(tài)發(fā)生變化,由ready變?yōu)閚ot-ready。此時不光會更新node condition,還會將該node上所有的pod狀態(tài)設(shè)置為not ready,這樣的話,如果有對應(yīng)的service資源選中該pod, 流量就可以從service上摘除了,但是此時并不會直接刪除pod。
node lifecycle controller會根據(jù)currentReadyCondition的狀態(tài)將該node加入到zoneNoExecuteTainter的隊列中,等待后面設(shè)置taint。如果此時已經(jīng)有了taint的話則會直接更新。zoneNoExecuteTainter隊列的出隊速度是根據(jù)node所處zone狀態(tài)決定的,主要是為了防止出現(xiàn)集群級別的故障時,node lifecycle controller進(jìn)行誤判,例如交換機(jī),loadbalancer等故障時,防止node lifecycle controller錯誤地認(rèn)為所有node都不健康而大規(guī)模的設(shè)置taint進(jìn)而導(dǎo)致錯誤地驅(qū)逐很多pod,造成更大的故障。
設(shè)置出隊速率由handleDisruption函數(shù)中來處理,首先會選擇出來各個zone中不健康的node, 并確定當(dāng)前zone所處的狀態(tài)。分為以下幾種情況:
- Initial: zone剛加入到集群中,初始化完成。
- Normal: zone處于正常狀態(tài)
- FullDisruption: 該zone中所有的node都notReady了
- PartialDisruption: 該zone中部分node notReady,此時已經(jīng)超過了unhealthyZoneThreshold設(shè)置的閾值
對于上述不同狀態(tài)所設(shè)置不同的rate limiter, 從而決定出隊速度。該速率由函數(shù)setLimiterInZone決定具體數(shù)值, 具體規(guī)則是:
- 當(dāng)所有zone都處于
FullDisruption時,此時limiter為0 - 當(dāng)只有部分zone處于
FullDisruption時,此時limiter為正常速率:--node-eviction-rate - 如果某個zone處于
PartialDisruption時,則此時limiter為二級速率:--secondary-node-eviction-rate
二. 設(shè)置node taint
根據(jù)node condition設(shè)置taint主要由兩個循環(huán)來負(fù)責(zé), 這兩個循環(huán)在程序啟動后會不斷執(zhí)行:
doNodeProcessingPassWorker中主要的邏輯就是:doNoScheduleTaintingPass, 該函數(shù)會根據(jù)node當(dāng)前的condition設(shè)置unschedulable的taint,便于調(diào)度器根據(jù)該值進(jìn)行調(diào)度決策,不再調(diào)度新pod至該node。doNoExecuteTaintingPass會不斷地從上面提到的zoneNoExecuteTainter隊列中獲取元素進(jìn)行處理,根據(jù)node condition設(shè)置對應(yīng)的NotReady或Unreachable的taint, 如果NodeReadycondition為false則taint為NotReady, 如果為unknown,則taint為Unreachable, 這兩種狀態(tài)只能同時存在一種!
上面提到從zoneNoExecuteTainter隊列中出隊時是有一定的速率限制,防止大規(guī)模快速驅(qū)逐pod。該元素是由RateLimitedTimedQueue數(shù)據(jù)結(jié)構(gòu)來實現(xiàn):
// RateLimitedTimedQueue is a unique item priority queue ordered by
// the expected next time of execution. It is also rate limited.
type RateLimitedTimedQueue struct {
queue UniqueQueue
limiterLock sync.Mutex
limiter flowcontrol.RateLimiter
}
從其定義就可以說明了這是一個 去重的優(yōu)先級隊列, 對于每個加入到其中的node根據(jù)執(zhí)行時間(此處即為加入時間)進(jìn)行排序,優(yōu)先級隊列肯定是通過heap數(shù)據(jù)結(jié)構(gòu)來實現(xiàn),而去重則通過set數(shù)據(jù)結(jié)構(gòu)來實現(xiàn)。在每次doNoExecuteTaintingPass執(zhí)行的時候,首先盡力從TokenBucketRateLimiter中獲取token,然后從隊頭獲取元素進(jìn)行處理,這樣就能控制速度地依次處理最先加入的node了。
三. 驅(qū)逐pod
在node lifecycle controller啟動的時候,會啟動一個NoExecuteTaintManager。 該模塊負(fù)責(zé)不斷獲取node taint信息,然后刪除其上的pod。
首先會利用informer會監(jiān)聽pod和node的各種事件,每個變化都會出發(fā)對應(yīng)的update事件。分為兩類: 1.優(yōu)先處理nodeUpdate事件; 2.然后是podUpdate事件
- 對于
nodeUpdate事件,會首先獲取該node的taint,然后獲取該node上面所有的pod,依次對每個pod調(diào)用processPodOnNode: 判斷是否有對應(yīng)的toleration,如果沒有則將其加入到對應(yīng)的taintEvictionQueue中,該queue是個定時器隊列,對于隊列中的每個元素會有一個定時器來來執(zhí)行,該定時器執(zhí)行時間由toleration中的tolerationSecond進(jìn)行設(shè)置。對于一些在退出時需要進(jìn)行清理的程序,toleration必不可少,可以保證給容器退出時留下足夠的時間進(jìn)行清理或者恢復(fù)。 出隊時調(diào)用的是回調(diào)函數(shù)deletePodHandler來刪除pod。 - 對于
podUpdate事件則相對簡單,首先獲取所在的node,然后從taintNodemap中獲取該node的taint, 最后調(diào)用processPodOnNode,后面的處理邏輯就同nodeUpdate事件一樣了。
為了加快處理速度,提高性能,上述處理會根據(jù)nodename hash之后交給多個worker進(jìn)行處理。
上述就是controller-manager中心跳處理邏輯,三個模塊層層遞進(jìn),依次處理,最后將一個異常node上的pod安全地遷移。

浙公網(wǎng)安備 33010602011771號