為什么Pod突然就不見了?

最近發(fā)生一件很詭異的事情, 某個(gè) ns 下的 pods 會(huì)莫名其妙地被刪了, 困擾了好一陣子,排查后發(fā)現(xiàn)問題的起因還是挺有意思。
問題現(xiàn)象
交代一下背景, 這些 pod 都是由 argo-workflow 發(fā)起的 pod, 執(zhí)行完特定的任務(wù)之后就會(huì)變成 Succeeded, 如果執(zhí)行時(shí)有問題,狀態(tài)可能是 Failed。
結(jié)果很直接,就是在 ns 下的某些狀態(tài)為 Failed pod 會(huì)被刪掉(后來證實(shí) Succeeded 狀態(tài)的也會(huì)被刪掉),所以會(huì)出現(xiàn)尷尬的情況是想找這個(gè) pod 的時(shí)候,發(fā)現(xiàn)這個(gè) pod 卻沒了,之前反映過類似的問題,但一直以為是被別人刪了,沒有在意,但是第二次出現(xiàn),感覺不是偶然。
開發(fā)同學(xué)肯定沒權(quán)限做這個(gè)事,運(yùn)維側(cè)也可以肯定沒有這類操作,排查了一圈幾乎可以肯定的是,不是人為的, 那不是人做的,就只能中 k8s 這邊的某些機(jī)制觸發(fā)了這個(gè)刪除的操作,kubernetes 可以管理千千萬萬的 pod 資源,因此 gc 機(jī)制是必不可少的,作者也是第一時(shí)間想到了可能是 gc 機(jī)制引起的。
在詳細(xì)追蹤 k8s 的 podGC 問題之前,其實(shí)還有一個(gè)嫌疑犯需要排查,那就是 argo-workflow, argo-workflow 做為一種任務(wù) workflow 的實(shí)現(xiàn)方式,argo-workflow 本身也可以通過CRD來檢測當(dāng) workflow 執(zhí)行到達(dá)什么狀態(tài)時(shí)進(jìn)行 podGC, 如下圖:

但作者可以肯定的是,那些被刪除的 pod 中并未使用 argo-workflow 的 podGC,因此 argo-workflow 的嫌疑可以排除.
那么現(xiàn)在就剩 k8s 本身的機(jī)制了
PodGC
k8s 中存在在各種各樣的 controller(感興趣的可以看看 controllermanager.go 中的 NewControllerInitializers 中列出來的 controllers 對(duì)象), 每一個(gè) controller 專注于解決一個(gè)方面的問題, podGC controller 也是如此,專門回收 pod。
既然 pod 被回收了,是不是可以從 controllermanager 的日志中看到什么呢?果然

