kubernetes中node心跳處理邏輯分析
最近在檢視一個kubernetes叢集中node not ready的奇怪現象,順便閱讀了一下kubernetes kube-controller-manager中管理node健康狀態的元件node lifecycle controller
。我們知道kubernetes是典型的master-slave架構,master node負責整個叢集元資料的管理,然後將具體的啟動執行pod的任務分發給各個salve node執行,各個salve node會定期與master通過心跳資訊來告知自己的存活狀態。其中slave node上負責心跳的是kubelet程式, 他會定期更新apiserver中node lease或者node status資料,然後kube-controller-manager會監聽這些資訊變化,如果一個node很長時間都沒有進行狀態更新,那麼我們就可以認為該node發生了異常,需要進行一些容錯處理,將該node上面的pod進行安全的驅逐,使這些pod到其他node上面進行重建。這部分工作是由node lifecycel controller
在目前的版本(v1.16)中,預設開啟了TaintBasedEvictions
, TaintNodesByCondition
這兩個feature gate,則所有node生命週期管理都是通過condition + taint的方式進行管理。其主要邏輯由三部分組成:
- 不斷地檢查所有node狀態,設定對應的condition
- 不斷地根據node condition 設定對應的taint
- 不斷地根據taint驅逐node上面的pod
一. 檢查node狀態
檢查node狀態其實就是迴圈呼叫monitorNodeHealth
函式,該函式首先呼叫tryUpdateNodeHealth
檢查每個node是否還有心跳,然後判斷如果沒有心跳則設定對應的condtion。
node lifecycle controller內部會維護一個nodeHealthMap
資料結構來儲存所有node的心跳資訊,每次心跳之後都會更新這個結構體,其中最重要的資訊就是每個node上次心跳時間probeTimestamp
, 如果該timestamp很長時間都沒有更新(超過--node-monitor-grace-period引數指定的值),則認為該node可能已經掛了,設定node的所有condition為unknown狀態。
gracePeriod, observedReadyCondition, currentReadyCondition, err = nc.tryUpdateNodeHealth(node)
tryUpdateNodeHealth
傳入的引數為每個要檢查的node, 返回值中observedReadyCondition
為當前從apiserver中獲取到的資料,也就是kubelet上報上來的最新的node資訊, currentReadyCondition
為修正過的資料。舉個例子,如果node很長時間沒有心跳的話,observedReadyCondition
中nodeReadyCondion為true, 但是currentReadyCondion
中所有的conditon已經被修正的實際狀態unknown了。
如果observedReadyCondition
狀態為true, 而currentReadyCondition
狀態不為true, 則說明node狀態狀態發生變化,由ready變為not-ready。此時不光會更新node condition,還會將該node上所有的pod狀態設定為not ready,這樣的話,如果有對應的service資源選中該pod, 流量就可以從service上摘除了,但是此時並不會直接刪除pod。
node lifecycle controller會根據currentReadyCondition
的狀態將該node加入到zoneNoExecuteTainter
的佇列中,等待後面設定taint。如果此時已經有了taint的話則會直接更新。zoneNoExecuteTainter
佇列的出隊速度是根據node所處zone狀態決定的,主要是為了防止出現叢集級別的故障時,node lifecycle controller進行誤判,例如交換機,loadbalancer等故障時,防止node lifecycle controller錯誤地認為所有node都不健康而大規模的設定taint進而導致錯誤地驅逐很多pod,造成更大的故障。
設定出隊速率由handleDisruption
函式中來處理,首先會選擇出來各個zone中不健康的node, 並確定當前zone所處的狀態。分為以下幾種情況:
- Initial: zone剛加入到叢集中,初始化完成。
- Normal: zone處於正常狀態
- FullDisruption: 該zone中所有的node都notReady了
- PartialDisruption: 該zone中部分node notReady,此時已經超過了unhealthyZoneThreshold設定的閾值
對於上述不同狀態所設定不同的rate limiter, 從而決定出隊速度。該速率由函式setLimiterInZone
決定具體數值, 具體規則是:
- 當所有zone都處於
FullDisruption
時,此時limiter為0 - 當只有部分zone處於
FullDisruption
時,此時limiter為正常速率:--node-eviction-rate
- 如果某個zone處於
PartialDisruption
時,則此時limiter為二級速率:--secondary-node-eviction-rate
二. 設定node taint
根據node condition設定taint主要由兩個迴圈來負責, 這兩個迴圈在程式啟動後會不斷執行:
doNodeProcessingPassWorker
中主要的邏輯就是:doNoScheduleTaintingPass
, 該函式會根據node當前的condition設定unschedulable
的taint,便於排程器根據該值進行排程決策,不再排程新pod至該node。doNoExecuteTaintingPass
會不斷地從上面提到的zoneNoExecuteTainter
佇列中獲取元素進行處理,根據node condition設定對應的NotReady
或Unreachable
的taint, 如果NodeReady
condition為false則taint為NotReady, 如果為unknown,則taint為Unreachable, 這兩種狀態只能同時存在一種!
上面提到從zoneNoExecuteTainter
佇列中出隊時是有一定的速率限制,防止大規模快速驅逐pod。該元素是由RateLimitedTimedQueue
資料結構來實現:
// 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
}
從其定義就可以說明了這是一個 去重的優先順序佇列
, 對於每個加入到其中的node根據執行時間(此處即為加入時間)進行排序,優先順序佇列肯定是通過heap資料結構來實現,而去重則通過set資料結構來實現。在每次doNoExecuteTaintingPass
執行的時候,首先盡力從TokenBucketRateLimiter中獲取token,然後從隊頭獲取元素進行處理,這樣就能控制速度地依次處理最先加入的node了。
三. 驅逐pod
在node lifecycle controller啟動的時候,會啟動一個NoExecuteTaintManager
。 該模組負責不斷獲取node taint資訊,然後刪除其上的pod。
首先會利用informer會監聽pod和node的各種事件,每個變化都會出發對應的update事件。分為兩類: 1.優先處理nodeUpdate事件; 2.然後是podUpdate事件
- 對於
nodeUpdate
事件,會首先獲取該node的taint,然後獲取該node上面所有的pod,依次對每個pod呼叫processPodOnNode
: 判斷是否有對應的toleration,如果沒有則將其加入到對應的taintEvictionQueue
中,該queue是個定時器佇列,對於佇列中的每個元素會有一個定時器來來執行,該定時器執行時間由toleration中的tolerationSecond進行設定。對於一些在退出時需要進行清理的程式,toleration必不可少,可以保證給容器退出時留下足夠的時間進行清理或者恢復。 出隊時呼叫的是回撥函式deletePodHandler
來刪除pod。 - 對於
podUpdate
事件則相對簡單,首先獲取所在的node,然後從taintNode
map中獲取該node的taint, 最後呼叫processPodOnNode
,後面的處理邏輯就同nodeUpdate
事件一樣了。
為了加快處理速度,提高效能,上述處理會根據nodename hash之後交給多個worker進行處理。
上述就是controller-manager中心跳處理邏輯,三個模組層層遞進,依次處理,最後將一個異常node上的pod安全地遷移