­

kubernetes之pod拓撲分布約束

在日常使用 kubernetes 的過程中中,很多時候我們並沒有過多的關心 pod 的到底調度在哪裡,只是通過多副本的測試,來提高的我們的業務的可用性,但是當多個相同業務 pod 在分布在相同節點時,一旦節點意外宕機,將會嚴重影響我們的業務的可用性,鑒於此,kubernetes 中引入了新的 pod 調度策略-topologySpreadConstraints,翻譯為拓撲分布約束,今天就讓我們揭開其神秘面紗,原來 pod 還可以這麼玩。

關鍵字:topologySpreadConstraints pod拓撲分布約束 kubernetes

pod 拓撲分布約束簡介

拓撲分布約束(Topology Spread Constraints)可以控制 Pods 在集群內故障域 之間的分布,例如區域(Region)、可用區(Zone)、節點和其他用戶自定義拓撲域。 這樣做有助於實現高可用並提升資源利用率。
此項功能在 1.18中將其提升為Beta,1.19 中為 stable 狀態,可以在生產環境中使用。

節點的故障域標識

拓撲分布約束依賴於節點標籤來標識每個節點所在的拓撲域。 例如,某節點可能具有標籤:node=node1,zone=us-east-1a,region=us-east-1

假設你擁有具有以下標籤的一個 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

在邏輯上看,我們的節點的結構圖如下:
微信截圖_20210427214133

由上圖可知,邏輯域有兩個層次,一是 node 層面,二是 zone 層面,我們可以靈活配置自己的故障域,使我們的業務有更高的可用性。

pod 拓撲分布約束實踐

正如我們日常寫 yaml 一樣,配置 topologySpreadConstraints,同樣只需要在 yaml 中定義即可,路徑為: pod.spec.topologySpreadConstraints
api

示例:

apiVersion: v1
kind: Pod
metadata:
  name: mypod
spec:
  topologySpreadConstraints:
    - maxSkew: <integer>
      topologyKey: <string>
      whenUnsatisfiable: <string>
      labelSelector: <object>

你可以定義一個或多個 topologySpreadConstraint 來指示 kube-scheduler 如何根據與現有的 Pod 的關聯關係將每個傳入的 Pod 部署到集群中。
欄位包括:

  • maxSkew 描述 Pod 分布不均的程度。
    這是給定拓撲類型中任意兩個拓撲域中 匹配的 pod 之間的最大允許差值。它必須大於零。取決於 whenUnsatisfiable 的 取值,其語義會有不同。

    • 當 whenUnsatisfiable 等於 “DoNotSchedule” 時,maxSkew 是目標拓撲域 中匹配的 Pod 數與全局最小值之間可存在的差異。
    • 當 whenUnsatisfiable 等於 “ScheduleAnyway” 時,調度器會更為偏向能夠降低 偏差值的拓撲域。
  • topologyKey 是節點標籤的鍵。
    如果兩個節點使用此鍵標記並且具有相同的標籤值, 則調度器會將這兩個節點視為處於同一拓撲域中。調度器試圖在每個拓撲域中放置數量 均衡的 Pod。

  • whenUnsatisfiable 指示如果 Pod 不滿足分布約束時如何處理:

    • DoNotSchedule(默認)告訴調度器不要調度。
    • ScheduleAnyway 告訴調度器仍然繼續調度,只是根據如何能將偏差最小化來對 節點進行排序。
  • labelSelector 用於查找匹配的 pod。
    匹配此標籤的 Pod 將被統計,以確定相應 拓撲域中 Pod 的數量。 有關詳細資訊,請參考標籤選擇算符。

你可以執行 kubectl explain Pod.spec.topologySpreadConstraints 命令以了解關於 topologySpreadConstraints 的更多資訊。

業務應用實踐

  1. 單個 TopologySpreadConstraint
    假設你擁有一個 4 節點集群,其中標記為 foo:bar 的 3 個 Pod 分別位於 node1、node2 和 node3 中:
    示意圖:
    2

如果希望新來的 Pod 均勻分布在現有的可用區域,可進行如下配置:

