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 /api | internal/health/handler.go → (*Handler).hello |
GET /api/health | internal/health/handler.go → (*Handler).health และ internal/health/health.go → Check() |
GET /livez, GET /healthz | internal/health/health.go → (*Checker).LivezHandler() (mount ใน server.New) |
GET /readyz | (*Checker).ReadyzHandler() |
internal/health/register.go→Register(api gin.IRouter, appName string)ทำหน้าที่ mount/apiและ/api/healthโดยถูกเรียกจากserver.Newโดยตรง ไม่ผ่าน variadicRegisterFuncเพราะinternal/healthไม่สามารถ importinternal/serverได้ (จะเกิด import cycle)internal/health/entity.goและdomain_test.go— โครงสร้าง Terminus resultinternal/obs/*— admin server:9100ที่ mirror health พร้อมกับ/metrics,/versionและ pprof (แบบ gated)
จุดเชื่อมต่อกับ Service อื่น
internal/deps.Registry.Probes()แปลงเมธอดPingของทุก dependency ให้กลายเป็นhealth.Probeinternal/supervisorใช้ probe ชุดเดียวกันนี้ในการตัดสินใจว่าจะแจ้งเตือนหรือ exit- ไม่แตะ database หรือตารางใดเป็นพิเศษ นอกจากการ ping
- ฝั่ง client-web ที่เกี่ยวข้องคือ feature
app-shellแบบทางอ้อม เนื่องจากเว็บไม่ได้เรียก probe เอง แต่ probe คือ contract กับ k8s และ ALB ที่ทำให้ API ที่เว็บเรียกใช้พร้อมให้บริการ