Health Check API
service ของคุณรันเป็น replica เหมือน ๆ กันหลายตัวอยู่หลัง load balancer โดยมี orchestrator คอยจัดตารางและสั่งเริ่มหรือหยุดได้ตลอดเวลา ปัญหาคือ replica ไม่ได้พร้อมทำงานทันทีที่ process เริ่มรัน อาจยังเปิด database connection pool ไม่เสร็จ ยัง warm cache อยู่ หรือกำลังรัน migration ค้างไว้ และ replica ก็กลายเป็นตัวป่วยได้นานหลัง startup ไปแล้ว เช่น database connection หลุด downstream dependency ที่ต้องใช้ดับไป หรือเกิด deadlock ขึ้นทั้งที่ในทางเทคนิค process ยังรันอยู่
แต่ platform ที่ห่อ service ของคุณอยู่ไม่รู้เรื่องพวกนี้เลย ในสายตา load balancer process ที่ยังรับ TCP connection ได้ก็คือ process ที่ยังมีชีวิต ต่อให้ทุก request ที่ให้บริการจะคืน error ออกมาก็ตาม
ถ้า orchestrator และ load balancer แยกไม่ออกว่า replica ตัวไหนแข็งแรงตัวไหนป่วย ก็จะส่ง traffic ของผู้ใช้จริงไปยัง instance ที่ให้บริการไม่ได้ต่อไปเรื่อย ๆ pod ที่ database connection หลุดไปแล้วยังได้รับ request หนึ่งในสาม แล้วก็ล้มเหลวทุกครั้ง ส่วน replica ที่เพิ่งเริ่มรันก็ถูกใส่เข้า rotation ก่อน warm up เสร็จ ผู้ใช้กลุ่มแรกจึงเจอ error กันถ้วนหน้า
คุณอยากให้ platform ตัดสินใจเรื่อง routing และ restart ได้เอง แต่สิ่งเดียวที่รู้จริง ๆ ว่า replica แข็งแรงหรือไม่คือตัว replica เอง คำถามจึงเป็นว่า service จะบอก platform ได้อย่างไร ให้บอกได้ต่อเนื่อง ต้นทุนต่ำ และอยู่ในรูปแบบที่ platform เข้าใจอยู่แล้ว ว่าตอนนี้ควรได้รับ traffic หรือยัง
วิธีแก้
หัวข้อที่มีชื่อว่า “วิธีแก้”แต่ละบริการเปิดเผย Health Check API คือ HTTP endpoint หนึ่งตัวหรือมากกว่าที่ platform poll ตามตารางเวลา บริการตรวจสอบสถานะของตัวเองและคืนค่า status code ที่ platform สามารถนำไปทำงานได้ — 200 สำหรับแข็งแรง, อะไรสักอย่างในช่วง 5xx สำหรับไม่แข็งแรง
ข้อคิดสำคัญคือ “แข็งแรง” มีสองความหมายที่ชัดเจน และ platform ที่เป็นผู้ใหญ่แล้วจะถามแยกกัน
- Liveness — process เสียหายจนต้อง restart แล้วหรือยัง liveness check ที่ล้มเหลวจะบอก orchestrator ให้ฆ่าทิ้งแล้วสร้าง instance ใหม่ ดังนั้นให้เขียน check นี้แบบต้นทุนต่ำและไม่พึ่ง dependency ควรล้มเหลวเฉพาะตอนที่ restart แล้วช่วยได้จริง เช่น deadlock ไม่ใช่ตอนที่ downstream แค่ช้า
- Readiness — ตอนนี้ instance นี้รับ traffic ไหวหรือยัง readiness check ที่ล้มเหลวจะบอก load balancer ให้หยุด route request มาที่นี่ แต่ ไม่ restart process ตรงนี้แหละคือจุดที่ควรตรวจ dependency ที่ service ต้องใช้จริง ทั้ง database, cache ที่จำเป็น และ downstream ที่สำคัญ เพื่อให้ replica ที่กำลัง warm up หรือทรุดชั่วคราวถูกดึงออกจาก rotation แบบเงียบ ๆ แล้วใส่กลับเข้ามาเมื่อฟื้น
flowchart LR
ORCH[Orchestrator] -->|GET /health/live every Ns| LIVE{liveness ok?}
LB[Load Balancer] -->|GET /health/ready every Ns| READY{readiness ok?}
LIVE -->|no| KILL[restart instance]
LIVE -->|yes| KEEP[leave running]
READY -->|no| DRAIN[stop routing traffic here]
READY -->|yes| SERVE[send traffic here] ตัวอย่าง
หัวข้อที่มีชื่อว่า “ตัวอย่าง”ตัวอย่าง readiness endpoint ที่ตรวจ dependency ซึ่ง service ต้องใช้จริง แล้วรายงานผลออกมาแบบมีโครงสร้าง จะคืน 200 เฉพาะตอนที่ทุก dependency ที่จำเป็นเข้าถึงได้ นอกนั้นคืน 503 เพื่อให้ load balancer drain instance นี้ออกได้เองโดยไม่ต้องรอคนตัดสินใจ
import express from 'express';
const app = express();
async function checkDb(): Promise<boolean> { try { await db.query('SELECT 1'); return true; } catch { return false; }}
// Liveness: cheap, no dependencies. Fail only if a restart would help.app.get('/health/live', (_req, res) => { res.status(200).json({ status: 'UP' });});
// Readiness: check required dependencies.app.get('/health/ready', async (_req, res) => { const checks = { database: await checkDb() }; const healthy = Object.values(checks).every(Boolean); res.status(healthy ? 200 : 503).json({ status: healthy ? 'UP' : 'DOWN', checks, });});
app.listen(8080);from fastapi import FastAPI, Response
app = FastAPI()
async def check_db() -> bool: try: await db.execute("SELECT 1") return True except Exception: return False
# Liveness: cheap, no dependencies. Fail only if a restart would help.@app.get("/health/live")async def live(): return {"status": "UP"}
# Readiness: check required dependencies.@app.get("/health/ready")async def ready(response: Response): checks = {"database": await check_db()} healthy = all(checks.values()) response.status_code = 200 if healthy else 503 return {"status": "UP" if healthy else "DOWN", "checks": checks}package main
import ( "encoding/json" "net/http")
func checkDB() bool { if err := db.Ping(); err != nil { return false } return true}
func main() { // Liveness: cheap, no dependencies. Fail only if a restart would help. http.HandleFunc("/health/live", func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{"status": "UP"}) })
// Readiness: check required dependencies. http.HandleFunc("/health/ready", func(w http.ResponseWriter, _ *http.Request) { checks := map[string]bool{"database": checkDB()} healthy := true for _, ok := range checks { if !ok { healthy = false } } w.Header().Set("Content-Type", "application/json") if healthy { w.WriteHeader(http.StatusOK) } else { w.WriteHeader(http.StatusServiceUnavailable) } json.NewEncoder(w).Encode(map[string]any{"checks": checks}) })
http.ListenAndServe(":8080", nil)}use axum::{routing::get, Json, Router};use axum::http::StatusCode;use serde_json::json;
async fn check_db() -> bool { sqlx::query("SELECT 1").execute(&pool).await.is_ok()}
// Liveness: cheap, no dependencies. Fail only if a restart would help.async fn live() -> Json<serde_json::Value> { Json(json!({ "status": "UP" }))}
// Readiness: check required dependencies.async fn ready() -> (StatusCode, Json<serde_json::Value>) { let db_ok = check_db().await; let code = if db_ok { StatusCode::OK } else { StatusCode::SERVICE_UNAVAILABLE }; (code, Json(json!({ "status": if db_ok { "UP" } else { "DOWN" }, "checks": { "database": db_ok } })))}
fn app() -> Router { Router::new() .route("/health/live", get(live)) .route("/health/ready", get(ready))}ผลลัพธ์ที่ตามมา
หัวข้อที่มีชื่อว่า “ผลลัพธ์ที่ตามมา”สิ่งที่คุณได้รับ:
- Routing และ restart ที่ self-healing platform ดึง replica ที่ป่วยออกจาก rotation และ restart ตัวที่เสียโดยไม่ต้องมีมนุษย์มาเกี่ยวข้อง ดังนั้น instance ที่ล้มเหลวจึงทำให้ความจุลดลงแทนที่จะก่อให้เกิด error ที่ผู้ใช้มองเห็น
- Rollout และ scaling ที่ปลอดภัย replica ใหม่จะได้รับทราฟฟิกก็ต่อเมื่อ readiness ผ่าน ดังนั้นการ deploy และ scale-up จึงไม่ก่อให้เกิด error แบบเย็น ๆ
สิ่งที่คุณต้องจ่าย:
- คุณต้องแยก liveness ออกจาก readiness อย่างระมัดระวัง ถ้าการตรวจ liveness ของคุณล้มเหลวเมื่อใดก็ตามที่ downstream ช้า orchestrator ก็จะ restart instance ที่แข็งแรงทุกครั้งที่ downstream สะดุด — เปลี่ยนการสะดุดเล็กน้อยของ dependency ให้กลายเป็นพายุ restart
- health check โกหกได้ หรือแพงเกินไปก็ได้ check ที่คืน
200เสมอไม่ว่าสถานะจริงจะเป็นอย่างไร แย่กว่าไม่มี check เลย ส่วน check ที่รัน query แพง ๆ ทุกครั้งที่ถูก poll ก็เพิ่มภาระจนกลายเป็นคอขวดเสียเอง เขียน check ให้มีความหมายแต่ต้นทุนต่ำ และ cache ผลไว้ถ้าถูก poll ถี่
เนื้อหาที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “เนื้อหาที่เกี่ยวข้อง”- Application Metrics — health เป็นสัญญาณใช่/ไม่ใช่; metrics แสดงการเสื่อมสภาพแบบค่อยเป็นค่อยไปที่นำหน้าก่อนการตรวจที่ล้มเหลว
- Log Aggregation — เมื่อการตรวจ readiness เริ่มล้มเหลว logs ที่ถูกรวบรวมจะบอกคุณว่า ทำไม
| ข้อดี | ข้อแลกเปลี่ยน |
|---|---|
| orchestrator (Kubernetes) รู้ว่า instance พร้อมรับ traffic หรือไม่ | health check ที่ผิดพลาดทำให้ route traffic ไปหา instance ที่ไม่ ready |
| auto-restart instance ที่ไม่ healthy | health check ที่ expensive ทำให้ load บน service เพิ่ม |
| liveness และ readiness แยกกัน — restart vs traffic routing | dependency ที่ check ใน health endpoint ล้ม ทำให้ทั้ง service ถูก restart |
| ง่ายต่อการ integrate กับ load balancer และ service mesh | ต้องดูแล health endpoint ให้ accurate ตลอด |
ข้อผิดพลาดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ข้อผิดพลาดที่พบบ่อย”Health Check ที่ Check ทุกอย่าง — รวม external dependency ทั้งหมดใน health check อาการ:
- database ช้า → health check fail → Kubernetes restart pod → restart ไม่ช่วย
- downstream service ล้ม → ทุก instance ถูก restart พร้อมกัน
Liveness = Readiness — ใช้ endpoint เดียวสำหรับทั้งสอง อาการ:
- service startup ช้า → liveness check fail → restart ก่อน start เสร็จ → restart loop
- แยก: liveness = “ฉันยังมีชีวิตอยู่”, readiness = “ฉันพร้อมรับ request”
💡 ตัวอย่างจากของจริง
Kubernetes:
- livenessProbe: restart pod ถ้า fail
- readinessProbe: หยุด route traffic ถ้า fail แต่ไม่ restart
- startupProbe: ให้เวลา startup นาน ก่อนที่ liveness จะเริ่ม check
Netflix:
- health endpoint แสดง status ของ service พร้อม dependency tree
- Eureka ใช้ health check สำหรับ Service Discovery — instance ที่ไม่ healthy ถูก deregister