Skip to main content

Troubleshooting Scenarios — ฝึก Debug สด

หน้านี้คือเวอร์ชัน "live coding" ของหมวด K8s — สถานการณ์ debug จริงที่มักถูกยกมาถามในสัมภาษณ์ สาย SRE/DevOps/Platform Engineer เน้น ลำดับการสืบสวน (investigation order) มากกว่าแค่คำตอบ

กลยุทธ์ทั่วไปตอนถูกถามสถานการณ์ debug​

  1. เริ่มจากกว้างไปแคบ: cluster-level → namespace-level → object-level → container-level
  2. คำสั่งที่ควรนึกถึงเป็นอันดับแรกเสมอ: kubectl get events --sort-by=.lastTimestamp (ดูเหตุการณ์ล่าสุดทั้ง namespace ภาพรวมก่อน)
  3. พูดสมมติฐานออกมาก่อนรันคำสั่ง ("ผมสงสัยว่าอาจเป็นเพราะ resource ไม่พอ ขอเช็ค Node capacity ก่อน") — แสดง reasoning ไม่ใช่แค่ท่องคำสั่ง
🧠 จำง่ายๆ

"กว้าง→แคบ = หมอตรวจคนไข้ ไม่ใช่เดาโรคจากอาการเดียว" — เริ่มจาก vital signs ภาพรวม (cluster events) ก่อนค่อยส่อง X-ray เฉพาะจุด (describe pod) ไม่ใช่กระโดดไปเดา diagnosis ตั้งแต่ต้นโดย ไม่เช็คภาพรวมก่อน

Scenario 1: Pod ติด CrashLoopBackOff หลัง deploy ใหม่​

อาการ: kubectl get pods แสดง my-app-7d9f8-xk2p9 0/1 CrashLoopBackOff 5 3m

ลำดับการสืบสวน:

kubectl describe pod my-app-7d9f8-xk2p9 | tail -20 # ดู Events
kubectl logs my-app-7d9f8-xk2p9 --previous # log ก่อน crash รอบล่าสุด

สาเหตุที่เป็นไปได้และวิธีแยกแยะ:

  • Log มี stack trace ของแอป → bug ในโค้ด หรือ config/env var ที่ขาดหาย
  • Events มี OOMKilled → resources.limits.memory ตั้งต่ำเกินไป เทียบกับ memory จริงที่แอปใช้
  • Events มี Liveness probe failed ถี่ๆ ก่อน restart → แอป boot ช้ากว่า initialDelaySeconds ที่ตั้งไว้ แก้ด้วยเพิ่ม startupProbe หรือปรับ initialDelaySeconds
🎯 Beyond: จุดที่ชาวบ้านไม่ค่อยรู้

ถ้า kubectl logs --previous ขึ้น error ว่า "previous terminated container not found" แปลว่า Kubernetes ไม่ได้เก็บ log ของ container เก่าไว้ตลอดไป — kubelet เก็บ log แค่ container instance ล่าสุดที่ terminate เท่านั้น (ไม่ใช่ทุกรอบที่เคย crash) ถ้า Pod crash วนไปหลายรอบแล้ว กว่าจะมาเช็ค log ของรอบที่ error จริงๆ อาจหายไปแล้ว นี่คือเหตุผลสำคัญที่ระบบ production ต้องส่ง log ออกไปที่ centralized logging (ELK, Loki, CloudWatch) แทนที่จะพึ่ง kubectl logs อย่างเดียว — เพราะ kubectl logs เป็นแค่หน้าต่างดู log ชั่วคราวที่หายไปพร้อม Pod ที่ถูกลบ

Scenario 2: Pod ติด Pending ไม่ยอมขึ้น​

อาการ: kubectl get pods แสดง Pod อยู่ Pending นานหลายนาที

ลำดับการสืบสวน:

kubectl describe pod <name> | grep -A5 Events
# มักเจอ: "0/5 nodes are available: 5 Insufficient cpu"
kubectl top nodes # เช็คว่า Node เหลือ resource จริงไหม
kubectl get nodes -o wide # เช็ค taint/label ของ Node
kubectl describe node <node> | grep Taints

สาเหตุที่เป็นไปได้: Node ทุกตัวไม่มี CPU/memory เหลือพอตาม requests, Pod มี nodeSelector/affinity ที่ไม่ match Node ไหนเลย, หรือ Node มี taint (เช่น node-role.kubernetes.io/control-plane:NoSchedule) ที่ Pod ไม่มี toleration รองรับ

