解讀 K8s Pod 的13種典型異常

在K8s中,Pod作爲工作負載的運行載體,是最爲核心的一個資源對象。Pod具有複雜的生命週期,在其生命週期的每一個階段,可能發生多種不同的異常情況。K8s作爲一個複雜系統,異常診斷往往要求強大的知識和經驗儲備。結合實戰經歷以及EDAS用戶真實場景的歸納,我們總結了K8s Pod的13種常見異常場景,給出各個場景的常見錯誤狀態,分析其原因和排查思路。

本文篇幅超過7千字,通讀全文大概需要20分鐘。文章內容源自大量真實場景的沉澱和分析,建議收藏,以供查閱。

Pod生命週期

在整個生命週期中,Pod會出現5種階段(Phase)。

  • Pending:Pod被K8s創建出來後,起始於Pending階段。在Pending階段,Pod將經過調度,被分配至目標節點開始拉取鏡像、加載依賴項、創建容器。
  • Running:當Pod所有容器都已被創建,且至少一個容器已經在運行中,Pod將進入Running階段。
  • Succeeded:當Pod中的所有容器都執行完成後終止,並且不會再重啓,Pod將進入Succeeded階段。
  • Failed:若Pod中的所有容器都已終止,並且至少有一個容器是因爲失敗終止,也就是說容器以非0狀態異常退出或被系統終止,Pod將進入Failed階段。
  • Unkonwn:因爲某些原因無法取得 Pod 狀態,這種情況Pod將被置爲Unkonwn狀態。

一般來說,對於Job類型的負載,Pod在成功執行完任務之後將會以Succeeded狀態爲終態。而對於Deployment等負載,一般期望Pod能夠持續提供服務,直到Pod因刪除消失,或者因異常退出/被系統終止而進入Failed階段。

Pod的5個階段是 Pod 在其生命週期中所處位置的簡單宏觀概述,並不是對容器或 Pod 狀態的綜合彙總。Pod有一些細分狀態( PodConditions ),例如Ready/NotReady、Initialized、 PodScheduled/Unschedulable等。這些細分狀態描述造成Pod所處階段的具體成因是什麼。比如,Pod 當前階段是Pending,對應的細分狀態是 Unschedulable,這就意味着Pod調度出現了問題。

容器也有其生命週期狀態(State):Waiting、Running和 Terminated。並且也有其對應的狀態原因(Reason),例如ContainerCreating、Error、OOMKilled、CrashLoopBackOff、Completed等。而對於發生過重啓或終止的容器,上一個狀態(LastState)字段不僅包含狀態原因,還包含上一次退出的狀態碼(Exit Code)。例如容器上一次退出狀態碼是137,狀態原因是OOMKilled,說明容器是因爲OOM被系統強行終止。在異常診斷過程中,容器的退出狀態是至關重要的信息。

除了必要的集羣和應用監控,一般還需要通過kubectl命令蒐集異常狀態信息。

// 獲取Pod當前對象描述文件
kubectl get pod <podName> -n <namespace> -o yaml 

// 獲取Pod信息和事件(Events)
kubectl describe pod <podName> -n <namespace>

// 獲取Pod容器日誌
kubectl logs <podName> <containerName> -n <namespace>

// 在容器中執行命令
kubectl exec <podName> -n <namespace> -c <containerName> -- <CMD> <ARGS>

Pod異常場景

Pod在其生命週期的許多時間點可能發生不同的異常,按照Pod容器是否運行爲標誌點,我們將異常場景大致分爲兩類:

  1. 在Pod進行調度並創建容器過程中發生異常,此時Pod將卡在Pending階段。
  2. Pod容器運行中發生異常,此時Pod按照具體場景處在不同階段。

下文將對這具體的13種場景進行描述和分析。

調度失敗

常見錯誤狀態:Unschedulable

Pod被創建後進入調度階段,K8s調度器依據Pod聲明的資源請求量和調度規則,爲Pod挑選一個適合運行的節點。當集羣節點均不滿足Pod調度需求時,Pod將會處於Pending狀態。造成調度失敗的典型原因如下:

  • 節點資源不足

