✦

Readiness vs Liveness — Pod Running mà traffic vẫn chết

•Phong Hy

Deploy xong, kubectl get pods báo Running 1/1, mình yên tâm đi pha cà phê. Năm phút sau user nhắn: web trắng trang. Log thì sạch trơn, không có exception nào. Hoá ra app đã "chạy" nhưng chưa kết nối được database — mà Kubernetes vẫn đẩy traffic vào vì không ai nói cho nó biết là chưa sẵn sàng.

Bài này nói về ba loại probe trong Kubernetes, cách phân biệt cho đúng, và cái bẫy mình từng sập.

Ba loại probe, ba câu hỏi khác nhau

Nhiều người gộp cả ba thành một endpoint /health rồi thắc mắc sao hệ thống cứ restart loạn xạ. Thực ra chúng trả lời ba câu hỏi khác nhau:

  • Liveness — "Container còn sống không?" Trả lời sai thì Kubernetes restart container.
  • Readiness — "Sẵn sàng nhận traffic chưa?" Trả lời sai thì container bị rút khỏi Service, nhưng không bị restart.
  • Startup — "Khởi động xong chưa?" Chỉ chạy lúc đầu; chưa xong thì hai probe kia chưa được bật.

Điểm mấu chốt nằm ở chỗ liveness restart, còn readiness chỉ tạm rút traffic. Nhầm hai cái này là tự tay đẩy hệ thống vào vòng xoáy chết.

Cái bẫy: liveness đi check database

Mình từng viết /health ping luôn database, rồi gắn nó cho cả liveness. Kết quả một hôm DB quá tải, mọi pod timeout cùng lúc → Kubernetes tưởng app chết → restart toàn bộ → pod mới lại ping DB đang ngộp → lại restart. Database chết vì một lý do, hệ thống sập vì một lý do hoàn toàn khác.

Quy tắc mình rút ra:

  • Liveness chỉ kiểm tra chính process. Handler trả 200 nghĩa là event loop còn quay, không deadlock. Không gọi DB, không gọi service khác.
  • Dependency để cho readiness. DB chậm thì pod tạm rút khỏi vòng nhận request, khi DB khoẻ lại thì tự vào. Không ai bị restart.
  • Readiness check phải có timeout ngắn. Probe treo 30 giây cũng tính là fail; chi bằng tự fail sau 300ms cho gọn.

Code Go cho hai endpoint

var ready atomic.Bool

// /livez : process còn sống? tuyệt đối không gọi dependency
func livez(w http.ResponseWriter, r *http.Request) {
	w.WriteHeader(http.StatusOK)
}

// /readyz : sẵn sàng nhận traffic?
func readyz(w http.ResponseWriter, r *http.Request) {
	if !ready.Load() { // đang shutdown, hoặc chưa init xong
		http.Error(w, "draining", http.StatusServiceUnavailable)
		return
	}
	ctx, cancel := context.WithTimeout(r.Context(), 300*time.Millisecond)
	defer cancel()
	if err := db.PingContext(ctx); err != nil {
		http.Error(w, "db not ready", http.StatusServiceUnavailable)
		return
	}
	w.WriteHeader(http.StatusOK)
}

Hai chi tiết nhỏ mà ăn tiền:

  • Trả 503, đừng trả 500. Kubernetes chỉ quan tâm status code >= 400, nhưng 503 nói đúng bản chất "tạm thời chưa phục vụ", còn 500 là "lỗi rồi". Log và dashboard đọc số này để phân biệt.
  • Cờ ready cho phép chủ động rút traffic khi shutdown. Lúc nhận SIGTERM: set ready = false, đợi vài giây cho endpoint cập nhật, rồi mới đóng listener — request đang bay không bị rớt.

Cấu hình YAML

startupProbe:              # app migrate lâu? cho nó thời gian
  httpGet: { path: /livez, port: 8080 }
  periodSeconds: 2
  failureThreshold: 30     # tối đa 60 giây
livenessProbe:
  httpGet: { path: /livez, port: 8080 }
  periodSeconds: 10
  failureThreshold: 3
readinessProbe:
  httpGet: { path: /readyz, port: 8080 }
  periodSeconds: 5
  failureThreshold: 2

Vài con số mình hay dùng:

  • failureThreshold: 2-3 cho liveness. Để 1 thì một lần GC dừng lâu cũng đủ giết pod oan.
  • Có startupProbe thì không cần initialDelaySeconds dài dòng. App khởi động 10 hay 90 giây đều ổn, và liveness chỉ bắt đầu sau khi startup pass.

Thứ tự shutdown mới là chỗ rớt request

Probe đúng vẫn chưa đủ. Khi rollout, Kubernetes gửi SIGTERM và tính giờ terminationGracePeriodSeconds. Vấn đề: việc rút pod khỏi danh sách endpoint của Service diễn ra bất đồng bộ, nhanh nhất cũng mất vài trăm mili giây. Nếu app tắt ngay khi nhận SIGTERM, request đang trên đường tới sẽ ăn 502.

Cách chữa gọn nhất là một preStop hook ngủ vài giây:

lifecycle:
  preStop:
    exec: { command: ["sleep", "5"] }
terminationGracePeriodSeconds: 30

Trong lúc ngủ đó, readiness đã fail (hoặc pod đã bị đánh dấu terminating), Service kịp cập nhật, kết nối mới không tới nữa, còn kết nối cũ thì xử lý xong. Mình có kể kỹ phần này trong bài Zero-downtime deployment.

Checklist mang về dùng

  • Liveness = tự thân process, không chạm dependency.
  • Readiness = dependency + trạng thái draining, timeout ngắn.
  • Log lại lý do probe fail (thiếu gì, chậm bao lâu) — fail mà không có log thì lần sau lại mò.
  • Sau khi deploy, kubectl describe pod xem cột Events: pod nào vừa bị restart là thấy ngay.

Pod Running chưa bao giờ đồng nghĩa với "phục vụ được". Ba probe là ba câu hỏi khác nhau, và Kubernetes chỉ làm đúng những gì mình khai báo — khai sai thì nó restart rất hăng.

📋 Phụ lục thuật ngữ

Thuật ngữ Giải thích
Liveness probe Kiểm tra container còn sống; fail quá số lần thì bị restart
Readiness probe Kiểm tra sẵn sàng nhận traffic; fail thì rút khỏi Service, không restart
Startup probe Chặn 2 probe kia cho tới khi app khởi động xong
Endpoint Danh sách IP:port mà Service dùng để đẩy traffic vào pod
preStop hook Lệnh chạy trước khi container bị tắt hẳn
SIGTERM Tín hiệu yêu cầu process dừng êm, trước khi bị kill cứng
✦