我是 LEE择葡,老李紧武,一個在 IT 行業(yè)摸爬滾打 17 年的技術(shù)老兵。
事件背景
昨天公司一套大數(shù)據(jù) Spark Job 運行的 Kubernetes 集群突然出現(xiàn)大量 Job Pod 被 Pending敏储,導(dǎo)致很多計算任務(wù)被卡住阻星,然后大量超時報警發(fā)到了大數(shù)據(jù)業(yè)務(wù)部門。沒一會就被小伙伴們叫到會議室準備一起解決問題已添,不到會議室不知道妥箕,那個熱鬧得跟趕集一樣。我剛進門就被其他的小伙伴抓住更舞,一起開始了確認大量 Job Pod 被 Pending 的問題畦幢,所有涉及上下游的小伙伴都在自查系統(tǒng),我也投入到容器相關(guān)的系統(tǒng)的檢查中缆蝉。
通過一段時間的排查宇葱,發(fā)現(xiàn) Pod 所運行的節(jié)點都很正常,而且整個 Kubernetes 所有節(jié)點資源都很充裕刊头,沒有理由 Pod 都集中運行到一個 Node 節(jié)點上黍瞧。不知不覺就進入了茫茫的日志海洋的排查,直到最大的領(lǐng)導(dǎo) GM 出現(xiàn)原杂,任然沒有找到出現(xiàn)這個問題位置印颤。沒有辦法,只能求教我們 Kubernetes 源代碼的大佬 - 吳老師穿肄,請他一起來“邊看源代碼年局,邊解決問題”。這個時候 Spark Job 的任務(wù)還在不停的創(chuàng)建被碗,集群上的 Job 任務(wù)還在瘋狂的堆積某宪,已經(jīng)嚴重影響到真實的生產(chǎn)流,GM 臉色越來越嚴肅锐朴。
現(xiàn)象獲取
我們做了很多假設(shè),都逐一被否定蔼囊。實在沒有辦法焚志,既然大量 Job Pod 被 Pending贩据,是因為被調(diào)度到一個固定的節(jié)點上導(dǎo)致的留特,大概率的是 Kubernetes 調(diào)度器的問題,我們把注意力集中到了 kube-scheduler 運行的 3 臺服務(wù)器上。通過長時間的觀測日志黔姜,總是發(fā)現(xiàn)相同的內(nèi)容,所有的 Job Pod 都被調(diào)度到了一個 Node 節(jié)點上分苇,不管怎么重啟 Job 或者 kube-scheduler 結(jié)果都一樣彤蔽,日志如下圖:
在我們一籌莫展的時候,吳老師提議我們把 kube-scheduler 的日志用最詳細的方式輸出挑社,再觀察下日志陨界。尤其要觀察下 kube-scheduler 的 Filter 和 Scope 兩個環(huán)節(jié)的數(shù)據(jù)變化。
一不做二不休痛阻,說干就干菌瘪,直接讓所有 kube-scheduler 的日志輸出都在最高模式的下,我們繼續(xù)觀察日志阱当,然后查看 kube-scheduler 中的 plugin 中數(shù)值的變化俏扩。果不其然,進過大概 10 分鐘觀察和數(shù)據(jù)統(tǒng)計弊添,我們在結(jié)果中看到了一些內(nèi)容录淡,kube-scheduler 中的一個 plugin 讓我們高度重視,而且對比了整個 kube-scheduler 調(diào)度結(jié)果油坝,基本確認就是這個 plugin 導(dǎo)致的嫉戚,它就是:ImageLocalityPriority。他影響了 kube-scheduler 調(diào)度結(jié)果免钻。
具體 ImageLocalityPriority 產(chǎn)生的分數(shù)如下:
從上面兩張圖對比就可以看出邏輯彼水,10.10.33.57 獲得 Scope 為 100,然后 Job Pod 都被調(diào)度到了 10.10.33.57 上极舔。是不是感覺非常有意思呢凤覆?
如果你覺得非常有意思,那么你跟我一樣拆魏,吳老師也是覺得非常有意思盯桦。 是不是很有想法跟我們一起往下看看具體原理呢?
原理分析
在定位到了問題以后渤刃,我們使用了一些“方法”解決了這個問題拥峦,讓卡住的大量 Job Pod 從新在整個集群上快速執(zhí)行起來。具體解決方案到下一部分我們再說卖子,我們先看看是什么原因?qū)е逻@個問題出現(xiàn)的略号,這樣我們才能真正的理解解決方案中的內(nèi)容。
(★)導(dǎo)致這次問題的真兇:ImageLocalityPriority
ImageLocalityPriority 前世今生
ImageLocalityPriority 插件的設(shè)計目的是通過優(yōu)先將 Pod 分配到已經(jīng)緩存了所需鏡像的節(jié)點上來提高 Kubernetes 調(diào)度器的性能和效率。
在 Kubernetes 集群中玄柠,每個節(jié)點都需要下載所有需要運行的容器鏡像突梦。如果集群中的所有節(jié)點都沒有所需鏡像,則 Kubernetes 將會選擇其中之一羽利,并將鏡像下載到該節(jié)點上宫患。這可能會導(dǎo)致不必要的網(wǎng)絡(luò)負載和較長的 Pod 啟動時間。
為了避免這種情況这弧,ImageLocalityPriority 插件引入了鏡像本地性的概念娃闲,即首選在已經(jīng)擁有所需鏡像的節(jié)點上啟動 Pod。這樣可以減少鏡像下載時間和網(wǎng)絡(luò)負載匾浪,并且可以提高調(diào)度效率和性能皇帮。
需要注意的是,使用 ImageLocalityPriority 插件會使節(jié)點之間的鏡像緩存不一致户矢,因此需要根據(jù)實際情況進行權(quán)衡和調(diào)整玲献。例如,在使用容器鏡像倉庫時梯浪,可以配置自己的鏡像緩存策略來確保節(jié)點之間的鏡像緩存一致性捌年。
ImageLocalityPriority 存在的目的
Kubernetes 調(diào)度器中的 ImageLocalityPriority 插件是通過優(yōu)先將 Pod 分配到已經(jīng)緩存了所需鏡像的節(jié)點上來提高調(diào)度性能的。
當需要將一個 Pod 分配給某個節(jié)點時挂洛,ImageLocalityPriority 插件會考慮該節(jié)點上是否已經(jīng)緩存了該 Pod 所需的鏡像礼预。如果該節(jié)點已經(jīng)擁有了所需鏡像,則該節(jié)點的得分會更高虏劲;否則托酸,該節(jié)點的得分會相應(yīng)降低。
為了確定一個節(jié)點是否已經(jīng)緩存了所需鏡像柒巫,ImageLocalityPriority 插件會查找該節(jié)點上的 Docker 版本和鏡像列表励堡,并與 Pod 的鏡像列表進行比較。如果發(fā)現(xiàn)鏡像列表匹配堡掏,則該節(jié)點的得分會更高应结。
需要注意的是,ImageLocalityPriority 插件只考慮節(jié)點上已經(jīng)緩存了的鏡像泉唁,而不考慮鏡像從其他節(jié)點下載的時間和網(wǎng)絡(luò)負載等因素鹅龄。因此,在使用 ImageLocalityPriority 插件時亭畜,需要根據(jù)實際情況進行權(quán)衡和調(diào)整扮休,并確保集群中的所有節(jié)點都能夠快速可靠地獲取所需鏡像
ImageLocalityPriority 算法解析
結(jié)合上面的提到的內(nèi)容,ImageLocalityPriority 算法實現(xiàn)非常簡單和粗暴拴鸵。
總共非常了兩部分:
- sumImageScores: 計算節(jié)點上應(yīng)用 Pod 中所有 Container 的容量打分玷坠,并最后匯總這個分數(shù)蜗搔。
- calculatePriority: 根據(jù) sumImageScores 和 Container 的數(shù)量計算這個 Pod 的分數(shù)。
有上面的兩部分計算的結(jié)果侨糟,最后通過 Scope 方法碍扔,將這個 plugin 計算的分數(shù)返回給 kube-scheduler瘩燥。
sumImageScores
pkg/scheduler/framework/plugins/imagelocality/image_locality.go
// sumImageScores returns the sum of image scores of all the containers that are already on the node.
// Each image receives a raw score of its size, scaled by scaledImageScore. The raw scores are later used to calculate
// the final score. Note that the init containers are not considered for it's rare for users to deploy huge init containers.
func sumImageScores(nodeInfo *framework.NodeInfo, containers []v1.Container, totalNumNodes int) int64 {
var sum int64
for _, container := range containers {
if state, ok := nodeInfo.ImageStates[normalizedImageName(container.Image)]; ok {
sum += scaledImageScore(state, totalNumNodes)
}
}
return sum
}
// scaledImageScore returns an adaptively scaled score for the given state of an image.
// The size of the image is used as the base score, scaled by a factor which considers how much nodes the image has "spread" to.
// This heuristic aims to mitigate the undesirable "node heating problem", i.e., pods get assigned to the same or
// a few nodes due to image locality.
func scaledImageScore(imageState *framework.ImageStateSummary, totalNumNodes int) int64 {
spread := float64(imageState.NumNodes) / float64(totalNumNodes)
return int64(float64(imageState.Size) * spread)
}
- scaledImageScore 負責(zé)計算已經(jīng)下載鏡像(待調(diào)度 Job Pod 的鏡像)節(jié)點數(shù)量占 Kubernetes 總節(jié)點數(shù)量的比重秕重,比重值與指定的 Container 鏡像大小值相乘,返回 int64 值厉膀。
- sumImageScores 負責(zé)將所有的 Pod 中所有的 Container 執(zhí)行 scaledImageScore 計算溶耘,將所有值進行求和,返回 int64 值服鹅。
數(shù)學(xué)公式:
calculatePriority
pkg/scheduler/framework/plugins/imagelocality/image_locality.go
// The two thresholds are used as bounds for the image score range. They correspond to a reasonable size range for
// container images compressed and stored in registries; 90%ile of images on dockerhub drops into this range.
const (
mb int64 = 1024 * 1024
minThreshold int64 = 23 * mb
maxContainerThreshold int64 = 1000 * mb
)
// calculatePriority returns the priority of a node. Given the sumScores of requested images on the node, the node's
// priority is obtained by scaling the maximum priority value with a ratio proportional to the sumScores.
func calculatePriority(sumScores int64, numContainers int) int64 {
// 1G 容量 * Pod 中 Container 的數(shù)量凳兵,獲得最大的上限
maxThreshold := maxContainerThreshold * int64(numContainers)
if sumScores < minThreshold {
sumScores = minThreshold
} else if sumScores > maxThreshold {
sumScores = maxThreshold
}
// 返回值在 0 - 100 之間
return int64(framework.MaxNodeScore) * (sumScores - minThreshold) / (maxThreshold - minThreshold)
}
calculatePriority 將 sumImageScores 計算的結(jié)果在函數(shù)內(nèi)做對比:
- 如果 sumScores < 23 * 1024 * 1024,則 calculatePriority 返回 0
- 如果 sumScores >= numContainers * 1000 * 1024 * 1024, 則 calculatePriority 返回 100
不管你的 Pod 中間有多少的 Container企软,最后 Pod 計算結(jié)果只會落在 0 - 100 之間庐扫。
TIPS:我們這邊出問題的應(yīng)用是單 Container 的 Pod,這個 Container 的 Image 鏡像已經(jīng)超過了 1G仗哨,所以我們看到的打分是 100形庭。
數(shù)學(xué)公式:
Scope
pkg/scheduler/framework/plugins/imagelocality/image_locality.go
// Score invoked at the score extension point.
func (pl *ImageLocality) Score(ctx context.Context, state *framework.CycleState, pod *v1.Pod, nodeName string) (int64, *framework.Status) {
// 從 Snapshot 中獲取 NodeInfo。
nodeInfo, err := pl.handle.SnapshotSharedLister().NodeInfos().Get(nodeName)
// 如果出現(xiàn)錯誤厌漂,則返回 0 和帶有錯誤信息的 Status萨醒。
if err != nil {
return 0, framework.AsStatus(fmt.Errorf("getting node %q from Snapshot: %w", nodeName, err))
}
// 獲取所有 NodeInfo 列表,并得到節(jié)點數(shù)苇倡。
nodeInfos, err := pl.handle.SnapshotSharedLister().NodeInfos().List()
if err != nil {
return 0, framework.AsStatus(err)
}
totalNumNodes := len(nodeInfos)
// 計算 pod 的 priority富纸,并返回 score。
score := calculatePriority(sumImageScores(nodeInfo, pod.Spec.Containers, totalNumNodes), len(pod.Spec.Containers))
return score, nil
}
Score 方法就是返回 calculatePriority 計算的結(jié)果給 kube-scheduler旨椒,然后 kube-scheduler 繼續(xù)通過其他的 plugin 打分晓褪,最后返回所有 plugin 分數(shù)總和,決定 Pod 在哪個 Node 節(jié)點上運行综慎。
處理方法
清楚了 ImageLocalityPriority 整體結(jié)構(gòu)涣仿,以及相關(guān)代碼以及算法實現(xiàn),那么對應(yīng)的解決方案也就有了寥粹。
確實如此变过,而且解決方案也非常的簡單:
- 在 kube-scheduler 中關(guān)閉 ImageLocalityPriority 插件。
- 在 kube-scheduler 中降低 ImageLocalityPriority 的優(yōu)先級涝涤。
- 全部節(jié)點上部署 Image 鏡像同步器媚狰,讓 ImageLocalityPriority 打分返回 100 的 Pod 使用鏡像在 Kubernetes 集群中所有節(jié)點都被下載。
這里提供兩種實操阔拳,第 3 種有很多實現(xiàn)方式崭孤,這個大家可以自行 baidu类嗤,然后選擇復(fù)合自己的方案。
關(guān)閉 ImageLocalityPriority
降低 ImageLocalityPriority 優(yōu)先級
最終效果
最后我們這邊為了緊急恢復(fù) Spark Job 在 Kubernetes 上的運行辨宠,我們用了最簡單的方案 1遗锣。當然我們這邊還有更多的工作要做,才能真正讓這個問題在我們這邊測底消除嗤形。
同時也通過一個 test-job 來驗證我們的解決方案的效果精偿,與預(yù)期相符。
多啰嗦一句:ImageLocalityPriority 導(dǎo)致 Spark Job 的 Pod 在 Kubernetes 調(diào)度不均的問題赋兵,確實沒有想過是因為鏡像的大小的導(dǎo)致的笔咽。我想這個問題也會導(dǎo)致很多小伙伴一頭霧水,最后在獲得吳老師同意后霹期,決定把我這次碰到問題成文叶组,提供給大家“避坑”。