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。您只能將 minDomainswhenUnsatisfiable: DoNotSchedule 一起指定。
    • 當符合拓撲鍵的符合條件網域的數量少於 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 標籤中查找值,這些鍵值標籤會與 labelSelector 進行 AND 運算,以選取現有的 Pod 群組,並針對即將到來的 Pod 計算擴散。同一個鍵禁止同時存在於 matchLabelKeyslabelSelector 中。當 labelSelector 未設定時,不能設定 matchLabelKeys。Pod 標籤中不存在的鍵將被忽略。空值或空清單表示僅根據 labelSelector 進行比對。

    透過 matchLabelKeys,您不需要在不同版本之間更新 pod.spec。控制器/運算子只需要為不同版本設定相同的標籤鍵的不同值。排程器將根據 matchLabelKeys 自動假定這些值。例如,如果您正在設定 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。所有節點都包含在計算中。

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

  • nodeTaintsPolicy 指出在計算 Pod 拓撲擴散傾斜時,我們將如何處理節點污點。選項如下:

    • Honor:包含沒有污點的節點,以及即將到來的 Pod 具有容忍度的受污節點。
    • Ignore:忽略節點污點。所有節點都包含在內。

    如果此值為空值,則行為等同於 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 均勻分佈在各個區域,您可以使用類似於以下的 Manifest

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

從該 Manifest 中,topologyKey: zone 表示均勻分佈將僅應用於標記為 zone: <任何值> 的節點(沒有 zone 標籤的節點將被跳過)。欄位 whenUnsatisfiable: DoNotSchedule 告訴排程器,如果排程器找不到滿足約束的方法,則讓即將到來的 Pod 保持 Pending 狀態。

如果排程器將此即將到來的 Pod 放置到 zone A 中,則 Pod 的分佈將變為 [3, 1]。這表示實際的傾斜度為 2(計算為 3 - 1),這違反了 maxSkew: 1。為了滿足此範例的約束和情境,即將到來的 Pod 只能放置在 zone 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 spec 以滿足各種需求

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

範例:多個拓撲擴散約束

這建立在先前的範例之上。假設您有一個 4 節點的叢集,其中 3 個現有的 Pod 標記為 foo: bar,分別位於 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 只能放置在 zone 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(來自先前範例的 Manifest)應用於叢集,您會看到 Pod mypod 保持在 Pending 狀態。發生這種情況是因為:為了滿足第一個約束,Pod mypod 只能放置在 zone B 中;而就第二個約束而言,Pod mypod 只能排程到節點 node2。兩個約束的交集返回一個空集合,並且排程器無法放置 Pod。

為了克服這種情況,您可以增加 maxSkew 的值,或修改其中一個約束以使用 whenUnsatisfiable: ScheduleAnyway。根據情況,您也可能決定手動刪除現有的 Pod,例如,如果您正在疑難排解為什麼錯誤修復的推出沒有進展。

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

如果即將到來的 Pod 已定義 spec.nodeSelectorspec.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。在這種情況下,您可以撰寫如下的 Manifest,以便 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 的節點。缺少任何這些 topologyKey 的節點將被繞過。這表示

    1. 位於這些繞過節點上的任何 Pod 都不會影響 maxSkew 計算 - 在上述 範例 中,假設節點 node1 沒有 "zone" 標籤,則這 2 個 Pod 將被忽略,因此即將到來的 Pod 將排程到 zone A
    2. 即將到來的 Pod 沒有機會排程到這類節點上 - 在上述範例中,假設節點 node5 具有拼寫錯誤的標籤 zone-typo: zoneC(並且沒有設定 zone 標籤)。在節點 node5 加入叢集後,它將被繞過,並且此工作負載的 Pod 不會排程到那裡。
  • 請注意,如果即將到來的 Pod 的 topologySpreadConstraints[*].labelSelector 與其自身的標籤不符,會發生什麼情況。在上述範例中,如果您移除即將到來的 Pod 的標籤,它仍然可以放置在 zone B 中的節點上,因為約束仍然滿足。但是,在放置之後,叢集的不平衡程度仍然不變 - 仍然是 zone A 有 2 個標記為 foo: bar 的 Pod,而 zone B 有 1 個標記為 foo: bar 的 Pod。如果這不是您期望的,請更新工作負載的 topologySpreadConstraints[*].labelSelector 以符合 Pod 範本中的標籤。

叢集層級的預設約束

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

  • Pod 的 .spec.topologySpreadConstraints 中未定義任何約束。
  • Pod 屬於 Service、ReplicaSet、StatefulSet 或 ReplicationController。

預設約束可以設定為 排程設定檔PodTopologySpread 外掛程式引數的一部分。約束以與上述 API 相同的方式 指定,但 labelSelector 必須為空。選擇器是從 Pod 所屬的 Service、ReplicaSet、StatefulSet 或 ReplicationController 計算而來。

範例組態可能如下所示

apiVersion: kubescheduler.config.k8s.io/v1beta3
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 擴散約束,您可以透過將 defaultingType 設定為 List 並在 PodTopologySpread 外掛程式組態中將 defaultConstraints 留空來停用這些預設值

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

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

與 podAffinity 和 podAntiAffinity 的比較

在 Kubernetes 中,Pod 間親和性和反親和性 控制 Pod 如何彼此相關地排程 - 更密集或更分散。

podAffinity
吸引 Pod;您可以嘗試將任意數量的 Pod 封裝到符合條件的拓撲網域中。
podAntiAffinity
排斥 Pod。如果您將其設定為 requiredDuringSchedulingIgnoredDuringExecution 模式,則只有一個 Pod 可以排程到單個拓撲網域中;如果您選擇 preferredDuringSchedulingIgnoredDuringExecution,則您將失去強制執行約束的能力。

為了更精細的控制,您可以指定拓撲擴散約束,以在不同的拓撲網域之間分佈 Pod - 以實現高可用性或節省成本。這也有助於滾動更新工作負載和平穩地擴展副本。

有關更多背景資訊,請參閱關於 Pod 拓撲擴散約束的增強提案的 Motivation 章節。

已知限制

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

    您可以使用諸如 Descheduler 之類的工具來重新平衡 Pod 分佈。

  • 與受污節點匹配的 Pod 將受到尊重。請參閱 Issue 80921

  • 排程器不事先知道叢集擁有的所有區域或其他拓撲網域。它們是從叢集中的現有節點確定的。當節點池(或節點群組)縮減為零節點,並且您期望叢集擴展時,這可能會導致問題,因為在這種情況下,在其中至少有一個節點之前,這些拓撲網域將不會被考慮在內。

    您可以透過使用叢集自動擴展工具來解決此問題,該工具知道 Pod 拓撲擴散約束,並且也知道拓撲網域的整體集合。

下一步

上次修改時間:2024 年 9 月 15 日下午 8:58 PST:修正 topologySpreadConstraints 中模稜兩可的句子 (04b6fdf80c)