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 指示合格域的最小數量。此欄位是可選的。域是拓撲的特定例項。合格域是其節點匹配節點選擇器的域。
注意
在 Kubernetes v1.30 之前,`minDomains` 欄位只有在 `MinDomainsInPodTopologySpread` 特性門控被啟用時才可用(自 v1.28 起預設啟用)。在舊的 Kubernetes 叢集中,它可能被顯式停用或該欄位可能不可用。- 當指定時,`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 上更新的標籤一起使用。即使你**直接**編輯 `matchLabelKeys` 中指定的 Pod 標籤(即你編輯 Pod 而不是 Deployment),kube-apiserver 也不會將標籤更新反映到合併的 `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 策略。
注意
`nodeAffinityPolicy` 在 1.26 中成為 Beta 版本,並在 1.33 中升級到 GA 版本。在 Beta 版本中預設啟用,你可以透過停用 `NodeInclusionPolicyInPodTopologySpread` 特性門控來停用它。nodeTaintsPolicy 指示在計算 Pod 拓撲分佈傾斜時如何處理節點汙點。選項包括:
- Honor: 不帶汙點的節點,以及傳入 Pod 具有容忍度的帶汙點節點,都包含在內。
- Ignore: 節點汙點被忽略。所有節點都包含在內。
如果此值為 null,則行為等同於 Ignore 策略。
注意
`nodeTaintsPolicy` 在 1.26 中成為 Beta 版本,並在 1.33 中升級到 GA 版本。在 Beta 版本中預設啟用,你可以透過停用 `NodeInclusionPolicyInPodTopologySpread` 特性門控來停用它。
當一個 Pod 定義了多個 `topologySpreadConstraint` 時,這些約束透過邏輯 AND 操作組合:kube-scheduler 為傳入的 Pod 尋找一個滿足所有已配置約束的節點。
節點標籤
拓撲分佈約束依賴節點標籤來識別每個節點所在的拓撲域。例如,一個節點可能具有以下標籤:
region: us-east-1
zone: us-east-1a
注意
為了簡潔起見,本示例不使用知名標籤鍵 topology.kubernetes.io/zone
和 topology.kubernetes.io/region
。然而,建議使用這些已註冊的標籤鍵,而不是此處使用的私有(不合格)標籤鍵 region
和 zone
。
在不同上下文之間,你無法對私有標籤鍵的含義做出可靠的假設。
假設你有一個 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
那麼叢集在邏輯上如下圖所示:
一致性
你應該在組中的所有 Pod 上設定相同的 Pod 拓撲分佈約束。
通常,如果你使用 Deployment 等工作負載控制器,Pod 模板會為你處理此問題。如果你混合使用不同的分佈約束,Kubernetes 會遵循欄位的 API 定義;但是,行為更容易令人困惑,並且故障排除也更不直接。
你需要一種機制來確保拓撲域中的所有節點(例如雲提供商區域)都具有一致的標籤。為了避免你手動標記節點,大多數叢集會自動填充知名標籤,例如 `kubernetes.io/hostname`。請檢查你的叢集是否支援此功能。
拓撲分佈約束示例
示例:一個拓撲分佈約束
假設你有一個 4 節點叢集,其中 3 個標記為 `foo: bar` 的 Pod 分別位於 node1、node2 和 node3:
如果你希望傳入的 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:
如果排程器將此傳入 Pod 放置到區域 `A` 中,Pod 的分佈將變為 `[3, 1]`。這意味著實際傾斜度為 2(計算為 `3 - 1`),這違反了 `maxSkew: 1`。為了滿足此示例的約束和上下文,傳入的 Pod 只能放置到區域 `B` 中的節點上:
或者
你可以調整 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:
你可以組合兩個拓撲分佈約束來控制 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 節點叢集:
如果你將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 節點叢集:
你知道區域 `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` 的節點將被繞過。這意味著:
- 位於這些被繞過節點上的任何 Pod 都不影響 `maxSkew` 計算——在上面的示例中,假設節點 `node1` 沒有標籤 "zone",那麼這兩個 Pod 將被忽略,因此傳入的 Pod 將被排程到區域 `A`。
- 傳入的 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` 外掛預設是停用的。
注意
`PodTopologySpread` 外掛不會對那些在分佈約束中沒有指定拓撲鍵的節點進行打分。這可能會導致在使用預設拓撲約束時,與舊版 `SelectorSpread` 外掛相比,產生不同的預設行為。
如果你的節點預期**不會同時**擁有 `kubernetes.io/hostname` 和 `topology.kubernetes.io/zone` 標籤,請定義自己的約束而不是使用 Kubernetes 預設值。
如果你不希望為叢集使用預設的 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 拓撲分佈約束並瞭解整體拓撲域集的節點自動擴縮器來解決此問題。
下一步
- 部落格文章介紹 PodTopologySpread 詳細解釋了 `maxSkew`,並涵蓋了一些高階用法示例。
- 閱讀 Pod API 參考的排程部分。