K8s Scheduler 在排程 pod 過程中遺漏部分節點的問題排查

騰訊雲原生發表於2021-05-13

問題現象

在TKE控制檯上新建版本為v1.18.4(詳細版本號 < v1.18.4-tke.5)的獨立叢集,其中,叢集的節點資訊如下:

有3個master node和1個worker node,並且worker 和 master在不同的可用區。

node 角色 label資訊
ss-stg-ma-01 master label[failure-domain.beta.kubernetes.io/region=sh,failure-domain.beta.kubernetes.io/zone=200002]
ss-stg-ma-02 master label[failure-domain.beta.kubernetes.io/region=sh,failure-domain.beta.kubernetes.io/zone=200002]
ss-stg-ma-03 master label[failure-domain.beta.kubernetes.io/region=sh,failure-domain.beta.kubernetes.io/zone=200002]
ss-stg-test-01 worker label[failure-domain.beta.kubernetes.io/region=sh,failure-domain.beta.kubernetes.io/zone=200004]

待叢集建立好之後,再建立出一個daemonset物件,會出現daemonset的某個pod一直卡住pending狀態的現象。
現象如下:

$ kubectl  get  pod  -o  wide
NAME        READY STATUS  RESTARTS AGE NODE 
debug-4m8lc 1/1   Running 1        89m  ss-stg-ma-01
debug-dn47c 0/1   Pending 0        89m  <none>
debug-lkmfs 1/1   Running 1        89m   ss-stg-ma-02
debug-qwdbc 1/1   Running 1        89m  ss-stg-test-01

(補充:TKE當前支援的最新版本號為v1.18.4-tke.8,新建叢集預設使用最新版本

問題結論

k8s的排程器在排程某個pod時,會從排程器的內部cache中同步一份快照(snapshot),其中儲存了pod可以排程的node資訊。
上面問題(daemonset的某個pod例項卡在pending狀態)的原因就是同步的過程發生了部分node資訊丟失,導致了daemonset的部分pod例項無法排程到指定的節點上,卡在了pending狀態。

接下來是詳細的排查過程。

日誌排查

截圖中出現的節點資訊(來自客戶線上叢集):
k8s master節點:ss-stg-ma-01、ss-stg-ma-02、ss-stg-ma-03
k8s worker節點:ss-stg-test-01

1、獲取排程器的日誌
這裡首先是通過動態調大排程器的日誌級別,比如,直接調大到V(10),嘗試獲取一些相關日誌。
當日志級別調大之後,有抓取到一些關鍵資訊,資訊如下:

解釋一下,當排程某個pod時,有可能會進入到排程器的搶佔preempt環節,而上面的日誌就是出自於搶佔環節。
叢集中有4個節點(3個master node和1個worker node),但是日誌中只顯示了3個節點,缺少了一個master節點。
所以,這裡暫時懷疑下是排程器內部快取cache中少了node info

2、獲取排程器內部cache資訊
k8s v1.18已經支援列印排程器內部的快取cache資訊。列印出來的排程器內部快取cache資訊如下:

可以看出,排程器的內部快取cache中的node info是完整的(3個master node和1個worker node)。
通過分析日誌,可以得到一個初步結論:排程器內部快取cache中的node info是完整的,但是當排程pod時,快取cache中又會缺少部分node資訊。

問題根因

在進一步分析之前,我們先一起再熟悉下排程器排程pod的流程(部分展示)和nodeTree資料結構。

pod排程流程(部分展示)

結合上圖,一次pod的排程過程就是 一次Scheduler Cycle。 在這個Cycle開始時,第一步就是update snapshot。snapshot我們可以理解為cycle內的cache,其中儲存了pod排程時所需的node info,而update snapshot,就是一次nodeTree(排程器內部cache中儲存的node資訊)到snapshot的同步過程。
而同步過程主要是通過nodeTree.next()函式來實現,函式邏輯如下:

// next returns the name of the next node. NodeTree iterates over zones and in each zone iterates
// over nodes in a round robin fashion.
func (nt *nodeTree) next() string {
	if len(nt.zones) == 0 {
		return ""
	}
	numExhaustedZones := 0
	for {
		if nt.zoneIndex >= len(nt.zones) {
			nt.zoneIndex = 0
		}
		zone := nt.zones[nt.zoneIndex]
		nt.zoneIndex++
		// We do not check the exhausted zones before calling next() on the zone. This ensures
		// that if more nodes are added to a zone after it is exhausted, we iterate over the new nodes.
		nodeName, exhausted := nt.tree[zone].next()
		if exhausted {
			numExhaustedZones++
			if numExhaustedZones >= len(nt.zones) { // all zones are exhausted. we should reset.
				nt.resetExhausted()
			}
		} else {
			return nodeName
		}
	}
}

再結合上面排查過程得出的結論,我們可以再進一步縮小問題範圍:nodeTree(排程器內部cache)到的同步過程丟失了某個節點資訊。

### nodeTree資料結構
(方便理解,本文使用了連結串列來展示)

在nodeTree資料結構中,有兩個遊標zoneIndex 和 lastIndex(zone級別),用來控制 nodeTree(排程器內部cache)到snapshot.nodeInfoList的同步過程。並且,重要的一點是:上次同步後的遊標值會被記錄下來,用於下次同步過程的初始值。

### 重現問題,定位根因

建立k8s叢集時,會先加入master node,然後再加入worker node(意思是worker node時間上會晚於master node加入叢集的時間)。

第一輪同步:3臺master node建立好,然後發生pod排程(比如,cni 外掛,以daemonset的方式部署在叢集中),會觸發一次nodeTree(排程器內部cache)到的同步。同步之後,nodeTree的兩個遊標就變成了如下結果:

nodeTree.zoneIndex = 1,
nodeTree.nodeArray[sh:200002].lastIndex = 3,

第二輪同步:當worker node加入叢集中後,然後新建一個daemonset,就會觸發第二輪的同步(nodeTree(排程器內部cache)到的同步)。同步過程如下:

1、 zoneIndex=1, nodeArray[sh:200004].lastIndex=0, we get ss-stg-test-01.

2、 zoneIndex=2 >= len(zones); zoneIndex=0, nodeArray[sh:200002].lastIndex=3, return.

3、 zoneIndex=1, nodeArray[sh:200004].lastIndex=1, return.

4、 zoneIndex=0, nodeArray[sh:200002].lastIndex=0, we get ss-stg-ma-01.

5、 zoneIndex=1, nodeArray[sh:200004].lastIndex=0, we get ss-stg-test-01.

6、 zoneIndex=2 >= len(zones); zoneIndex=0, nodeArray[sh:200002].lastIndex=1, we get ss-stg-ma-02.

同步完成之後,排程器的snapshot.nodeInfoList得到如下的結果:

[
    ss-stg-test-01,
    ss-stg-ma-01,
    ss-stg-test-01,
    ss-stg-ma-02,
]

ss-stg-ma-03 去哪了?在第二輪同步的過程中丟了。

解決方案

問題根因的分析中,可以看出,導致問題發生的原因,在於 nodeTree 資料結構中的遊標 zoneIndex 和 lastIndex(zone級別)值被保留了,所以,解決的方案就是在每次同步SYNC時,強制重置遊標(歸0)。
相關issue:https://github.com/kubernetes/kubernetes/issues/97120
相關pr(k8s v1.18): https://github.com/kubernetes/kubernetes/pull/93387
TKE修復版本:v1.18.4-tke.5

相關文章