ข้ามไปยังเนื้อหา

Probe: Liveness, Readiness, และ Startup

probe สามแบบถามคำถามคนละข้อ — ยังมีชีวิตอยู่ไหม, พร้อมรับ traffic ไหม, และ start เสร็จหรือยัง — และ Kubernetes ตอบสนองต่างกันตามคำตอบแต่ละแบบ การตั้งผิดตรงนี้เป็นวิธีที่พบบ่อยที่สุดวิธีหนึ่งที่ application ที่แข็งแรงดีอยู่แล้วจบลงด้วย restart loop ไม่รู้จบ

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: v1
kind: Pod
metadata:
name: web
labels:
app: web
spec:
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

k8s/probes-liveness-readiness-startup/slow-warmer-bad.yaml
apiVersion: v1
kind: Pod
metadata:
name: slow-warmer-bad
namespace: probes-demo
spec:
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-bad
NAME READY STATUS RESTARTS AGE
slow-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=.lastTimestamp
LAST SEEN TYPE REASON OBJECT MESSAGE
45s Normal Killing pod/slow-warmer-bad Container c failed liveness probe, will be restarted
5s 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 refused
3s 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 refused

RESTARTS ไต่ขึ้นเรื่อย ๆ (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 วินาทีที่มันต้องการจริง ๆ อย่างสบาย ๆ

k8s/probes-liveness-readiness-startup/slow-warmer-fixed.yaml
apiVersion: v1
kind: Pod
metadata:
name: slow-warmer-fixed
namespace: probes-demo
spec:
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-fixed
NAME READY STATUS RESTARTS AGE
slow-warmer-fixed 1/1 Running 0 50s
$ kubectl get events --field-selector involvedObject.name=slow-warmer-fixed --sort-by=.lastTimestamp
LAST SEEN TYPE REASON OBJECT MESSAGE
50s Normal Started pod/slow-warmer-fixed Container started
20s 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 refused

RESTARTS: 0 — container ตัวเดียวกันเป๊ะ ๆ warm-up 30 วินาทีเหมือนเดิม รอดครั้งนี้แบบสะอาด ๆ warning Startup probe failed ตัวเดียวที่ log ระหว่างทางเป็นเรื่องปกติและไม่เป็นอันตราย: มันยังอยู่ในงบ 6 × 10 = 60 วินาทีสบาย ๆ และไม่มี event Killing/liveness เกิดขึ้นเลย เพราะ liveness ยังไม่ได้เริ่มรันด้วยซ้ำตอนนั้น

probe ทุกแบบ (liveness, readiness, startup) ใช้กลไกเช็คแบบใดแบบหนึ่งได้

  • httpGet — ส่ง HTTP GET request; status code ตั้งแต่ 200 จนถึงก่อน 400 นับว่าสำเร็จ
  • tcpSocket — ลองเปิด TCP connection ไปที่ port; connect สำเร็จนับว่าสำเร็จ
  • exec — รันคำสั่งข้างใน container; exit code 0 นับว่าสำเร็จ
  • grpc — เรียก gRPC health-checking service ของ container (container ต้อง implement gRPC health protocol มาตรฐาน) ตาม เอกสาร probe ของ Kubernetes field grpc เป็น GA มาตั้งแต่ Kubernetes 1.27 (บทนี้พูดถึงแค่ภาพรวมเชิงเอกสาร — พิสูจน์แบบ live ต้องมี container ที่ implement gRPC health protocol ซึ่ง demo image เล็ก ๆ ของคอร์สนี้ไม่มีตัวไหนทำ):
readinessProbe:
grpc:
port: 5000

field การปรับแต่งใช้ได้กับทุกกลไก: 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 ที่ 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 จะออกได้เร็วแค่ไหน

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 อยู่นอกขอบเขตของบทนี้

Terminal window
# Watch probe-driven restarts and readiness state
kubectl get pods -w
kubectl 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"]
วงจรชีวิตของ container: startup probe กั้น liveness กับ readiness ซึ่งจะรันต่อเนื่องหลังจากนั้น

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 กับ readinesswarm-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 และ preStop hook — sequence การปิดตัวแบบเดียวกับการ terminate Pod แบบอื่นทุกประการ
  • “timeoutSeconds default ก็นานพอสำหรับการเช็คส่วนใหญ่แล้ว” default มันแค่ 1 วินาที — การเช็คที่ทำงานจริง (DB query, TLS handshake) เกินเวลานี้ได้ง่ายมากภายใต้โหลดใด ๆ ทำให้เกิด false failure ที่หน้าตาเหมือน container พังจริงเป๊ะ
  • “readiness probe กับ readinessGates ทำหน้าที่เดียวกัน” readinessProbe คือความเห็นของ container เอง readinessGates ให้ controller แยกต่างหากเพิ่ม condition ของตัวเองที่ต้องเป็น True ด้วย — Pod ผ่าน readiness probe ของตัวเองได้ แต่ยังถูก readiness gate กันไม่ให้ Ready อยู่ดี

ตอน 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 ในโค้ดนี้

k8s/probes-liveness-readiness-startup/slow-app.yaml
apiVersion: v1
kind: Pod
metadata:
name: slow-app
spec:
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 เดียวกันร่วมกัน

จะเกิดอะไรขึ้นเมื่อ liveness probe พังติดกันเกิน failureThreshold
Pod นี้มี liveness กับ readiness probe เหมือนกันเป๊ะและไม่มี startupProbe บน container ที่ใช้เวลา 30 วินาทีกว่าจะเริ่ม listen ทำไม RESTARTS ถึงไต่ขึ้นเรื่อย ๆ
$ kubectl get pod slow-warmer-bad
NAME              READY   STATUS    RESTARTS      AGE
slow-warmer-bad   0/1     Running   2 (20s ago)   110s

Warning  Unhealthy  Liveness probe failed: ... connection refused
นี่คือ container กับ warm-up 30 วินาทีเดียวกันเป๊ะกับ slow-warmer-bad แต่ตอนนี้เพิ่ม startupProbe periodSeconds: 10, failureThreshold: 6 เข้าไป ทำไมมันถึงรอดโดยไม่มี restart เลย
$ kubectl get pod slow-warmer-fixed
NAME                READY   STATUS    RESTARTS   AGE
slow-warmer-fixed   1/1     Running   0          50s
ค่า default ของ timeoutSeconds สำหรับ probe ใด ๆ คือเท่าไหร่ และทำไมมันถึงเป็น gotcha ที่พบบ่อย
field grpc probe เป็น GA ตั้งแต่ version ไหน ตามเอกสาร Kubernetes
readinessGates เพิ่มอะไรให้เกินกว่า readinessProbe ของ container เอง