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ự):
NodeAffinity/NodeSelector: Node có label khớp không?PodAffinity/PodAntiAffinity: Vị trí của Pod khác có thỏa điều kiện không?Taints/Tolerations: Node có taint mà Pod không tolerate không?Resources: Node có đủrequests.cpuvàrequests.memorykhông? (lưu ý: đây là requests, không phải actual usage)Volume: Volume có thể được attach trên node đó không? (topology constraints)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:
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.
kubectl describe node NODE_NAME | grep -A10 "Allocated resources"
# Xem: Requests và % của allocatable đang được dùngResourceQuota exceeded:
kubectl describe resourcequota -n NAMESPACE
# Xem: Hard limit vs Used, namespace nào đang hết quotaResourceQuota 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):
kubectl get pdb -n NAMESPACETaint không có toleration tương ứng:
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/SpotPending 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ó
nodeAffinityyêu cầu zone A - Nhưng PVC ở zone B (đã được provision trước)
- → Pod stuck Pending mãi mãi
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:
- Container exit (vì lý do nào đó)
- Kubelet thấy container đã exit → restart ngay (lần đầu: 0 giây delay)
- Container exit lần 2 → wait 10 giây rồi restart
- Exit lần 3 → wait 20 giây
- Và cứ nhân đôi: 40s → 80s → 160s → 5 phút (max)
- 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ĩa | Nguyên nhân thường gặp |
|---|---|---|
0 | Successful exit | Container process tự kết thúc (bug: worker thoát sớm, init container không persistent) |
1 | General application error | Application exception, configuration error |
2 | Misuse of shell builtin | Script error |
137 | Killed by SIGKILL | OOM kill (cgroup OOM killer) hoặc kubectl delete pod --grace-period=0 |
139 | Segmentation fault | Memory corruption trong application |
143 | SIGTERM + graceful shutdown | Normal termination (upgrade, scale down) nhưng nếu liên tục → liveness probe killing pod |
255 | Exit code out of range | Container runtime error hoặc Dockerfile entrypoint không tìm thấy |
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:45OOM 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
RESTARTScount
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:
# 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=2hOOM 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
Unhealthytrướ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)
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:
Runningstatus 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:
- Readiness probe fail → không nhận traffic từ Service (nhưng kubelet không restart)
- Không có readiness probe → luôn nhận traffic từ Service ngay khi Running
- 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
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 selectorBước 2: Test application từ trong pod
kubectl exec -it POD_NAME -n NAMESPACE -- /bin/sh
# Trong container:
curl localhost:PORT/health # hoặc endpoint của readiness probeNếu timeout hoặc connection refused từ bên trong container → application deadlock/hang.
Bước 3: Xem readiness probe configuration
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)
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àoDeadlock 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/goroutineendpoint (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
# 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 --previousPattern 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:
# Test connectivity từ trong init container
kubectl exec -it POD_NAME -c INIT_CONTAINER_NAME -- nslookup service-nameImagePullBackOff 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):
- Image tag không tồn tại (
:latestbị overwrite, image bị delete) - Registry authentication fail (Workload Identity chưa setup đúng, imagePullSecrets sai)
- Network connectivity từ node đến registry (private cluster cần Cloud NAT hoặc access qua VPC)
- Rate limiting từ registry (Docker Hub rate limit cho public images)
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_NAMEDebugging 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.