Pod lifecycle, probes và graceful shutdown

4 câu hỏi phỏng vấn~25 phútTừ khóa: Pod phase, startupProbe, readinessProbe, livenessProbe, Actuator, SIGTERM, preStop, terminationGracePeriodSeconds, restartPolicy, CrashLoopBackOff

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 đề

3. Giải pháp: Pod lifecycle + 3 loại probe + graceful shutdown

Pod phase (trường status.phase):

PhaseNghĩ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).
SucceededMọ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.
UnknownMấ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):

ProbeCâu hỏiNếu fail
startupProbeApp start xong chưa?Chưa chạy liveness/readiness. Hết hạn mà vẫn fail thì restart.
readinessProbeNhận traffic được không?Gỡ Pod khỏi Service endpoints. Không restart.
livenessProbeCò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ố:

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

restartPolicyDùng cho
Always (mặc định)Deployment, StatefulSet. Deployment chỉ cho phép giá trị này.
OnFailureJob: chỉ restart khi exit code khác 0.
NeverJob 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 restartPod bị thay
Ai làmkubelet trên nodeReplicaSet controller
Khi nàoLiveness fail, process exit, OOMKilledPod bị xóa, node chết, bị evict
Tên Pod, IPGiữ nguyênTên mới, IP mới
Dấu hiệuCột RESTARTS tăngPod cũ biến mất, Pod mới xuất hiện
Dữ liệu emptyDirCònMất

7. Khi nào nên dùng / không nên dùng

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

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ự:

  1. kubelet gửi SIGTERM cho container.
  2. 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/liveness với periodSeconds 5 và failureThreshold khoả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/liveness with periodSeconds 5 and a failureThreshold around 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/readiness and 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 --previous and check kubectl describe pod for 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à đặt terminationGracePeriodSeconds lớ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=graceful so 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 set terminationGracePeriodSeconds above 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


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.