K8s將節點資源(CPU、內存、磁盤等)進行數值量化,定義出節點資源容量(Capacity)和節點資源可分配額(Allocatable)。資源容量是指 Kubelet 獲取的計算節點當前的資源信息,而資源可分配額是Pod可用的資源。Pod容器有兩種資源額度概念:請求值Request和限制值Limit,容器至少能獲取請求值大小、至多能獲取限制值的資源量。Pod 的資源請求量是Pod中所有容器的資源請求之和,Pod的資源限制量是Pod中所有容器的資源限制之和。K8s默認調度器按照較小的請求值作爲調度依據,保障可調度節點的資源可分配額一定不小於Pod資源請求值。當集羣沒有一個節點滿足Pod的資源請求量,則Pod將卡在Pending狀態。

Pod因爲無法滿足資源需求而被Pending,可能是因爲集羣資源不足,需要進行擴容,也有可能是集羣碎片導致。以一個典型場景爲例,用戶集羣有10幾個4c8g的節點,整個集羣資源使用率在60%左右,每個節點都有碎片,但因爲碎片太小導致擴不出來一個2c4g的Pod。一般來說,小節點集羣會更容易產生資源碎片,而碎片資源無法供Pod調度使用。如果想最大限度地減少資源浪費,使用更大的節點可能會帶來更好的結果。

  • 超過Namespace資源配額

K8s用戶可以通過資源配額(Resource Quota)對Namespace進行資源使用量限制,包括兩個維度:

1. 限定某個對象類型(如Pod)可創建對象的總數。

2. 限定某個對象類型可消耗的資源總數。

如果在創建或更新Pod時申請的資源超過了資源配額,則Pod將調度失敗。此時需要檢查Namespace資源配額狀態,做出適當調整。

  • 不滿足 NodeSelector節點選擇器

Pod通過NodeSelector節點選擇器指定調度到帶有特定Label的節點,若不存在滿足 NodeSelector的可用節點,Pod將無法被調度,需要對NodeSelector或節點Label進行合理調整。

  • 不滿足親和性

節點親和性(Affinity)和反親和性(Anti-Affinity)用於約束Pod調度到哪些節點,而親和性又細分爲軟親和(Preferred)和硬親和(Required)。對於軟親和規則,K8s調度器會嘗試尋找滿足對應規則的節點,如果找不到匹配的節點,調度器仍然會調度該 Pod。而當硬親和規則不被滿足時,Pod將無法被調度,需要檢查Pod調度規則和目標節點狀態,對調度規則或節點進行合理調整。

  • 節點存在污點

K8s提供污點(Taints)和容忍(Tolerations)機制,用於避免 Pod 被分配到不合適的節點上。假如節點上存在污點,而 Pod 沒有設置相應的容忍,Pod 將不會調度到該 節點。此時需要確認節點是否有攜帶污點的必要,如果不必要的話可以移除污點;若Pod可以分配到帶有污點的節點,則可以給Pod增加污點容忍。

  • 沒有可用節點

節點可能會因爲資源不足、網絡不通、Kubelet未就緒等原因導致不可用(NotReady)。當集羣中沒有可調度的節點,也會導致Pod卡在Pending狀態。此時需要查看節點狀態,排查不可用節點問題並修復,或進行集羣擴容。

鏡像拉取失敗

常見錯誤狀態:ImagePullBackOff

Pod經過調度後分配到目標節點,節點需要拉取Pod所需的鏡像爲創建容器做準備。拉取鏡像階段可能存在以下幾種原因導致失敗:

  • 鏡像名字拼寫錯誤或配置了錯誤的鏡像

出現鏡像拉取失敗後首先要確認鏡像地址是否配置錯誤。

  • 私有倉庫的免密配置錯誤

