這個話題,想必玩過kubernetes的同學當不陌生,我會分Pod和Namespace分別來談。
開門見山,為什麼Pod會卡在Terminating狀態?
一句話,本質是API Server雖然標記了物件的刪除,但是作為實際清理的控制器kubelet, 並不能關停Pod或相關資源, 因而沒能通知API Server做實際物件的清理。
原因何在?要解開這個原因,我們先來看Pod Terminating的基本流程:
- 客戶端(比如kubectl)提交刪除請求到API Server
- 可選傳遞 --grace-period 引數
- API Server接受到請求之後,做 Graceful Deletion 檢查
- 若需要 graceful 刪除時,則更新物件的 metadata.deletionGracePeriodSeconds和metadata.deletionTimestamp欄位。這時候describe檢視物件的話,會發現其已經變成Terminating狀態了
- Pod所在的節點,kubelet檢測到Pod處於Terminating狀態時,就會開啟Pod的真正刪除流程
- 如果Pod中的容器有定義preStop hook事件,那kubelet會先執行這些容器的hook事件
- 之後,kubelet就會Trigger容器執行時發起
TERM
signal 給該Pod中的每個容器
- 在Kubelet開啟Graceful Shutdown的同時,Control Plane也會從目標Service的Endpoints中摘除要關閉的Pod。ReplicaSet和其他的workload服務也會認定這個Pod不是個有效副本了。同時,Kube-proxy 也會摘除這個Pod的Endpoint,這樣即使Pod關閉很慢,也不會有流量再打到它上面。
- 如果容器正常關閉那很好,但如果在grace period 時間內,容器仍然執行,kubelet會開始強制shutdown。容器執行時會傳送
SIGKILL
訊號給Pod中所有執行的程式進行強制關閉 - 注意在開啟Pod刪除的同時,kubelet的其它控制器也會處理Pod相關的其他資源的清理動作,比如Volume。而待一切都清理乾淨之後,Kubelet才通過把Pod的grace period時間設為0來通知API Server強制刪除Pod物件。
參考連結: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-termination
只有執行完第六步,Pod的API物件才會被真正刪除。那怎樣才認為是"一切都清理乾淨了"呢?我們來看原始碼:
// PodResourcesAreReclaimed returns true if all required node-level resources that a pod was consuming have
// been reclaimed by the kubelet. Reclaiming resources is a prerequisite to deleting a pod from theAPI Server.
func (kl *Kubelet) PodResourcesAreReclaimed(pod *v1.Pod, status v1.PodStatus) bool {
if kl.podWorkers.CouldHaveRunningContainers(pod.UID) {
// We shouldn't delete pods that still have running containers
klog.V(3).InfoS("Pod is terminated, but some containers are still running", "pod", klog.KObj(pod))
return false
}
if count := countRunningContainerStatus(status); count > 0 {
// We shouldn't delete pods until the reported pod status contains no more running containers (the previous
// check ensures no more status can be generated, this check verifies we have seen enough of the status)
klog.V(3).InfoS("Pod is terminated, but some container status has not yet been reported", "pod", klog.KObj(pod), "running", count)
return false
}
if kl.podVolumesExist(pod.UID) && !kl.keepTerminatedPodVolumes {
// We shouldn't delete pods whose volumes have not been cleaned up if we are not keeping terminated pod volumes
klog.V(3).InfoS("Pod is terminated, but some volumes have not been cleaned up", "pod", klog.KObj(pod))
return false
}
if kl.kubeletConfiguration.CgroupsPerQOS {
pcm := kl.containerManager.NewPodContainerManager()
if pcm.Exists(pod) {
klog.V(3).InfoS("Pod is terminated, but pod cgroup sandbox has not been cleaned up", "pod", klog.KObj(pod))
return false
}
}
// Note: we leave pod containers to be reclaimed in the background since dockershim requires the
// container for retrieving logs and we want to make sure logs are available until the pod is
// physically deleted.
klog.V(3).InfoS("Pod is terminated and all resources are reclaimed", "pod", klog.KObj(pod))
return true
}
是不是很清晰?總結下來就三個原因:
- Pod裡沒有Running的容器
- Pod的Volume也清理乾淨了
- Pod的cgroup設定也沒了
如是而已。
自然,其反向對應的就是各個異常場景了。我們來細看:
- 容器停不掉 - 這種屬於CRI範疇,常見的一般使用docker作為容器執行時。筆者就曾經遇到過個場景,用
docker ps
能看到目標容器是Up
狀態,但是執行docker stop or rm
卻沒有任何反應,而執行docker exec
,會報no such container
的錯誤。也就是說此時這個容器的狀態是錯亂的,docker自己都沒法清理這個容器,可想而知kubelet更是無能無力。workaround恢復操作也簡單,此時我只是簡單的重啟了下docker,目標容器就消失了,Pod的卡住狀態也很快恢復了。當然,若要深究,就需要看看docker側,為何這個容器的狀態錯亂了。- 更常見的情況是出現了殭屍程式,對應容器清理不了,Pod自然也會卡在Terminating狀態。此時要想恢復,可能就只能重啟機器了。
- Volume清理不了 - 我們知道在PV的"兩階段處理流程中",Attach&Dettach由Volume Controller負責,而Mount&Unmount則是kubelet要參與負責。筆者在日常中有看到一些因為自定義CSI的不完善,導致kubelet不能Unmount Volume,從而讓Pod卡住的場景。所以我們在日常開發和測試自定義CSI時,要小心這一點。
- cgroups沒刪除 - 啟用QoS功能來管理Pod的服務質量時,kubelet需要為Pod設定合適的cgroup level,而這是需要在相應的位置寫入合適配置檔案的。自然,這個配置也需要在Pod刪除時清理掉。筆者日常到是沒有碰到過cgroups清理不了的場景,所以此處暫且不表。
現實中導致Pod卡住的細分場景可能還有很多,但不用擔心,其實多數情況下通過檢視kubelet日誌都能很快定位出來的。之後順藤摸瓜,恢復方案也大多不難。
當然還有一些系統級或者基礎設施級異常,比如kubelet掛了,節點訪問不了API Server了,甚至節點當機等等,已經超過了kubelet的能力範疇,不在此討論範圍之類。
還有個注意點,如果你發現kubelet裡面的日誌有效資訊很少,要注意看是不是Log Level等級過低了。從原始碼看,很多更具體的資訊,是需要大於等於3級別才輸出的。
那Namespace卡在Terminating狀態的原因是啥?
顯而易見,刪除Namespace意味著要刪除其下的所有資源,而如果其中Pod刪除卡住了,那Namespace必然也會卡在Terminating狀態。
除此之外,結合日常使用,筆者發現CRD資源發生刪不掉的情況也比較高。這是為什麼呢?至此,那就不得不聊聊 Finalizers機制了。
官方有篇部落格專門講到了這個,裡面有個實驗挺有意思。隨便給一個configmap,加上個finalizers欄位之後,然後使用kubectl delete
刪除它就會發現,直接是卡住的,kubernetes自身永遠也刪不了它。
參考: https://kubernetes.io/blog/2021/05/14/using-finalizers-to-control-deletion/#understanding-finalizers
原因何在?
原來Finalizers在設計上就是個pre-delete的鉤子,其目的是讓相關控制器有機會做自定義的清理動作。通常控制器在清理完資源後,會將物件的finalizers欄位清空,然後kubernetes才能接著刪除物件。而像上面的實驗,沒有相關控制器能處理我們隨意新增的finalizers欄位,那物件當然會一直卡在Terminating狀態了。
自己開發CRD及Controller,因成熟度等因素,發生問題的概率自然比較大。除此之外,引入webhook(mutatingwebhookconfigurations/validatingwebhookconfigurations)出問題的概率也比較大,日常也要比較注意。
綜合來看,遇Namespace刪除卡住的場景,筆者認為,基本可以按以下思路排查:
kubectl get ns $NAMESPACE -o yaml
, 檢視conditions
欄位,看看是否有相關資訊- 如果上面不明顯,那就可以具體分析空間下,還遺留哪些資源,然後做更針對性處理
- 參考命令:
kubectl api-resources --verbs=list --namespaced -o name | xargs -n 1 kubectl get --show-kind --ignore-not-found -n $NAMESPACE
- 參考命令:
找準了問題原因,然後做相應處理,kubernetes自然能夠清理對應的ns物件。不建議直接清空ns的finalizers欄位做強制刪除,這會引入不可控風險。
參考: https://github.com/kubernetes/kubernetes/issues/60807#issuecomment-524772920
相關閱讀
前同事也有幾篇關於kubernetes資源刪除的文章,寫的非常好,推薦大家讀讀: