Pod 拓撲擴充套件約束

你可以使用**拓撲分佈約束**來控制 Pod 如何在叢集的故障域中(例如區域、可用區、節點和其他使用者定義的拓撲域)進行分佈。這有助於實現高可用性並高效利用資源。

你可以將叢集級別約束設定為預設值,或者為單個工作負載配置拓撲分佈約束。

動機

假設你有一個多達二十個節點的叢集,並且你希望執行一個自動調整副本數量的工作負載。Pod 的數量可以少至兩個,多至十五個。當只有兩個 Pod 時,你寧願不讓這兩個 Pod 都執行在同一個節點上:你將面臨單個節點故障導致工作負載離線的風險。

除了這種基本用法,還有一些高階用法示例,使你的工作負載能夠從高可用性和叢集利用率中受益。

隨著 Pod 數量的增加,一個不同的問題變得重要。假設你有三個節點,每個節點執行五個 Pod。這些節點有足夠的容量來執行這麼多副本;但是,與此工作負載互動的客戶端分佈在三個不同的資料中心(或基礎設施區域)中。現在你不再擔心單個節點故障,但你注意到延遲高於預期,並且你正在為不同區域之間傳送網路流量相關的網路成本付費。

你決定在正常操作下,你更希望將數量相似的副本排程到每個基礎設施區域中,並且你希望叢集在出現問題時能夠自我修復。

Pod 拓撲分佈約束為你提供了一種宣告式方式來配置它。

topologySpreadConstraints 欄位

Pod API 包含一個欄位,spec.topologySpreadConstraints。該欄位的用法如下所示:

---
apiVersion: v1
kind: Pod
metadata:
  name: example-pod
spec:
  # Configure a topology spread constraint
  topologySpreadConstraints:
    - maxSkew: <integer>
      minDomains: <integer> # optional
      topologyKey: <string>
      whenUnsatisfiable: <string>
      labelSelector: <object>
      matchLabelKeys: <list> # optional; beta since v1.27
      nodeAffinityPolicy: [Honor|Ignore] # optional; beta since v1.26
      nodeTaintsPolicy: [Honor|Ignore] # optional; beta since v1.26
  ### other Pod fields go here

你可以透過執行 `kubectl explain Pod.spec.topologySpreadConstraints` 或參考 Pod API 參考中排程部分來了解更多關於此欄位的資訊。

分佈約束定義

