「健康探針」模式談的是應用如何把自己的健康狀態傳達給 Kubernetes。要能被完全自動化,雲原生應用必須具備高度可觀測性,讓外界能推知它的狀態,好讓 Kubernetes 判斷應用是否已啟動、是否已準備好服務請求。這些觀測結果會影響 Pod 的生命週期管理,以及流量如何被導向應用。

問題#

Kubernetes 會定期檢查容器的行程狀態,偵測到問題就重啟它。但從實務經驗我們知道,單看行程狀態並不足以判斷應用是否健康——很多情況下應用已經卡死,行程卻還活著:

  • Java 應用可能拋出 OutOfMemoryError,JVM 行程卻仍在執行。
  • 應用可能因無窮迴圈、死結(deadlock)或某種輾轉(快取、堆積、行程 thrashing)而凍結。

要偵測這類狀況,Kubernetes 需要一套可靠的方式來檢查應用健康。重點不是去理解應用內部如何運作,而是取得一個指標,說明應用是否如預期運作、是否有能力服務使用者。

解法#

軟體業已經接受一個事實:不可能寫出零缺陷的程式碼;而在分散式應用中,失敗的機率只會更高。因此,處理失敗的重心已從「避免失敗」轉向「偵測故障並復原」。

偵測失敗並非可以一體適用於所有應用的簡單工作——每個應用對「失敗」的定義不同,而不同類型的失敗也需要不同的矯正動作:有些暫時性失敗只要給足時間就會自行恢復,有些則需要重啟應用。以下是 Kubernetes 用來偵測與矯正失敗的檢查。

行程健康檢查#

行程健康檢查是 kubelet 對容器行程持續執行的最簡單檢查:容器行程若沒在跑,就重啟它。因此即使沒有其他健康檢查,光靠這個通用檢查也能讓應用稍微更健壯一些。

如果你的應用有能力偵測任何形式的失敗並自行關閉,那麼行程健康檢查就夠了。但多數情況下這並不足夠,仍需要其他類型的健康檢查。

存活探針(Liveness Probes)#

若你的應用陷入死結,從行程健康檢查的角度看它仍然是健康的。為了偵測這類問題、以及依你應用商業邏輯定義的其他失敗類型,Kubernetes 提供存活探針——由 kubelet 代理定期執行的檢查,要求容器確認自己依然健康。

健康檢查從外部執行很重要,不能交給應用自己,因為某些失敗恰恰會讓應用的看門狗(watchdog)無法回報失敗。

就矯正動作而言,存活探針與行程健康檢查相同:偵測到失敗就重啟容器。但它在檢查手段上更有彈性:

  • HTTP 探針:對容器 IP 位址發出 HTTP GET 請求,期待回應碼落在 200 到 399 之間。
  • TCP Socket 探針:以能成功建立 TCP 連線為準。
  • Exec 探針:在容器的核心命名空間中執行任意指令,期待成功的結束碼(0)。
apiVersion: v1
kind: Pod
metadata:
  name: pod-with-liveness-check
spec:
  containers:
    - image: k8spatterns/random-generator:1.0
      name: random-generator
      env:
        - name: DELAY_STARTUP
          value: "20"
      ports:
        - containerPort: 8080
      protocol: TCP
      livenessProbe:
        httpGet: # 對健康檢查端點發出 HTTP 探測
          path: /actuator/health
          port: 8080
        initialDelaySeconds: 30 # 首次探測前先等 30 秒,給應用暖機時間

依應用性質選擇最合適的方法;至於「什麼時候算健康」,由你的實作決定。

但要記住:健康檢查沒通過的後果就是容器被重啟。如果重啟容器根本無濟於事,那麼設一個註定失敗的健康檢查毫無好處——Kubernetes 只會不斷重啟你的容器,卻沒有解決底層問題。

就緒探針(Readiness Probes)#

存活檢查藉由殺掉不健康的容器、換上新容器來維持應用健康。但有時容器並非不健康,重啟也於事無補,最常見的例子是:

  • 容器還在啟動中,尚未準備好處理任何請求;
  • 容器過載、延遲上升,你希望它暫時擋掉額外負載、自我保護一陣子。

