Health Check, Metrics และการเฝ้าระวัง Dependency
ภาพรวม
webhook-go เป็นบริการที่รับ traffic สูงที่สุดในระบบ และ LINE จะไม่ retry ให้หากเราตอบช้า ชั้น observability จึงถูกออกแบบมาให้ Kubernetes ถอด pod ที่ยังไม่พร้อมออกจาก load balancer ได้อย่างรวดเร็ว และให้ pod ที่ dependency ล่มเป็นเวลานานฆ่าตัวเองเพื่อให้ถูก restart
หลักการที่ยึดถือมี 3 ข้อ
- แยก liveness ออกจาก readiness —
/livezตรวจเพียงว่า process ยังทำงานอยู่ ห้ามผูกกับ dependency มิฉะนั้นการที่ Redis ล่มจะทำให้ Kubernetes kill pod ทั้งฝูง ส่วน/readyzจะตอบ503จนกว่า dependency ที่เปิดใช้งานทุกตัวจะ ping ผ่านและ pod ไม่ได้อยู่ในสถานะ drain /metricsและ pprof อยู่บน admin port:9100เท่านั้น ห้าม expose ออกทาง ingress- health endpoint มีอยู่บนทั้งสอง port รวมถึง business port ด้วย เนื่องจาก probe ของ ALB หรือ Kubernetes บางตัวยิงเข้ามาที่พอร์ตธุรกิจ
Business Flow
Health endpoint
| Endpoint | Business port (APP_PORT ค่าเริ่มต้น 6570) | Admin port (:9100) | ความหมาย |
|---|---|---|---|
/livez | ✅ | ✅ | process ยังทำงานอยู่ ไม่ตรวจ dependency |
/healthz | ✅ | ✅ | เหมือน /livez |
/readyz | ✅ | ✅ | ตอบ 503 จนกว่า dependency ทุกตัวจะ ping ผ่านและไม่ได้ drain |
/metrics | ❌ | ✅ | รูปแบบ Prometheus ผ่าน VictoriaMetrics client |
/version | ❌ | ✅ | ชื่อ service และเวอร์ชัน |
/debug/pprof/* | ❌ | ✅ เมื่อเปิด PPROF_ENABLED | profiling |
GET /api/health | ✅ | — | รูปแบบ Terminus เพื่อ parity กับ NestJS |
ทั้งสอง port อ่านค่าจาก health.Checker ตัวเดียวกัน ซึ่งถูกสร้างเพียงครั้งเดียวใน app.New
Dependency contract
backend ทุกตัว implement interface 4 เมธอด ได้แก่ Name, Connect, Ping และ Close
จากนั้น deps.Registry จะเชื่อมต่อทุกตัวตอน boot (ชื่อซ้ำถือเป็น fail fast) แปลง Ping
ให้เป็น health.Probe เพื่อให้ readiness และ supervisor ใช้ร่วมกัน และปิดการเชื่อมต่อย้อนลำดับ
ตอน shutdown
ใน role api มี dependency 3 กลุ่มที่ลงทะเบียนไว้ ได้แก่ sqlx pool (deps/sqldb),
Redis ทุก instance ที่ตั้งชื่อไว้ (deps/redisclient) และ AMQP publisher (deps/amqppub)
dependency แต่ละตัวจะเปิดใช้งานอัตโนมัติเมื่อ env ของ host หรือ URL ถูกตั้งค่า
(TYPEORM_HOST, REDIS_URL, AMQP_URLS) หากไม่ตั้ง จะไม่ถูกลงทะเบียนและไม่ถูกตรวจสอบ
โดยที่ service ยัง boot ได้ตามปกติ
นโยบายเมื่อ dependency ล่ม (supervisor)
- retry ทุก 5 วินาที ในช่วง 1 นาทีแรก
- หลังจากนั้น retry ทุก 10 วินาที
- เมื่อครบ 1 นาที หรือ fail ติดต่อกันตามจำนวนที่ตั้งไว้ จะยิง alert เข้า chat webhook
(
ALERT_WEBHOOK_URLรองรับทั้ง Google Chat และ Slack หากไม่ตั้งค่าจะเป็น no-op) - เมื่อครบ 3 นาที จะส่ง alert ซ้ำ cancel root context เพื่อ drain อย่างนุ่มนวล แล้วเรียก
os.Exit(1)หลังหมด grace period เพื่อให้ Kubernetes restart pod ใหม่
ทุก threshold ปรับได้ผ่าน env ที่ขึ้นต้นด้วย DEP_ และตัวตัดสินใจคือ pure function
supervisor.evaluate ซึ่งมี unit test กำกับ
Metrics ที่เก็บ
RecordHTTP(method, route, code, duration)— RED metrics รายเส้นทาง โดยใช้ route pattern เช่น/api/line/:idไม่ใช่ path ดิบ เพื่อไม่ให้จำนวน label ระเบิดSetDepConnected(dep, up)— สถานะการเชื่อมต่อของ dependency แต่ละตัวRecordDepOp(dep, op, d, err)— การวัด operation ต่อ dependencyNewTask(name).Track(...)— business metricsLogMemStats— บันทึกสถิติหน่วยความจำเป็นระยะ
Graceful shutdown
ลำดับการทำงานมีความสำคัญ เมื่อได้รับ SIGTERM ระบบจะเรียก Health.SetDraining() ก่อน
เพื่อให้ readiness fail และ load balancer ถอน pod ออก จากนั้นเรียก srv.Shutdown(ctx)
เพื่อปิด HTTP server และรอ request ที่ค้างอยู่ (จำกัดด้วย SHUTDOWN_TIMEOUT) ต่อด้วย
app.Shutdown() ซึ่งหน่วงตาม SHUTDOWN_DRAIN_DELAY ปิด admin port แล้วปิด dependency
ย้อนลำดับ
signalx.Multiplex ทำหน้าที่แยก SIGTERM และ SIGINT (ซึ่งหมายถึงการ drain) ออกจาก SIGHUP
(ซึ่งหมายถึงการ reload LOG_LEVEL)
Logging
log ออกเป็น JSON บรรทัดเดียวผ่าน log/slog ไปยัง stdout เท่านั้น ห้ามใช้ fmt.Println
timestamp เป็นแบบ TZ-aware มี logx.Err(err) สำหรับแนบ stack ปรับ level ขณะรันได้ด้วย SIGHUP
และแนบ CLS field (userId, sessionId, transactionId) ที่มาจาก
แกนกลาง HTTP และ Middleware Pipeline
ไฟล์และฟังก์ชันหลัก
| ไฟล์ | ของสำคัญ |
|---|---|
internal/health/health.go | Checker, Probe, New, Run, SetDraining |
internal/health/handler.go | hello (GET /api), health (GET /api/health) |
internal/health/register.go | Register(api, appName) — mount 2 route บน /api |
internal/obs/admin.go | ServeAdmin(addr, checker, opts, log) สำหรับ :9100 |
internal/obs/metrics.go | RecordHTTP, RecordDepOp, SetDepConnected, NewTask, WriteMetrics |
internal/obs/pprof.go | pprof ที่ถูก gate ด้วย PPROF_ENABLED |
internal/supervisor/supervisor.go | Start และ evaluate (pure function ที่มี test) |
internal/alert/alert.go | ส่ง chat webhook รูปแบบ {"text": ...} |
internal/signalx/signalx.go | Multiplex(ctx, cancel, reload, log) |
internal/deps/registry.go, dep.go | Registry และ interface Dependency |
internal/logx/logx.go | logger JSON พร้อม dynamic level |
deploy/values.yaml | Helm contract — probe ชี้ /livez และ /readyz, scrape :9100, CPU limit อย่างน้อย 1000m |
จุดเชื่อมต่อกับ Service อื่น
- Kubernetes — liveness probe ชี้ไปที่
/livezและ readiness probe ชี้ไปที่/readyzส่วน supervisor พึ่งพา Kubernetes ให้ restart pod หลังจากเรียกos.Exit(1) - Prometheus / VictoriaMetrics — ServiceMonitor scrape ที่
:9100/metrics - Chat webhook — ปลายทางสำหรับแจ้งเตือน outage ตั้งค่าผ่าน
ALERT_WEBHOOK_URL - dependency ที่ถูกเฝ้าระวัง ได้แก่ Postgres pool, Redis แต่ละ instance และ AMQP publisher หากตัวใดตัวหนึ่งล่มนานเกิน 3 นาที pod ทั้งตัวจะถูก restart แม้ dependency ตัวอื่นยังปกติ
:::caution ข้อควรระวังด้าน ops
ห้าม expose port :9100 ออกทาง ingress และห้ามเพิ่ม endpoint /metrics ตัวที่สอง
บน business port
:::