🎯 Beyond: จุดที่ชาวบ้านไม่ค่อยรู้

มี Pending case ที่คนมักมองข้าม: PVC ที่รอ provision ไม่เสร็จ — ถ้า Pod ใช้ PersistentVolumeClaim ที่ StorageClass เป็นแบบ dynamic provisioning แต่ cloud provider มีปัญหาชั่วคราว (เช่น quota เต็ม, zone ไม่ตรงกับ Node) Pod จะค้าง Pending โดยที่ Events ของ Pod เองอาจไม่บอกสาเหตุชัดเจน ต้องไปเช็คที่ kubectl describe pvc <name> แยกต่างหากถึงจะเห็น error ที่แท้จริง — เป็นจุดที่ engineer ที่ debug เฉพาะ Pod events อย่างเดียว (โดยไม่คิดถึง PVC เป็น dependency แยก) มักหาสาเหตุไม่เจอ

Scenario 3: เรียก Service แล้ว connection refused / timeout​

อาการ: Pod A เรียก http://my-app แล้วไม่ได้ response

ลำดับการสืบสวน:

kubectl get endpoints my-app # ว่างเปล่า? = ไม่มี Pod ไหน match selector หรือ Pod ยังไม่ Ready
kubectl get pods -l app=my-app # เช็ค label ของ Pod เทียบกับ selector ของ Service
kubectl describe svc my-app # เช็ค port/targetPort ตรงกับที่ container ฟังจริงไหม

สาเหตุที่พบบ่อยเรียงตามความถี่: (1) label mismatch ระหว่าง Pod กับ Service selector (2) targetPort ผิด ไม่ตรงกับ port ที่ container เปิดจริง (3) Pod ไม่ Ready เพราะ readinessProbe ไม่ผ่าน (4) NetworkPolicy บล็อก traffic (ถ้า cluster มีใช้งานอยู่)

🎯 Beyond: จุดที่ชาวบ้านไม่ค่อยรู้

ถ้า kubectl get endpoints มี IP ขึ้นมา (ไม่ว่าง) แต่ยัง connection refused อยู่ ให้สงสัย ทันทีว่าเป็น "connection refused" ระดับ container ไม่ใช่ระดับ network — แปลว่า packet ไปถึง Pod แล้วจริงๆ แต่ไม่มี process ไหนฟังอยู่ที่ port นั้น (เช่น container ฟัง port 3000 แต่ targetPort ใน Service ตั้งเป็น 8080) วิธีแยกให้ชัดคือ kubectl exec เข้า Pod อื่นแล้ว curl ไปที่ Pod IP ตรงๆ (ข้าม Service ไปเลย) ถ้ายัง refused แสดงว่าปัญหาอยู่ที่ container เอง ไม่เกี่ยวกับ Service/Endpoints ที่เพิ่งเช็คไปเลย — เทคนิคนี้ช่วยตัดตัวแปรด้าน networking ออกจากสมการได้เร็วมากในสถานการณ์จริง

Scenario 4: Deployment rollout ค้าง ไม่ขึ้นเวอร์ชันใหม่สักที​

อาการ: kubectl rollout status deployment/my-app ค้างไม่จบ

ลำดับการสืบสวน:

kubectl rollout status deployment/my-app
kubectl describe deployment my-app | grep -A10 Conditions
kubectl get pods -l app=my-app # ดูว่า Pod ใหม่ (ReplicaSet ใหม่) อยู่สถานะอะไร

สาเหตุที่พบบ่อย: Pod เวอร์ชันใหม่ติด CrashLoopBackOff หรือ ImagePullBackOff (image tag พิมพ์ผิด/ไม่มีสิทธิ์ pull) ทำให้ readinessProbe ไม่มีวันผ่าน Deployment จึงหยุด rollout ไว้ตรงนั้นเพื่อป้องกันความเสียหายเพิ่ม (ตาม maxUnavailable/maxSurge ที่ตั้งไว้) — วิธีแก้เฉพาะหน้า: kubectl rollout undo deployment/my-app เพื่อ rollback กลับเวอร์ชันก่อนหน้า

🎯 Beyond: จุดที่ชาวบ้านไม่ค่อยรู้

