Skip to main content

Health Check และ Probe

ภาพรวม

ชุด endpoint ที่ใช้บอกว่า service ยังมีชีวิตอยู่และพร้อมรับ traffic หรือไม่ แบ่งเป็น 2 ระดับตามเจตนา ได้แก่ liveness (process ยังไม่ตาย) และ readiness (dependency ทุกตัวที่เปิดใช้งาน ping ผ่าน และ process ไม่ได้อยู่ในสถานะ drain) นอกจากนี้ยังมี endpoint สไตล์ NestJS Terminus ที่ /api/health และ hello endpoint ที่ GET /api ซึ่ง port มาจาก app.controller เดิม

Business Flow

  • GET /api — ตอบกลับเป็น text/plain ข้อความ This api for {APP_NAME} (parity กับ getHello ของ NestJS) ใช้เป็น smoke test ว่า engine ประกอบสำเร็จ และทำงานได้แม้ไม่มี backend ใดเลย
  • GET /api/health — เรียก health.Check() แล้วคืน HealthCheckResult ทรง Terminus คือ {status, info, error, details} หาก indicator ทุกตัวอยู่ในสถานะ up จะได้ 200 แต่ถ้ามีตัวใด down จะได้ 503 (parity กับ ServiceUnavailableException)
  • GET /livez และ GET /healthz — ตรวจสอบเฉพาะตัว process เท่านั้น และต้องไม่ผูกกับ dependency ใด เนื่องจากการรีสตาร์ต pod ไม่ช่วยแก้ปัญหาหาก database ล่ม การให้ liveness fail เพราะ DB จะทำให้ pod ถูกฆ่าวนซ้ำไปเรื่อย ๆ
  • GET /readyz — ตอบ 503 จนกว่า dependency ที่เปิดใช้งานทุกตัวจะ ping ผ่านและ process ไม่ได้อยู่ในสถานะ draining โดย response จะแสดง breakdown แยกราย dependency ซึ่งใช้ Dependency.Name() เป็น key จึงเป็นเหตุผลที่ระบบปฏิเสธชื่อซ้ำตั้งแต่ตอน boot
  • probe ทั้งสามตัว mount ที่ root ของ business port ไม่ได้อยู่ใต้ /api เพราะ k8s probe และ ALB healthcheck ยิงเข้ามาที่พอร์ตนี้ และยัง mount ซ้ำบน admin port :9100 ด้วย โดยใช้ health.Checker ตัวเดียวกัน
  • health.Checker รัน ticker ping เป็น background goroutine ทุก ๆ HEALTH_PROBE_INTERVAL (timeout ต่อ probe 2 วินาที) แล้ว cache ผลลัพธ์ไว้ ทำให้ endpoint ตอบได้เร็วและไม่ยิง DB ทุกครั้งที่มี probe เข้ามา
  • ตอน shutdown ระบบจะเรียก SetDraining() ก่อน ปิด HTTP server เพื่อให้ load balancer ถอน pod ออกก่อนที่ service จะหยุดรับงาน โดยหน่วงตามค่า SHUTDOWN_DRAIN_DELAY

ไฟล์และฟังก์ชันหลัก

Routeที่มา
GET /apiinternal/health/handler.go(*Handler).hello
GET /api/healthinternal/health/handler.go(*Handler).health และ internal/health/health.goCheck()
GET /livez, GET /healthzinternal/health/health.go(*Checker).LivezHandler() (mount ใน server.New)
GET /readyz(*Checker).ReadyzHandler()
  • internal/health/register.goRegister(api gin.IRouter, appName string) ทำหน้าที่ mount /api และ /api/health โดยถูกเรียกจาก server.New โดยตรง ไม่ผ่าน variadic RegisterFunc เพราะ internal/health ไม่สามารถ import internal/server ได้ (จะเกิด import cycle)
  • internal/health/entity.go และ domain_test.go — โครงสร้าง Terminus result
  • internal/obs/* — admin server :9100 ที่ mirror health พร้อมกับ /metrics, /version และ pprof (แบบ gated)

จุดเชื่อมต่อกับ Service อื่น

  • internal/deps.Registry.Probes() แปลงเมธอด Ping ของทุก dependency ให้กลายเป็น health.Probe
  • internal/supervisor ใช้ probe ชุดเดียวกันนี้ในการตัดสินใจว่าจะแจ้งเตือนหรือ exit
  • ไม่แตะ database หรือตารางใดเป็นพิเศษ นอกจากการ ping
  • ฝั่ง client-web ที่เกี่ยวข้องคือ feature app-shell แบบทางอ้อม เนื่องจากเว็บไม่ได้เรียก probe เอง แต่ probe คือ contract กับ k8s และ ALB ที่ทำให้ API ที่เว็บเรียกใช้พร้อมให้บริการ