你可以定義一個或多個 `topologySpreadConstraints` 條目,以指導 kube-scheduler 如何將每個傳入 Pod 放置到叢集中現有 Pod 的相關位置。這些欄位是:

  • maxSkew 描述了 Pod 分佈不均勻的程度。你必須指定此欄位,並且該數字必須大於零。其語義根據 `whenUnsatisfiable` 的值而不同:

    • 如果你選擇 `whenUnsatisfiable: DoNotSchedule`,那麼 `maxSkew` 定義了目標拓撲中匹配 Pod 數量與**全域性最小值**(合格域中匹配 Pod 的最小數量,如果合格域的數量小於 `MinDomains`,則為零)之間的最大允許差異。例如,如果你有 3 個區域分別有 2、2 和 1 個匹配 Pod,`MaxSkew` 設定為 1,則全域性最小值為 1。
    • 如果你選擇 `whenUnsatisfiable: ScheduleAnyway`,排程器會優先選擇有助於減少傾斜的拓撲。
  • minDomains 指示合格域的最小數量。此欄位是可選的。域是拓撲的特定例項。合格域是其節點匹配節點選擇器的域。

    • 當指定時,`minDomains` 的值必須大於 0。你只能在 `whenUnsatisfiable: DoNotSchedule` 的情況下指定 `minDomains`。
    • 當匹配拓撲鍵的合格域數量小於 `minDomains` 時,Pod 拓撲分佈會將全域性最小值視為 0,然後執行 `skew` 的計算。全域性最小值是合格域中匹配 Pod 的最小數量,如果合格域的數量小於 `minDomains`,則為零。
    • 當匹配拓撲鍵的合格域數量等於或大於 `minDomains` 時,此值對排程沒有影響。
    • 如果你不指定 `minDomains`,則約束的行為如同 `minDomains` 為 1。
  • topologyKey節點標籤的鍵。具有此鍵和相同值的標籤的節點被認為是屬於同一拓撲。我們將拓撲的每個例項(換句話說,一個 對)稱為一個域。排程器將嘗試將平衡數量的 Pod 放入每個域。此外,我們將合格域定義為其節點滿足 nodeAffinityPolicy 和 nodeTaintsPolicy 要求的域。

  • whenUnsatisfiable 指示當 Pod 不滿足分佈約束時如何處理它:

    • DoNotSchedule(預設)告訴排程器不要排程它。
    • ScheduleAnyway 告訴排程器仍然排程它,同時優先選擇最小化傾斜的節點。
  • labelSelector 用於查詢匹配的 Pod。匹配此標籤選擇器的 Pod 將被計數,以確定其相應拓撲域中的 Pod 數量。有關更多詳細資訊,請參閱標籤選擇器

  • matchLabelKeys 是用於選擇計算分佈傾斜的 Pod 組的 Pod 標籤鍵列表。在 Pod 建立時,kube-apiserver 使用這些鍵從傳入的 Pod 標籤中查詢值,並且這些鍵值標籤將與任何現有的 `labelSelector` 合併。相同的鍵不允許同時存在於 `matchLabelKeys` 和 `labelSelector` 中。當未設定 `labelSelector` 時,不能設定 `matchLabelKeys`。Pod 標籤中不存在的鍵將被忽略。空列表或 null 表示僅匹配 `labelSelector`。

    使用 `matchLabelKeys`,你無需在不同的修訂版本之間更新 `pod.spec`。控制器/操作員只需為不同的修訂版本設定相同標籤鍵的不同值。例如,如果你正在配置 Deployment,你可以使用由 Deployment 控制器自動新增的以 pod-template-hash 鍵控的標籤,以區分單個 Deployment 中的不同修訂版本。

        topologySpreadConstraints:
            - maxSkew: 1
              topologyKey: kubernetes.io/hostname
              whenUnsatisfiable: DoNotSchedule
              labelSelector:
                matchLabels:
                  app: foo
              matchLabelKeys:
                - pod-template-hash
    
  • nodeAffinityPolicy 指示在計算 Pod 拓撲分佈傾斜時如何處理 Pod 的 nodeAffinity/nodeSelector。選項包括:

    • Honor: 只有匹配 nodeAffinity/nodeSelector 的節點才包含在計算中。
    • Ignore: nodeAffinity/nodeSelector 被忽略。所有節點都包含在計算中。

    如果此值為 null,則行為等同於 Honor 策略。

  • nodeTaintsPolicy 指示在計算 Pod 拓撲分佈傾斜時如何處理節點汙點。選項包括:

    • Honor: 不帶汙點的節點,以及傳入 Pod 具有容忍度的帶汙點節點,都包含在內。
    • Ignore: 節點汙點被忽略。所有節點都包含在內。

    如果此值為 null,則行為等同於 Ignore 策略。

當一個 Pod 定義了多個 `topologySpreadConstraint` 時,這些約束透過邏輯 AND 操作組合:kube-scheduler 為傳入的 Pod 尋找一個滿足所有已配置約束的節點。

節點標籤

拓撲分佈約束依賴節點標籤來識別每個節點所在的拓撲域。例如,一個節點可能具有以下標籤:

  region: us-east-1
  zone: us-east-1a

假設你有一個 4 節點叢集,具有以下標籤:

NAME    STATUS   ROLES    AGE     VERSION   LABELS
node1   Ready    <none>   4m26s   v1.16.0   node=node1,zone=zoneA
node2   Ready    <none>   3m58s   v1.16.0   node=node2,zone=zoneA
node3   Ready    <none>   3m17s   v1.16.0   node=node3,zone=zoneB
node4   Ready    <none>   2m43s   v1.16.0   node=node4,zone=zoneB

