Probe: Liveness, Readiness, และ Startup
ไอเดียในหนึ่งประโยค
หัวข้อที่มีชื่อว่า “ไอเดียในหนึ่งประโยค”probe สามแบบถามคำถามคนละข้อ — ยังมีชีวิตอยู่ไหม, พร้อมรับ traffic ไหม, และ start เสร็จหรือยัง — และ Kubernetes ตอบสนองต่างกันตามคำตอบแต่ละแบบ การตั้งผิดตรงนี้เป็นวิธีที่พบบ่อยที่สุดวิธีหนึ่งที่ application ที่แข็งแรงดีอยู่แล้วจบลงด้วย restart loop ไม่รู้จบ
Liveness vs Readiness: restart vs เอาออกจาก Service
หัวข้อที่มีชื่อว่า “Liveness vs Readiness: restart vs เอาออกจาก Service”liveness probe ตอบว่า “container นี้ยังทำงานอยู่ไหม” ถ้ามันพังติดกันครบจำนวนที่กำหนด kubelet จะฆ่า container แล้วให้ restartPolicy ตัดสินว่าจะเกิดอะไรต่อ (ปกติคือ restart) นี่มีไว้สำหรับ container ที่ยังรันอยู่แต่ค้าง — deadlock, ติดขัด, ไปต่อไม่ได้ — ที่การ restart คือทางแก้
readiness probe ตอบคำถามคนละข้อเลย: “container นี้พร้อมรับ traffic ตอนนี้เลย ไหม” readiness probe ที่พังไม่ restart อะไรเลย แต่ Pod จะถูกเอาออกจาก Endpoints/EndpointSlice ของ Service ทำให้ Service หยุด route traffic มาให้มันเฉย ๆ container ยังรันต่อไปไม่ถูกแตะเลย และทันทีที่ probe กลับมาผ่าน Pod ก็ถูกเพิ่มกลับเข้าไป
apiVersion: v1kind: Podmetadata: name: web labels: app: webspec: containers: - name: web image: web-app:1.4.0 ports: - containerPort: 8080 livenessProbe: httpGet: path: /healthz port: 8080 initialDelaySeconds: 10 periodSeconds: 5 timeoutSeconds: 3 failureThreshold: 3 readinessProbe: httpGet: path: /ready port: 8080 periodSeconds: 5 failureThreshold: 1พิสูจน์: endpoint เดียวกันสำหรับ liveness และ readiness ทำให้ server ที่ warm-up ช้าเข้าสู่ restart loop
หัวข้อที่มีชื่อว่า “พิสูจน์: endpoint เดียวกันสำหรับ liveness และ readiness ทำให้ server ที่ warm-up ช้าเข้าสู่ restart loop”การตั้งค่าผิดที่พบบ่อยจริง ๆ คือชี้ทั้ง liveness และ readiness probe ไปที่ endpoint เดียวกัน โดยไม่มี startup probe บน container ที่ใช้เวลาจริงกว่าจะ responsive ได้ นี่คือ container ที่จงใจใช้เวลา 30 วินาทีกว่าจะเริ่ม listen
apiVersion: v1kind: Podmetadata: name: slow-warmer-bad namespace: probes-demospec: containers: - name: c image: busybox command: ["sh", "-c", "sleep 30 && while true; do { printf 'HTTP/1.1 200 OK\\r\\nContent-Length: 2\\r\\n\\r\\nok'; } | nc -l -p 8080; done"] ports: - containerPort: 8080 livenessProbe: httpGet: { path: /, port: 8080 } periodSeconds: 5 failureThreshold: 3 readinessProbe: httpGet: { path: /, port: 8080 } periodSeconds: 5 failureThreshold: 3ด้วย initialDelaySeconds: 0 ที่เป็น default liveness probe เริ่มเช็คทันที — นานก่อนที่ warm-up 30 วินาทีจะเสร็จเสียอีก และด้วย failureThreshold: 3/periodSeconds: 5 มันยอมแพ้หลังแค่ 15 วินาที output จริงจาก cluster ที่รันอยู่จริง
$ kubectl get pod slow-warmer-badNAME READY STATUS RESTARTS AGEslow-warmer-bad 0/1 Running 1 (15s ago) 60s...slow-warmer-bad 0/1 Running 2 (20s ago) 110s
$ kubectl get events --field-selector involvedObject.name=slow-warmer-bad --sort-by=.lastTimestampLAST SEEN TYPE REASON OBJECT MESSAGE45s Normal Killing pod/slow-warmer-bad Container c failed liveness probe, will be restarted5s Warning Unhealthy pod/slow-warmer-bad Liveness probe failed: Get "http://10.244.2.8:8080/": dial tcp 10.244.2.8:8080: connect: connection refused3s Warning Unhealthy pod/slow-warmer-bad Readiness probe failed: Get "http://10.244.2.8:8080/": dial tcp 10.244.2.8:8080: connect: connection refusedRESTARTS ไต่ขึ้นเรื่อย ๆ (1 แล้วก็ 2 ไปเรื่อย ๆ ไม่รู้จบ) — container ถูก liveness probe ของตัวเองฆ่าทุกครั้ง เสมอก่อนที่ warm-up 30 วินาทีจะเสร็จ เพราะการ restart รีเซ็ตนาฬิกากลับไปที่ศูนย์ container ไม่เคยมีโอกาส start ให้เสร็จเลย นี่คือ restart loop ที่ เกิดจากตัวเอง ไม่ใช่ bug ของ application จริง ๆ
startupProbe: ให้เวลา container ที่ start ช้าได้ boot จริง พิสูจน์แล้ว
หัวข้อที่มีชื่อว่า “startupProbe: ให้เวลา container ที่ start ช้าได้ boot จริง พิสูจน์แล้ว”startup probe แก้ปัญหานี้ได้ตรง ๆ ตราบใดที่ startup probe ยังไม่ผ่าน liveness กับ readiness probe จะถูกปิดไปเลย — ดังนั้น container ที่ boot ช้าจะไม่ถูก liveness probe ที่ใจร้อนฆ่าได้ พอ startup probe ผ่านครั้งเดียวมันหยุดรันถาวร แล้ว liveness/readiness ก็เข้ามาดูแลตลอดชีวิตที่เหลือของ container
สูตรคำนวณคือ failureThreshold × periodSeconds = งบเวลารวมก่อนที่ kubelet จะยอมแพ้และ restart container server ที่ warm-up 30 วินาทีตัวเดิม ตอนนี้ตั้ง startup probe budget เป็น 6 × 10 = 60 วินาที — มากกว่า 30 วินาทีที่มันต้องการจริง ๆ อย่างสบาย ๆ
apiVersion: v1kind: Podmetadata: name: slow-warmer-fixed namespace: probes-demospec: containers: - name: c image: busybox command: ["sh", "-c", "sleep 30 && while true; do { printf 'HTTP/1.1 200 OK\\r\\nContent-Length: 2\\r\\n\\r\\nok'; } | nc -l -p 8080; done"] ports: - containerPort: 8080 startupProbe: httpGet: { path: /, port: 8080 } periodSeconds: 10 failureThreshold: 6 livenessProbe: httpGet: { path: /, port: 8080 } periodSeconds: 5 failureThreshold: 3 readinessProbe: httpGet: { path: /, port: 8080 } periodSeconds: 5 failureThreshold: 3$ kubectl get pod slow-warmer-fixedNAME READY STATUS RESTARTS AGEslow-warmer-fixed 1/1 Running 0 50s
$ kubectl get events --field-selector involvedObject.name=slow-warmer-fixed --sort-by=.lastTimestampLAST SEEN TYPE REASON OBJECT MESSAGE50s Normal Started pod/slow-warmer-fixed Container started20s Warning Unhealthy pod/slow-warmer-fixed Startup probe failed: Get "http://10.244.2.9:8080/": dial tcp 10.244.2.9:8080: connect: connection refusedRESTARTS: 0 — container ตัวเดียวกันเป๊ะ ๆ warm-up 30 วินาทีเหมือนเดิม รอดครั้งนี้แบบสะอาด ๆ warning Startup probe failed ตัวเดียวที่ log ระหว่างทางเป็นเรื่องปกติและไม่เป็นอันตราย: มันยังอยู่ในงบ 6 × 10 = 60 วินาทีสบาย ๆ และไม่มี event Killing/liveness เกิดขึ้นเลย เพราะ liveness ยังไม่ได้เริ่มรันด้วยซ้ำตอนนั้น
กลไก probe
หัวข้อที่มีชื่อว่า “กลไก probe”probe ทุกแบบ (liveness, readiness, startup) ใช้กลไกเช็คแบบใดแบบหนึ่งได้
httpGet— ส่ง HTTP GET request; status code ตั้งแต่ 200 จนถึงก่อน 400 นับว่าสำเร็จtcpSocket— ลองเปิด TCP connection ไปที่ port; connect สำเร็จนับว่าสำเร็จexec— รันคำสั่งข้างใน container; exit code0นับว่าสำเร็จgrpc— เรียก gRPC health-checking service ของ container (container ต้อง implement gRPC health protocol มาตรฐาน) ตาม เอกสาร probe ของ Kubernetes fieldgrpcเป็น GA มาตั้งแต่ Kubernetes 1.27 (บทนี้พูดถึงแค่ภาพรวมเชิงเอกสาร — พิสูจน์แบบ live ต้องมี container ที่ implement gRPC health protocol ซึ่ง demo image เล็ก ๆ ของคอร์สนี้ไม่มีตัวไหนทำ):
readinessProbe: grpc: port: 5000field การปรับแต่งใช้ได้กับทุกกลไก: initialDelaySeconds (รอก่อนเช็คครั้งแรก), periodSeconds (เช็คบ่อยแค่ไหน, default 10), timeoutSeconds (รอ response นานแค่ไหน — default แค่ 1 วินาที ง่ายมากที่จะไม่พอสำหรับอะไรที่ทำงานจริงอย่าง query database หรือ TLS handshake ใน health check), failureThreshold (พังติดกันกี่ครั้งก่อนจะลงมือ), และ successThreshold (สำเร็จติดกันกี่ครั้งถึงจะกลับมาเป็น healthy — ต้องเป็น 1 สำหรับ liveness และ startup probe) demo slow-warmer-bad ด้านบนบังเอิญพังเพราะไม่มีเวลาพอจะ start แต่อีก gotcha ที่พบบ่อยพอ ๆ กันในโลกจริงคือ probe ที่ start ตรงเวลาแต่ยัง timeout อยู่ดี แค่เพราะ default 1 วินาทีสั้นกว่าที่การเช็คนั้นใช้เวลาจริง
restart ที่ probe สั่งยังเคารพ terminationGracePeriodSeconds
หัวข้อที่มีชื่อว่า “restart ที่ probe สั่งยังเคารพ terminationGracePeriodSeconds”restart ที่ liveness probe สั่งไม่ใช่การฆ่าทันที — มันผ่าน sequence เดียวกันเป๊ะ ๆ คือ SIGTERM → grace period → SIGKILL ที่พูดถึงลึกกว่านี้ใน Pods: The Atomic Unit รวมถึง preStop hook ใด ๆ ด้วย container ที่มี terminationGracePeriodSeconds ยาวและ request ที่กำลัง drain ช้า ใช้เวลาหยุดจริง ๆ หลังจาก liveness probe พังพอ ๆ กับหลัง kubectl delete pod ปกติเลย — probe แค่ตัดสินใจว่า ควรจะ restart ไหม ไม่ใช่กำหนดว่า container จะออกได้เร็วแค่ไหน
Readiness gate: ขยาย readiness ให้เกินกว่า probe ของ container เอง
หัวข้อที่มีชื่อว่า “Readiness gate: ขยาย readiness ให้เกินกว่า probe ของ container เอง”readinessGates ให้ controller ภายนอกร่วมตัดสินว่า Pod นับว่า Ready ไหม มากกว่าแค่ readinessProbe ของตัวมันเอง condition ที่ระบุชื่อไว้แต่ละตัวต้องเป็น True ใน status.conditions ของ Pod — ตั้งโดย controller อื่นที่กำลังจับตา Pod อยู่ — ก่อนที่ Pod จะนับว่า ready ได้ นอกเหนือจากทุก container ต้องผ่าน readiness probe ของตัวเองด้วย
spec: readinessGates: - conditionType: "www.example.com/feature-1"นี่คือวิธีที่ pattern อย่าง Gateway/load-balancer controller ยืนยันว่า Pod ถูก register และรับ traffic จริงในระดับ infrastructure (ไม่ใช่แค่ “probe ของฉันเองผ่าน”) ถูกพับเข้าไปในสัญญาณ Ready เดียวกันที่ kubectl get pods และ rollout ใช้อยู่แล้ว — เป็นแค่ภาพรวมตรงนี้ ฝั่ง controller อยู่นอกขอบเขตของบทนี้
# Watch probe-driven restarts and readiness statekubectl get pods -wkubectl describe pod web | grep -A5 -E 'Liveness|Readiness|Startup'flowchart LR
boot["Container starts"] --> sp{"startupProbe\nสำเร็จหรือยัง?"}
sp -- "ยัง ลองต่อ\n(ไม่เกิน failureThreshold x periodSeconds)" --> sp
sp -- "สำเร็จ" --> live["livenessProbe\nรันต่อเนื่อง"]
sp -- "สำเร็จ" --> ready["readinessProbe\nรันต่อเนื่อง"]
live -- "พัง" --> restart["kubelet restart container\n(SIGTERM -> grace -> SIGKILL)"]
ready -- "พัง" --> remove["Pod ถูกเอาออกจาก Service Endpoints"]
ready -- "สำเร็จอีกครั้ง" --> add["Pod ถูกเพิ่มกลับเข้า Endpoints"] Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”probe ทั้งสามตัวตอบแค่คำถามแคบ ๆ ของตัวเองเท่านั้น และปฏิกิริยาของ kubelet ถูก hardwire ตาม probe ตัวไหนที่พัง — มันไม่เคยให้เหตุผลว่า ทำไม เลย การ restart (liveness) กับการดึง traffic ออก (readiness) เป็นเครื่องมือคนละแบบสำหรับรูปแบบความพังคนละแบบ: “ค้าง ต้องการ restart” กับ “ให้บริการไม่ได้ชั่วคราว อย่า restart แค่รอ” การชี้ทั้งสองไปที่ endpoint เดียวกันลบความแตกต่างนี้ไปเลย — พิสูจน์แล้วด้านบน มันหมายความว่า “กำลัง warm-up ชั่วคราว” ถูกปฏิบัติเหมือน “ค้างถาวร” เป๊ะ ๆ และ container ถูกฆ่าเพื่อเงื่อนไขที่ restart แก้ไม่ได้เลย เพราะการ restart แค่รีเซ็ตนาฬิกา warm-up กลับไปที่ศูนย์ startup probe มีไว้เพื่อซื้อเวลาให้ container ที่ boot ช้า ก่อน ที่ปฏิกิริยา hardwired นั้นจะเปิดใช้งานด้วยซ้ำ ซึ่งเป็นเหตุผลที่มันปิดอีกสองตัวแทนที่จะเป็นแค่ “ความเห็นที่สาม”
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ทางเลือก | ข้อดี | ต้นทุน |
|---|---|---|
| แยก endpoint liveness กับ readiness | warm-up ช้าหรือ dependency ที่ล่มชั่วคราวไม่ทำให้เกิด restart ที่ไม่จำเป็นเลย (พิสูจน์แล้วด้านบนกับ failure mode ตอนที่เป็น endpoint เดียวกัน) | ต้อง implement endpoint เพิ่มอีกตัวและทำให้มันต่างจากอีกตัวอย่างมีความหมายจริง |
| endpoint เดียวกันทั้งคู่ | เขียนโค้ดน้อยกว่า | พิสูจน์แล้วด้านบน: restart-loop ทุก container ที่สถานะ readiness ของมันเป็น false ได้ด้วยเหตุผลที่ restart แก้ไม่ได้ |
เพิ่ม startupProbe | พิสูจน์แล้วด้านบน: ซับเวลา boot ที่ยาวแค่ไหนก็ได้แบบคาดเดาได้ด้วย failureThreshold x periodSeconds โดยไม่เสี่ยงต่อความไวของ liveness ตอน steady-state เลย | มี object เพิ่มอีกตัวที่ต้อง tune; ตั้งงบน้อยเกินไปก็แค่ย้าย restart-loop จาก “เสมอ” เป็น “บางครั้ง” ตอนโหลดสูง |
timeoutSeconds สั้น (default 1s) | ตรวจจับ process ที่ค้างจริงได้เร็ว | พิสูจน์เป็น gotcha ที่มีเอกสารรองรับด้านบน: การเช็คที่ช้าแต่ healthy จริง ๆ (DB ping, TLS handshake) อาจถูกอ่านผิดว่าพัง |
readinessGates | ให้ controller ภายนอก (Gateway, service mesh) veto readiness ได้เกินกว่า probe ของ container เอง | มีชิ้นส่วนเคลื่อนไหวเพิ่ม; controller ภายนอกที่ค้างอาจกัน Pod ที่ healthy จริงไม่ให้ Ready ได้ไม่รู้จบ |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “การใช้ endpoint สุขภาพเดียวกันสำหรับ liveness กับ readiness เป็นการลดความซับซ้อนที่ไม่เป็นอันตราย” พิสูจน์แล้วด้านบนด้วย restart count และ event จริง: มัน restart-loop container ที่ปัญหาเดียวคือแค่ start ช้า เพราะ restart แก้ “ยังไม่ warm-up” ไม่ได้ แค่รีเซ็ตนาฬิกา
- “startup probe ก็แค่ initialDelaySeconds ที่ยาวขึ้นบน liveness probe” พิสูจน์แล้วด้านบน: startup probe ปิด liveness/readiness เต็ม ๆ จนกว่ามันจะผ่าน พร้อมงบ
failureThreshold x periodSecondsของตัวเอง —initialDelaySecondsที่ยาวอย่างเดียวยังทิ้ง delay คงที่เดิมไว้ให้ liveness check ทุกครั้งถัดไป และไม่หดตัวเองแม้ container จะ up แล้วจริง ๆ - “container ที่พัง liveness probe ถูกฆ่าทันที” พิสูจน์ผ่านการอ้างอิงกลับไปที่บท Pods: การ restart ที่ probe สั่งยังผ่าน
terminationGracePeriodSecondsและpreStophook — sequence การปิดตัวแบบเดียวกับการ terminate Pod แบบอื่นทุกประการ - “
timeoutSecondsdefault ก็นานพอสำหรับการเช็คส่วนใหญ่แล้ว” default มันแค่1วินาที — การเช็คที่ทำงานจริง (DB query, TLS handshake) เกินเวลานี้ได้ง่ายมากภายใต้โหลดใด ๆ ทำให้เกิด false failure ที่หน้าตาเหมือน container พังจริงเป๊ะ - “readiness probe กับ readinessGates ทำหน้าที่เดียวกัน”
readinessProbeคือความเห็นของ container เองreadinessGatesให้ controller แยกต่างหากเพิ่ม condition ของตัวเองที่ต้องเป็นTrueด้วย — Pod ผ่าน readiness probe ของตัวเองได้ แต่ยังถูก readiness gate กันไม่ให้Readyอยู่ดี
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”ตอน review Pod spec หรือ Deployment ที่มี probe ซึ่ง AI สร้าง ให้เช็ค
path/portเดียวกันเป๊ะสำหรับทั้งlivenessProbeและreadinessProbe— พิสูจน์แล้วด้านบน มัน restart-loop container ที่มี warm-up จริง ควรเช็คคนละเรื่องกัน (หรืออย่างน้อย liveness ต้องอดทนกว่า readiness มาก)- container ที่ start ช้าไม่มี
startupProbeเลย พึ่งแค่initialDelaySecondsใหญ่ ๆ บน liveness probe — พิสูจน์แล้วด้านบนfailureThreshold x periodSecondsบนstartupProbeเฉพาะคือกลไกที่ออกแบบมาสำหรับเรื่องนี้จริง ๆ และปิด liveness/readiness ไปเลยจนกว่า boot จะเสร็จ - ไม่ตั้ง
timeoutSecondsบน probe ที่ทำงานจริง (DB ping, เรียก upstream) — default 1 วินาทีเกินได้ง่ายมาก ทำให้เกิด false failure ที่แยกไม่ออกจาก outage จริง - สมมติว่า probe ฆ่า container ทันที โดยไม่คิดถึง
terminationGracePeriodSeconds/preStopตอนคำนวณ capacity หรือ timing ของ rollout — พิสูจน์แล้วด้านบน restart ที่ probe สั่งผ่าน sequence graceful-shutdown เดียวกับการ terminate แบบอื่นทุกประการ readinessProbeที่ตั้งfailureThresholdต่ำมาก (เช่น1) คู่กับการเช็ค dependency ที่ flaky หรือช้า — ปัญหาชั่วคราวครั้งเดียวดึง Pod ออกจาก Endpoints ทันที ซึ่งอาจจะรุนแรงเกินไปขึ้นอยู่กับว่า dependency นั้น flaky แค่ไหนจริง ๆ
🐛 หา bug ในโค้ดนี้
apiVersion: v1kind: Podmetadata:name: slow-appspec:containers: - name: web image: web-app:2.1.0 ports: - containerPort: 8080 livenessProbe: httpGet: path: /health port: 8080 periodSeconds: 5 failureThreshold: 3 readinessProbe: httpGet: path: /health port: 8080 periodSeconds: 5 failureThreshold: 3เฉลย
probe ทั้งสองชี้ไปที่ /health เหมือนกันเป๊ะ โดยไม่มี startupProbe เลย พิสูจน์แล้วด้านบนด้วยรูปร่างเดียวกันเป๊ะ (slow-warmer-bad): ถ้า container นี้ใช้เวลาจริงกว่าจะ warm-up liveness probe จะฆ่ามันก่อนที่มันจะ start เสร็จ event จริง Liveness probe failed ตามด้วย Killing และ RESTARTS ไต่ขึ้นไม่รู้จบเพราะทุก restart รีเซ็ตนาฬิกา warm-up กลับไปที่ศูนย์ วิธีแก้ พิสูจน์แล้วด้านบนเช่นกัน (slow-warmer-fixed): เพิ่ม startupProbe ที่ขนาดตรงกับเวลา boot จริง (failureThreshold x periodSeconds >= เวลา startup ที่แย่ที่สุด) และควรให้ /health กับ readiness มีความหมายต่างกันจริง ๆ แทนที่จะใช้ endpoint เดียวกันร่วมกัน