Podトポロジー分散制約

トポロジー分散制約 を使用するとリージョン、ゾーン、ノードおよびその他のユーザー定義されたトポロジー領域などの障害ドメインをまたがって、クラスター内のPodがどのように分散されるかを制御できます。 これにより、高可用性と効率的なリソース活用を実現できます。

クラスターレベルの制約をデフォルトとして設定するか、個々のワークロードに対してトポロジー分散制約を設定できます。

動機

最大20ノードのクラスターがあり、使用するレプリカ数を自動的にスケーリングするワークロードを実行したいとします。 このワークロードには、2つのPodが存在する場合もあれば、15のPodが存在する場合もあります。 Podが2つだけの場合、単一ノードの障害でワークロードがオフラインになるリスクがあるため、同じノードで両方のPodを実行したくありません。

この基本的な使用方法に加えて、高可用性とクラスターの利用率を向上させるための高度な使用方法があります。

スケールアップしてより多くのPodを実行すると、また別の懸念事項が重要になります。 5つのPodを実行する3つのノードがあるとします。 ノードは該当数のレプリカを実行するために十分なキャパシティを持っていますが、このワークロードとやり取りするクライアントは3つの異なるデータセンター(またはインフラストラクチャゾーン)に分散されています。 単一ノードの障害についての懸念は減りましたが、レイテンシーが予想よりも高く、異なるゾーン間でネットワークトラフィックを送信する際にネットワークコストがかかっていることに気づきます。

通常の運用では、各インフラストラクチャゾーンに同数のレプリカをスケジュールし、問題が発生した場合はクラスターが自己修復するようにしたいと考えるでしょう。

Podトポロジー分散制約は、このようなシナリオに対処するための手段を提供します。

topologySpreadConstraintsフィールド

Pod APIには、spec.topologySpreadConstraintsフィールドが含まれています。 このフィールドの使用方法は次のようになります:

---
apiVersion: v1
kind: Pod
metadata:
  name: example-pod
spec:
  # トポロジー分散制約を設定
  topologySpreadConstraints:
    - maxSkew: <integer>
      minDomains: <integer> # オプション
      topologyKey: <string>
      whenUnsatisfiable: <string>
      labelSelector: <object>
      matchLabelKeys: <list> # オプション; v1.27以降ベータ
      nodeAffinityPolicy: [Honor|Ignore] # オプション; v1.26以降ベータ
      nodeTaintsPolicy: [Honor|Ignore] # オプション; v1.26以降ベータ
  ### 他のPodのフィールドはここにあります

kubectl explain Pod.spec.topologySpreadConstraintsを実行するか、PodのAPIリファレンスのschedulingセクションを参照して、このフィールドについて詳しく読むことができます。

分散制約の定義