那麼叢集在邏輯上如下圖所示:

graph TB subgraph "zoneB" n3(Node3) n4(Node4) end subgraph "zoneA" n1(Node1) n2(Node2) end classDef plain fill:#ddd,stroke:#fff,stroke-width:4px,color:#000; classDef k8s fill:#326ce5,stroke:#fff,stroke-width:4px,color:#fff; classDef cluster fill:#fff,stroke:#bbb,stroke-width:2px,color:#326ce5; class n1,n2,n3,n4 k8s; class zoneA,zoneB cluster;

一致性

你應該在組中的所有 Pod 上設定相同的 Pod 拓撲分佈約束。

通常,如果你使用 Deployment 等工作負載控制器,Pod 模板會為你處理此問題。如果你混合使用不同的分佈約束,Kubernetes 會遵循欄位的 API 定義;但是,行為更容易令人困惑,並且故障排除也更不直接。

你需要一種機制來確保拓撲域中的所有節點(例如雲提供商區域)都具有一致的標籤。為了避免你手動標記節點,大多數叢集會自動填充知名標籤,例如 `kubernetes.io/hostname`。請檢查你的叢集是否支援此功能。

拓撲分佈約束示例

示例:一個拓撲分佈約束

假設你有一個 4 節點叢集,其中 3 個標記為 `foo: bar` 的 Pod 分別位於 node1、node2 和 node3:

graph BT subgraph "zoneB" p3(Pod) --> n3(Node3) n4(Node4) end subgraph "zoneA" p1(Pod) --> n1(Node1) p2(Pod) --> n2(Node2) end classDef plain fill:#ddd,stroke:#fff,stroke-width:4px,color:#000; classDef k8s fill:#326ce5,stroke:#fff,stroke-width:4px,color:#fff; classDef cluster fill:#fff,stroke:#bbb,stroke-width:2px,color:#326ce5; class n1,n2,n3,n4,p1,p2,p3 k8s; class zoneA,zoneB cluster;

如果你希望傳入的 Pod 與現有 Pod 在區域之間均勻分佈,你可以使用類似於以下內容的清單:

kind: Pod
apiVersion: v1
metadata:
  name: mypod
  labels:
    foo: bar
spec:
  topologySpreadConstraints:
  - maxSkew: 1
    topologyKey: zone
    whenUnsatisfiable: DoNotSchedule
    labelSelector:
      matchLabels:
        foo: bar
  containers:
  - name: pause
    image: registry.k8s.io/pause:3.1

從該清單中,`topologyKey: zone` 意味著均勻分佈將僅應用於標記為 `zone: ` 的節點(沒有 `zone` 標籤的節點將被跳過)。欄位 `whenUnsatisfiable: DoNotSchedule` 告訴排程器,如果排程器找不到滿足約束的方法,則讓傳入的 Pod 保持 Pending 狀態。

如果排程器將此傳入 Pod 放置到區域 `A` 中,Pod 的分佈將變為 `[3, 1]`。這意味著實際傾斜度為 2(計算為 `3 - 1`),這違反了 `maxSkew: 1`。為了滿足此示例的約束和上下文,傳入的 Pod 只能放置到區域 `B` 中的節點上:

graph BT subgraph "zoneB" p3(Pod) --> n3(Node3) p4(mypod) --> n4(Node4) end subgraph "zoneA" p1(Pod) --> n1(Node1) p2(Pod) --> n2(Node2) end classDef plain fill:#ddd,stroke:#fff,stroke-width:4px,color:#000; classDef k8s fill:#326ce5,stroke:#fff,stroke-width:4px,color:#fff; classDef cluster fill:#fff,stroke:#bbb,stroke-width:2px,color:#326ce5; class n1,n2,n3,n4,p1,p2,p3 k8s; class p4 plain; class zoneA,zoneB cluster;

或者