集羣需要進行免密配置才能拉取私有鏡像。自建鏡像倉庫時需要在集羣創建免密憑證Secret,在Pod指定ImagePullSecrets,或者將Secret嵌入ServicAccount,讓Pod使用對應的ServiceAccount。而對於acr等鏡像服務雲產品一般會提供免密插件,需要在集羣中正確安裝免密插件才能拉取倉庫內的鏡像。免密插件的異常包括:集羣免密插件未安裝、免密插件Pod異常、免密插件配置錯誤,需要查看相關信息進行進一步排查。

  • 網絡不通

網絡不通的常見場景有三個:

1. 集羣通過公網訪問鏡像倉庫,而鏡像倉庫未配置公網的訪問策略。對於自建倉庫,可能是端口未開放,或是鏡像服務未監聽公網IP;對於acr等鏡像服務雲產品,需要確認開啓公網的訪問入口,配置白名單等訪問控制策略。

2. 集羣位於專有網絡,需要爲鏡像服務配置專有網絡的訪問控制,才能建立集羣節點與鏡像服務之間的連接。

3. 拉取海外鏡像例如http://gcr.io倉庫鏡像,需配置鏡像加速服務。

  • 鏡像拉取超時

常見於帶寬不足或鏡像體積太大,導致拉取超時。可以嘗試在節點上手動拉取鏡像,觀察傳輸速率和傳輸時間,必要時可以對集羣帶寬進行升配,或者適當調整 Kubelet 的 --image-pull-progress-deadline 和 --runtime-request-timeout 選項。

  • 同時拉取多個鏡像,觸發並行度控制

常見於用戶彈性擴容出一個節點,大量待調度Pod被同時調度上去,導致一個節點同時有大量Pod啓動,同時從鏡像倉庫拉取多個鏡像。而受限於集羣帶寬、鏡像倉庫服務穩定性、容器運行時鏡像拉取並行度控制等因素,鏡像拉取並不支持大量並行。這種情況可以手動打斷一些鏡像的拉取,按照優先級讓鏡像分批拉取。

依賴項錯誤

常見錯誤狀態:Error

在 Pod 啓動之前,Kubelet將嘗試檢查與其他 K8s 元素的所有依賴關係。主要存在的依賴項有三種:PersistentVolume、ConfigMap和Secret。當這些依賴項不存在或者無法讀取時,Pod容器將無法正常創建,Pod會處於Pending狀態直到滿足依賴性。當這些依賴項能被正確讀取,但出現配置錯誤時,也會出現無法創建容器的情況。比如將一個只讀的持久化存儲卷PersistentVolume以可讀寫的形式掛載到容器,或者將存儲卷掛載到/proc等非法路徑,也會導致容器創建失敗。

容器創建失敗

常見錯誤狀態:Error

Pod容器創建過程中出現了錯誤。常見原因包括:

  • 違反集羣的安全策略,比如違反了 PodSecurityPolicy 等。
  • 容器無權操作集羣內的資源,比如開啓 RBAC 後,需要爲 ServiceAccount 配置角色綁定。
  • 缺少啓動命令,Pod描述文件和鏡像Dockerfile中均未指定啓動命令。
  • 啓動命令配置錯誤。Pod配置文件可以通過command字段定義命令行,通過args字段給命令行定義參數。啓動命令配置錯誤的情況非常多見,要格外注意命令及參數的格式。正確的填寫方式可參考:

初始化失敗

常見錯誤狀態:CrashLoopBackOff

K8s提供Init Container特性,用於在啓動應用容器之前啓動一個或多個初始化容器,完成應用程序所需的預置條件。Init container與應用容器本質上是一樣的,但它們是僅運行一次就結束的任務,並且必須在執行完成後,系統才能繼續執行下一個容器。如果 Pod 的Init Container執行失敗,將會block業務容器的啓動。通過查看Pod狀態和事件定位到Init Container故障後,需要查看Init Container日誌進一步排查故障點。

回調失敗

常見錯誤狀態:FailedPostStartHook或FailedPreStopHook事件

