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 sự cố, cột STATUS của mỗi cái, và nơi thật sự chứa nguyên nhân

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 > 0 thì 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

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ì reasonOOMKilled, còn hết hạn ân hạn thì reasonError. 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.

Thử ba mươi giây

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.