Skip to main content

Health Check, Metrics และการเฝ้าระวัง Dependency

ภาพรวม

webhook-go เป็นบริการที่รับ traffic สูงที่สุดในระบบ และ LINE จะไม่ retry ให้หากเราตอบช้า ชั้น observability จึงถูกออกแบบมาให้ Kubernetes ถอด pod ที่ยังไม่พร้อมออกจาก load balancer ได้อย่างรวดเร็ว และให้ pod ที่ dependency ล่มเป็นเวลานานฆ่าตัวเองเพื่อให้ถูก restart

หลักการที่ยึดถือมี 3 ข้อ

  1. แยก liveness ออกจาก readiness/livez ตรวจเพียงว่า process ยังทำงานอยู่ ห้ามผูกกับ dependency มิฉะนั้นการที่ Redis ล่มจะทำให้ Kubernetes kill pod ทั้งฝูง ส่วน /readyz จะตอบ 503 จนกว่า dependency ที่เปิดใช้งานทุกตัวจะ ping ผ่านและ pod ไม่ได้อยู่ในสถานะ drain
  2. /metrics และ pprof อยู่บน admin port :9100 เท่านั้น ห้าม expose ออกทาง ingress
  3. health endpoint มีอยู่บนทั้งสอง port รวมถึง business port ด้วย เนื่องจาก probe ของ ALB หรือ Kubernetes บางตัวยิงเข้ามาที่พอร์ตธุรกิจ

Business Flow

Health endpoint

EndpointBusiness port (APP_PORT ค่าเริ่มต้น 6570)Admin port (:9100)ความหมาย
/livezprocess ยังทำงานอยู่ ไม่ตรวจ dependency
/healthzเหมือน /livez
/readyzตอบ 503 จนกว่า dependency ทุกตัวจะ ping ผ่านและไม่ได้ drain
/metricsรูปแบบ Prometheus ผ่าน VictoriaMetrics client
/versionชื่อ service และเวอร์ชัน
/debug/pprof/*✅ เมื่อเปิด PPROF_ENABLEDprofiling
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)

  1. retry ทุก 5 วินาที ในช่วง 1 นาทีแรก
  2. หลังจากนั้น retry ทุก 10 วินาที
  3. เมื่อครบ 1 นาที หรือ fail ติดต่อกันตามจำนวนที่ตั้งไว้ จะยิง alert เข้า chat webhook (ALERT_WEBHOOK_URL รองรับทั้ง Google Chat และ Slack หากไม่ตั้งค่าจะเป็น no-op)
  4. เมื่อครบ 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 ต่อ dependency
  • NewTask(name).Track(...) — business metrics
  • LogMemStats — บันทึกสถิติหน่วยความจำเป็นระยะ

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.goChecker, Probe, New, Run, SetDraining
internal/health/handler.gohello (GET /api), health (GET /api/health)
internal/health/register.goRegister(api, appName) — mount 2 route บน /api
internal/obs/admin.goServeAdmin(addr, checker, opts, log) สำหรับ :9100
internal/obs/metrics.goRecordHTTP, RecordDepOp, SetDepConnected, NewTask, WriteMetrics
internal/obs/pprof.gopprof ที่ถูก gate ด้วย PPROF_ENABLED
internal/supervisor/supervisor.goStart และ evaluate (pure function ที่มี test)
internal/alert/alert.goส่ง chat webhook รูปแบบ {"text": ...}
internal/signalx/signalx.goMultiplex(ctx, cancel, reload, log)
internal/deps/registry.go, dep.goRegistry และ interface Dependency
internal/logx/logx.gologger JSON พร้อม dynamic level
deploy/values.yamlHelm 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 :::