K8s提供PostStart和PreStop兩種容器生命週期回調,分別在容器中的進程啓動前或者容器中的進程終止之前運行。PostStart 在容器創建之後立即執行,但由於是異步執行,無法保證和容器啓動命令的執行順序相關聯。PreStop 在容器終止之前被同步阻塞調用,常用於在容器結束前優雅地釋放資源。如果PostStart或者PreStop 回調程序執行失敗,容器將被終止,按照重啓策略決定是否重啓。當出現回調失敗,會出現FailedPostStartHook或FailedPreStopHook事件,進一步結合容器打出的日誌進行故障排查。

就緒探針失敗

常見錯誤狀態:容器已經全部啓動,但是Pod處於NotReady狀態,服務流量無法從Service達到Pod

K8s使用Readiness Probe(就緒探針)來確定容器是否已經就緒可以接受流量。只有當Pod 中的容器都處於就緒狀態時,K8s 才認定該Pod 處於就緒狀態,纔會將服務流量轉發到該容器。一般就緒探針失敗分爲幾種情況:

  • 容器內應用原因: 健康檢查所配置規則對應的端口或者腳本,無法成功探測,如容器內應用沒正常啓動等。
  • 探針配置不當:寫錯檢查端口導致探測失敗;檢測間隔和失敗閾值設置不合理,例如每次檢查間隔1s,一次不通過即失敗;啓動延遲設置太短,例如應用正常啓動需要15s,而設置容器啓動10s後啓用探針。
  • 系統層問題:節點負載高,導致容器進程hang住。
  • CPU資源不足:CPU資源限制值過低,導致容器進程響應慢。

需要特別說明的是,對於微服務應用,服務的註冊和發現由註冊中心管理,流量不會經過Service,直接從上游Pod流到下游Pod。然而註冊中心並沒有如K8s就緒探針的檢查機制,對於啓動較慢的JAVA應用來說,服務註冊成功後所需資源仍然可能在初始化中,導致出現上線後流量有損的情況。對於這一類場景,EDAS提供延遲註冊和服務預熱等解決方案,解決K8s微服務應用上線有損的問題。

存活探針失敗

常見錯誤狀態:CrashLoopBackOff

K8s使用Liveness Probe(存活探針)來確定容器是否正在運行。如果存活態探測失敗,則容器會被殺死,隨之按照重啓策略決定是否重啓。存活探針失敗的原因與就緒探針類似,然而存活探針失敗後容器會被kill消失,所以排障過程要棘手得多。一個典型的用戶場景是,用戶在壓測期間通過HPA彈性擴容出多個新Pod,然而新Pod一啓動就被大流量阻塞,無法響應存活探針,導致Pod被kill。kill後又重啓,重啓完又掛掉,一直在Running和CrashLoopBackOff狀態中振盪。微服務場景下可以使用延遲註冊和服務預熱等手段,避免瞬時流量打掛容器。如果是程序本身問題導致運行阻塞,建議先將Liveness探針移除,通過Pod啓動後的監控和進程堆棧信息,找出流量湧入後進程阻塞的根因。

容器退出

常見錯誤狀態:CrashLoopBackOff

容器退出分爲兩種場景:

  • 啓動後立即退出,可能原因是:

1. 啓動命令的路徑未包含在環境變量PATH中。

2. 啓動命令引用了不存在的文件或目錄。

3. 啓動命令執行失敗,可能因爲運行環境缺少依賴,也可能是程序本身原因。

4. 啓動命令沒有執行權限。

5. 容器中沒有前臺進程。容器應該至少包含一個long-running的前臺進程,不能後臺運行,比如通過nohup這種方式去啓動進程,或是用tomcat的startup.sh腳本。

對於容器啓動後立即退出的情況,通常因爲容器直接消失,無法獲取其輸出流日誌,很難直接通過現場定位問題。一個簡易的排查方式是,通過設置特殊的啓動命令卡住容器(比如使用tail -f /dev/null),然後進到容器中手動執行命令看看結果,確認問題原因。

  • 運行一段時間後退出,這種情況一般是容器內1進程Crash或者被系統終止導致退出。此時首先查看容器退出狀態碼,然後進一步查看上下文信息進行錯誤定位。這種情況發生時容器已經刪除消失,無法進入容器中查看日誌和堆棧等現場信息,所以一般推薦用戶對日誌、錯誤記錄等文件配置持久化存儲,留存更多現場信息。幾種常見的狀態碼如下:
狀態碼 含義 分析
0 正常退出 容器的啓動程序不是一個long-running的程序。如果正常退出不符合預期,需要檢查容器日誌,對程序的執行邏輯進行調整。
137 外部終止 137 表示容器已收到來自主機操作系統的 SIGKILL 信號。該信號指示進程立即終止,沒有寬限期。可能原因包含:容器運行時將容器kill,例如docker kill命令;Linux 用戶向進程發送 kill -9 命令觸發;K8s嘗試終止容器,超過優雅下線窗口期後直接kill容器;由節點系統觸發,比如遭遇了OOM。
139 段錯誤 139表示容器收到了來自操作系統的 SIGSEGV 信號。這表示分段錯誤 —— 內存違規,由容器試圖訪問它無權訪問的內存位置引起。
143 優雅終止 143 表示容器收到來自操作系統的 SIGTERM 信號,該信號要求容器正常終止。該退出碼可能的原因是:容器引擎停止容器,例如使用 docker stop 停止了容器;K8s終止了容器,比如縮容行爲將Pod刪除。

OOMKilled

常見錯誤狀態:OOMKilled

K8s中有兩種資源概念:可壓縮資源(CPU)和不可壓縮資源(內存,磁盤 )。當CPU這種可壓縮資源不足時,Pod只會“飢餓”,但不會退出;而當內存和磁盤IO這種不可壓縮資源不足時,Pod會被kill或者驅逐。因爲內存資源不足/超限所導致的Pod異常退出的現象被稱爲Pod OOMKilled。K8s存在兩種導致Pod OOMKilled的場景:

  • Container Limit Reached,容器內存用量超限

Pod內的每一個容器都可以配置其內存資源限額,當容器實際佔用的內存超額,該容器將被OOMKilled並以狀態碼137退出。OOMKilled往往發生在Pod已經正常運行一段時間後,可能是由於流量增加或是長期運行累積的內存逐漸增加。這種情況需要查看程序日誌以瞭解爲什麼Pod使用的內存超出了預期,是否出現異常行爲。如果發現程序只是按照預期運行就發生了OOM,就需要適當提高Pod內存限制值。一個很常見的錯誤場景是,JAVA容器設置了內存資源限制值Limit,然而JVM堆大小限制值比內存Limit更大,導致進程在運行期間堆空間越開越大,最終因爲OOM被終止。對於JAVA容器來說,一般建議容器內存限制值Limit需要比JVM 最大堆內存稍大一些。

  • Limit Overcommit,節點內存耗盡

K8s有兩種資源額度概念:請求值Request和限制值Limit,默認調度器按照較小的請求值作爲調度依據,保障節點的所有Pod資源請求值總和不超過節點容量,而限制值總和允許超過節點容量,這就是K8s資源設計中的Overcommit(超賣)現象。超賣設計在一定程度上能提高吞吐量和資源利用率,但會出現節點資源被耗盡的情況。當節點上的Pod實際使用的內存總和超過某個閾值,K8s將會終止其中的一個或多個Pod。爲了儘量避免這種情況,建議在創建Pod時選擇大小相等或相近的內存請求值和限制值,也可以利用調度規則將內存敏感型Pod打散到不同節點。

Pod驅逐

常見錯誤狀態:Pod Evicted

當節點內存、磁盤這種不可壓縮資源不足時,K8s會按照QoS等級對節點上的某些Pod進行驅逐,釋放資源保證節點可用性。當Pod發生驅逐後,上層控制器例如Deployment會新建Pod以維持副本數,新Pod會經過調度分配到其他節點創建運行。對於內存資源,前文已經分析過可以通過設置合理的請求值和限制值,避免節點內存耗盡。而對於磁盤資源,Pod在運行期間會產生臨時文件、日誌,所以必須對Pod磁盤容量進行限制,否則某些Pod可能很快將磁盤寫滿。類似限制內存、CPU 用量的方式,在創建Pod時可以對本地臨時存儲用量(ephemeral-storage)進行限制。同時,Kubelet驅逐條件默認磁盤可用空間在10%以下,可以調整雲監控磁盤告警閾值以提前告警。