kubectl rollout undo ไม่ได้ย้อนกลับไปที่ "YAML ไฟล์เดิมในเครื่องคุณ" — มันย้อนกลับไปที่ ReplicaSet revision ที่เก็บไว้ใน cluster เท่านั้น ถ้า Git repo (ที่เป็น source of truth ตาม แนวทาง GitOps) มี YAML ไฟล์ล่าสุดอยู่ แต่ cluster ถูก rollout undo ไปแล้ว state ของ Git กับ cluster จะไม่ตรงกันทันที — เครื่องมือ GitOps อย่าง ArgoCD/Flux ที่คอย sync cluster ให้ ตรงกับ Git จะเห็นว่า cluster "drift" ออกจาก Git แล้วพยายาม apply YAML เดิมกลับไปใหม่ (ย้อนการ undo ที่เพิ่งทำ) โดยอัตโนมัติ! ทีมที่ใช้ GitOps ต้อง revert ที่ Git ก่อนเสมอ แล้ว ปล่อยให้ GitOps controller sync ตาม ไม่ใช่ใช้ kubectl rollout undo เป็นวิธีแก้ปัญหาถาวรใน ระบบที่มี GitOps ควบคุมอยู่

Scenario 5: Node หนึ่งมี Pod เยอะผิดปกติ ("noisy neighbor")​

อาการ: Pod บาง Node ทำงานช้าลงทั้งที่โค้ดไม่เปลี่ยน

ลำดับการสืบสวน:

kubectl get pods -o wide --all-namespaces | grep <node-name> # ดู Pod ทั้งหมดบน Node นั้น
kubectl top pods --all-namespaces --sort-by=cpu # ดูว่า Pod ไหนกิน resource เยอะ

สาเหตุที่พบบ่อย: มี Pod ที่ไม่ได้ตั้ง resources.limits ใช้ CPU/memory เกินตัว กระทบ Pod อื่นบน Node เดียวกัน (ย้อนกลับไปดู Best Practices → Resource Requests & Limits) วิธีแก้ระยะยาว: บังคับทุก namespace ต้องตั้ง LimitRange/ResourceQuota

🎯 Beyond: จุดที่ชาวบ้านไม่ค่อยรู้

Kubernetes มีกลไกจัดลำดับความสำคัญเวลา Node ขาด resource จริงๆ เรียกว่า QoS Class ซึ่งคำนวณ จาก requests/limits ที่ตั้งไว้โดยอัตโนมัติ (ไม่ต้องตั้งเอง): Guaranteed (requests == limits ทุก resource), Burstable (ตั้ง requests แต่ limits สูงกว่าหรือไม่ตั้ง), BestEffort (ไม่ตั้งอะไรเลย) — เวลา Node ขาด memory จริงจนต้อง evict Pod, kubelet จะ ไล่ BestEffort ออก ก่อนเสมอ ตามด้วย Burstable แล้วค่อยถึง Guaranteed เป็นลำดับสุดท้าย การไม่ตั้ง resources.requests/limits เลยจึงไม่ได้แค่ "เสี่ยง noisy neighbor" แต่ยังทำให้ Pod นั้นเป็น ตัวแรกที่โดนเชือดทิ้ง เมื่อ Node ตึง resource ด้วย — ทีมที่รู้เรื่องนี้มักตั้งใจตั้ง requests == limits (Guaranteed QoS) ให้ workload สำคัญที่สุดของระบบโดยเฉพาะ

English Summary

Debugging order should go broad-to-narrow: cluster events → namespace → object → container. CrashLoopBackOff → check kubectl logs --previous, but remember kubelet only retains the most recent terminated instance's log, so centralized logging matters for anything beyond immediate debugging; also note kubelet's exponential backoff timer between restarts. Pending → check FailedScheduling events, but also check kubectl describe pvc when storage provisioning is involved, since PVC issues don't always surface clearly in Pod events. Unreachable Service → check kubectl get endpoints first; if endpoints exist but connections still fail, curl the Pod IP directly to isolate a container-level listening problem from a Service/networking one. Stuck rollout → kubectl rollout undo only reverts the live ReplicaSet, not your Git source of truth — in GitOps setups, revert in Git first or the controller will just re-apply the bad version. Noisy-neighbor slowdowns trace back to missing resource limits, which also determines QoS Class (Guaranteed/Burstable/BestEffort) — BestEffort Pods are evicted first under memory pressure, so unset requests/limits isn't just a performance risk, it's an eviction-priority risk.