##  示例1-yaml
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: k8s.gcr.io/pause:3.1
  • topologyKey: zone 意味著均勻分布將只應用於存在標籤鍵值對為 “zone:” 的節點。
  • whenUnsatisfiable: DoNotSchedule 告訴調度器如果新的 Pod 不滿足約束,則讓它保持懸決狀態

如果調度器將新的 Pod 放入 “zoneA”,Pods 分布將變為 [3, 1],因此實際的偏差 為(3 – 1)= 2 。這違反了 maxSkew: 1 的約定。此示例中,新 Pod 只能放置在 “zoneB” 上:
3

或者

4

你可以調整 Pod 的配置以滿足各種要求:

  • 將 maxSkew 更改為更大的值,比如 “2”,這樣新的 Pod 也可以放在 “zoneA” 上。
  • 將 topologyKey 更改為 “node”,以便將 Pod 均勻分布在節點上而不是區域中。 在上面的例子中,如果 maxSkew 保持為 “1”,那麼傳入的 Pod 只能放在 “node4” 上。
  • 將 whenUnsatisfiable: DoNotSchedule 更改為 whenUnsatisfiable: ScheduleAnyway, 以確保新的 Pod 始終可以被調度(假設滿足其他的調度 API)。 但是,最好將其放置在匹配 Pod 數量較少的拓撲域中。 (請注意,這一優先判定會與其他內部調度優先順序(如資源使用率等)排序準則一起進行標準化。)
  1. 多個 TopologySpreadConstraints
    下面的例子建立在前面例子的基礎上。假設你擁有一個 4 節點集群,其中 3 個標記為 foo:bar 的 Pod 分別位於 node1、node2 和 node3 上:
    示意圖:
    5

可以使用 2 個 TopologySpreadConstraint 來控制 Pod 在 區域和節點兩個維度上的分布:

## 示例2-yaml
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: k8s.gcr.io/pause:3.1

在這種情況下,為了匹配第一個約束,新的 Pod 只能放置在 “zoneB” 中;而在第二個約束中, 新的 Pod 只能放置在 “node4” 上。最後兩個約束的結果加在一起,唯一可行的選擇是放置 在 “node4” 上。

多個約束之間可能存在衝突。假設有一個跨越 2 個區域的 3 節點集群:
6
如果對集群應用示例2-yaml配置,會發現 “mypod” 處於 Pending 狀態。 這是因為:為了滿足第一個約束,”mypod” 只能放在 “zoneB” 中,而第二個約束要求 “mypod” 只能放在 “node2” 上。Pod 調度無法滿足兩種約束。

為了克服這種情況,你可以增加 maxSkew 或修改其中一個約束,讓其使用 whenUnsatisfiable: ScheduleAnyway

約定

  • 只有與新的 Pod 具有相同命名空間的 Pod 才能作為匹配候選者。
  • 沒有 topologySpreadConstraints[*].topologyKey 的節點將被忽略。這意味著:
    1. 位於這些節點上的 Pod 不影響 maxSkew 的計算。 在上面的例子中,假設 “node1” 沒有標籤 “zone”,那麼 2 個 Pod 將被忽略, 因此傳入的 Pod 將被調度到 “zoneA” 中。
      2.新的 Pod 沒有機會被調度到這類節點上。 在上面的例子中,假設一個帶有標籤 {zone-typo: zoneC} 的 “node5” 加入到集群, 它將由於沒有標籤鍵 “zone” 而被忽略。
  • 注意,如果新 Pod 的 topologySpreadConstraints[].labelSelector 與自身的 標籤不匹配,將會發生什麼。 在上面的例子中,如果移除新 Pod 上的標籤,Pod 仍然可以調度到 “zoneB”,因為約束仍然滿足。 然而,在調度之後,集群的不平衡程度保持不變。zoneA 仍然有 2 個帶有 {foo:bar} 標籤的 Pod, zoneB 有 1 個帶有 {foo:bar} 標籤的 Pod。 因此,如果這不是你所期望的,建議工作負載的 topologySpreadConstraints[].labelSelector 與其自身的標籤匹配。
  • 如果新 Pod 定義了 spec.nodeSelector 或 spec.affinity.nodeAffinity,則 不匹配的節點會被忽略。

