☕ 5-Min Tech Story #34

OOMKilled (Exit Code 137): ทำไม Kubernetes ถึงยิงเป้าแอปทิ้ง ทั้งที่ในกราฟ JVM แรมยังเหลือเฟือ?

⏱️ ใช้เวลาอ่านประมาณ: 4 นาที 30 วินาที

บนแดชบอร์ด Grafana กราฟ JVM Heap Memory นิ่งสนิท สวยงาม ใช้ไปแค่ 60% จากที่จองไว้...

แต่พอกะพริบตาอีกที Pod บน Kubernetes ก็ดับวูบและ Restart ตัวเองใหม่อย่างน่าฉงน พอพิมพ์คำสั่ง kubectl describe pod ตรวจสอบ สาเหตุการตายกลับโผล่ขึ้นมาเป็นตัวแดงแจ๋:

Last State: Terminated
Reason: OOMKilled
Exit Code: 137

ไม่มี Exception, ไม่มี Error Log, และไม่มีแม้แต่ OutOfMemoryError พ่นออกมาในแอป! ทำไมระบบถึงสั่งประหารชีวิต Container ของเราอย่างเลือดเย็น ทั้งที่โปรแกรมรายงานว่าแรมยังไม่เต็ม?

🔫 มือปืนประจำระบบ: Linux OOM Killer & cgroups

ก่อนอื่นต้องเข้าใจรหัสลับก่อน: Exit Code 137 เกิดจากสัญญาณ 128 + 9 (SIGKILL) แปลว่ากระบวนการไม่ได้ปิดตัวเองอย่างสุภาพ แต่ถูก Linux Kernel สั่งยิงเป้าทิ้งทันที

บน Kubernetes เมื่อคุณกำหนด:

resources:
  limits:
    memory: "1Gi"

Linux จะสร้างรั้วกั้นขึ้นมาเรียกว่า Control Groups (cgroups) หากโปรแกรมภายใน Container เบิกใช้หน่วยความจำรวมแตะเส้นขอบ 1Gi เมื่อไหร่ มือปืนที่ชื่อ OOM Killer (Out-of-Memory Killer) จะเข้ามาชักปืนยิง SIGKILL ปลิดชีพกระบวนการนั้นทิ้งในระดับฮาร์ดแวร์ทันทีโดยไม่มีสิทธิ์สั่งเสีย!

👻 กับดักภาพลวงตา: Memory ไม่ได้มีแค่ Heap

หลายคนตั้งค่า JVM ไว้แบบตายใจ เช่น:

Container Limit = 1GB
JVM -Xmx (Max Heap) = 800MB

แล้วคิดว่าเหลือพื้นที่ปลอดภัยตั้ง 200MB! แต่ความจริงคือ Heap เป็นแค่ส่วนหนึ่งของภูเขาน้ำแข็ง ที่โปรแกรมใช้งาน:

"เมื่อ Heap กินไป 600MB แต่ Netty กับ Thread Stacks พากันแอบจอง Native Memory เพิ่มอีก 450MB... ผลรวมทะลุ 1,050MB ปุ๊บ cgroups จะสั่งลั่นไกสังหารแอปทันที โดยที่ JVM ยังไม่ทันรู้ตัวเลยว่าตัวเองผิดอะไร!"

🛡️ 3 คาถาปัดเป่ารหัส 137

1. ใช้สูตร 70/30 สำหรับ Container Memory Limit:

อย่าตั้งค่า -Xmx ให้ปริ่มขอบ Container Limit โดยทั่วไปควรคุมให้ Heap ไม่เกิน 60% - 70% ของขนาด Container Memory Limit ทั้งหมด เพื่อเว้นที่ว่าง 30% ให้กับ Off-Heap, Metaspace และ OS Overhead

2. เลิกฮาร์ดโค้ด -Xmx แล้วให้ JVM คำนวณตาม cgroups อัตโนมัติ:

ตั้งแต่ Java 10 (และ Java 8u191+) JVM รองรับคอนเทนเนอร์อย่างชาญฉลาด ให้ใช้ Flag สัดส่วนแทนค่าคงที่:

# ปล่อยให้ JVM แบ่ง Heap สูงสุดไม่เกิน 70% ของ cgroup limit อัตโนมัติ
-XX:+UseContainerSupport
-XX:MaxRAMPercentage=70.0

3. วัดด้วยตัวชี้วัดของ Container เสมอ:

บนระบบมอนิเตอร์ ให้เลิกดูเฉพาะกราฟแอปภายใน แต่ให้ดู Metric container_memory_working_set_bytes จาก cAdvisor/Prometheus เป็นหลัก เพราะนี่คือตัวเลขจริงที่ Linux Kernel ใช้ตัดสินใจว่าจะเหนี่ยวไก OOM Killer หรือไม่

💡 5-Min Takeaway

"บนโลก Container พื้นที่หน่วยความจำไม่ได้ถูกตัดสินจากมุมมองของโปรแกรม แต่ถูกตัดสินจากกฎเหล็กของ Linux Kernel" อย่าหลงเชื่อเพียงตัวเลข Heap Memory ที่ดูสงบสุข เผื่อพื้นที่ให้ Native Memory และ Stack ของเธรดเสมอ และตรวจสอบ Working Set Size ของ Container เป็นประจำ เพื่อไม่ให้แอปพลิเคชันของคุณถูกมือปืน OOM Killer สั่งประหารกลางอากาศในยามที่โหลดพีค