最近在查看一個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安全地遷移。