假設你有一個跨越 zoneA 到 zoneC 的 5 節點集群
7
而且你知道 “zoneC” 必須被排除在外。在這種情況下,可以按如下方式編寫 yaml, 以便將 “mypod” 放置在 “zoneB” 上,而不是 “zoneC” 上。同樣,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: k8s.gcr.io/pause:3.1

集群基本的Pod 拓撲分布約束

為集群設置默認的拓撲分布約束也是可能的。默認拓撲分布約束在且僅在以下條件滿足 時才會應用到 Pod 上:

  • Pod 沒有在其 .spec.topologySpreadConstraints 設置任何約束;
  • Pod 隸屬於某個服務、副本控制器、ReplicaSet 或 StatefulSet;

你可以在 調度方案(Schedulingg Profile) 中將默認約束作為 PodTopologySpread 插件參數的一部分來設置。 約束的設置採用如前所述的 API,只是 labelSelector 必須為空。 選擇算符是根據 Pod 所屬的服務、副本控制器、ReplicaSet 或 StatefulSet 來設置的。
示例配置:

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

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

與 PodAffinity/PodAntiAffinity 相比較

在 Kubernetes 中,與「親和性」相關的指令控制 Pod 的調度方式(更密集或更分散)。

  • 對於 PodAffinity,你可以嘗試將任意數量的 Pod 集中到符合條件的拓撲域中。
  • 對於 PodAntiAffinity,只能將一個 Pod 調度到某個拓撲域中。

要實現更細粒度的控制,你可以設置拓撲分布約束來將 Pod 分布到不同的拓撲域下, 從而實現高可用性或節省成本。這也有助於工作負載的滾動更新和平穩地擴展副本規模。

高階用法

  1. 結合NodeSelector/NodeAffinity一起使用

在pod的拓撲分布約束配置中,可以看到我們只有topologyKey的配置選項,並沒有任何關於topologyValues的配置欄位,也就是並沒有規定pod具體安排在哪些拓撲域,默認情況下,它將搜索所有節點並按”topologyKey”對它們進行分組。有時這可能不是我們想要的結果。例如,假設有一個集群,其節點分別用”env = prod”,”env = staging”和”env = qa”標記,現在您想將Pod均勻地跨區域放置到”qa”環境中,能辦到么?

答案是肯定的。您可以利用NodeSelector或NodeAffinity API規範。在幕後,PodTopologySpread功能將兌現這一點,並計算滿足選擇器的節點之間的傳播約束。
示意圖:
advanced-usage-1

如上所示,您可以指定spec.affinity.nodeAffinity將搜索範圍限制為qa環境,並且在該範圍內,會將Pod調度到一個滿足topologySpreadConstraints的區域。在這種情況下,它是”zone2″。

  1. 高階多拓撲分布約束

了解一個TopologySpreadConstraint的工作原理很直觀。多個TopologySpreadConstraints是什麼情況?在內部,每個TopologySpreadConstraint都是獨立計算的,結果集將合併以生成最終的結果集-即合適的節點。

在以下示例中,我們希望同時將Pod調度到具有2個需求的集群中:

  • Pod跨區域均勻放置
  • Pod跨節點均勻放置

示意圖:
advanced-usage-2

對於第一個約束,zone1中有3個Pod,zone2中有2個Pod,因此只能將傳入的Pod放入zone2中,以滿足”maxSkew = 1″約束。換句話說,結果集是nodeX和nodeY。

對於第二個約束,nodeB和nodeX中的Pod過多,因此只能將傳入的Pod放入nodeA和nodeY。

現在我們可以得出結論,唯一合格的節點是nodeY-從集合{nodeX,nodeY}(來自第一個約束)和{nodeA,nodeY}(來自第二個約束)的交集中得出。

多個TopologySpreadConstraints功能強大,但是一定要了解與前面的”NodeSelector/NodeAffinity”示例的區別:一個是獨立計算結果集,然後將其互連;另一種是根據節點約束的過濾結果來計算topologySpreadConstraints。

注意:如果將兩個TopologySpreadConstraints應用於同一{topologyKey,whenUnsatisfiable}元組,則Pod的創建將被阻止,並返回驗證錯誤。

個人部落格
歡迎關注 DevOps充電寶,獲取更多文章內容