聊聊 Kubernetes Pod or Namespace 卡在 Terminating 狀態的場景

Changjun Ji發表於2021-10-29

這個話題,想必玩過 kubernetes 的同學當不陌生,我會分 Pod 和 Namespace 分別來談。

開門見山,為什麼 Pod 會卡在 Terminating 狀態?

一句話,本質是 API Server 雖然標記了物件的刪除,但是作為實際清理的控制器 kubelet, 並不能關停 Pod 或相關資源, 因而沒能通知 API Server 做實際物件的清理。

原因何在?要解開這個原因,我們先來看 Pod Terminating 的基本流程:

  1. 客戶端 (比如 kubectl) 提交刪除請求到 API Server
    • 可選傳遞 --grace-period 引數
  2. API Server 接受到請求之後,做 Graceful Deletion 檢查
    • 若需要 graceful 刪除時,則更新物件的 metadata.deletionGracePeriodSeconds 和 metadata.deletionTimestamp 欄位。這時候 describe 檢視物件的話,會發現其已經變成 Terminating 狀態了
  3. Pod 所在的節點,kubelet 檢測到 Pod 處於 Terminating 狀態時,就會開啟 Pod 的真正刪除流程
    • 如果 Pod 中的容器有定義 preStop hook 事件,那 kubelet 會先執行這些容器的 hook 事件
    • 之後,kubelet 就會 Trigger 容器執行時發起TERMsignal 給該 Pod 中的每個容器
  4. 在 Kubelet 開啟 Graceful Shutdown 的同時,Control Plane 也會從目標 Service 的 Endpoints 中摘除要關閉的 Pod。ReplicaSet 和其他的 workload 服務也會認定這個 Pod 不是個有效副本了。同時,Kube-proxy 也會摘除這個 Pod 的 Endpoint,這樣即使 Pod 關閉很慢,也不會有流量再打到它上面。
  5. 如果容器正常關閉那很好,但如果在 grace period 時間內,容器仍然執行,kubelet 會開始強制 shutdown。容器執行時會傳送SIGKILL訊號給 Pod 中所有執行的程序進行強制關閉
  6. 注意在開啟 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
}

原始碼位置: https://github.com/kubernetes/kubernetes/blob/1f2813368eb0eb17140caa354ccbb0e72dcd6a69/pkg/kubelet/kubelet_pods.go#L923

是不是很清晰?總結下來就三個原因:

  1. Pod 裡沒有 Running 的容器
  2. Pod 的 Volume 也清理乾淨了
  3. 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 刪除卡住的場景,筆者認為,基本可以按以下思路排查:

  1. kubectl get ns $NAMESPACE -o yaml, 檢視conditions欄位,看看是否有相關資訊
  2. 如果上面不明顯,那就可以具體分析空間下,還遺留哪些資源,然後做更針對性處理
    • 參考命令: 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 資源刪除的文章,寫的非常好,推薦大家讀讀:

  • https://zhuanlan.zhihu.com/p/164601470
  • https://zhuanlan.zhihu.com/p/161072336

相關文章