クラスター全体の既存のPodに対して、新しいPodをどのように配置するかをkube-schedulerに指示するために、1つまたは複数のtopologySpreadConstraintsエントリを定義できます。 これらのフィールドは次の通りです:

  • maxSkewは、Podが不均等に分散される程度を表します。 このフィールドは必須であり、0より大きい数値を指定する必要があります。 このフィールドのセマンティクスは、whenUnsatisfiableの値によって異なります:

    • whenUnsatisfiable: DoNotScheduleを選択した場合、maxSkewは対象トポロジー内の一致するPodの数と、グローバル最小値(適格ドメイン内の一致するPodの最小数、または適格ドメインの数がMinDomainsより少ない場合は0)の間で許容される最大の差を定義します。 例えば、3つのゾーンがあり、それぞれ2、2、1つの一致するPodがありMaxSkewが1に設定されている場合、グローバル最小値は1です。
    • whenUnsatisfiable: ScheduleAnywayを選択した場合、スケジューラーはスキューの削減に役立つトポロジーを優先します。
  • minDomainsは、適格ドメインの最小数を示します。 このフィールドはオプションです。 ドメインとは、特定のトポロジーのインスタンスを指します。 適格ドメインとは、ノードセレクターに一致するノードのドメインを指します。

    • minDomainsの値を指定する場合、その値は0より大きくする必要があります。 minDomainsは、whenUnsatisfiable: DoNotScheduleと組み合わせてのみ指定できます。
    • トポロジーキーに一致する適格ドメインの数がminDomainsより少ない場合、Podトポロジー分散はグローバル最小値を0として扱い、skewの計算を行います。 グローバル最小値は、適格ドメイン内の一致するPodの最小数であり、適格ドメインの数がminDomainsより少ない場合は0になります。
    • トポロジーキーに一致する適格ドメインの数がminDomainsと等しいかそれ以上の場合、この値はスケジューリングに影響しません。
    • minDomainsを指定しない場合、制約はminDomainsが1であるかのように動作します。
  • topologyKeyノードラベルのキーです。 このキーと同じ値を持つノードは、同じトポロジー内にあると見なされます。 トポロジー内の各インスタンス(つまり、<key, value>ペア)をドメインと呼びます。 スケジューラーは、各ドメインに均等な数のPodを配置しようとします。 また、適格ドメインはnodeAffinityPolicyとnodeTaintsPolicyの要件を満たすノードのドメインとして定義します。

  • whenUnsatisfiableは、Podが分散制約を満たさない場合の処理方法を示します:

    • DoNotSchedule(デフォルト)は、スケジューラーにスケジュールしないように指示します。
    • ScheduleAnywayは、スケジューラーにスキューを最小化するノードを優先してスケジュールするよう指示します。
  • labelSelectorは、一致するPodを見つけるために使用されます。 このラベルセレクターに一致するPodは、対応するトポロジードメイン内のPodの数を決定するためにカウントされます。 詳細については、ラベルセレクターを参照してください。

  • matchLabelKeysは、分散を計算するPodを選択するためのPodラベルキーのリストです。 このキーは、Podラベルから値を検索するために使用され、これらのkey-valueラベルはlabelSelectorとAND演算され、新しいPodのために分散が計算される既存のPodのグループを選択します。 同じキーはmatchLabelKeyslabelSelectorの両方に存在してはいけません。 labelSelectorが設定されていない場合は、matchLabelKeysを設定することはできません。 Podラベルに存在しないキーは無視されます。 nullまたは空のリストは、labelSelectorに対してのみ一致します。

    matchLabelKeysを使用すると、異なるリビジョン間でpod.specを更新する必要がありません。 コントローラー/オペレーターは、異なるリビジョンに対して同じラベルキーを異なる値に設定するだけです。 スケジューラーは、matchLabelKeysに基づいて値を自動的に推定します。 例えば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のトポロジー分散スキューを計算する際に、ノードのtaintをどのように扱うかを示します。 オプションは次の通りです:

    • Honor: taintのないノード、新しいPodがtolerationを持つtaintされたノードが含まれます。
    • Ignore: ノードのtaintは無視されます。 すべてのノードが含まれます。

    この値がnullの場合、動作はIgnoreポリシーと同等です。

PodにtopologySpreadConstraintsが複数定義されている場合、これらの制約は論理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

クラスターは論理的には以下のように表されます:

zoneA
Node1
Node2
zoneB
Node3
Node4

一貫性

グループ内のすべてのPodに同じトポロジー分散制約を適用する必要があります。

通常、Deploymentなどのワークロードコントローラーを使用している場合、Podテンプレートがこれを自動的に処理します。 異なる分散制約を混在させると、KubernetesはフィールドのAPI定義に従いますが、その動作は混乱しやすくなりトラブルシューティングがより困難になる可能性があります。

トポロジードメイン(例えばクラウドプロバイダーのリージョン)内のすべてのノードに、一貫してラベルが付与されていることを保証するメカニズムが必要です。 手動でノードにラベルを付与する必要がないように、多くのクラスターはkubernetes.io/hostnameのようなwell-knownラベルを自動的に設定します。 ご自身のクラスターがこれをサポートしているかどうかを確認してください。

