Cột STATUS trong kubectl get pods giống nhãn dán một người bảo vệ treo lên cửa: "đang thử mở lại". Nhãn đó cho biết người bảo vệ đang làm gì, không cho biết vì sao cửa không mở. Cửa kẹt vì hỏng ổ khoá, hay vì có người chặn bên trong, hay vì chưa ai đưa chìa — với người bảo vệ thì cả ba đều dẫn tới đúng một hành động: đứng đợi rồi thử lại. Nên cái nhãn "đang thử mở lại" đúng cho cả ba, và hoàn toàn vô dụng để biết nên gọi thợ khoá hay gọi bảo vệ. CrashLoopBackOff chính là cái nhãn đó. Bài này dựng bảy sự cố thật rồi đo xem chỗ nào mới chứa sự thật.
Ba phần trước đo từng công cụ riêng lẻ: nhật ký, sự kiện, kubectl debug. Bài này dựng bảy sự cố thật rồi đo xem công cụ nào trả lời được, và công cụ nào nói sai.
Bảy pod hỏng
STATUS ở kubectl get pods |
Nguyên nhân thật |
|---|---|
ImagePullBackOff |
image không tồn tại |
CreateContainerConfigError |
ConfigMap chưa có |
Pending |
không node nào đủ RAM |
CrashLoopBackOff |
ứng dụng thoát mã 3 |
CrashLoopBackOff |
lệnh khởi động không tồn tại |
ContainerCreating |
Secret chưa có |
OOMKilled |
vượt limit bộ nhớ |
Bốn dòng đầu và dòng cuối tương đối thẳng thắn. Hai dòng giữa là chỗ cột STATUS nói sai.
Dòng thứ năm: CrashLoopBackOff giả
Pod chạy lệnh /khong-co-lenh-nay. kubectl get pods báo:
CrashLoopBackOff 4 restarts
Nghe như ứng dụng khởi động rồi chết đi chết lại. Nhưng container chưa bao giờ chạy:
lastState: reason=StartError exitCode=128
kubectl logs -> rỗng
kubectl logs --previous -> rỗng
Sự thật nằm ở sự kiện:
Error: failed to create containerd task: failed to create shim task:
OCI runtime create failed: runc create failed: unable to start ...
CrashLoopBackOff chỉ mô tả cái kubelet đang làm — chờ rồi thử lại — chứ không mô tả cái đã xảy ra. Với kubelet, "container thoát mã 1" và "container không tạo nổi" đều dẫn tới cùng một hành vi, nên cùng một cột STATUS.
Quy tắc rút ra, dùng được ngay:
Nhật ký rỗng mà
restarts > 0thì container không crash — nó không khởi động nổi. Đọc sự kiện, đừng đọc nhật ký.
Ba nguyên nhân hay gặp của nhóm này: command trỏ vào tệp không có trong image; tệp có nhưng không có cờ thực thi; hoặc image dựng cho kiến trúc khác (exec format error). Trường hợp thứ ba đáng nói riêng: image amd64 chạy trên node arm64 báo lỗi đúng như vậy, và nó rất hay xảy ra khi máy phát triển là Mac còn cụm là x86.
Dòng thứ sáu: trạng thái nghe như đang tiến triển
Pod gắn một Secret chưa tồn tại:
ContainerCreating 104s
ContainerCreating nghe như "đang tạo, chờ chút". Nó sẽ đứng ở đó vĩnh viễn. kubectl get pods không hề đánh dấu đây là lỗi — không có chữ Error, không có BackOff, không có số restart. Một cái nhìn lướt qua danh sách pod sẽ bỏ sót nó.
Chỉ sự kiện mới nói:
FailedMount: MountVolume.SetUp failed for volume "v" :
secret "bi-mat-khong-ton-tai" not found
Nên với mọi pod ở ContainerCreating quá khoảng ba mươi giây, đừng chờ thêm — xem sự kiện ngay. Đó gần như luôn là volume, và volume thì không tự xuất hiện.
Dòng thứ tư: sự kiện vô dụng, nhật ký cứu
Pod thoát mã 3, có in lý do ra stderr. Sự kiện duy nhất:
BackOff: Back-off restarting failed container c in pod sc4-crashloop_kh(...)
Không nói gì cả. Ở đây sự kiện là chỗ tệ nhất để nhìn. Nhật ký thì nói thẳng:
loi cau hinh: thieu bien DB_HOST
Đây đúng là chiều ngược lại của dòng thứ năm — và đó chính là lý do phải phân biệt hai kiểu. Cùng một cột STATUS, hai chỗ tra hoàn toàn khác nhau. Nhớ --previous khi pod đang trong chu kỳ chờ: nhật ký của lần chạy hiện tại thường chưa có gì.
Dòng thứ ba: câu trả lời đầy đủ nhất
Pending là trạng thái được giải thích tốt nhất trong cả bảy:
0/4 nodes are available: 1 node(s) had untolerated taint(s),
3 Insufficient memory.
Nó đếm rành mạch từng nhóm node và lý do bị loại — đúng cái bộ lọc của scheduler đã tính. Không cần đoán gì. Chỗ chứa nó là conditions của pod, không phải containerStatuses (chưa có container nào):
kubectl get pod <ten> -o jsonpath='{.status.conditions[?(@.type=="PodScheduled")].message}'
Mã thoát nói container chết thế nào
| Mã | Nghĩa | Tra ở đâu |
|---|---|---|
| 0 | Thoát bình thường (restartPolicy: Always vẫn khởi động lại) |
— |
| 1–125 | Ứng dụng tự thoát | logs --previous |
| 128 | Không khởi động nổi | events |
| 137 | 128 + 9 = SIGKILL |
OOM, hoặc hết hạn ân hạn |
| 143 | 128 + 15 = SIGTERM |
Bị dừng tử tế |
Pod OOM đo được exitCode: 137 và STATUS OOMKilled — một trong số ít trường hợp cột STATUS nói đủ. Phân biệt 137 do OOM với 137 do hết hạn ân hạn: OOM thì reason là OOMKilled, còn hết hạn ân hạn thì reason là Error. Và nếu là hết hạn ân hạn, hãy đọc lại phần về tín hiệu dừng — nhiều khả năng PID 1 của bạn là một cái shell.
Thứ tự nên đọc
kubectl get pod <ten> -o wide # STATUS, restarts, node
kubectl describe pod <ten> | tail -25 # Events nằm ở cuối
kubectl logs <ten> --previous # nếu đã từng chạy
kubectl get pod <ten> -o jsonpath='{.status.containerStatuses[0].lastState}'
Bước hai giải quyết đa số. Bước ba và bốn là để phân biệt hai kiểu CrashLoopBackOff ở trên — mà đó lại là kiểu sự cố hay làm mất nhiều thời gian nhất, vì hai nguyên nhân khác hẳn nhau đội chung một cái tên.
Mẫu số chung: một mã trạng thái thô gộp nhiều nguyên nhân, vì nó gọi tên hành vi chứ không gọi tên lỗi
Bài này thật ra là về một cái bẫy có ở mọi lớp phần mềm: một nhãn trạng thái thô gộp nhiều nguyên nhân rất khác nhau vào một cái tên, bởi vì nó mô tả hành vi quan sát được hoặc phản ứng của bộ giám sát, chứ không mô tả cái đã sai. CrashLoopBackOff gọi tên việc kubelet đang làm (chờ, thử lại), nên "app chết" và "không tạo nổi container" đội chung nó — và bằng chứng thật thì nằm ở hai kho khác nhau (logs so với events). Hệ quả: nghe tên trạng thái mà đoán nguyên nhân là đi lạc; phải biết kho nào chứa sự thật cho từng nguyên nhân.
Trong thế giới web, HTTP 500 "Internal Server Error" là đúng cái nhãn thô đó — nó gộp hàng chục nguyên nhân, và sự thật nằm trong stack trace/log chứ không trong con số. Chính vì vậy, bắt catch (Exception) rồi trả 500 cho mọi thứ là một lỗi kinh điển: nó đè phẳng những mã lẽ ra phân biệt được — 400 (đầu vào sai), 404 (không thấy), 415 (sai kiểu) — thành một con số vô nghĩa, xoá mất chính thông tin để chẩn đoán. Cùng họ với lời khuyên "đừng đoán nguyên nhân của một ngoại lệ chung" — cùng một DataIntegrityViolationException có thể là chuỗi quá dài, trùng khoá, hay ký tự không lưu được; đoán bừa làm người gỡ lỗi đi lạc hẳn hướng. Và bảng mã thoát ở trên không riêng Kubernetes: quy ước 128 + số hiệu tín hiệu là chuẩn dùng chung với shell khắp Unix — 137 = 128 + 9 là SIGKILL ở bất cứ đâu.
Mẫu số chung: khi một trạng thái/mã lỗi gọi tên hành vi ("đang thử lại", "lỗi máy chủ", "thoát bất thường"), coi nó là điểm bắt đầu chứ không phải chẩn đoán — hỏi ngay "cùng nhãn này còn che những nguyên nhân nào nữa, và bằng chứng của từng cái nằm ở kho nào". Và cảnh giác với những trạng thái nghe như đang-tiến-triển (ContainerCreating, "pending", "connecting"): tên chúng là một động từ, không phải một lời hứa về sức khoẻ — quá vài chục giây thì đó là kẹt cứng, không phải đang chạy.
Muốn tự soi, quét mọi pod không khoẻ trong cụm và in ngay chỗ chứa nguyên nhân:
kubectl get pods -A -o json | python3 -c '
import sys, json
for p in json.load(sys.stdin)["items"]:
m, s = p["metadata"], p["status"]
if s.get("phase") == "Succeeded": continue
if s.get("phase") == "Running" and \
all(c.get("ready") for c in s.get("containerStatuses", [])): continue
print("\n" + m["namespace"] + "/" + m["name"] + " [" + str(s.get("phase")) + "]")
for c in s.get("conditions", []):
if c["status"] != "True" and c.get("message"):
print(" ", c["type"], "->", c["message"][:150])
for c in s.get("containerStatuses", []):
w = c["state"].get("waiting") or {}
t = (c.get("lastState") or {}).get("terminated") or {}
if w: print(" ", c["name"], "waiting:", w.get("reason"), w.get("message", "")[:110])
if t: print(" ", c["name"], "lan truoc:", t.get("reason"), "exit", t.get("exitCode"))
'
Cái này gộp cả conditions lẫn lastState vào một chỗ, nên nó đọc được cả Pending lẫn hai kiểu CrashLoopBackOff mà không cần gõ bốn lệnh cho từng pod.
Phần sau: chẩn đoán pod chạy được nhưng chậm — quy trình đầy đủ từ triệu chứng tới nguyên nhân.