Buộc số phận của cả một đàn vào một sợi dây chung là công thức chắc chắn để cả đàn cùng ngã một lúc. Nếu mỗi con cừu được cột vào cùng một cái cọc, cái cọc bật lên là cả đàn đổ — trong khi lẽ ra mỗi con vấp ngã riêng lẻ còn cứu được đàn. Rất nhiều cấu hình livenessProbe phạm đúng lỗi ấy: nó buộc "còn sống hay không" của mỗi pod vào một thứ dùng chung, và biến một trục trặc cục bộ thành một cú sập toàn đàn. Probe là thứ hiếm hoi trong Kubernetes mà cấu hình sai còn tệ hơn không cấu hình; bài này dựng ba kiểu sai phổ biến nhất và đo hậu quả.
Bẫy 1: livenessProbe kiểm tra phụ thuộc bên ngoài
Đây là kiểu sai nguy hiểm nhất, và nó trông rất hợp lý khi viết.
livenessProbe:
exec: {command: ["sh","-c","curl -sf -m 2 http://db-svc/"]}
periodSeconds: 3
failureThreshold: 2
"Ứng dụng không nói chuyện được với cơ sở dữ liệu thì nó có sống cũng vô ích." Nghe đúng. Bốn pod ứng dụng, cơ sở dữ liệu còn sống: tổng RESTARTS = 0. Giảm cơ sở dữ liệu về 0 bản:
ung-...-6tdhz Running RESTARTS=1
ung-...-dnrvb Running RESTARTS=1
ung-...-jm5jk Running RESTARTS=1
ung-...-zqxnv Running RESTARTS=1
sau 40 giây nữa: tổng RESTARTS = 8
Toàn bộ fleet vào vòng lặp khởi động lại, và nó tăng đều: 4 rồi 8 rồi tiếp. Vấn đề là khởi động lại không sửa được gì. Lỗi không nằm ở ứng dụng. Container mới khởi động lên, probe vẫn hỏng, lại bị giết. Và nó làm mọi thứ tệ hơn: khi cơ sở dữ liệu quay lại, mọi pod đang ở giữa chừng khởi động, không pod nào sẵn sàng, và cả đám cùng kết nối lại một lúc — chính là kiểu tải mà cơ sở dữ liệu vừa phục hồi không chịu nổi.
Quy tắc: livenessProbe chỉ kiểm tra chính tiến trình. Một endpoint trả về 200 khi vòng lặp sự kiện còn quay là đủ. Kiểm tra phụ thuộc thuộc về readinessProbe — ở đó, mất kết nối cơ sở dữ liệu chỉ khiến pod ngừng nhận lưu lượng, và nó tự quay lại khi mọi thứ ổn.
Bẫy 2: timeoutSeconds mặc định là 1 giây
Probe mất 3 giây để trả lời:
RESTARTS |
|
|---|---|
timeoutSeconds = 1 |
1 |
timeoutSeconds = 5 |
0 |
Unhealthy: Liveness probe failed: command timed out
Ứng dụng hoàn toàn khoẻ. Nó chỉ trả lời chậm hơn một giây — điều bình thường khi node đang bận, khi bộ dọn rác vừa chạy, hoặc khi ứng dụng đang xử lý một lô lớn. timeoutSeconds mặc định là 1, và đó là con số gây tai nạn nhiều nhất trong sáu tham số probe. Rất ít người khai nó, vì nó không xuất hiện trong ví dụ mẫu.
Quy tắc: đặt timeoutSeconds bằng p99 của thời gian probe, nhân đôi. Với endpoint trả lời trong 50 ms thì 1 giây là rộng rãi; với endpoint kiểm tra vài thứ nội bộ thì không. Và chú ý một chi tiết: probe exec tạo một tiến trình mới mỗi lần chạy. Trên node đang tải nặng, chỉ riêng việc khởi tạo tiến trình có thể vượt một giây.
Bẫy 3: readinessProbe quá nhạy
readinessProbe:
periodSeconds: 1
failureThreshold: 1
Một lần trượt là ra khỏi Endpoints ngay.
bình thường: 4 endpoint
sau khi mọi pod cùng trượt probe: 0 endpoint
nhay-...-bmv4d 0/1 Running RESTARTS=0
nhay-...-g2nr5 0/1 Running RESTARTS=0
nhay-...-jtq9f 0/1 Running RESTARTS=0
nhay-...-stsj4 0/1 Running RESTARTS=0
Dịch vụ chết hoàn toàn, và không pod nào khởi động lại. Đây là kiểu hỏng khó chịu nhất vì Kubernetes không thấy có gì hỏng để sửa. Mọi pod đều Running, không có CrashLoopBackOff, không có event lỗi. kubectl get pods trông gần như bình thường — chỉ cột READY là 0/1.
Trong hệ thống thật, nguyên nhân thường là một thứ ảnh hưởng tới mọi pod cùng lúc: cơ sở dữ liệu chậm đi, một dịch vụ chung tăng độ trễ, hoặc node bị nghẽn mạng. Với failureThreshold: 1, một nhịp chậm 200 mili giây là đủ.
Quy tắc: failureThreshold ít nhất là 3. Kết hợp với periodSeconds hợp lý, nó cho ứng dụng vài giây để vượt qua một trục trặc thoáng qua thay vì bị đá ra ngay.
Ba quy tắc, gộp lại
# liveness: rẻ, chỉ kiểm chính mình, khoan dung
livenessProbe:
httpGet: {path: /healthz, port: 8080}
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
# readiness: được phép kiểm phụ thuộc, nhưng đừng quá nhạy
readinessProbe:
httpGet: {path: /ready, port: 8080}
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
# startup: cho ứng dụng đủ thời gian lên
startupProbe:
httpGet: {path: /healthz, port: 8080}
periodSeconds: 5
failureThreshold: 30 # tối đa 150 giây
Hai endpoint khác nhau: /healthz chỉ trả lời "tiến trình còn quay", /ready kiểm cả phụ thuộc. Dùng chung một endpoint cho cả hai probe là cách nhanh nhất rơi vào bẫy 1.
Khi nào không nên có livenessProbe
Đây là quan điểm ít được nói: với nhiều ứng dụng, không có livenessProbe là lựa chọn đúng. Nếu ứng dụng của bạn sập thì tiến trình thoát, và Kubernetes khởi động lại container — không cần probe nào. livenessProbe chỉ có ích cho tình huống còn sống nhưng kẹt, và nhiều ứng dụng đơn giản không có tình huống đó.
Một livenessProbe không cần thiết chỉ thêm một cách để mọi thứ hỏng. Ba bẫy ở trên đều là bẫy của liveness hoặc của việc nhầm nó với readiness. readinessProbe thì gần như luôn nên có — phần 50 đo được nó là thứ tạo ra cập nhật không gián đoạn.
Mẫu số chung: kiểm sức khoẻ chỉ nên đo cái mình kiểm soát, và mỗi lá chắn thêm vào là một cách hỏng mới
Bài này gói hai nguyên tắc về độ tin cậy. Thứ nhất: một phép kiểm sức khoẻ chỉ nên đo cái mà bản thân đối tượng này kiểm soát — buộc nó vào một phụ thuộc dùng chung biến một trục trặc cục bộ thành một cú sập tương quan của cả đàn. Khi livenessProbe của mọi pod cùng gọi một cơ sở dữ liệu, DB chớp tắt là tất cả pod cùng "chết" một lúc; và cơ chế tự-chữa (khởi động lại) vừa không sửa được một lỗi không thuộc về pod, vừa làm phục hồi tệ hơn qua một cơn bão kết nối lúc DB vừa trở lại. Cùng cái bẫy khắp nơi: một health check của bộ cân bằng tải ping tới một DB chung rồi đánh dấu mọi backend down cùng lúc; một loạt circuit breaker cùng nhảy vì một phụ thuộc; cái nghịch lý "chính hệ giám sát gây ra sự cố nó định ngăn". Hai hệ quả cần nhớ: đừng buộc sự sống của mỗi thực thể vào một thứ chung, và cơ chế tự-chữa phải khớp với lỗi thật — khởi động lại chỉ sửa được trạng-thái-sập, không sửa được lỗi cấu hình hay phụ thuộc, và một hành động chữa sai còn khuếch đại sự cố.
Thứ hai: mỗi lá chắn bạn thêm vào cũng là một cách hỏng mới — chỉ thêm cái nào đáng công nuôi. Một livenessProbe không cần thiết (khi ứng dụng sập thì tự thoát và Kubernetes đã khởi động lại) là net-negative: nó không cứu gì, chỉ mở thêm ba cửa để hỏng. Cùng tinh thần với một retry quá rộng, một khoá không cần, một cache có thể lỗi thời: mọi cơ chế thêm vào đều có thể tự nó hỏng. Và với những lá chắn bạn giữ, cho chúng một chút dung sai (hysteresis) — failureThreshold ≥ 3 để một nhịp chậm 200 ms không đá pod ra khỏi dịch vụ.
Mẫu số chung: cho một phép kiểm chỉ đo phạm vi đối tượng tự kiểm soát (đừng kéo phụ thuộc chung vào), khớp cơ chế tự-chữa với lỗi thật, và nhớ rằng mỗi lá chắn là một điểm hỏng — thêm ít, và cho cái giữ lại đủ dung sai với trục trặc thoáng qua.
Muốn tự soi, tìm probe nguy hiểm trong cụm của bạn:
kubectl get pods -A -o json | python3 -c '
import sys,json
d=json.load(sys.stdin)
for p in d["items"]:
for c in p["spec"].get("containers",[]):
lp=c.get("livenessProbe")
if not lp: continue
t=lp.get("timeoutSeconds",1); f=lp.get("failureThreshold",3)
e=lp.get("exec",{}).get("command",[])
canh=[]
if t<=1: canh.append("timeout=1")
if f<=1: canh.append("failureThreshold=1")
if any("curl" in str(x) or "wget" in str(x) for x in e): canh.append("goi ra ngoai?")
if canh: print(p["metadata"]["namespace"], p["metadata"]["name"], c["name"], ",".join(canh))
'
Mỗi dòng hiện ra là một pod có thể bị giết vì lý do không phải lỗi của nó.
Phần sau đo Service: cách một cái tên DNS biến thành lưu lượng tới đúng pod.