這類情境要用就緒探針。執行就緒檢查的方法與存活檢查相同(HTTP、TCP、Exec),但矯正動作不同:就緒探針失敗不會重啟容器,而是把容器從服務端點(service endpoint)移除,使它不再收到新流量。

就緒探針在容器準備好時發出訊號,讓它在被服務請求打中之前有時間暖機;由於就緒探針和存活檢查一樣會定期執行,它在後續階段也能用來為服務擋流量。

apiVersion: v1
kind: Pod
metadata:
  name: pod-with-readiness-check
spec:
  containers:
    - image: k8spatterns/random-generator:1.0
      name: random-generator
      readinessProbe:
        exec:
          # 檢查應用「準備好服務請求時才建立」的檔案是否存在。
          # 檔案不存在時 stat 回傳錯誤,就緒檢查即告失敗。
          command: ["stat", "/var/run/random-generator-ready"]

同樣地,「應用何時準備好開工、何時該被暫時放過」由你的健康檢查實作決定。

兩者的哲學差異:行程健康檢查與存活檢查的意圖是「重啟容器以從失敗中復原」,而就緒檢查則是為你的應用爭取時間,期待它自行恢復。

還要留意,當容器正在關閉時,即使它收到 SIGTERM 後就緒檢查仍然通過,Kubernetes 依然會設法阻止它接收新請求。

許多情況下,存活與就緒探針執行的是同樣的檢查。但就緒探針的存在讓容器有啟動的時間:只有通過就緒檢查,一次 Deployment 才被視為成功——例如在滾動更新中,舊版本的 Pod 要等到這一刻才會被終止。

存活與就緒探針是雲原生應用自動化的根本建構單元。Spring actuator、WildFly Swarm health check、Karaf health checks,或 Java 的 MicroProfile 規格等應用框架,都提供了健康探針的實作。

討論#

要能被完全自動化,雲原生應用必須具備高度可觀測性——提供一種手段,讓管理平台能讀取並詮釋應用健康,必要時採取矯正動作。健康檢查在部署、自我修復、擴縮等活動的自動化中扮演根本角色。除此之外,應用還有其他方式能揭露自身健康:

  • 日誌(logging):最明顯也最古老的做法。容器最好把所有重要事件輸出到 system out 與 system error,並將日誌集中收集以供分析。日誌通常不用來觸發自動化動作,而是用來發出警示與後續調查;它更有用的面向是失敗的事後分析與偵測那些不易察覺的錯誤。
  • 終止訊息:除了輸出到標準串流之外,也建議把容器退出的原因寫到 /dev/termination-log——這是容器在永遠消失之前留下遺言的地方。

圖 4-1:容器與執行期平台溝通的各種可觀測性選項

容器把應用當成黑盒子,提供了統一的封裝與執行方式。但任何想成為雲原生公民的容器,都必須提供 API 讓執行期環境觀測其健康並據以行動。這項支援是「以統一方式自動化容器更新與生命週期」的根本前提,而後者又進一步提升系統韌性與使用者體驗。

實務上,這意味著你的容器化應用至少必須提供各類健康檢查(存活與就緒)的 API。

表現更好的應用還應整合 OpenTracing 或 Prometheus 這類追蹤與指標收集函式庫,提供更多手段讓管理平台觀測應用狀態。把你的應用當成黑盒子,但要實作出所有必要的 API,幫助平台以最好的方式觀測與管理它。

下一個模式「受管生命週期」同樣是關於應用與 Kubernetes 管理層之間的溝通,但方向相反——談的是你的應用如何被告知重要的 Pod 生命週期事件

更多資訊#

  • Health Probe Example
  • Configuring Liveness and Readiness Probes
  • Setting Up Health Checks With Readiness and Liveness Probes
  • Resource Quality of Service
  • Graceful Shutdown with Node.js and Kubernetes
  • Advanced Health-Check Patterns in Kubernetes