☕ 5-Min Tech Story #46

กับดัก Health Check: เมื่อ Kubernetes สวมบทเพชฌฆาต สั่งฆ่า Pod ตัวเองยกฝูงจนระบบพังพินาศ

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

คุณตั้งค่า Kubernetes Deployment อย่างรัดกุมตามคู่มือ Best Practice: "ทำ livenessProbe ยิงเช็กที่ /actuator/health ทุกๆ 10 วินาที ถ้าเจ๊งเกิน 3 ครั้ง ให้ Kubernetes สั่ง Restart Pod ใหม่ทันทีเพื่อความทนทาน"

บ่ายวันมหกรรมลดราคาสินค้า... ฐานข้อมูลหลัก (Database) เกิดทำงานช้าลงชั่วคราวเพราะมีคิวรี่หนักหน่วงวิ่งเข้ามา ทำให้ Response Time ของแอปขยับจาก 50ms ขึ้นเป็น 3 วินาที

แอปพลิเคชันของคุณยังมีชีวิตอยู่และยังคงทยอยเคลียร์ออเดอร์ของลูกค้าได้เรื่อยๆ... แต่ทันใดนั้น ปรากฏการณ์ฆ่าล้างเผ่าพันธุ์ก็เกิดขึ้น:

Pod ทั้ง 20 ตัวบนคลัสเตอร์ พร้อมใจกันติดสถานะ CrashLoopBackOff และ Restart ตัวเองวนไปไม่หยุด! เว็บไซต์กลายเป็นจอดำสนิท ลูกค้าหลายหมื่นคนหลุดออกจากระบบในวินาทีเดียว เกิดอะไรขึ้น? ใครกันแน่ที่สั่งยิงเป้าเซิร์ฟเวอร์ของเรา?

💀 สัญญาณชีพอาถรรพ์: The Deep Health Check Trap

เมื่อวิศวกรเข้าไปตรวจดูโค้ดของ Endpoint /health มักจะพบตรรกะที่ดูเหมือนจะฉลาด แต่แฝงยาพิษร้ายแรงไว้:

// ตรวจสอบสุขภาพแบบลึกเกินไป (Deep Health Check)
@GetMapping("/health")
public ResponseEntity checkHealth() {
  boolean dbAlive = checkDatabasePing(); // ⚠️ จุดตาย: แอบยิง ping หา DB!
  boolean redisAlive = checkRedisPing();
  
  if (!dbAlive || !redisAlive) {
    return ResponseEntity.status(503).body("DOWN");
  }
  return ResponseEntity.ok("UP");
}
"เมื่อฐานข้อมูลช้าหรือสะอึก... Pod ทุกตัวที่ยิง checkDatabasePing() จะตอบกลับ 503 DOWN หรือหมดเวลา Timeout พร้อมกัน Kubernetes เห็นดังนั้นจึงเข้าใจว่า 'Pod ทุกตัวสมองตายแล้ว' เลยชักปืนยิงฆ่าทิ้งทั้ง 20 เครื่องพร้อมเพรียงกัน!"

🌪️ โดมิโนมรณะ: The Death Spiral

การสั่งรีสตาร์ตไม่ได้ช่วยอะไร มิหนำซ้ำยังเร่งปฏิกิริยาล่มสลาย (Cascading Failure):

  1. Pod ทั้ง 20 ตัวถูกสั่งฆ่าพร้อมกันเพื่อบูตใหม่
  2. ตอนแอปพลิเคชันกำลังสตาร์ตขึ้นมา มันต้องสร้าง Connection Pool, โหลดแคช, และต่อสายหา Database พร้อมกัน 20 เครื่อง
  3. Database ที่กำลังเหนื่อยล้าอยู่แล้ว โดนคลื่น Connection ขาใหม่ซัดกระแทกจนสำลักตายสนิท
  4. เมื่อ Database ไม่ตอบ... Pod ใหม่ที่เพิ่งเกิดมาก็ตรวจสุขภาพไม่ผ่าน ➔ โดนสั่งฆ่าทิ้งซ้ำรอบสอง
  5. ระบบติดอยู่ในลูปนรก Death Spiral จนกว่าจะมีคนตื่นมาปิด Probe กลางดึก!

⚖️ สองพี่น้องที่ต้องแยกให้ออก: Liveness vs Readiness

Kubernetes ออกแบบ Probe สองตัวนี้มาเพื่อหน้าที่ที่ แตกต่างกันโดยสิ้นเชิง แต่คนส่วนใหญ่กลับชี้ไปที่ Endpoint เดียวกัน:

1. Liveness Probe (คุณยังมีชีวิตอยู่ไหม?):

ถามแค่ว่า: "กระบวนการแอปพลิเคชันค้างในระดับ Deadlock หรือ Out of Memory หรือไม่?"
➔ บทลงโทษเมื่อล้มเหลว: ฆ่าทิ้งแล้วรีสตาร์ตใหม่ (Kill & Restart)
➔ กฎเหล็ก: ห้ามตรวจเช็กบริการภายนอก (DB, Redis, Third-party) เด็ดขาด! ตรวจแค่ว่าตัวแอปเองยังหมุน Thread ได้ก็พอ


2. Readiness Probe (คุณพร้อมรับแขกไหม?):

ถามว่า: "ตอนนี้เครื่องคุณพร้อมที่จะรับคำขอของลูกค้าเข้ามาประมวลผลหรือยัง?"
➔ บทลงโทษเมื่อล้มเหลว: ไม่ฆ่า! แค่สั่งดึง IP ของ Pod ออกจาก Load Balancer ชั่วคราว แขกจะได้ไม่เดินเข้ามาชนกำแพง
➔ กฎเหล็ก: ถ้า DB ล่ม สามารถแจ้งว่า Unready ได้ แต่เมื่อ DB ฟื้น Pod เดิมจะกลับมารับแขกต่อได้ทันทีโดยไม่ต้องเสียเวลาบูตใหม่

🛡️ ท่าแก้เผ็ด: แยกเขี้ยวเล็บด้วย Spring Boot Actuator

ตั้งแต่ Spring Boot 2.3+ เป็นต้นมา ทีมงานได้แยก Probes อัตโนมัติสำหรับ Kubernetes โดยเฉพาะ:

# ตั้งค่า Kubernetes Probe ให้ถูกต้อง
livenessProbe:
  httpGet:
    path: /actuator/health/liveness # เช็กแค่ว่า JVM ยังไม่ตาย
    port: 8080
  failureThreshold: 3

readinessProbe:
  httpGet:
    path: /actuator/health/readiness # เช็กความพร้อมในการทำงาน
    port: 8080

💡 5-Min Takeaway

"อย่าสั่งประหารชีวิตแอปพลิเคชัน เพียงเพราะเพื่อนข้างบ้านป่วย" การรีสตาร์ต Pod ไม่เคยช่วยรักษาฐานข้อมูลที่กำลังช้า จงใช้ Liveness Probe ตรวจสอบเฉพาะความผิดปกติภายในตัวแอป (Internal Deadlock) เท่านั้น และปล่อยให้หน้าที่จัดการความพร้อมของระบบภายนอกเป็นของ Readiness Probe เพื่อป้องกันไม่ให้เกิดโดมิโนสังหารหมู่ในวันที่ระบบเผชิญกับคลื่นทราฟฟิกสูงสุด