graph BT subgraph "zoneB" p3(Pod) --> n3(Node3) p4(mypod) --> n3 n4(Node4) end subgraph "zoneA" p1(Pod) --> n1(Node1) p2(Pod) --> n2(Node2) end classDef plain fill:#ddd,stroke:#fff,stroke-width:4px,color:#000; classDef k8s fill:#326ce5,stroke:#fff,stroke-width:4px,color:#fff; classDef cluster fill:#fff,stroke:#bbb,stroke-width:2px,color:#326ce5; class n1,n2,n3,n4,p1,p2,p3 k8s; class p4 plain; class zoneA,zoneB cluster;

你可以調整 Pod 規約以滿足各種要求:

  • 將 `maxSkew` 更改為更大的值(例如 `2`),以便傳入的 Pod 也可以放置到區域 `A` 中。
  • 將 `topologyKey` 更改為 `node`,以便將 Pod 均勻分佈在節點之間而不是區域之間。在上面的示例中,如果 `maxSkew` 仍為 `1`,則傳入的 Pod 只能放置到節點 `node4` 上。
  • 將 `whenUnsatisfiable: DoNotSchedule` 更改為 `whenUnsatisfiable: ScheduleAnyway`,以確保傳入的 Pod 始終可排程(假設其他排程 API 均滿足)。但是,最好將其放置在匹配 Pod 較少的拓撲域中。(請注意,此偏好與資源使用率等其他內部排程優先順序共同標準化)。

示例:多個拓撲分佈約束

這建立在前面的示例之上。假設你有一個 4 節點叢集,其中 3 個標記為 `foo: bar` 的現有 Pod 分別位於 node1、node2 和 node3:

graph BT subgraph "zoneB" p3(Pod) --> n3(Node3) n4(Node4) end subgraph "zoneA" p1(Pod) --> n1(Node1) p2(Pod) --> n2(Node2) end classDef plain fill:#ddd,stroke:#fff,stroke-width:4px,color:#000; classDef k8s fill:#326ce5,stroke:#fff,stroke-width:4px,color:#fff; classDef cluster fill:#fff,stroke:#bbb,stroke-width:2px,color:#326ce5; class n1,n2,n3,n4,p1,p2,p3 k8s; class p4 plain; class zoneA,zoneB cluster;

你可以組合兩個拓撲分佈約束來控制 Pod 按節點和區域的分佈:

kind: Pod
apiVersion: v1
metadata:
  name: mypod
  labels:
    foo: bar
spec:
  topologySpreadConstraints:
  - maxSkew: 1
    topologyKey: zone
    whenUnsatisfiable: DoNotSchedule
    labelSelector:
      matchLabels:
        foo: bar
  - maxSkew: 1
    topologyKey: node
    whenUnsatisfiable: DoNotSchedule
    labelSelector:
      matchLabels:
        foo: bar
  containers:
  - name: pause
    image: registry.k8s.io/pause:3.1

在這種情況下,為了匹配第一個約束,傳入的 Pod 只能放置到區域 `B` 中的節點上;而對於第二個約束,傳入的 Pod 只能排程到節點 `node4`。排程器只考慮滿足所有已定義約束的選項,因此唯一的有效放置是到節點 `node4` 上。

示例:衝突的拓撲分佈約束

多個約束可能導致衝突。假設你有一個跨 2 個區域的 3 節點叢集:

graph BT subgraph "zoneB" p4(Pod) --> n3(Node3) p5(Pod) --> n3 end subgraph "zoneA" p1(Pod) --> n1(Node1) p2(Pod) --> n1 p3(Pod) --> n2(Node2) end classDef plain fill:#ddd,stroke:#fff,stroke-width:4px,color:#000; classDef k8s fill:#326ce5,stroke:#fff,stroke-width:4px,color:#fff; classDef cluster fill:#fff,stroke:#bbb,stroke-width:2px,color:#326ce5; class n1,n2,n3,n4,p1,p2,p3,p4,p5 k8s; class zoneA,zoneB cluster;

如果你將two-constraints.yaml(上一個示例中的清單)應用到**這個**叢集,你將看到 Pod `mypod` 保持在 `Pending` 狀態。這是因為:為了滿足第一個約束,Pod `mypod` 只能放置到區域 `B` 中;而對於第二個約束,Pod `mypod` 只能排程到節點 `node2`。兩個約束的交集返回一個空集,排程器無法放置 Pod。

