← 返回上一頁
Kubernetes

為什麼你的 K8s 部署會中斷使用者請求?Spring 微服務 Pod 關閉的陷阱與修復

本頁目錄

前言

「明明只是做了一次 Deployment 更新,為什麼使用者就收到 502 了?」——這是很多團隊在 Kubernetes 上跑 Spring 微服務時都遇過的問題。問題的根源在於 Pod 關閉時的處理不當。本篇將從問題現象出發,拆解 K8s 刪除 Pod 的完整流程,並給出一套可以直接套用的解決方案。

Pod 關閉時到底發生了什麼?

當 Kubernetes 要終止一個 Pod(不管是滾動更新、手動刪除、還是節點資源不足),有兩種可能的結局:

理想情況:優雅關閉

容器在寬限期內完成以下流程:執行 preStop Hook → 接收 SIGTERM 訊號 → 程式正常結束 → Kubelet 從 API Server 清除 Pod 記錄。

最壞情況:強制終止

如果容器在寬限期內沒有自行結束,Kubelet 會直接發送 SIGKILL。可能原因:

  • 程式沒有處理 SIGTERM 訊號
  • preStop Hook 跑太久
  • 應用程式清理資源超時

結果就是進行中的請求被硬切斷,使用者看到錯誤頁面。

兩個會影響使用者的問題

每次 Deployment 更新都是「建新 Pod + 刪舊 Pod」同時進行。如果沒有做好優雅關閉,會碰到兩個問題:

問題一:進行中的請求被中斷

Pod 被刪除時,如果程式沒有處理 SIGTERM,容器會立即退出。正在處理的 HTTP 請求直接斷掉。如果這個請求涉及資料庫寫入且不是冪等操作,可能造成資料不一致。

問題二:流量還在往已死的 Pod 送

這個問題更隱蔽。Pod 刪除過程中有兩條平行的時間線

時間線一:網路規則更新 時間線二:Pod 終止
1. API Server 標記 Pod 為 Terminating 1. API Server 標記 Pod 為 Terminating
2. Endpoint Controller 移除 Pod IP 2. Kubelet 清理容器資源
3. kube-proxy 更新 iptables 規則 3. Kubelet 發送 SIGTERM
4. 30秒後若未退出,發送 SIGKILL

關鍵在於:這兩條時間線是並行執行的,沒有先後順序保證。Pod 可能在 kube-proxy 還沒更新完 iptables 規則前就已經被殺掉了,這時候新進來的請求會被路由到一個已經不存在的 Pod。

三步解決方案

第一步:Spring Boot 開啟優雅關閉

讓應用程式本身能正確回應 SIGTERM 訊號:

server:
  shutdown: graceful

spring:
  lifecycle:
    timeout-per-shutdown-phase: 30s

效果:收到 SIGTERM 後,Spring Boot 會拒絕新請求,等待所有進行中的請求處理完畢(最多等待設定的超時時間),然後才結束程序。

第二步:加入 preStop Hook

解決時間線競爭問題——讓 Pod 在開始關閉前先「睡一會」,給 kube-proxy 足夠時間更新網路規則:

lifecycle:
  preStop:
    exec:
      command: ["sh", "-c", "sleep 10"]

這 10 秒的等待,確保新流量不再被路由到這個 Pod 後,才開始真正的關閉流程。

第三步:調整 terminationGracePeriodSeconds

K8s 預設給 30 秒寬限期,但我們現在的流程是:preStop 10 秒 + Spring 優雅關閉 30 秒 = 40 秒。必須把寬限期調大:

terminationGracePeriodSeconds: 45

完整的 Deployment 範本

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-spring-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: my-spring-app
  template:
    metadata:
      labels:
        app: my-spring-app
    spec:
      terminationGracePeriodSeconds: 45
      containers:
      - name: app
        image: my-spring-app:latest
        ports:
        - containerPort: 8080
        lifecycle:
          preStop:
            exec:
              command: ["sh", "-c", "sleep 10"]

各設定之間的關係

務必確保:

terminationGracePeriodSeconds >= preStop 等待秒數 + 應用優雅關閉超時秒數

以上例為例:45 >= 10 + 30,多出 5 秒作為緩衝。如果你的應用需要更長的關閉時間,各數值都要相應調整。

不同框架的對應做法

框架 優雅關閉設定方式
Spring Boot 2.3+ server.shutdown=graceful
Go (net/http) 攔截 SIGTERM,呼叫 server.Shutdown(ctx)
Node.js (Express) 監聽 process.on('SIGTERM'),呼叫 server.close()
Python (Gunicorn) 設定 graceful_timeout 參數

核心邏輯都一樣:攔截終止訊號 → 停止接受新請求 → 等待進行中請求完成 → 乾淨退出。

總結

Pod 關閉時的使用者請求中斷,根本原因是「網路規則更新」和「Pod 終止」這兩條時間線的競爭條件。解決方案是三管齊下:應用層優雅關閉 + preStop Hook 延遲 + 充足的寬限期。這三個設定缺一不可,建議作為所有 Spring(或任何 Web 框架)微服務在 K8s 上的標準配置模板。

分享這篇
X LinkedIn Facebook Hacker News Reddit

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

這個網站採用 Akismet 服務減少垃圾留言。進一步了解 Akismet 如何處理網站訪客的留言資料