本文發表於一年多前。舊文章可能包含過時內容。請檢查頁面中的資訊自發布以來是否已變得不正確。

Kubernetes 1.24:StatefulSet 的最大不可用副本數

Kubernetes StatefulSet 自 1.5 版本引入並在 1.9 版本穩定以來,已被廣泛用於執行有狀態應用。它們提供穩定的 Pod 標識、每個 Pod 的持久化儲存以及有序的優雅部署、擴縮和滾動更新。你可以將 StatefulSet 視為運行復雜有狀態應用的原子構建塊。隨著 Kubernetes 的使用越來越廣泛,需要 StatefulSet 的場景也越來越多。其中許多場景需要比當前支援的“一次一個 Pod”更新更快的滾動更新,特別是在你為 StatefulSet 使用 OrderedReady Pod 管理策略時。

以下是一些例子

  • 我正在使用 StatefulSet 來編排一個多例項、基於快取的應用,其中快取的規模很大。快取從冷啟動開始,需要相當長的時間才能啟動容器。可能還需要執行更多初始啟動任務。對此 StatefulSet 進行滾動更新(RollingUpdate)會花費大量時間才能完全更新應用。如果 StatefulSet 支援一次更新多個 Pod,更新速度會快得多。

  • 我的有狀態應用由領導者和跟隨者,或者一個寫入者和多個讀取者組成。我有多個讀取者或跟隨者,我的應用可以容忍多個 Pod 同時宕機。我希望一次更新多個 Pod,以便快速推出新的更新,特別是當我的應用例項數量很大時。請注意,我的應用仍然需要每個 Pod 具有唯一的標識。

為了支援這類場景,Kubernetes 1.24 引入了一個新的 Alpha 功能。在使用這個新功能之前,你必須啟用 MaxUnavailableStatefulSet 特性門控。啟用後,你可以在 StatefulSet 的 spec 中指定一個新欄位 maxUnavailable。例如:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: web
  namespace: default
spec:
  podManagementPolicy: OrderedReady  # you must set OrderedReady
  replicas: 5
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      # image changed since publication (previously used registry "k8s.gcr.io")
      - image: registry.k8s.io/nginx-slim:0.8
        imagePullPolicy: IfNotPresent
        name: nginx
  updateStrategy:
    rollingUpdate:
      maxUnavailable: 2 # this is the new alpha field, whose default value is 1
      partition: 0
    type: RollingUpdate

如果你啟用了這個新功能,但沒有在 StatefulSet 中為 maxUnavailable 指定值,Kubernetes 會應用預設值 maxUnavailable: 1。這與未啟用新功能時的行為一致。

我將透過一個基於該示例清單的場景來演示此功能的工作原理。我將部署一個 StatefulSet,它有 5 個副本,maxUnavailable 設定為 2,partition 設定為 0。

我可以透過將映象更改為 registry.k8s.io/nginx-slim:0.9 來觸發滾動更新。一旦我啟動滾動更新,我可以觀察到 Pod 每次更新 2 個,因為當前 maxUnavailable 的值為 2。以下輸出顯示了一段時間內的情況,並不完整。maxUnavailable 可以是一個絕對數(例如 2),也可以是所需 Pods 的百分比(例如 10%)。絕對數是透過將百分比向上取整到最接近的整數計算得出的。

