Skip to content

Pod Debugging: Pending, Crashed & Hung States

Tại sao Pod States là Knowledge Quan Trọng

Pod status string (Pending, Running, CrashLoopBackOff, OOMKilled) không phải chỉ là label. Mỗi trạng thái là kết quả của một state machine bên trong kubelet, được driven bởi chuỗi events từ scheduler, container runtime, và kernel. Nếu bạn không hiểu state machine đó, bạn sẽ đọc status như đọc oracle mà không hiểu lý do — và sẽ apply fix sai.

Cụ thể, một trong những pattern sai phổ biến nhất trong debugging là: thấy CrashLoopBackOff → ngay lập tức kubectl delete pod để restart. Điều này có thể giải quyết triệu chứng trong 5 phút nhưng fail mode sẽ quay lại, và bạn đã phá hủy evidence (--previous logs của lần crash trước).


Internal Model: Kubernetes Pod State Machine

Trước khi đi vào từng trạng thái, cần hiểu Pod lifecycle ở mức state machine:

[Pending] ──(scheduler bind)──→ [Running containers starting]

                         ┌────────────┴────────────────┐
                         │                             │
                   (init containers                (containers start)
                    complete)                           │
                         │                    ┌─────────┴──────────┐
                         ↓                    │                    │
                   [Running]            (exit code 0)         (exit code ≠0
                         │              + restartPolicy=Never)   hoặc OOM)
                    (probe fail          → Succeeded            → (restart)
                     → restart)                                     │
                         │                            ┌────────────┘
                         ↓                            │
                   (container dies)              (tăng restart count)
                         │                            │
              (restart count ≥ threshold)     (backoff: 10s→20s→40s→...→5min)
                         │                            │
                         └──────→ [CrashLoopBackOff] ←┘

State machine này được kubelet drive. Mỗi transition có event tương ứng trong Kubernetes Events.


Trạng Thái Pending: Scheduling Pipeline Bên Trong

Khi nào Pod ở Pending?

Pod ở Pending nghĩa là nó đã được tạo trong API server (và lưu vào etcd) nhưng chưa được bind vào một node.

Scheduling Decision Tree

Kubernetes Scheduler chạy hai phase cho mỗi Pod:

Phase 1 — Filtering: Loại bỏ các node không thể chạy Pod.

Filter predicates được kiểm tra (theo thứ tự):

  1. NodeAffinity / NodeSelector: Node có label khớp không?
  2. PodAffinity / PodAntiAffinity: Vị trí của Pod khác có thỏa điều kiện không?
  3. Taints/Tolerations: Node có taint mà Pod không tolerate không?
  4. Resources: Node có đủ requests.cpurequests.memory không? (lưu ý: đây là requests, không phải actual usage)
  5. Volume: Volume có thể được attach trên node đó không? (topology constraints)
  6. PodTopologySpread: Phân phối topology có thỏa không?

Nếu tất cả nodes bị loại bỏ ở Phase 1, Pod ở Pending với event FailedScheduling và message giải thích lý do.

Phase 2 — Scoring: Rank các node còn lại theo nhiều tiêu chí (resource utilization, spread, etc.) và chọn node có điểm cao nhất.

Phase 3 — Binding: Scheduler tạo Binding object trong API server, kubelet trên node được chọn watch và bắt đầu start Pod.

Các Lý Do Pending Phổ Biến và Cách Test

Insufficient resources:

bash
kubectl describe pod POD_NAME | grep -A5 "Events:"
# Tìm: "0/3 nodes are available: 3 Insufficient memory"

Lý do tại sao "Insufficient" có thể không rõ ràng: scheduler so sánh resources.requests của Pod với allocatable resources của node (không phải capacity). allocatable = capacity - node system reserved - kubelet reserved. Một node 8GB RAM có thể chỉ có 6.5GB allocatable sau khi trừ system overhead.

bash
kubectl describe node NODE_NAME | grep -A10 "Allocated resources"
# Xem: Requests và % của allocatable đang được dùng

ResourceQuota exceeded:

bash
kubectl describe resourcequota -n NAMESPACE
# Xem: Hard limit vs Used, namespace nào đang hết quota

ResourceQuota là namespace-level limit, riêng biệt với node-level capacity. Pod có thể fail schedule vì namespace đã dùng hết quota, dù node vẫn còn nhiều resource.

PodDisruptionBudget blocking eviction (không trực tiếp gây Pending nhưng gây stall):

bash
kubectl get pdb -n NAMESPACE

Taint không có toleration tương ứng:

bash
kubectl describe node NODE_NAME | grep Taint
# Xem: node.kubernetes.io/not-ready:NoSchedule (node chưa Ready)
# hoặc custom taints của node pool GPU/Spot

Pending do Volume Topology Constraints

Đây là loại Pending khó debug nhất. Persistent Volume với WaitForFirstConsumer binding mode chỉ được provision sau khi scheduler biết Pod sẽ vào node nào (vì PV phải ở cùng zone với node). Nếu:

  • Pod có nodeAffinity yêu cầu zone A
  • Nhưng PVC ở zone B (đã được provision trước)
  • → Pod stuck Pending mãi mãi
bash
kubectl describe pvc PVC_NAME -n NAMESPACE
# Tìm: binding mode và topology constraints
kubectl describe pod POD_NAME | grep -A20 "Events"
# Tìm: "volume node affinity conflict"

Trạng Thái CrashLoopBackOff: Cơ Chế Exponential Backoff

Cơ Chế Bên Trong

CrashLoopBackOff không phải là một "state" mà là kết quả của policy restart với backoff. Kubelet thực hiện:

  1. Container exit (vì lý do nào đó)
  2. Kubelet thấy container đã exit → restart ngay (lần đầu: 0 giây delay)
  3. Container exit lần 2 → wait 10 giây rồi restart
  4. Exit lần 3 → wait 20 giây
  5. Và cứ nhân đôi: 40s → 80s → 160s → 5 phút (max)
  6. Sau mỗi lần restart thành công (container chạy ít nhất 10 phút), backoff reset về 0

CrashLoopBackOff xuất hiện khi container đang trong trạng thái đang wait backoff trước khi restart tiếp theo. Status này không nói gì về nguyên nhân crash.

Exit Code Taxonomy

Exit code là key evidence quan trọng nhất để xác định loại crash:

Exit CodeÝ nghĩaNguyên nhân thường gặp
0Successful exitContainer process tự kết thúc (bug: worker thoát sớm, init container không persistent)
1General application errorApplication exception, configuration error
2Misuse of shell builtinScript error
137Killed by SIGKILLOOM kill (cgroup OOM killer) hoặc kubectl delete pod --grace-period=0
139Segmentation faultMemory corruption trong application
143SIGTERM + graceful shutdownNormal termination (upgrade, scale down) nhưng nếu liên tục → liveness probe killing pod
255Exit code out of rangeContainer runtime error hoặc Dockerfile entrypoint không tìm thấy
bash
kubectl describe pod POD_NAME | grep -A5 "Last State:"
# Output:
# Last State: Terminated
#   Reason: OOMKilled
#   Exit Code: 137
#   Finished: Mon, 30 Jun 2025 14:23:45

OOM Kill: Cơ Chế Cgroup và Khác Biệt v1 vs v2

Container memory limits trong Kubernetes được implement bằng cgroup memory limits. Khi container vượt limits.memory, cgroup OOM killer được kích hoạt.

Cgroup v1 (kernel cũ, GKE nodes cũ):

  • OOM killer có thể kill bất kỳ process nào trong cgroup, kể cả child processes
  • Có trường hợp child process bị kill nhưng PID 1 (main process) tiếp tục chạy
  • Kubernetes không detect OOM vì PID 1 vẫn sống → Pod không được restart
  • Triệu chứng: memory usage cao, application behave oddly, không có restart trong RESTARTS count

Cgroup v2 (GKE nodes từ node image version gần đây): Theo GKE documentation:

  • OOM killer kill toàn bộ task trong cgroup đồng thời hoặc không kill gì
  • Kết quả deterministic: hoặc toàn bộ container fail (Kubernetes detect và restart), hoặc không ảnh hưởng
  • Đây là behavior mong muốn cho Kubernetes lifecycle management

Tìm OOM kill evidence:

bash
# Check container exit code
kubectl describe pod POD_NAME | grep "OOMKilled"

# Check kernel OOM messages trên node
gcloud logging read \
  'resource.type="k8s_node" 
   AND textPayload:"oom_reaper" 
   OR textPayload:"Out of memory"' \
  --freshness=1h --format=json | jq '.[].textPayload'

# Check kubelet log về container being killed
gcloud logging read \
  'resource.type="k8s_node"
   AND resource.labels.node_name="NODE_NAME"
   AND logName:"kubelet"
   AND textPayload:"OOM"' \
  --freshness=2h

OOM Kill vs Liveness Probe Kill

Cả hai đều tạo exit code 137 và restart container. Cách phân biệt:

  • OOM kill: Xuất hiện đột ngột khi memory spike, có kernel-level OOM message, container bị kill từ ngoài
  • Liveness probe kill: Có event Unhealthy trước khi kill, restart có thể xảy ra ngay cả khi memory bình thường, thường có pattern: probe fail → kubelet send SIGTERM → wait grace period → SIGKILL (exit 137)
bash
kubectl describe pod POD_NAME | grep -B5 -A5 "Unhealthy"

Trạng Thái Hung: Running Nhưng Không Serve

Đây là loại lỗi tinh tế nhất vì Pod ở trạng thái Running nhưng không xử lý requests.

Readiness vs Liveness: Semantics Khác Nhau

Hiểu lầm phổ biến nhất: Running = đang hoạt động tốt. Thực ra:

  • Running status chỉ có nghĩa là kubelet đã start container và process đang chạy (PID 1 alive)
  • Readiness probe kiểm tra xem container có sẵn sàng nhận traffic không. Fail → Pod bị remove khỏi Service Endpoints
  • Liveness probe kiểm tra xem container có cần được restart không. Fail → kubelet kill và restart container

Một Pod có thể ở Running nhưng:

  1. Readiness probe fail → không nhận traffic từ Service (nhưng kubelet không restart)
  2. Không có readiness probe → luôn nhận traffic từ Service ngay khi Running
  3. Application deadlock → PID 1 alive nhưng không xử lý requests

Debugging Hung Pod

Bước 1: Xác định xem Pod có trong Service Endpoints không

bash
kubectl get endpoints SERVICE_NAME -n NAMESPACE
# Nếu Pod IP không trong list → readiness probe fail hoặc pod không có label matching service selector

Bước 2: Test application từ trong pod

bash
kubectl exec -it POD_NAME -n NAMESPACE -- /bin/sh
# Trong container:
curl localhost:PORT/health  # hoặc endpoint của readiness probe

Nếu timeout hoặc connection refused từ bên trong container → application deadlock/hang.

Bước 3: Xem readiness probe configuration

bash
kubectl describe pod POD_NAME | grep -A10 "Readiness:"

Kiểm tra initialDelaySeconds — nếu quá ngắn, application chưa kịp start đã bị probe check.

Bước 4: Check process state trong container (nếu có debugging tools)

bash
kubectl exec -it POD_NAME -- ps aux
# Hoặc nếu có strace:
kubectl exec -it POD_NAME -- strace -p PID  # xem process đang blocked ở syscall nào

Deadlock thường biểu hiện bằng process stuck ở futex_wait (mutex lock không được release), epoll_wait (event loop không có event), hoặc read (waiting for external resource).

Goroutine Leak trong Go Applications

Đặc biệt với Go services trên GKE: goroutine leak khiến số goroutines tăng dần, memory tăng, CPU tăng. Application vẫn "sống" (PID alive, liveness probe pass) nhưng response latency tăng dần đến timeout.

Evidence:

  • Memory growth tuyến tính theo thời gian (Cloud Monitoring)
  • /debug/pprof/goroutine endpoint (nếu exposed) cho thấy số lượng goroutines bất thường
  • Response latency p99 tăng dần trước khi incident

Init Container Failures

Cơ Chế Init Container

Init containers chạy theo thứ tự tuần tự và phải complete thành công (exit code 0) trước khi bất kỳ regular container nào start. Nếu init container fail, kubelet restart theo restartPolicy (nếu là Always hoặc OnFailure) — nhưng không có CrashLoopBackOff backoff mechanism cho init containers theo cách tương tự.

Pod sẽ ở trạng thái Init:0/N (N là số init containers) nếu init container chưa complete.

Debugging Init Container

bash
# Xem trạng thái init containers
kubectl describe pod POD_NAME | grep -A20 "Init Containers:"

# Logs của init container đang fail (nếu vẫn running)
kubectl logs POD_NAME -c INIT_CONTAINER_NAME

# Logs của init container đã fail (previous)
kubectl logs POD_NAME -c INIT_CONTAINER_NAME --previous

Pattern phổ biến: Init container chờ external dependency (database, service khác) và fail nếu không kết nối được trong timeout. Nếu dependency thực sự không có → Pod stuck ở Init:0/1 mãi.

Kiểm tra:

bash
# Test connectivity từ trong init container
kubectl exec -it POD_NAME -c INIT_CONTAINER_NAME -- nslookup service-name

ImagePullBackOff và ErrImagePull

Một trạng thái Pending đặc biệt: container image không thể được pull.

Cơ chế: Kubelet pull image từ registry. Nếu fail, kubelet retry với exponential backoff tương tự CrashLoopBackOff.

Nguyên nhân phổ biến (ordered by frequency):

  1. Image tag không tồn tại (:latest bị overwrite, image bị delete)
  2. Registry authentication fail (Workload Identity chưa setup đúng, imagePullSecrets sai)
  3. Network connectivity từ node đến registry (private cluster cần Cloud NAT hoặc access qua VPC)
  4. Rate limiting từ registry (Docker Hub rate limit cho public images)
bash
kubectl describe pod POD_NAME | grep -A10 "Events:"
# Message sẽ nói rõ: "unauthorized" vs "not found" vs "timeout"

# Test pull từ node (SSH vào node trước)
# toolbox:
# crictl pull IMAGE_NAME

Debugging Checklist Tổng Hợp

Khi gặp Pod có vấn đề, theo thứ tự:

1. kubectl get pod POD_NAME -n NAMESPACE -o wide
   → Biết: status, node, IP, restart count

2. kubectl describe pod POD_NAME -n NAMESPACE
   → Biết: events, conditions, resource requests vs limits, probe config
   → Chú ý đặc biệt: Events section (cuối output)

3. kubectl logs POD_NAME -n NAMESPACE --tail=100
   → Application stdout/stderr

4. kubectl logs POD_NAME -n NAMESPACE --previous --tail=100
   → Logs của container trước khi restart (nếu có)

5. Xác định exit code từ describe output
   → 137 = OOM hoặc force kill → check memory usage
   → 1 = application error → check application logs
   → 143 = SIGTERM → check probe config và graceful shutdown

6. Nếu OOM:
   kubectl top pod POD_NAME -n NAMESPACE --containers
   → Check actual memory vs limits

7. Nếu Pending:
   kubectl get events -n NAMESPACE --field-selector involvedObject.name=POD_NAME
   → Scheduler failure message

8. Nếu Hung (Running nhưng không serve):
   kubectl get endpoints SERVICE_NAME -n NAMESPACE
   → Pod IP có trong list không?

Anti-Pattern: Delete Pod Trước Khi Thu Thập Evidence

Tại sao đây là sai lầm phổ biến:

kubectl delete pod (hoặc restart deployment) xóa Pod object, container bị terminate. Kubernetes tạo Pod mới. Tất cả state của Pod cũ — đặc biệt là --previous logs, kubectl describe output với events — bị mất.

Hậu quả:

  • Bạn không còn log của container lần crash trước
  • Bạn không còn events của scheduling fail lần trước
  • Kubernetes Events có TTL 1 giờ nhưng delete pod xóa involvedObject → events có thể bị orphaned

Quy tắc: Luôn chạy evidence collection script trước bất kỳ mutating action nào. Save output ra file. Chỉ sau đó mới restart/delete.


References