從上面的日志也可以證實(shí),pod 確實(shí)是 controller 被回收了,但是怎么個(gè)回收法呢?依據(jù)是什么,時(shí)間間隔多久等等一系列問題相繼涌出
gc_controller.go
源碼能夠得到一切答案,大多數(shù)都來自于pkg/controller/podgc/gc_controller.go
const (
// gcCheckPeriod defines frequency of running main controller loop
gcCheckPeriod = 20 * time.Second
// quarantineTime defines how long Orphaned GC waits for nodes to show up
// in an informer before issuing a GET call to check if they are truly gone
quarantineTime = 40 * time.Second
)
首先是 gc 的時(shí)間間隔,很顯然是 20s,而且這個(gè)數(shù)值不支持從命令參數(shù)中配置
quarantineTime 是在刪除孤兒 pod 時(shí)等待節(jié)點(diǎn) ready 前的時(shí)間
那根據(jù)什么刪除的呢, 同樣,在源碼中給了答案
pod.status.phase
func (gcc *PodGCController) gcTerminated(pods []*v1.Pod) {
terminatedPods := []*v1.Pod{}
for _, pod := range pods {
if isPodTerminated(pod) {
terminatedPods = append(terminatedPods, pod)
}
}
terminatedPodCount := len(terminatedPods)
deleteCount := terminatedPodCount - gcc.terminatedPodThreshold
if deleteCount > terminatedPodCount {
deleteCount = terminatedPodCount
}
if deleteCount <= 0 {
return
}
klog.Infof("garbage collecting %v pods", deleteCount)
// sort only when necessary
sort.Sort(byCreationTimestamp(terminatedPods))
var wait sync.WaitGroup
for i := 0; i < deleteCount; i++ {
wait.Add(1)
go func(namespace string, name string) {
defer wait.Done()
if err := gcc.deletePod(namespace, name); err != nil {
// ignore not founds
defer utilruntime.HandleError(err)
}
}(terminatedPods[i].Namespace, terminatedPods[i].Name)
}
wait.Wait()
}
這里的日志輸出剛好也是 controllermanager.go 中的日志輸出,主要的邏輯在如何判定一個(gè) pod 是否需要被刪除
func isPodTerminated(pod *v1.Pod) bool {
if phase := pod.Status.Phase; phase != v1.PodPending && phase != v1.PodRunning && phase != v1.PodUnknown {
return true
}
return false
}
判斷一個(gè) pod 是否需要被刪除,主要看一個(gè) pod 的狀態(tài),在 k8s,一個(gè) pod 大概會(huì)有以下的狀態(tài)(phases)
Pending Running Succeeded Failed Unknown
得到所有的 pods 實(shí)例,對(duì)于 status.phase 不等于 Pending、Running、Unknown 的且與 terminatedPodThreshold 的差值的部分的 pod 進(jìn)行清除,會(huì)對(duì)要?jiǎng)h除的 pod 的創(chuàng)建時(shí)間戳進(jìn)行排序后刪除差值個(gè)數(shù)的 pod,注意這里也會(huì)把 succeeded 的狀態(tài) pod 給刪除,作者對(duì)這個(gè)把 succeeded 狀態(tài)的 pod 給 gc 了還是比較奇怪的
gcOrphaned
另外,回收那些 Binded 的 Nodes 已經(jīng)不存在的 pods,這個(gè)沒什么好說的,node 都不存在了,pod 也沒存在的必要了
邏輯是調(diào)用 apiserver 接口,獲取所有的 Nodes,然后遍歷所有 pods,如果 pod bind 的 NodeName 不為空且不包含在剛剛獲取的所有 Nodes 中,最后串行逐個(gè)調(diào)用 gcc.deletePod 刪除對(duì)應(yīng)的 pod
func (gcc *PodGCController) gcOrphaned(pods []*v1.Pod, nodes []*v1.Node) {
klog.V(4).Infof("GC'ing orphaned")
existingNodeNames := sets.NewString()
for _, node := range nodes {
existingNodeNames.Insert(node.Name)
}
// Add newly found unknown nodes to quarantine
for _, pod := range pods {
if pod.Spec.NodeName != "" && !existingNodeNames.Has(pod.Spec.NodeName) {
gcc.nodeQueue.AddAfter(pod.Spec.NodeName, quarantineTime)
}
}
// Check if nodes are still missing after quarantine period
deletedNodesNames, quit := gcc.discoverDeletedNodes(existingNodeNames)
if quit {
return
}
// Delete orphaned pods
for _, pod := range pods {
if !deletedNodesNames.Has(pod.Spec.NodeName) {
continue
}
klog.V(2).Infof("Found orphaned Pod %v/%v assigned to the Node %v. Deleting.", pod.Namespace, pod.Name, pod.Spec.NodeName)
if err := gcc.deletePod(pod.Namespace, pod.Name); err != nil {
utilruntime.HandleError(err)
} else {
klog.V(0).Infof("Forced deletion of orphaned Pod %v/%v succeeded", pod.Namespace, pod.Name)
}
}
}
gcUnscheduledTerminating
另外,回收 Unscheduled 并且 Terminating 的 pods,邏輯是遍歷所有 pods,過濾那些 terminating(pod.DeletionTimestamp != nil)并且未調(diào)度成功的(pod.Spec.NodeName 為空)的 pods, 然后串行逐個(gè)調(diào)用 gcc.deletePod 刪除對(duì)應(yīng)的 pod
func (gcc *PodGCController) gcUnscheduledTerminating(pods []*v1.Pod) {
klog.V(4).Infof("GC'ing unscheduled pods which are terminating.")
for _, pod := range pods {
if pod.DeletionTimestamp == nil || len(pod.Spec.NodeName) > 0 {
continue
}
klog.V(2).Infof("Found unscheduled terminating Pod %v/%v not assigned to any Node. Deleting.", pod.Namespace, pod.Name)
if err := gcc.deletePod(pod.Namespace, pod.Name); err != nil {
utilruntime.HandleError(err)
} else {
klog.V(0).Infof("Forced deletion of unscheduled terminating Pod %v/%v succeeded", pod.Namespace, pod.Name)
}
}
}
disable podGC controller
Podgc 是不是可以配置呢?
很遺憾的是,配置項(xiàng)不是很多,可以定義是否開啟 podgc controller
controller-manager 的啟動(dòng)參數(shù)中有個(gè)參數(shù):
--terminated-pod-gc-threshold int32 Default: 12500
Number of terminated pods that can exist before the terminated pod garbage collector starts deleting terminated pods. If <= 0, the terminated pod garbage collector is disabled.
這個(gè)參數(shù)指的是在 pod gc 前可以保留多少個(gè) terminated pods, 默認(rèn)是 12500 個(gè),這個(gè)數(shù)值還是挺大的,一般集群怕是很難能到,作者由于是訓(xùn)練集群,存在著大量的短時(shí)間任務(wù),因此會(huì)出現(xiàn)大于該值的 pod,當(dāng)該值小于等于 0 時(shí),相當(dāng)于不對(duì) terminated pods 進(jìn)行刪除,但還是會(huì)對(duì)孤兒 pod 及處于 terminating 狀態(tài)且沒有綁定到 node 的 pod 進(jìn)行清除.
參考: https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/[1]
作者只查到這一個(gè)跟 podgc 相關(guān)的參數(shù),目測好像在不修改 controllermanager 的情況下是沒辦法直接禁用 podgc
到此,真相大白:
同時(shí)也給作者糾正了一個(gè)錯(cuò)誤, 不是只有 Failed 狀態(tài)的 pod 才會(huì)被 gc,Successed 狀態(tài)的 pod 也會(huì)被 gc 掉,這個(gè)出乎作者意料之外
最后,想說的是,podgc 跟 k8s 中的垃圾回收還不是一回事,雖然他們都是以 controller 運(yùn)行,
podgc 解決的是 pod 到達(dá) gc 的條件后會(huì)被 delete 掉.
而 garbage 則解決的是對(duì)節(jié)點(diǎn)上的無用鏡像和容器的清除
從 k8s 的源碼也能夠看出來這兩者的不同.
參考文章
https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/[2] https://cloud.tencent.com/developer/article/1097385[3]
腳注
https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/: https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/
[2]https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/: https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/
[3]https://cloud.tencent.com/developer/article/1097385: https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/
原文鏈接:https://izsk.me/2021/07/26/Kubernetes-podGC/


你可能還喜歡
點(diǎn)擊下方圖片即可閱讀

云原生是一種信仰 ??
關(guān)注公眾號(hào)
后臺(tái)回復(fù)?k8s?獲取史上最方便快捷的 Kubernetes 高可用部署工具,只需一條命令,連 ssh 都不需要!


點(diǎn)擊 "閱讀原文" 獲取更好的閱讀體驗(yàn)!
發(fā)現(xiàn)朋友圈變“安靜”了嗎?


