解讀Kubernetes常見退出碼
一、退出碼歷史
退出碼的歷史可以追溯到Unix操作系統(tǒng)的早期。在Unix系統(tǒng)中,進程退出碼是進程終止時向其父進程傳遞的一個整數(shù)值,用于表示進程的終止狀態(tài)。這個整數(shù)值通常在0到255之間,其中0表示進程成功終止,其他值通常用來表示不同的錯誤或異常情況。
進程退出碼最初被設(shè)計用于提供一種簡單的機制,使父進程能夠了解子進程的執(zhí)行結(jié)果。這使得父進程能夠根據(jù)子進程的退出碼來采取適當(dāng)?shù)男袆樱热缣幚礤e誤情況或繼續(xù)執(zhí)行其他操作。
在Unix系統(tǒng)中,特定的退出碼值通常具有特定的含義,例如:
- 0:表示成功執(zhí)行,沒有錯誤。
- 1:通常表示通用的錯誤。
- 2:表示命令的語法錯誤。
- 127:表示命令未找到。
隨著時間的推移,Unix操作系統(tǒng)的發(fā)展和不同的實現(xiàn),進程退出碼的含義可能有所不同,但基本的概念保持不變。
在Linux系統(tǒng)中,進程退出碼的使用與Unix系統(tǒng)類似。Linux繼承了Unix的進程管理機制,并在其基礎(chǔ)上進行了擴展和改進。因此,Linux中的進程退出碼仍然是一個重要的概念,用于幫助理解和診斷進程的執(zhí)行狀態(tài)。
進程退出碼的歷史可以追溯到早期的Unix系統(tǒng),是Unix和Linux操作系統(tǒng)中的一個重要概念,為進程間通信提供了一種簡單而有效的機制。當(dāng)應(yīng)用程序或命令因致命錯誤而終止或執(zhí)行失敗時,將產(chǎn)生 128 系列退出碼(128+n),其中 n 為信號編號。n 包括所有類型的終止代碼,如 SIGTERM、SIGKILL 等。
二、退出碼 127
退出碼 127 不是特定于 Kubernetes 的錯誤代碼,而是 Linux 和類 Unix 操作系統(tǒng)中使用的標準退出碼。當(dāng)然,我們在Kubernetes中經(jīng)常看到它,并且通常表示容器內(nèi)執(zhí)行的命令或二進制文件找不到。
一些標準的退出碼包括:
圖片
常見原因
讓我們看一下退出碼 127 的一些常見原因:
- 命令或二進制文件未安裝Kubernetes 容器的 command 字段中指定的可執(zhí)行文件未安裝在容器的文件系統(tǒng)中。需要確保所需的二進制文件或命令可用。
- 路徑或命令不正確Pod 定義中指定的命令不正確或在指定的路徑中不存在。這是錯誤的最常見原因之一,通常是由于 Dockerfile 或 pod spec中的entrypoint或command輸入不正確造成的。
- 缺少依賴在容器內(nèi)運行的應(yīng)用程序或腳本未安裝相關(guān)依賴。需要確保所有必需的依賴項包含在容器映像中。
- shell 解釋器如果指定了腳本作為命令,需要確保腳本有效 (例如#!/bin/bash),且在容器中可用。
- shell 腳本語法錯誤如果 shell 腳本退出碼是127,請檢查腳本是否存有語法錯誤或可能阻止其執(zhí)行的問題。
- 權(quán)限不足在容器內(nèi)運行命令的用戶可能沒有執(zhí)行指定命令所需的必要權(quán)限。確保容器以適當(dāng)?shù)奶貦?quán)運行。
- 鏡像兼容性問題確保使用的容器鏡像與宿主機架構(gòu)和操作系統(tǒng)兼容。不匹配的映像可能導(dǎo)致命令找不到,比如x86的鏡像運行在arm的機器上
- 卷掛載如果命令是卷掛載的文件,請檢查卷掛載是否配置正確,且所需的文件可以被訪問到。
- 環(huán)境變量一些命令可能依賴于特定的環(huán)境變量。確保必需的環(huán)境變量設(shè)置正確。
- Kubernetes RBAC 策略 如果啟用了RBAC,需要確保具有執(zhí)行指定命令所需的權(quán)限。
如何排查
要排除問題,可以使用以下命令檢查 Pod 的日志:
kubectl logs -f <pod-name>
還可以檢查 Pod 狀態(tài),該狀態(tài)提供有關(guān) Pod 的詳細信息,包括其當(dāng)前狀態(tài)、最近事件和任何錯誤消息。
kubectl describe pod <pod-name>
還可以為把調(diào)試容器attach到Pod 中,該容器包括一個 shell(例如 BusyBox)。這允許您進入容器并手動檢查環(huán)境、路徑和命令的可用性。
使用 BusyBox 進行調(diào)試的示例:
containers:
- name: my-container
image: my-image:latest
command: ["/bin/sleep", "infinity"]
- name: debug-container
image: busybox:latest
command: ["/bin/sh"]
tty: true
stdin: true
如果是高版本K8s,也可以使用Ephemeral Containers,它就是一個臨時容器。這是一個自Kubernetes v1.16中作為alpha引入的新功能,啟用臨時容器的特性也非常簡單,在kubernetes v1.16之后的版本中將啟動參數(shù)--feature-gates=EphemeralCnotallow=true配置到kube-api和kubelet服務(wù)上重啟即可。
通過仔細查看日志并排查上述幾個方向,應(yīng)該能夠確定退出碼 127 問題的原因。
如何修復(fù)
我們知道了退出碼 127 的常見原因以及排查方式,現(xiàn)在讓我們看看如何修復(fù)它們。
- 命令或二進制文件未安裝
如果所需的命令或二進制文件丟失,則可能需要在容器鏡像中安裝。修改 Dockerfile 或構(gòu)建過程安裝所需軟件。
示例:
FROM alpine:latest
RUN apk --no-cache add <package-name>
- 路徑或命令不正確
在 Pod 定義中指定命令時,考慮使用二進制文件的絕對路徑。這有助于確保不受當(dāng)前工作目錄的影響, runtime可以找到二進制文件。
示例:
containers:
- name: my-container
image: my-image:latest
command: ["/usr/local/bin/my-command"]
- 缺少依賴項
導(dǎo)致命令無法運行的原因可能是容器鏡像需要安裝額外的軟件。如果命令需要額外的設(shè)置或安裝步驟,可以使用init容器在主容器啟動之前執(zhí)行這些任務(wù)。
示例(使用init容器安裝軟件包):
initContainers:
- name: install-package
image: alpine:latest
command: ["apk", "--no-cache", "add", "<package-name>"]
volumeMounts:
- name: shared-data
mountPath: /data
- shell解釋器
如果指定了腳本作為命令,需要確保腳本有效 (例如#!/bin/bash),且在容器中可用。
示例:
#!/bin/bash
- 卷掛載
檢查Pod的配置,確保卷已正確掛載。驗證卷名稱、掛載路徑和 subPaths是否正確。
示例:
volumes:
- name: my-volume
emptyDir: {}
containers:
- name: my-container
image: my-image:latest
volumeMounts:
- name: my-volume
mountPath: /path/in/container
同時我們需要確認Pod 定義指定的卷存在且可用。如果是持久卷(PV),需要檢查其狀態(tài)。如果是 emptyDir 或其他類型的卷,需要驗證其是否正確創(chuàng)建和掛載。如果在卷掛載中使用了 subPaths,需要確保源目錄或文件中存在指定的 subPaths。
示例:
volumeMounts:
- name: my-volume
mountPath: /path/in/container
subPath: my-file.txt
三、退出碼 137
在Kubernetes中,137退出碼表示進程被強制終止。在Unix和Linux系統(tǒng)中,當(dāng)進程由于信號而終止時,退出碼由信號編號加上128確定。信號編號為9,意味著“SIGKILL”,因此將9加上128,得到137退出碼。
當(dāng)Kubernetes集群中容器超出其內(nèi)存限制時,它可能會被Kubernetes系統(tǒng)終止,并顯示“OOMKilled”錯誤,這表示進程因內(nèi)存不足而被終止。此錯誤的退出碼為137OOM代表“內(nèi)存耗盡(out-of-memory)”。
如果Pod狀態(tài)將顯示為“OOMKilled”,你可以使用以下命令查看:
kubectl describe pod <podname>
OOMKiller
OOMKiller是Linux內(nèi)核中的一種機制,它負責(zé)通過終止消耗過多內(nèi)存的進程來防止系統(tǒng)耗盡內(nèi)存。當(dāng)系統(tǒng)內(nèi)存耗盡時,內(nèi)核會調(diào)用OOMKiller來選擇一個要終止的進程,以釋放內(nèi)存并保持系統(tǒng)運行。
內(nèi)核中有兩種不同的OOM Killer;一種是全局的OOM Killer,另一種是基于cgroup內(nèi)存控制器的OOM Killer,可以是cgroup v1或cgroup v2。
簡單來說是,當(dāng)內(nèi)核在分配物理內(nèi)存頁面時遇到問題時,全局的OOM Killer 會觸發(fā)。當(dāng)內(nèi)核嘗試分配內(nèi)存頁面(無論是用于內(nèi)核使用還是用于需要頁面的進程),并且最初失敗時,它將嘗試各種方式來回收和整理內(nèi)存。如果這種嘗試成功或者至少取得了一些進展,內(nèi)核將繼續(xù)重試分配;如果無法釋放頁面或者取得進展,在許多情況下它將觸發(fā)OOM Killer。
一旦OOMKiller選擇要終止的進程,它會向該進程發(fā)送信號,要求其優(yōu)雅地終止。如果進程不響應(yīng)信號,則內(nèi)核會強制終止該進程并釋放其內(nèi)存。
注意:由于內(nèi)存問題而被終止的Pod不一定會被節(jié)點驅(qū)逐,如果其設(shè)置的重啟策略設(shè)置為“Always”,它將嘗試重新啟動Pod。
在系統(tǒng)層面,Linux內(nèi)核為運行在主機上的每個進程維護一個oom_score。進程被終止的機率取決于分數(shù)有多高。
oom_score_adj值允許用戶自定義OOM進程,并定義何時應(yīng)終止進程。Kubernetes在定義Pod的Quality of Service(QoS)時使用oom_score_adj值。
K8s針對Pod定義了三種QoS,每個類型具有對應(yīng)的oom_score_adj值:
- Guaranteed: -997
- BestEffort: 1000
- Burstable: min(max(2, 1000 — (1000 * memoryRequestBytes) / machineMemoryCapacityBytes), 999)
其中Pod為Guaranteed QoS,則其oom_score_adj的值是-997,因此它們在節(jié)點內(nèi)存不足時最后一個被終止。BestEffort Pod配置的是1000,所以它們第一個被被終止。
要查看Pod的QoS,可以通過下述命令:
kubectl get pod -o jsnotallow='{.status.qosClass}'
下面是定義PodGuaranteed QoS 類型的計算策略:
- Pod 中的每個容器必須有內(nèi)存 limit 和內(nèi)存 request。
- 對于 Pod 中的每個容器,內(nèi)存 limit 必須等于內(nèi)存 request。
- Pod 中的每個容器必須有 CPU limit 和 CPU request。
- 對于 Pod 中的每個容器,CPU limit 必須等于 CPU request。
退出碼137通常有兩種情況:
1. 最常見的原因是與資源限制相關(guān)。通常情況下,Kubernetes超出了容器的分配內(nèi)存限制。
2. 另一種情況是手動干預(yù) - 用戶或腳本可能會向容器進程發(fā)送“SIGKILL”信號,導(dǎo)致此退出碼。
如何排查
- 檢查Pod日志
診斷OOMKilled錯誤的第一步是檢查Pod日志,查看是否有任何內(nèi)存相關(guān)的錯誤消息。
kubectl describe pod <podname>
State: Running
Started: Fri, 12 May 2023 11:14:13 +0200
Last State: Terminated
Reason: OOMKilled
Exit Code: 137
...
您還可以查詢Pod日志:
cat /var/log/pods/<podname>
當(dāng)然也可以通過(標準輸出)
kubectl logs -f <podname>
- 監(jiān)視內(nèi)存使用情況
使用監(jiān)視系統(tǒng)(如Prometheus或Grafana)監(jiān)視Pod和容器中的內(nèi)存使用情況。這可以幫助我們排查出哪些容器消耗了過多的內(nèi)存從而觸發(fā)了OOMKilled錯誤,同時也可以在容器宿主機使用dmesg查看當(dāng)時oomkiller的現(xiàn)場
- 使用內(nèi)存分析器
使用內(nèi)存分析器(如pprof)來識別可能導(dǎo)致過多內(nèi)存使用的內(nèi)存泄漏或低效代碼。
如何修復(fù)
以下是OOMKilled Kubernetes錯誤的常見原因及其解決方法。
- 容器內(nèi)存限制已達到
這可能是由于在容器指定的內(nèi)存限制值設(shè)置不當(dāng)導(dǎo)致的。解決方法是增加內(nèi)存限制的值,或者調(diào)查導(dǎo)致負載增加的根本原因并進行糾正。導(dǎo)致這種情況的常見原因包括大文件上傳,因為上傳大文件可能會消耗大量內(nèi)存資源,特別是當(dāng)多個容器在一個Pod內(nèi)運行時,以及突然增加的流量量。
- 因為應(yīng)用程序內(nèi)存泄漏,容器內(nèi)存使用達到上限
需要調(diào)試應(yīng)用程序來定位內(nèi)存泄漏的原因,
- 所有Pod使用的總內(nèi)存大于節(jié)點可用內(nèi)存
通過增加節(jié)點可用內(nèi)存來增加節(jié)點內(nèi)存,或者將Pod遷移到內(nèi)存更多的節(jié)點。當(dāng)然也可以調(diào)整運行在節(jié)點上的Pod的內(nèi)存限制,使其符合內(nèi)存限制,注意你還應(yīng)該注意內(nèi)存請求設(shè)置,它指定了Pod應(yīng)該使用的最小內(nèi)存量。如果設(shè)置得太高,可能不是有效利用可用內(nèi)存,關(guān)于資源配置相關(guān)的建議,可以參看VPA組件
在調(diào)整內(nèi)存請求和限制時,當(dāng)節(jié)點過載時,Kubernetes按照以下優(yōu)先級順序終止Pod:
- 沒有請求或限制的Pod。
- 具有請求但沒有限制的Pod。
- 使用超過其內(nèi)存請求值的內(nèi)存 - 指定的最小內(nèi)存值 - 但低于其內(nèi)存限制的Pod。
- 使用超過其內(nèi)存限制的Pod。
如何預(yù)防
有幾種方法可以防止OOMKilled的發(fā)生:
- 設(shè)置適當(dāng)?shù)膬?nèi)存限制
通過壓測及監(jiān)控來確定應(yīng)用程序的內(nèi)存使用,通過上述方式配置容器允許使用的最大內(nèi)存量。過度保守可能會導(dǎo)致因資源利用率低效而造成資金的浪費,同時低估會導(dǎo)致頻繁出現(xiàn)OOMKilled現(xiàn)象。
- HPA
最佳做法是利用K8s提供的HPA機制,當(dāng)應(yīng)用程序的內(nèi)存使用升高時自動增加Pod副本數(shù)量。
- 節(jié)點資源分配
確保節(jié)點具有足夠的資源來處理業(yè)務(wù)。
- 優(yōu)化應(yīng)用程序內(nèi)存使用
監(jiān)視應(yīng)用程序并進行適當(dāng)優(yōu)化,以減少內(nèi)存消耗。
- 避免應(yīng)用程序中的內(nèi)存泄漏
從應(yīng)用程序來看,需要長期檢查并修復(fù)內(nèi)存泄漏。
由于筆者時間、視野、認知有限,本文難免出現(xiàn)錯誤、疏漏等問題,期待各位讀者朋友、業(yè)界專家指正交流。
參考文獻
1. https://spacelift.io/blog/oomkilled-exit-code-137
2. https://spacelift.io/blog/exit-code-127
3. https://cloud.tencent.com/developer/news/1152344
4. https://utcc.utoronto.ca/~cks/space/blog/linux/OOMKillerWhen
本文轉(zhuǎn)載自微信公眾號「 DCOS」,作者「DCOS」,可以通過以下二維碼關(guān)注。
轉(zhuǎn)載本文請聯(lián)系「DCOS」公眾號。