kubectl get pods --watch 
NAME    READY   STATUS    RESTARTS   AGE
web-0   1/1     Running   0          85s
web-1   1/1     Running   0          2m6s
web-2   1/1     Running   0          106s
web-3   1/1     Running   0          2m47s
web-4   1/1     Running   0          2m27s
web-4   1/1     Terminating   0          5m43s ----> start terminating 4
web-3   1/1     Terminating   0          6m3s  ----> start terminating 3
web-3   0/1     Terminating   0          6m7s
web-3   0/1     Pending       0          0s
web-3   0/1     Pending       0          0s
web-4   0/1     Terminating   0          5m48s
web-4   0/1     Terminating   0          5m48s
web-3   0/1     ContainerCreating   0          2s
web-3   1/1     Running             0          2s
web-4   0/1     Pending             0          0s
web-4   0/1     Pending             0          0s
web-4   0/1     ContainerCreating   0          0s
web-4   1/1     Running             0          1s
web-2   1/1     Terminating         0          5m46s ----> start terminating 2 (only after both 4 and 3 are running)
web-1   1/1     Terminating         0          6m6s  ----> start terminating 1
web-2   0/1     Terminating         0          5m47s
web-1   0/1     Terminating         0          6m7s
web-1   0/1     Pending             0          0s
web-1   0/1     Pending             0          0s
web-1   0/1     ContainerCreating   0          1s
web-1   1/1     Running             0          2s
web-2   0/1     Pending             0          0s
web-2   0/1     Pending             0          0s
web-2   0/1     ContainerCreating   0          0s
web-2   1/1     Running             0          1s
web-0   1/1     Terminating         0          6m6s ----> start terminating 0 (only after 2 and 1 are running)
web-0   0/1     Terminating         0          6m7s
web-0   0/1     Pending             0          0s
web-0   0/1     Pending             0          0s
web-0   0/1     ContainerCreating   0          0s
web-0   1/1     Running             0          1s

請注意,滾動更新一開始,序號最高的兩個 Pod 4 和 3 會同時開始終止。序號為 4 和 3 的 Pod 可能會按各自的節奏就緒。一旦 Pod 4 和 3 都就緒,Pod 2 和 1 就會同時開始終止。當 Pod 2 和 1 都執行並就緒後,Pod 0 開始終止。

在 Kubernetes 中,StatefulSet 的更新在更新 Pod 時遵循嚴格的順序。在這個例子中,更新從副本 4 開始,然後是副本 3,接著是副本 2,依此類推,一次一個 Pod。當一次只更新一個 Pod 時,3 不可能在 4 之前執行並就緒。當 maxUnavailable 大於 1 時(在示例場景中,我將 maxUnavailable 設定為 2),副本 3 可能會在副本 4 就緒之前就緒並執行——這是可以的。如果你是開發人員,並且將 maxUnavailable 設定為大於 1,你應該知道這種結果是可能發生的,並且你必須確保你的應用能夠處理任何可能出現的此類排序問題。當你將 maxUnavailable 設定為大於 1 時,更新順序在每批 Pod 之間是有保證的。這個保證意味著更新批次 2(副本 2 和 1)中的 Pod 在批次 0(副本 4 和 3)的 Pods 就緒之前不能開始更新。

儘管 Kubernetes 將這些稱為**副本(replicas)**,但你的有狀態應用可能有不同的視角,StatefulSet 的每個 Pod 可能持有與其他 Pod 完全不同的資料。這裡的重點是,StatefulSet 的更新是分批進行的,現在你可以擁有大於 1 的批次大小(作為一個 Alpha 功能)。

另請注意,上述行為是在 podManagementPolicy: OrderedReady 的情況下。如果你將 StatefulSet 定義為 podManagementPolicy: Parallel,不僅會有 maxUnavailable 數量的副本同時被終止,同樣數量的副本也會同時進入 ContainerCreating 階段。這被稱為突發(bursting)。

那麼,現在你可能會有很多關於以下方面的問題:

  • 當你設定 podManagementPolicy: Parallel 時,行為是怎樣的?
  • partition 設定為非 0 的值時,行為又是怎樣的?

最好還是親自嘗試一下。這是一個 Alpha 功能,Kubernetes 的貢獻者們正在尋求對此功能的反饋。這個功能是否幫助你實現了你的有狀態場景?你是否發現了 bug,或者你認為當前的實現行為不直觀,可能會破壞應用或讓它們措手不及?請提交一個 issue 讓我們知道。

進一步閱讀和後續步驟