Pod lifecycle, probes và graceful shutdown
Tiếp đi: Pod lifecycle và probes.
Bài này trả lời một câu: process đang chạy có đồng nghĩa application healthy không? Không.
1. Bài toán
payment-service chạy trong Pod. Có hai tình huống:
Tình huống A Tình huống B
Java process ✅ running Java process ✅
Tomcat ✅ running Spring Boot đang initialize...
Thread pool ❌ stuck Hikari đang connect postgres...
App ❌ không phục vụ API ❌ chưa ready
Trong cả hai, container vẫn "Running". Nếu K8s chỉ nhìn PID, nó sẽ gửi request của order-service vào một Pod hỏng hoặc chưa sẵn sàng.
2. Vấn đề
- Không có probe: Pod vừa start là Service gửi traffic ngay, trong khi Spring Boot cần 20–40 giây. Order nhận lỗi 502/connection refused.
- App deadlock: process không chết, K8s không bao giờ restart.
- Khi Pod bị xóa (deploy, scale down), app đang xử lý thanh toán dở thì bị kill ngang.
3. Giải pháp: Pod lifecycle + 3 loại probe + graceful shutdown
Pod phase (trường status.phase):
| Phase | Nghĩa |
|---|---|
| Pending | Đã được API server nhận, chưa chạy: chờ schedule hoặc đang pull image. |
| Running | Đã gán node, ít nhất một container đang chạy (chưa chắc Ready). |
| Succeeded | Mọi container kết thúc với exit code 0 (Job). |
| Failed | Ít nhất một container kết thúc lỗi và không restart nữa. |
| Unknown | Mất liên lạc với node. |
CrashLoopBackOff, ImagePullBackOff, OOMKilled không phải phase. Chúng là reason của trạng thái container, hiện ở cột STATUS của kubectl get pods.
Ba câu hỏi, ba probe (phần phân biệt liveness/readiness cơ bản đã có ở bài 9.7, ở đây đi sâu vào cấu hình cho Spring Boot):
| Probe | Câu hỏi | Nếu fail |
|---|---|---|
| startupProbe | App start xong chưa? | Chưa chạy liveness/readiness. Hết hạn mà vẫn fail thì restart. |
| readinessProbe | Nhận traffic được không? | Gỡ Pod khỏi Service endpoints. Không restart. |
| livenessProbe | Còn sống, có cần restart không? | kubelet restart container. |
Probe có 4 kiểu kiểm tra: httpGet, tcpSocket, exec, grpc. Spring Boot dùng httpGet vào Actuator.
4. Code từng bước
Bước 1: Dependency
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
Bước 2: Config application.yml
management:
endpoints:
web:
exposure:
include: health,info
endpoint:
health:
probes:
enabled: true # tự bật khi chạy trong K8s, khai báo cho rõ
group:
readiness:
include: readinessState,db # DB vào readiness, KHÔNG vào liveness
server:
shutdown: graceful
spring:
lifecycle:
timeout-per-shutdown-phase: 20s
Spring Boot có sẵn:
/actuator/health/liveness → LivenessState (CORRECT / BROKEN)
/actuator/health/readiness → ReadinessState (ACCEPTING_TRAFFIC / REFUSING_TRAFFIC)
Bước 3: Code Java: tự báo trạng thái khi cần
@Component
@RequiredArgsConstructor
public class PaymentGatewayWatcher {
private final ApplicationEventPublisher publisher;
// Bank API mất kết nối lâu: tạm ngừng nhận traffic, không cần restart
public void onGatewayDown() {
AvailabilityChangeEvent.publish(publisher, this, ReadinessState.REFUSING_TRAFFIC);
}
public void onGatewayUp() {
AvailabilityChangeEvent.publish(publisher, this, ReadinessState.ACCEPTING_TRAFFIC);
}
// Trạng thái nội bộ hỏng không tự phục hồi được: xin restart
public void onUnrecoverableState() {
AvailabilityChangeEvent.publish(publisher, this, LivenessState.BROKEN);
}
}
Bước 4: Deployment YAML với ba probe + graceful shutdown
apiVersion: apps/v1
kind: Deployment
metadata:
name: payment-service
spec:
replicas: 3
selector:
matchLabels: { app: payment-service }
template:
metadata:
labels: { app: payment-service }
spec:
terminationGracePeriodSeconds: 40
containers:
- name: payment
image: myregistry/payment-service:1.0
ports: [ { containerPort: 8080 } ]
startupProbe:
httpGet: { path: /actuator/health/liveness, port: 8080 }
periodSeconds: 5
failureThreshold: 12 # 12 x 5s = tối đa ~60s để start
readinessProbe:
httpGet: { path: /actuator/health/readiness, port: 8080 }
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 3
livenessProbe:
httpGet: { path: /actuator/health/liveness, port: 8080 }
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 3
lifecycle:
preStop:
exec:
command: ["sh", "-c", "sleep 5"]
Ý nghĩa tham số:
initialDelaySeconds: đợi bao lâu mới probe lần đầu (có startupProbe thì thường không cần).periodSeconds: bao lâu probe một lần.timeoutSeconds: endpoint không trả lời trong thời gian này thì lần đó tính là fail.failureThreshold: fail liên tiếp bao nhiêu lần mới tính là fail thật.
Test:
kubectl apply -f payment-deployment.yaml
kubectl get pods -w
# NAME READY STATUS RESTARTS
# payment-service-x7k2p 0/1 Running 0 ← đang start, chưa Ready
# payment-service-x7k2p 1/1 Running 0 ← readiness pass
kubectl port-forward deploy/payment-service 8080:8080
curl localhost:8080/actuator/health/readiness
# {"status":"UP"}
5. Cách hoạt động
Startup → readiness/liveness
Container starts
│
▼
STARTUP PROBE "start xong chưa?"
│ NO → retry (tối đa failureThreshold × period) → hết hạn → restart
│ YES
▼
┌─────┴──────────┐
▼ ▼
READINESS LIVENESS
"nhận traffic?" "còn sống?"
NO → gỡ khỏi NO → kubelet
Service restart container
Lỗi kinh điển: liveness quá gắt, không có startupProbe
Spring Boot cần 40s để start, livenessProbe initialDelaySeconds: 5
0s container start
5s liveness FAIL
15s FAIL
25s FAIL → restart
0 → 25s → restart → 0 → 25s → restart ... App không bao giờ start xong.
Graceful termination: khi Pod bị xóa
kubectl delete / rolling update / scale down
│
├──────────────────────────────┐ (song song)
▼ ▼
Pod → Terminating Endpoint controller gỡ Pod khỏi Service
│ kube-proxy trên các node cập nhật rule (mất vài giây)
▼
preStop: sleep 5 ← chờ các node cập nhật xong, tránh request mới rơi vào Pod sắp chết
│
▼
SIGTERM → Spring Boot graceful shutdown
(ngừng nhận request mới, xử lý nốt request đang chạy, tối đa 20s)
│
▼
Hết terminationGracePeriodSeconds (40s) mà chưa thoát → SIGKILL
Điều kiện: preStop + timeout-per-shutdown-phase < terminationGracePeriodSeconds. App phải là PID 1 hoặc nhận được SIGTERM (Dockerfile dùng exec form ENTRYPOINT ["java","-jar","app.jar"]).
restartPolicy và CrashLoopBackOff
| restartPolicy | Dùng cho |
|---|---|
| Always (mặc định) | Deployment, StatefulSet. Deployment chỉ cho phép giá trị này. |
| OnFailure | Job: chỉ restart khi exit code khác 0. |
| Never | Job chạy một lần, lỗi thì để Job tạo Pod mới hoặc dừng. |
Container cứ start rồi chết, kubelet restart với thời gian chờ tăng dần: 10s, 20s, 40s... tối đa 5 phút. Trạng thái chờ đó là CrashLoopBackOff. Nó là triệu chứng, nguyên nhân nằm ở log lần chạy trước:
kubectl get pods
# payment-service-x7k2p 0/1 CrashLoopBackOff 6
kubectl logs payment-service-x7k2p --previous
# ... Failed to configure a DataSource: 'url' attribute is not specified
6. So sánh: container restart và Pod bị thay
| Container restart | Pod bị thay | |
|---|---|---|
| Ai làm | kubelet trên node | ReplicaSet controller |
| Khi nào | Liveness fail, process exit, OOMKilled | Pod bị xóa, node chết, bị evict |
| Tên Pod, IP | Giữ nguyên | Tên mới, IP mới |
| Dấu hiệu | Cột RESTARTS tăng | Pod cũ biến mất, Pod mới xuất hiện |
| Dữ liệu emptyDir | Còn | Mất |
7. Khi nào nên dùng / không nên dùng
- Luôn có readinessProbe cho service nhận traffic. Không có nó, rolling update sẽ gây lỗi.
- startupProbe cho app start chậm (Spring Boot, JVM lớn) thay vì tăng
initialDelaySecondscủa liveness. - livenessProbe chỉ kiểm tra trạng thái nội bộ. Không đưa DB, Redis, API ngoài vào liveness. DB chết thì restart Spring cũng không sửa được DB, 20 Pod restart cùng lúc còn làm sự cố nặng hơn.
- Không chắc liveness đúng thì không dùng còn hơn dùng sai: process crash thì kubelet đã tự restart theo restartPolicy rồi.
Pod hiện Running mà cột READY lại là 0/1 là sao nhỉ?
Đúng là hai cột này đo hai thứ khác nhau:
NAME READY STATUS RESTARTS
payment-service-x7k2p 0/1 Running 0
- STATUS Running: container đã start, process đang chạy.
- READY 0/1: 0 trên 1 container pass readinessProbe. Service chưa gửi traffic vào Pod này.
Lúc mới start thì bình thường. Nếu kéo dài, xem lý do:
kubectl describe pod payment-service-x7k2p
# Events:
# Warning Unhealthy Readiness probe failed: HTTP probe failed with statuscode: 503
503 ở readiness của Spring Boot thường do một health indicator trong group readiness đang DOWN, ví dụ không kết nối được postgres. Gọi thử endpoint để xem component nào DOWN:
kubectl exec payment-service-x7k2p -- wget -qO- localhost:8080/actuator/health/readiness
Câu nên nhớ: Running ≠ Ready.
Nếu không cấu hình preStop thì graceful shutdown của Spring Boot có đủ không?
Gần đủ, nhưng còn một khe hở nhỏ. Khi Pod bị xóa, hai việc xảy ra song song, không theo thứ tự:
- kubelet gửi SIGTERM cho container.
- Pod bị gỡ khỏi Service, kube-proxy trên từng node cập nhật rule.
Nếu (1) nhanh hơn (2), Spring Boot đã ngừng nhận kết nối mới trong khi vài node vẫn còn gửi request tới Pod, order-service nhận connection refused. preStop: sleep 5 giữ app nhận request thêm vài giây cho (2) kịp xong rồi mới SIGTERM.
Image distroless không có sh thì dùng lifecycle.preStop.sleep (K8s 1.30+):
lifecycle:
preStop:
sleep:
seconds: 5
Phỏng vấn hỏi về probes và vòng đời Pod thì trả lời sao?
Q1. Spring Boot khởi động mất 60 giây, bạn cấu hình probes thế nào?
🇻🇳 Tiếng Việt:
Em dùng startupProbe trỏ vào
/actuator/health/livenessvớiperiodSeconds5 vàfailureThresholdkhoảng 15 đến 18, tức cho tối đa 75 đến 90 giây để start. Khi startupProbe pass thì readiness và liveness mới bắt đầu, nên liveness có thể để ngắn mà không giết app đang khởi động. Readiness trỏ vào/actuator/health/readiness, có thể kèm DB; liveness chỉ kiểm tra trạng thái nội bộ.
🇬🇧 English:
I add a startupProbe on
/actuator/health/livenesswithperiodSeconds5 and afailureThresholdaround 15 to 18, giving it up to 75 to 90 seconds to start. Readiness and liveness only begin after the startup probe succeeds, so liveness can stay tight without killing an app that is still booting. Readiness points to/actuator/health/readinessand may include the database; liveness only checks internal state.
Q2. CrashLoopBackOff là gì, bạn xử lý thế nào?
🇻🇳 Tiếng Việt:
Đó là trạng thái container cứ start rồi thoát, kubelet restart với thời gian chờ tăng dần đến 5 phút. Nó là triệu chứng, không phải nguyên nhân. Em xem
kubectl logs --previousđể đọc log lần chạy bị crash, vàkubectl describe podđể xem exit code, reason như OOMKilled hay liveness fail. Nguyên nhân hay gặp là thiếu config/secret, sai URL database, hết memory, hoặc liveness quá gắt.
🇬🇧 English:
It means the container keeps starting and exiting, and the kubelet restarts it with an increasing back-off of up to five minutes. It's a symptom, not a cause. I read the crashed run with
kubectl logs --previousand checkkubectl describe podfor the exit code and reason, such as OOMKilled or a failed liveness probe. Common causes are missing config or secrets, a wrong database URL, running out of memory, or an overly aggressive liveness probe.
Q3. Làm sao để Pod bị xóa trong lúc deploy không làm rơi request?
🇻🇳 Tiếng Việt:
Em bật
server.shutdown=gracefulđể Spring Boot xử lý nốt request đang chạy khi nhận SIGTERM, thêm preStop sleep vài giây để Pod kịp bị gỡ khỏi endpoints trên mọi node trước khi app ngừng nhận kết nối, và đặtterminationGracePeriodSecondslớn hơn tổng preStop cộng thời gian shutdown. Dockerfile dùng exec form để Java là PID 1 và nhận được SIGTERM.
🇬🇧 English:
I enable
server.shutdown=gracefulso Spring Boot finishes in-flight requests on SIGTERM, add a preStop sleep of a few seconds so the Pod is removed from endpoints on every node before the app stops accepting connections, and setterminationGracePeriodSecondsabove the preStop delay plus the shutdown timeout. The Dockerfile uses the exec form so Java runs as PID 1 and actually receives SIGTERM.
Q4. Container restart khác Pod bị tạo lại thế nào?
🇻🇳 Tiếng Việt:
Container restart do kubelet làm trên cùng node, Pod giữ nguyên tên và IP, chỉ cột RESTARTS tăng; nguyên nhân là process thoát, OOMKilled hoặc liveness fail. Pod bị tạo lại do ReplicaSet controller làm khi Pod cũ bị xóa hoặc node chết; Pod mới có tên và IP khác, có thể nằm ở node khác, và dữ liệu emptyDir mất.
🇬🇧 English:
A container restart is done by the kubelet on the same node; the Pod keeps its name and IP and only the RESTARTS count goes up. It happens when the process exits, gets OOMKilled or fails liveness. A replacement Pod is created by the ReplicaSet controller when the old Pod is deleted or its node dies; it gets a new name and IP, may land on another node, and emptyDir data is lost.
Cách nhớ ngắn gọn
- Startup = cổng chờ khởi động; readiness = traffic; liveness = restart.
- Running ≠ Ready. DB không vào liveness.
- Xóa Pod: gỡ endpoint và SIGTERM chạy song song → preStop sleep + graceful shutdown.
- CrashLoopBackOff là triệu chứng:
logs --previous+describe.
Tự kiểm tra
Readiness fail thì Pod có bị restart không?
Không. Pod chỉ bị gỡ khỏi Service endpoints, hồi phục thì nhận traffic lại.
Vì sao dùng startupProbe thay vì tăng initialDelaySeconds của liveness?
startupProbe chỉ chạy lúc khởi động. Sau đó liveness vẫn phát hiện treo nhanh, không phải chờ delay dài.
Pod có terminationGracePeriodSeconds: 30, app cần 45s để shutdown. Chuyện gì xảy ra?
Hết 30s kubelet gửi SIGKILL, request đang chạy bị cắt ngang.
Deployment có dùng được restartPolicy: Never không?
Không. Deployment chỉ cho phép Always. OnFailure/Never dành cho Job.
Xem log của lần container bị crash trước đó bằng lệnh nào?
kubectl logs <pod> --previous.