Pod失聯

常見錯誤狀態:Unkonwn

Pod處於Unkonwn狀態,無法獲取其詳細信息,一般是因爲所在節點Kubelet異常,無法向APIServer上報Pod信息。首先檢查節點狀態,通過Kubelet和容器運行時的日誌信息定位錯誤,進行修復。如果無法及時修復節點,可以先將該節點從集羣中刪除。

無法被刪除

常見錯誤狀態:卡在Terminating

當一個Pod被執行刪除操作後,卻長時間處於Terminating狀態,這種情況的原因有幾種:

  • Pod關聯的finalizer未完成。首先查看Pod的metadata字段是否包含finalizer,通過一些特定上下文信息確認finalizer任務具體是什麼,通常finalizer的任務未完成可能是因爲與Volume相關。如果finalizer已經無法被完成,可以通過patch操作移除對應的Pod上的finalizer完成刪除操作。
  • Pod對中斷信號沒有響應。Pod沒有被終止可能是進程對信號沒有響應,可以嘗試強制刪除Pod。
  • 節點故障。通過查看相同節點上的其他Pod狀態確認是否節點故障,嘗試重啓Kubelet和容器運行時。如果無法修復,先將該節點從集羣中刪除。

EDAS排障工具鏈

EDAS對應用全生命週期的大部分異常都有沉澱和分析,降低用戶學習成本,縮短排障時間。EDAS提供一系列解決方案和工具幫助用戶解決應用生命週期中的異常問題,包括應用變更前的變更預檢、應用變更和運行的事件追蹤可觀測、應用異常時的診斷工具。

應用變更預檢

EDAS在應用變更任務下發前將經過預檢環節,應用變更預檢可以在應用部署前檢查集羣狀態及變更參數是否有效,能夠有效避免應用變更過程出錯,降低變更風險。當前應用變更預檢提供集羣可用資源檢查、集羣健康檢查、各項依賴配置檢查等項目,對於非預期的預檢結果給出分析和處置建議。例如對於集羣資源餘量不滿足Pod調度需求的異常場景,變更預檢結果將顯示資源檢查不通過,用戶能夠第一時間做出針對性調整。

應用事件觀測

EDAS對應用生命週期中的事件進行追蹤提供可觀測能力。對於應用變更過程提供完整的事項展示,讓用戶能夠白屏觀測到變更中的每一個步驟和相關上下文信息。當出現異常變更情況時,將具體的事件和相關資源信息在白屏透出,並對異常事件進行分析解讀並給出操作建議。例如給Pod配置了容器服務倉庫鏡像,但並未正確配置集羣免密插件,EDAS將鏡像拉取失敗事件拋出,並引導用戶檢查鏡像拉取權限。

診斷工具箱

對於異常Pod,通常需要連接到Pod容器,對業務進程進行診斷,必要時候還需要對異常進行復現。EDAS提供雲原生工具箱,讓用戶在網頁上連接Pod容器Shell,並且提供Arthas、Tcpdump等工具,彌補鏡像軟件工具包的缺失。對於Pod已經消失、不適合在業務Pod進行診斷等場景,雲原生工具箱提供Pod複製能力,根據診斷場景不同,用戶可以按需選擇開啓診斷Pod。

對於上文中提到的容器進程被大流量阻塞,導致Pod被Liveness打掛的場景,用戶可以通過雲原生工具箱,開啓一個移除Liveness的診斷Pod,設置全鏈路流量控制規則,打入一些測試流量,使用Arthas提供的trace、stack、watch等工具精準定位問題

參考文檔

原文鏈接

本文爲阿里雲原創內容,未經允許不得轉載。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章