トポロジー分散制約の例

例: 単一のトポロジー分散制約

4ノードのクラスターがあり、foo: barというラベルの付いた3つのPodがそれぞれ node1、node2、node3に配置されているとします:

zoneA
Node1
Pod
Pod
Node2
zoneB
Node3
Pod
Node4

新しい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: zonezone: <任意の値>というラベルが付いたノードにのみ均等に分散が適用されることを意味します(ラベルzoneがないノードはスキップされます)。 whenUnsatisfiable: DoNotScheduleフィールドは、スケジューラーが制約を満たせない場合に、新しいPodを保留状態にするようにスケジューラーに指示します。

スケジューラーがこの新しいPodをゾーンAに配置した場合、Podの分布は[3, 1]になります。 これは実際のスキューが2(3 - 1として計算)であることを意味し、maxSkew: 1に違反します。 この例の制約とコンテキストを満たすためには、新しいPodはゾーンBのノードにのみ配置される必要があります:

zoneA
Node1
Pod
Pod
Node2
zoneB
Node3
Pod
mypod
Node4

または

zoneA
Node1
Pod
Pod
Node2
zoneB
Node3
Pod
mypod
Node4

Podの仕様を調整することで、様々な要件に対応できます:

  • maxSkewをより大きい値(例えば2)に変更すると、新しいPodをゾーンAに配置できるようになります。
  • topologyKeynodeに変更すると、ゾーン単位ではなくノード単位でPodを均等に分散させるようになります。 上記の例では、maxSkew1のままである場合、新しいPodはnode4にのみ配置可能です。
  • whenUnsatisfiable: DoNotSchedulewhenUnsatisfiable: ScheduleAnywayに変更すると、新しいPodが常にスケジュール可能になります(他のスケジュールAPIが満たされていると仮定)。 ただし、一致するPodが少ないトポロジードメインに配置されることが好ましいです。 (この設定は、リソース使用率などの他の内部スケジューリング優先度と共に正規化されることに注意してください)。

例: 複数のトポロジー分散制約

これは前の例に基づいています。 4ノードのクラスターがあり、foo: barというラベルの付いた3つのPodがそれぞれ node1、node2、node3に配置されているとします:

zoneA
Node1
Pod
Pod
Node2
zoneB
Node3
Pod
Node4

2つのトポロジー分散制約を組み合わせて、ノードとゾーンの両方で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のノードにのみ配置できます。 一方、2番目の制約に関しては、新しいPodはnode4にのみスケジュールできます。 スケジューラーは、定義されたすべての制約を満たすオプションのみを考慮するため、有効な配置はnode4のみです。

例: トポロジー分散制約の競合

複数の制約は競合する可能性があります。 2つのゾーンにまたがる3ノードのクラスターがあるとします:

zoneA
Node1
Pod
Pod
Pod
Node2
zoneB
Node3
Pod
Pod

このクラスターにtwo-constraints.yaml(前の例のマニフェスト)を適用すると、Pod mypodPending状態のままであることがわかります。 これは、最初の制約を満たすために、Pod mypodはゾーンBにしか配置できないのに対して、2番目の制約に関しては、Pod mypodはノードnode2にしかスケジュールできないために発生します。 2つの制約の共通部分として空集合が返され、スケジューラーはPodを配置できません。

この状況を克服するためには、maxSkewの値を増やすか、制約の1つをwhenUnsatisfiable: ScheduleAnywayに変更します。 状況によっては、バグ修正のロールアウトが進まない理由をトラブルシューティングする場合などで、既存のPodを手動で削除することもあります。

ノードアフィニティとノードセレクターとの相互作用

新しいPodにspec.nodeSelectorまたはspec.affinity.nodeAffinityが定義されている場合、スケジューラーは一致しないノードをスキュー計算からスキップします。

例: ノードアフィニティを使用したトポロジー分散制約

ゾーンAからCにまたがる5ノードクラスターがあるとします:

zoneA
Node1
Pod
Pod
Node2
zoneB
Node3
Pod
Node4
zoneC
Node5

そして、ゾーンCを除外する必要があることがわかっているとします。 この場合、以下のようにマニフェストを作成して、Pod mypodをゾーンCではなくゾーンBに配置することができます。 同様に、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と同じNamespaceを持つPodのみが一致する候補となります。

  • スケジューラーは、すべてのtopologySpreadConstraints[*].topologyKeyが同時に存在するノードのみを考慮します。 これらのtopologyKeysのいずれかが欠落しているノードはバイパスされます。 これは次のことを意味します:

    1. これらのバイパスされたノードにあるPodはmaxSkewの計算に影響しません。 上記のでは、ノードnode1に"zone"ラベルがない場合、2つのPodは無視され、新しいPodはゾーンAにスケジュールされます。
    2. 新しいPodがこれらのノードにスケジュールされる可能性はありません。 上記の例では、ノードnode5誤字のあるラベルzone-typo: zoneCがある(かつzoneラベルが設定されていない)とします。 node5がクラスターに参加しても、バイパスされ、このワークロードのPodはそのノードにスケジュールされません。
  • 新しいPodのtopologySpreadConstraints[*].labelSelectorが自身のラベルと一致しない場合に何が起こるかに注意してください。 上記の例では、新しいPodのラベルを削除しても、すでに制約が満たされているため、ゾーンBのノードに配置できます。 ただしその配置後、クラスターの不均衡の度合いは変わらず、ゾーンAにはfoo: barというラベルが付いた2つのPodがあり、ゾーンBにはfoo: barというラベルが付いた1つのPodがあります。 これが期待する動作ではない場合、ワークロードのtopologySpreadConstraints[*].labelSelectorを更新して、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

ビルトインのデフォルト制約

FEATURE STATE: 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プラグイン構成のdefaultingTypeListに設定し、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がより密集させるか分散させるかといった、Podが互いにどのようにスケジュールされるかを制御できます。

podAffinity
Podを引き付けます。 適切なトポロジードメインに任意の数のPodを配置できます。
podAntiAffinity
Podを遠ざけます。 requiredDuringSchedulingIgnoredDuringExecutionモードに設定すると、単一のトポロジードメインには1つのPodしかスケジュールできません。 preferredDuringSchedulingIgnoredDuringExecutionモードに設定すると、この制約を強制できません。

より細かい制御を行うには、トポロジー分散制約を指定して、異なるトポロジードメインにPodを分散させることで、高可用性やコスト削減を実現できます。 またワークロードのローリングアップデートやレプリカのスムーズなスケールアウトにも役立ちます。

詳しくは、Podのトポロジー分散制約に関する機能強化提案のMotivationセクションを参照してください。

既知の制限

  • Podの削除後も、制約が満たされていることは保証されません。 例えば、Deploymentのスケールダウンによって、Podの分布が不均衡になる可能性があります。

    Podの分布をリバランスするには、Deschedulerなどのツールを使用できます。

  • taintされたノードに一致するPodは優先されます。 Issue 80921を参照してください。

  • スケジューラーは、クラスター内のすべてのゾーンやその他のトポロジードメインを事前に把握しているわけではありません。 これらはクラスター内の既存のノードから決定されます。 オートスケールされるクラスターでは、ノードプール(またはノードグループ)が0ノードにスケールされ、クラスターがスケールアップすることを期待している場合に問題が発生する可能性があります。 この場合、トポロジードメインはその中に少なくとも1つのノードが存在するまで考慮されないためです。

    これを回避するためには、Podのトポロジー分散制約を認識し、全体のトポロジードメインの集合も認識しているノードオートスケーラーを使用することができます。

次の項目

  • ブログ記事Introducing PodTopologySpreadでは、いくつかの高度な使用例を含め、maxSkewについて詳しく説明しています。
  • PodのAPIリファレンスのschedulingセクションを読んでください。