為了解決這種情況,你可以增加 `maxSkew` 的值,或者修改其中一個約束以使用 `whenUnsatisfiable: ScheduleAnyway`。根據情況,你也可以決定手動刪除現有 Pod——例如,如果你正在排查錯誤修復滾動升級為何沒有進展。

與節點親和性和節點選擇器的互動

如果傳入的 Pod 定義了 `spec.nodeSelector` 或 `spec.affinity.nodeAffinity`,排程器將跳過不匹配的節點進行傾斜計算。

示例:帶節點親和性的拓撲分佈約束

假設你有一個跨越 A 到 C 區域的 5 節點叢集:

graph BT subgraph "zoneB" p3(Pod) --> n3(Node3) n4(Node4) end subgraph "zoneA" p1(Pod) --> n1(Node1) p2(Pod) --> n2(Node2) end classDef plain fill:#ddd,stroke:#fff,stroke-width:4px,color:#000; classDef k8s fill:#326ce5,stroke:#fff,stroke-width:4px,color:#fff; classDef cluster fill:#fff,stroke:#bbb,stroke-width:2px,color:#326ce5; class n1,n2,n3,n4,p1,p2,p3 k8s; class p4 plain; class zoneA,zoneB cluster;
graph BT subgraph "zoneC" n5(Node5) end classDef plain fill:#ddd,stroke:#fff,stroke-width:4px,color:#000; classDef k8s fill:#326ce5,stroke:#fff,stroke-width:4px,color:#fff; classDef cluster fill:#fff,stroke:#bbb,stroke-width:2px,color:#326ce5; class n5 k8s; class zoneC cluster;

你知道區域 `C` 必須被排除。在這種情況下,你可以按如下方式編寫清單,以便 Pod `mypod` 將被放置到區域 `B` 而不是區域 `C`。同樣,Kubernetes 也遵守 `spec.nodeSelector`。

kind: Pod
apiVersion: v1
metadata:
  name: mypod
  labels:
    foo: bar
spec:
  topologySpreadConstraints:
  - maxSkew: 1
    topologyKey: zone
    whenUnsatisfiable: DoNotSchedule
    labelSelector:
      matchLabels:
        foo: bar
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: zone
            operator: NotIn
            values:
            - zoneC
  containers:
  - name: pause
    image: registry.k8s.io/pause:3.1

隱式約定

這裡有一些值得注意的隱式約定:

  • 只有與傳入 Pod 處於同一名稱空間的 Pod 才能成為匹配候選者。

  • 排程器只考慮同時存在所有 `topologySpreadConstraints[*].topologyKey` 的節點。缺少任何這些 `topologyKeys` 的節點將被繞過。這意味著:

    1. 位於這些被繞過節點上的任何 Pod 都不影響 `maxSkew` 計算——在上面的示例中,假設節點 `node1` 沒有標籤 "zone",那麼這兩個 Pod 將被忽略,因此傳入的 Pod 將被排程到區域 `A`。
    2. 傳入的 Pod 沒有機會排程到這種節點上——在上面的示例中,假設節點 `node5` 具有**拼寫錯誤**的標籤 `zone-typo: zoneC`(並且沒有設定 `zone` 標籤)。在節點 `node5` 加入集群后,它將被繞過,並且此工作負載的 Pod 將不會排程到那裡。
  • 請注意,如果傳入 Pod 的 `topologySpreadConstraints[*].labelSelector` 與其自身的標籤不匹配會發生什麼。在上面的示例中,如果你刪除傳入 Pod 的標籤,它仍然可以放置到區域 `B` 中的節點上,因為約束仍然滿足。但是,在該放置之後,叢集的不平衡程度保持不變——區域 `A` 仍然有 2 個標記為 `foo: bar` 的 Pod,區域 `B` 有 1 個標記為 `foo: bar` 的 Pod。如果這不是你所期望的,請更新工作負載的 `topologySpreadConstraints[*].labelSelector` 以匹配 Pod 模板中的標籤。

叢集級別預設約束

可以為叢集設定預設的拓撲分佈約束。預設拓撲分佈約束僅在以下情況下應用於 Pod:

  • 它沒有在其 `.spec.topologySpreadConstraints` 中定義任何約束。
  • 它屬於 Service、ReplicaSet、StatefulSet 或 ReplicationController。

預設約束可以作為排程配置檔案中 `PodTopologySpread` 外掛引數的一部分進行設定。約束使用上面相同的 API 進行指定,只是 `labelSelector` 必須為空。選擇器從 Pod 所屬的 Service、ReplicaSet、StatefulSet 或 ReplicationController 中計算得出。

示例配置可能如下所示:

apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration

profiles:
  - schedulerName: default-scheduler
    pluginConfig:
      - name: PodTopologySpread
        args:
          defaultConstraints:
            - maxSkew: 1
              topologyKey: topology.kubernetes.io/zone
              whenUnsatisfiable: ScheduleAnyway
          defaultingType: List

內建預設約束

特性狀態: Kubernetes v1.24 [stable]

如果你沒有為 Pod 拓撲分佈配置任何叢集級別預設約束,那麼 kube-scheduler 的行為就好像你指定了以下預設拓撲約束:

defaultConstraints:
  - maxSkew: 3
    topologyKey: "kubernetes.io/hostname"
    whenUnsatisfiable: ScheduleAnyway
  - maxSkew: 5
    topologyKey: "topology.kubernetes.io/zone"
    whenUnsatisfiable: ScheduleAnyway

此外,提供相同行為的舊版 `SelectorSpread` 外掛預設是停用的。

如果你不希望為叢集使用預設的 Pod 分佈約束,可以透過將 `PodTopologySpread` 外掛配置中的 `defaultingType` 設定為 `List` 並將 `defaultConstraints` 留空來停用這些預設值:

apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration

profiles:
  - schedulerName: default-scheduler
    pluginConfig:
      - name: PodTopologySpread
        args:
          defaultConstraints: []
          defaultingType: List

與 PodAffinity 和 PodAntiAffinity 的比較

在 Kubernetes 中,Pod 間親和性與反親和性控制 Pod 如何相互排程——是更緊密地打包還是更分散地分佈。

PodAffinity
吸引 Pods;你可以嘗試將任意數量的 Pods 打包到符合條件的拓撲域中。
PodAntiAffinity
排斥 Pods。如果你將其設定為 `requiredDuringSchedulingIgnoredDuringExecution` 模式,那麼單個拓撲域中只能排程單個 Pod;如果你選擇 `preferredDuringSchedulingIgnoredDuringExecution`,那麼你將失去強制執行約束的能力。

為了進行更精細的控制,你可以指定拓撲分佈約束,將 Pod 分佈到不同的拓撲域中——以實現高可用性或成本節約。這也有助於平滑地進行工作負載的滾動更新和副本的擴縮。

更多背景資訊,請參閱 Pod 拓撲分佈約束增強提案的動機部分。

已知限制

  • 當 Pod 被移除時,無法保證約束仍然滿足。例如,縮減 Deployment 可能會導致 Pod 分佈不平衡。

    你可以使用 Descheduler 等工具來重新平衡 Pod 的分佈。

  • 匹配到被汙染節點上的 Pod 仍然受到尊重。請參閱 Issue 80921

  • 排程器事先不瞭解叢集中所有的區域或其他拓撲域。它們是從叢集中的現有節點確定的。這可能導致自動擴縮叢集中的問題,當節點池(或節點組)擴縮到零個節點時,你期望叢集擴縮,因為在這種情況下,直到這些拓撲域中至少有一個節點,它們才會被考慮。

    你可以透過使用瞭解 Pod 拓撲分佈約束並瞭解整體拓撲域集的節點自動擴縮器來解決此問題。

下一步

上次修改時間:2025 年 7 月 8 日上午 7:35 (PST):fix1 (8ad8c109d1)