Pod Anti-Affinity と Topology Spread の違い
v1.37 時点(2026年10月26日 確認)
目次
この記事ではほかの Pod のラベルを手がかりに Pod を分散させたりまとめたりする Pod 間のアンチアフィニティ(Pod Anti-Affinity)と Pod Topology Spread Constraints の違いを、図で説明します。どちらもノードを「トポロジーのドメイン」というまとまりで数えます。自分と同じラベルの Pod を相手にしたアンチアフィニティの required は、そのラベルの Pod を 1 つのドメインに 1 つまでしか置かせず、Topology Spread はドメインごとの Pod の数の偏り(skew)を maxSkew の値をもとに抑えます。Pod を同じ場所に集める Pod 間のアフィニティもあわせて扱います。後半では kind のマルチノードのクラスタで、Pod をまとめる配置と分散させる配置を比べる手順を示します。
第31回の nodeSelector と Node Affinity の内容を前提にしています。第31回では、ほかの Pod のラベルで置き場所を決める書き方はこの回で扱う、と予告していました。まだの方は先にこちらをどうぞ。
nodeSelector と Node Affinity の違いPod を置くノードをノードのラベルで選ぶ nodeSelector と Node Affinity の違いを図で説明します。required と preferred、演算子、IgnoredDuringExecution の意味、nodeName との違いを押さえ、kind のマルチノードのクラスタでワーカーにラベルを付けて Pod を寄せる・避ける手順を紹介します。以下の図は、後半の「手元で試す」で作る Deployment のうち、Pod を分散させる 2 つ(web-anti と web-spread-honor)が、それぞれ 3 つの Pod をどう置くかを表しています。ほかの Deployment の Pod は省いています。
web-anti は、自分と同じラベルの Pod がいるノードには置かない、というアンチアフィニティを持つので、2 つのワーカーに 1 つずつ置かれ、3 つ目は置けるノードが無く待つはずです。図の赤は、この Pod がこのままでは動き出せない状態を表しています。web-spread-honor はノードごとの数の差を 1 までに抑える Topology Spread を持つので、公式ドキュメントの定義どおりに数えると、2 つと 1 つに分かれて 3 つとも動くはずです。これは nodeTaintsPolicy: Honor を付けた場合の予想で、既定のままだと 3 つ目が待つと考えられる理由は「nodeAffinityPolicy と nodeTaintsPolicy」の節で説明します。後半では cache と同じノードに 3 つを集める web-together も作ります(図には描いていません)。どのワーカーに何個置かれるかは手元で変わり得るので、図の左右は一例です。コントロールプレーンのノードに Pod が無いのは、第15回で見たとおり、kind のマルチノードのクラスタでもあるはずの NoSchedule の Taint のためです。
参考:Assigning Pods to Nodes | Kubernetes、Pod Topology Spread Constraints | Kubernetes、Taints and Tolerations | Kubernetes、Implementation details | Kubernetes
Pod 間のアフィニティと Topology Spread とは
第31回で見たとおり、公式ドキュメントは Pod を置くノードを制限するやり方として、nodeSelector、アフィニティとアンチアフィニティ、nodeName、Pod Topology Spread Constraints の 4 つを挙げています。アフィニティには Node Affinity と Pod 間のアフィニティ・アンチアフィニティの 2 種類があり、第31回では Node Affinity を扱いました。この回は残りの 2 つ、Pod 間のアフィニティ・アンチアフィニティと Pod Topology Spread Constraints を扱います。
Pod 間のアフィニティとアンチアフィニティ(inter-pod affinity/anti-affinity)は ノードのラベルではなく、そのノードですでに動いている Pod のラベルをもとに、Pod を置けるノードを制限する仕組み です。公式ドキュメントは規則の形を「この Pod は、規則 Y に合う Pod がすでに 1 つ以上動いている X で動くべき(should)。アンチアフィニティの場合は動くべきでない(should not)」と説明しています。
X はノード、ラック(サーバーを収める棚)、クラウドプロバイダーのゾーンやリージョンなどの トポロジーのドメイン(topology domain) です。トポロジーは、ここではノードがどこに置かれているかという配置のことを指します。Y は Kubernetes が満たそうとする規則で、ラベルセレクタ(第11回)と、省略できる Namespace の並びで書きます。この記事ではノードやゾーンのような「ひとまとまりに数える範囲」をドメインと呼びます。
Pod Topology Spread Constraints(以下、Topology Spread)は リージョン、ゾーン、ノードなどの障害ドメイン(failure domain)や、利用者が決めたドメインに、Pod をどう散らばらせるかを制御する仕組み です。障害ドメインは一般に、1 つの故障で一緒に止まり得る範囲のことです。公式ドキュメントによると、Topology Spread は高い可用性と、資源の効率のよい使い方を実現する助けになり得ます。クラスタ全体の既定として設定することも、ワークロードごとに設定することもできます。
参考:Pod Topology Spread Constraints | Kubernetes、Assigning Pods to Nodes | Kubernetes
この回で扱う 3 つのフィールドを、v1.37 時点の公式ドキュメントに沿って次の表にまとめます。どれも Pod の spec に書き、Pod を置くノードを選ぶのは第31回と同じく kube-scheduler です。
| フィールド | 書くもの | 公式ドキュメントの説明 |
|---|---|---|
spec.affinity.podAffinity | どの Pod と同じドメインに置くか | Pod を引き寄せる。条件を満たすドメインに、何個でも Pod を詰めようとできる |
spec.affinity.podAntiAffinity | どの Pod と同じドメインに置かないか | Pod を遠ざける。required で自分のラベルを相手にすると、1 ドメインに 1 つまでになる |
spec.topologySpreadConstraints | どの Pod の数を、どのドメインの単位で、どこまでの差に抑えるか | Pod をドメインに分散させる。DoNotSchedule では、全体の最小との差を maxSkew までに抑える(詳しくは「skew と maxSkew」の節) |
参考:Assigning Pods to Nodes | Kubernetes、Pod Topology Spread Constraints | Kubernetes、Pod Topology Spread Constraints | Kubernetes
なぜ Pod 間のアフィニティと Topology Spread が必要なのか
公式ドキュメントの Topology Spread のページは、動機を次のように説明しています。最大 20 ノードのクラスタで、Pod の数が 2 から 15 まで変わるワークロードを動かすとします。Pod が 2 つしかないときに 2 つとも同じノードで動いていると、ノードが 1 つ故障しただけでワークロードが止まってしまいます。第11回・第12回の ReplicaSet や Deployment は Pod の数を保ちますが、どのノードに置くかは kube-scheduler に任せています。
Pod が増えると別の心配も出てきます。公式ドキュメントの例では、クライアントが 3 つのデータセンター(ゾーン)に分かれていて、レイテンシ(応答の遅れ)が望むより大きいことに気付き、各ゾーンに同じくらいの数の Pod を置きたくなります。Topology Spread はこれを宣言的に設定する方法です。
逆に、Pod を同じ場所に集めたい場合もあります。第31回で見た、やり取りの多い 2 つのアプリケーションの Pod を同じゾーンに置きたい、という例です。公式ドキュメントは、Pod 間のアフィニティの required を使えば、よくやり取りする 2 つのサービスの Pod を同じゾーンに置くようスケジューラーに伝えられる、と説明しています。preferred のアンチアフィニティを使えば、1 つのサービスの Pod を複数のゾーンに分散させられる、とも書いています。
公式ドキュメントのもう 1 つの例は、3 ノードのクラスタで Web アプリケーションと、メモリ上のキャッシュ(Redis など)を動かす場合です。キャッシュの Deployment にはアンチアフィニティを書いて、1 つのノードにキャッシュを 2 つ以上置かないようにします。Web サーバーの Deployment にはキャッシュの Pod があるノードに置くアフィニティと、Web サーバーどうしを同じノードに置かないアンチアフィニティを書きます。結果として、3 つのノードのそれぞれで Web サーバーとキャッシュが 1 つずつ並びます。公式ドキュメントは、この置き方が偏り(skew)とレイテンシの両方を小さくすることをねらったもの、と説明しています。
Pod 間のアフィニティと Topology Spread の仕組み
トポロジーのドメインと topologyKey
どちらの仕組みもドメインを topologyKey で指定します。topologyKey はノードのラベルのキーです。API リファレンスによると、そのキーのラベルを持ち、値が同じノードは、同じトポロジーにあるとみなされます。kubernetes.io/hostname を指定すれば各ノードがそれぞれ 1 つのドメインに、topology.kubernetes.io/zone を指定すれば各ゾーンが 1 つのドメインになります。
参考:Pod | Kubernetes API Reference、Assigning Pods to Nodes | Kubernetes
よく使われる 2 つのラベルを次の表にまとめます。
| ラベルのキー | 付けるもの | 公式ドキュメントの説明 |
|---|---|---|
kubernetes.io/hostname | kubelet | ノードのホスト名が入る(第31回)。トポロジーの階層の一部としても使われる |
topology.kubernetes.io/zone | kubelet か、外部の cloud-controller-manager | クラウドプロバイダーの情報から入る。クラウドプロバイダーを使っている場合だけ付く |
参考:Well-Known Labels, Annotations and Taints | Kubernetes、Well-Known Labels, Annotations and Taints | Kubernetes
公式ドキュメントは topology.kubernetes.io/zone について、自分のトポロジーに合うならノードに付けることを検討してもよい、とも書いています。kind のクラスタは手元のコンテナで動き、クラウドプロバイダーの情報を持たないと考えられるので、kind のノードにはゾーンのラベルが付かないはずです(kind のドキュメントでは確かめていない推測です)。この回の手順では、ゾーンではなく kubernetes.io/hostname を使い、ノードごとに数えます。
公式ドキュメントの Topology Spread のページは、ドメインの中のすべてのノードに一貫してラベルが付くようにする仕組みが要る(need)、と書いています。多くのクラスタが kubernetes.io/hostname のようなよく知られたラベルを自動で付けるので、自分のクラスタがそうなっているか確かめるよう勧めています。
Pod 間のアフィニティとアンチアフィニティ
Pod 間のアフィニティは Pod の spec.affinity.podAffinity に、アンチアフィニティは spec.affinity.podAntiAffinity に書きます。Node Affinity と同じく、どちらにも required(requiredDuringSchedulingIgnoredDuringExecution)と preferred(preferredDuringSchedulingIgnoredDuringExecution)の 2 種類があります。公式ドキュメントが説明する、新しい Pod をスケジュールするときの扱いを次の表にまとめます。
| 種類 | スケジューラーの扱い |
|---|---|
| required | 絞り込み(filtering)で働く。すでにある Pod をもとに、規則を満たすノードに割り当てる |
| preferred | 点数付け(scoring)で働く。規則をどれだけ満たすかでノードに点数を付ける |
preferred の各項目には 1〜100 の weight を付けます。API リファレンスによると、アフィニティでは合う Pod があるノードの合計に weight を足し、アンチアフィニティでは引きます。合計が最も高いノードが最も好まれますが、規則に合わないノードが選ばれることもあります(may)。スケジューラーの設定のページによると、Pod 間のアフィニティとアンチアフィニティを実装しているのは InterPodAffinity というプラグインで、filter と score を含む段階で働きます。
参考:Pod | Kubernetes API Reference、Pod | Kubernetes API Reference、Scheduler Configuration | Kubernetes
アンチアフィニティで「1 つのドメインに 1 つまで」になるのは、相手が自分と同じラベルの Pod のときです。自分の Deployment のラベルを相手にした required のアンチアフィニティでは、そのラベルの Pod がすでにいるドメインに次の Pod を置けないので、そのラベルの Pod は 1 つのドメインに 1 つまでになります。相手の labelSelector に合わないほかのラベルの Pod は相手になりません(いても置けます)。公式ドキュメントの Topology Spread のページも、アンチアフィニティを required にすると 1 つのドメインに 1 つの Pod しか置けない、と説明しています。
参考:Pod Topology Spread Constraints | Kubernetes、Assigning Pods to Nodes | Kubernetes、Pod | Kubernetes API Reference
以下はアフィニティとアンチアフィニティを 1 つずつ書いた Pod のマニフェストです。公式ドキュメントの例(pods/pod-with-pod-affinity.yaml)をそのまま引用しています。
apiVersion: v1
kind: Pod
metadata:
name: with-pod-affinity
spec:
affinity:
podAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: security
operator: In
values:
- S1
topologyKey: topology.kubernetes.io/zone
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: security
operator: In
values:
- S2
topologyKey: topology.kubernetes.io/zone
containers:
- name: with-pod-affinity
image: registry.k8s.io/pause:3.8公式ドキュメントはこの例を次のように説明しています。アフィニティの規則(required)により、スケジューラーがこの Pod を置けるのは、security=S1 のラベルの Pod がすでに動いているゾーンのノードだけです。topology.kubernetes.io/zone=V のラベルを持つノードの集まりを「ゾーン V」と呼ぶと、ゾーン V に security=S1 の Pod が 1 つでもあれば、ゾーン V のどのノードにも置けます。アンチアフィニティの規則(preferred)により、スケジューラーは security=S2 の Pod がいるゾーンのノードを避けようとします。security=S2 の Pod がいないゾーンへの配置には影響しません。
マニフェストの形を見ると、required には条件のまとまり(term)を直接並べ、preferred には weight と term(podAffinityTerm)の組を並べています。term に書くフィールドを API リファレンスと公式ドキュメントに沿って次の表にまとめます。
| フィールド | 内容 |
|---|---|
labelSelector | 相手にする Pod を選ぶラベルセレクタ。null だと、どの Pod にも合わない |
topologyKey | ドメインを決めるノードのラベルのキー。必須で、空にはできない |
参考:Pod | Kubernetes API Reference、Assigning Pods to Nodes | Kubernetes
labelSelector の operator には In、NotIn、Exists、DoesNotExist を使えます。第31回の Node Affinity の演算子から Gt と Lt を除いたものです。
term にはほかに、相手を探す Namespace を決める namespaces と namespaceSelector、新しい Pod のラベルの値で相手を絞る matchLabelKeys と mismatchLabelKeys もあります。namespaces と namespaceSelector を省くと、規則を書いた Pod と同じ Namespace の Pod だけが相手になります(Namespace は第37回)。matchLabelKeys は、たとえば pod-template-hash(第6回)を書いて、ローリングアップデートのあいだも同じリビジョンの Pod だけを相手にするのに使えます。matchLabelKeys と mismatchLabelKeys について、v1.37 の機能ゲート(機能を有効・無効にする切り替え)の一覧は v1.33 から GA としていますが、概念のページの注記はベータとしています。
参考:Pod | Kubernetes API Reference、Assigning Pods to Nodes | Kubernetes、Assigning Pods to Nodes | Kubernetes、Feature Gates | Kubernetes
公式ドキュメントによると、自分自身に合うアフィニティを持つ Pod の集まりでも、最初の 1 つはほかの確認を通れば置けるので、互いに待ち続けて動けなくなることはありません。
公式ドキュメントは、処理が重いので 数百ノードを超えるクラスタで使うことは勧められていない ことと、アンチアフィニティでは すべてのノードが topologyKey のラベルを持たなければならない(must) ことを注意しています(注意点の表)。
名前の後ろ半分の IgnoredDuringExecution の意味は第31回の Node Affinity と同じで、API リファレンスは、実行中に条件を満たさなくなった Pod をシステムが追い出そうとするかもしれないし、しないかもしれない(may or may not)と書いています。
参考:Pod | Kubernetes API Reference、Pod | Kubernetes API Reference
Topology Spread のフィールド
Topology Spread は Pod の spec.topologySpreadConstraints に書きます。以下は公式ドキュメントが示すフィールドの一覧です。そのまま引用しています(<integer> などは値の種類を表す書き方で、そのままでは apply できません)。
---
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各フィールドを公式ドキュメントと API リファレンスに沿って次の表にまとめます。段階は v1.37 時点の機能ゲートの一覧によります。
| フィールド | 内容 | 必須・既定・段階 |
|---|---|---|
maxSkew | Pod がどこまで偏ってよいか。意味は whenUnsatisfiable で変わる | 必須。0 より大きい整数 |
topologyKey | ドメインを決めるノードのラベルのキー | 必須 |
whenUnsatisfiable | 制約を満たせないときの扱い。DoNotSchedule か ScheduleAnyway | API リファレンスでは必須。値の説明では DoNotSchedule が既定とされる |
labelSelector | 数える Pod を選ぶラベルセレクタ | 省略できる |
minDomains | 対象になるドメインの最小の数。足りないと全体の最小を 0 とみなす | 省略すると 1 とみなされる。DoNotSchedule のときだけ書ける |
matchLabelKeys | 新しい Pod のラベルのキーを並べ、その値で数える Pod を絞る | 省略できる。v1.27 からベータで、既定で有効 |
nodeAffinityPolicy | 偏りの計算での nodeAffinity と nodeSelector の扱い(後の節) | 省略すると Honor。v1.33 から GA |
nodeTaintsPolicy | 偏りの計算でのノードの Taint の扱い(後の節) | 省略すると Ignore。v1.33 から GA |
参考:Pod Topology Spread Constraints | Kubernetes、Pod | Kubernetes API Reference、Feature Gates | Kubernetes、Feature Gates | Kubernetes
引用した一覧のコメントは「beta since v1.26」のままですが、同じページの注記と機能ゲートの一覧では、nodeAffinityPolicy と nodeTaintsPolicy は v1.33 から GA です。
参考:Pod Topology Spread Constraints | Kubernetes、Feature Gates | Kubernetes
公式ドキュメントによると、1 つの Pod に 2 つ以上の制約を書くと、制約は AND でつながります。kube-scheduler はすべての制約を満たすノードを探します。ただし、同じ topologyKey と whenUnsatisfiable の組み合わせの制約は 1 つしか書けません。スケジューラーの設定のページによると、Topology Spread を実装しているのは PodTopologySpread というプラグインで、InterPodAffinity と同じく filter と score を含む段階で働きます。
参考:Pod Topology Spread Constraints | Kubernetes、Pod Topology Spread Constraints | Kubernetes、Scheduler Configuration | Kubernetes
skew と maxSkew
skew は新しい Pod をあるドメインに置いたあとの、そのドメインの Pod の数と「全体の最小(global minimum)」との差です。数えるのは labelSelector に合う Pod だけです。全体の最小は対象になるドメイン(eligible domain)のうち、合う Pod が最も少ないドメインの数です。whenUnsatisfiable: DoNotSchedule のとき、maxSkew は skew として許す最大の値です。
参考:Pod Topology Spread Constraints | Kubernetes、Pod | Kubernetes API Reference
以下の図は公式ドキュメントの例で、新しい Pod をどちらのゾーンに置けるかを表しています。4 つのノードが zone のラベルで 2 つのゾーンに分かれ、foo: bar のラベルの Pod が node1、node2、node3 で 1 つずつ動いています。この例のノードには説明のために付けた zone(値は zoneA か zoneB)と node(値は node1〜node4)のラベルがあります。実際には topology.kubernetes.io/zone などのよく知られたキーが勧められています。新しい Pod には maxSkew: 1、topologyKey: zone、whenUnsatisfiable: DoNotSchedule の制約が書いてあります。
公式ドキュメントによると、新しい Pod をゾーン A に置くと数は 3 と 1 になり、skew は 2(3 − 1)で maxSkew: 1 を超えます。そのため、新しい Pod はゾーン B のノード(node3 か node4)にしか置けません。maxSkew を 2 にすればゾーン A にも置けます。topologyKey を node に変えてノードごとに数えると、maxSkew が 1 のままなら置けるのは node4 だけです。
whenUnsatisfiable は制約を満たせないときの扱いを決めます。DoNotSchedule では、スケジューラーは Pod をスケジュールせず、満たせる置き方が見つかるまで Pod は Pending のままです。ScheduleAnyway では、制約を満たせなくてもスケジューラーは Pod をスケジュールし、skew を小さくするノードを優先します。API リファレンスは DoNotSchedule について、クラスタは偏ったままのことがあるが、スケジューラーがそれ以上偏らせることはない、と説明しています。
参考:Pod Topology Spread Constraints | Kubernetes、Pod Topology Spread Constraints | Kubernetes、Pod | Kubernetes API Reference
公式ドキュメントは数え方の暗黙の決まりとして次の 3 つを挙げています。
- 新しい Pod と同じ Namespace の Pod だけが数える対象になる
topologyKeyのラベルが無いノードは飛ばされ、その上の Pod は数えられず、新しい Pod も置かれないlabelSelectorが新しい Pod 自身のラベルに合わないと、置いたあとも偏りの数は変わらない
3 つ目について、公式ドキュメントは、ふつう Pod は自分の制約のセレクタに合うべき(should)だとしています。
参考:Pod Topology Spread Constraints | Kubernetes、Pod Topology Spread Constraints | Kubernetes
nodeAffinityPolicy と nodeTaintsPolicy
「対象になるドメイン」は nodeAffinityPolicy と nodeTaintsPolicy の条件を満たすノードのドメインです。2 つのフィールドの値の意味を公式ドキュメントに沿って次の表にまとめます。
| フィールド | Honor | Ignore |
|---|---|---|
nodeAffinityPolicy(既定は Honor) | Pod の nodeAffinity と nodeSelector に合うノードだけを計算に入れる | nodeAffinity と nodeSelector を無視し、すべてのノードを計算に入れる |
nodeTaintsPolicy(既定は Ignore) | Taint の無いノードと、新しい Pod が許容する Taint のあるノードを計算に入れる | Taint を無視し、すべてのノードを計算に入れる |
参考:Pod Topology Spread Constraints | Kubernetes、Pod | Kubernetes API Reference、Pod Topology Spread Constraints | Kubernetes
既定の値が 2 つで逆になっている点に気を付けます。nodeTaintsPolicy を書かないと、Pod が許容しない Taint のあるノードも、偏りの計算に入ります。この回のクラスタでいえば、NoSchedule の Taint があるコントロールプレーンのノードです。
以下の図は、この回のクラスタで topologyKey: kubernetes.io/hostname、maxSkew: 1 の Pod を 3 つ作ったとき(後半で作る web-spread と web-spread-honor)、nodeTaintsPolicy の値で 3 つ目の Pod の扱いがどう変わるかを表しています。公式ドキュメントの定義どおりに数えた予想で、手元で確かめた結果ではありません。
既定の Ignore では、コントロールプレーンのノードも対象になるドメインで、そこに合う Pod は 0 個なので、全体の最小は 0 になると考えられます。2 つ目の Pod は、1 つ目と同じワーカーに置くと 2 − 0 = 2 になるので、もう片方のワーカーに置かれるはずです。2 つの Pod がワーカーに 1 つずつ置かれたあとは、3 つ目をどちらのワーカーに置いても 2 − 0 = 2 で、maxSkew: 1 を超える計算になります。コントロールプレーンのノードには Taint があって置けないので、3 つ目は Pending のままになるはずです。図の上の枠は冒頭の図の web-anti と同じ置き方になります。Honor にすると、Pod が許容しない Taint のあるコントロールプレーンのノードは計算から外れ、ワーカーの 2 つだけで数えます。3 つ目を置くと 2 − 1 = 1 で、maxSkew: 1 に収まるはずです。後半の手順で、この 2 つを比べます。
参考:Pod Topology Spread Constraints | Kubernetes、Taints and Tolerations | Kubernetes、Implementation details | Kubernetes
クラスタ全体の既定の制約
公式ドキュメントによると、Pod に制約を 1 つも書いていなくても、Service や ReplicaSet、StatefulSet などに属する Pod には、クラスタ全体の既定の制約が使われます。既定の制約を設定していないと、kube-scheduler は次の制約を指定したかのように動きます(v1.24 から stable)。公式ドキュメントの例をそのまま引用しています。
defaultConstraints:
- maxSkew: 3
topologyKey: "kubernetes.io/hostname"
whenUnsatisfiable: ScheduleAnyway
- maxSkew: 5
topologyKey: "topology.kubernetes.io/zone"
whenUnsatisfiable: ScheduleAnywayどちらも ScheduleAnyway なので、Pod を Pending にはしない、ゆるい分散だと読めます。公式ドキュメントは、ノードが kubernetes.io/hostname と topology.kubernetes.io/zone の両方のラベルを持つとは限らないなら、既定の制約を使わずに自分で制約を書くよう勧めています。
参考:Pod Topology Spread Constraints | Kubernetes、Pod Topology Spread Constraints | Kubernetes
アンチアフィニティと Topology Spread の違い
公式ドキュメントの Topology Spread のページによると、Pod 間のアフィニティとアンチアフィニティは、Pod どうしの関係で Pod をより詰めて置くか、より散らして置くかを制御します。より細かく制御したいときは、Topology Spread で Pod をドメインに分散させられます。ローリングアップデートや、Pod の数を増やすときにも役立つ、とも書かれています。
分散させる 2 つの書き方を、守れないときの動きで比べると、次の表のとおりです。
| 書き方 | 1 つのドメインに置ける数 | 守れないとき |
|---|---|---|
podAntiAffinity の required(自分のラベルが相手) | 1 つまで | Pod はスケジュールされず、Pending のまま待つ |
podAntiAffinity の preferred | 強制できない(点数で避ける) | 規則に合わないノードにも置かれる |
Topology Spread の DoNotSchedule | 全体の最小との差が maxSkew までになる数 | Pod はスケジュールされず、Pending のまま待つ |
Topology Spread の ScheduleAnyway | 強制しない(点数で偏りを減らす) | 制約を満たせなくてもスケジュールされる(skew を小さくするノードを優先) |
参考:Pod Topology Spread Constraints | Kubernetes、Pod Topology Spread Constraints | Kubernetes、Pod | Kubernetes API Reference、Assigning Pods to Nodes | Kubernetes
違いをひとことで言うと、アンチアフィニティは相手の Pod がそのドメインに「いるか、いないか」を見て、Topology Spread はドメインごとの Pod の「数」を数えて差を抑えます。アンチアフィニティの規則は「規則に合う Pod がすでに 1 つ以上動いているドメイン」を対象にし、Topology Spread は labelSelector に合う Pod の数をドメインごとに数えるからです。
参考:Assigning Pods to Nodes | Kubernetes、Pod Topology Spread Constraints | Kubernetes
公式ドキュメントの説明から読める使い分けを、次の表にまとめます。
| やりたいこと | 使う書き方 |
|---|---|
| 1 つのドメインに 1 つまでを必ず守らせたい | 自分のラベルを相手にした、required のアンチアフィニティ |
| Pod の数がドメインより多くても、均等に散らしたい | Topology Spread の DoNotSchedule。Pod が許容しない Taint のあるノードがあるなら、nodeTaintsPolicy: Honor も検討する(手順の web-spread-honor) |
| 散らしたいが、置けないよりは置くほうを選びたい | Topology Spread の ScheduleAnyway か、preferred のアンチアフィニティ |
| ほかの Pod の近くに置きたい | Pod 間のアフィニティ |
「なぜ」の節で見た Web サーバーとキャッシュの例は、1 つ目の書き方(キャッシュどうし、Web サーバーどうし)と 4 つ目の書き方(Web サーバーをキャッシュの近くに)を組み合わせたものです。
手元で試す
ここまでの内容を手元の kind のクラスタで試す手順を示します。kubernetes.io/hostname をドメインにして、ノードごとに数えます。まず Pod 間のアフィニティで 3 つの Pod を 1 つのノードにまとめます。次にアンチアフィニティの required と Topology Spread で 3 つの Pod を分散させ、置き方の違いを比べます。Topology Spread では nodeTaintsPolicy を書かない場合と Honor にした場合も比べます。
各段で何が表示されるかは、どれも公式ドキュメントから予想される結果で、手元で実行して確かめたものではありません。各段の最後に、成功かどうかを見分ける目安を書きます。
コマンドは macOS や Linux のシェル(bash や zsh)で実行する前提で書いています。Windows では WSL(Windows の中で Linux を動かす仕組み)などを使ってください。試す環境は次のとおりです。
| 項目 | 内容 |
|---|---|
| kind | v0.33.0 |
| Kubernetes | v1.37.0(kind v0.33.0 の既定のノードイメージ) |
| ノード構成 | マルチノード(クラスタ名 multi-node):コントロールプレーン 1、ワーカー 2 |
| 必要なもの | Docker、kubectl(v1.36〜v1.38)、テキストエディタ、端末(ターミナル)2 つ |
参考:Release v0.33.0 · kubernetes-sigs/kind、Version Skew Policy | Kubernetes
kind でクラスタを作る手順は、以下の記事で解説しています。この回は第31回と同じマルチノードのクラスタを使います。分散させる様子を見るには、Pod を置けるノードが 2 つ以上要るからです。
kind で検証用のクラスタを作るDocker のコンテナをノードにして、手元のマシンに Kubernetes のクラスタを作るツール kind の仕組みを図で説明します。kind create cluster で何が起きるかを押さえ、シングルノードとマルチノードのクラスタを作って、ノードの正体が Docker のコンテナであることを確かめる手順を紹介します。マルチノードのクラスタを用意する
第2回・第31回と同じ次の設定ファイルを使います。公式ドキュメントの例をそのまま引用しています。kind-multi-node.yaml という名前で、コマンドを実行するディレクトリに保存します。前の回までに保存したファイルが残っていれば、それを使えます。
# three node (two workers) cluster config
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
- role: worker
- role: workerまず、前の回のクラスタが残っていないかを確かめます。
$ kind get clusters
multi-node が表示されたら、作り直さずにそのまま使えるので、クラスタを作る次のコマンドは飛ばします。表示されなければ、クラスタを作ってから、もう一度一覧します。
$ kind create cluster --name multi-node --config kind-multi-node.yaml
$ kind get clusters
kind get clusters に multi-node が表示されれば、クラスタができています。第3回で決めたとおり、ここからのコマンドには --context kind-multi-node を付けて、操作するクラスタをはっきりさせます。
続けて、ノードを一覧します。
$ kubectl get nodes --context kind-multi-node
ノードが 3 つ表示され、STATUS 列が Ready になっていれば準備ができています。Ready になっていなければ、少し待ってからもう一度実行します。
次に、第31回と同じコマンドで、ワーカーのノードだけを一覧します。node-role.kubernetes.io/control-plane のラベルを持たないノードを選ぶ書き方で、! をシェルに解釈させないよう一重引用符で囲みます。
$ kubectl get nodes -l '!node-role.kubernetes.io/control-plane' --context kind-multi-node
参考:Labels and Selectors | Kubernetes、Well-Known Labels, Annotations and Taints | Kubernetes
ノードが 2 つ表示されるはずです。この 2 つの名前をメモしておき、あとで Pod の NODE 列と見比べます。表示された順に「1 つ目のワーカー」「2 つ目のワーカー」と呼びます。
ゾーンのラベルが付いていないことも確かめておきます。-L は第11回・第31回で使った、ラベルの値を列にして表示する指定です。
$ kubectl get nodes -L topology.kubernetes.io/zone --context kind-multi-node
ゾーンのラベルの列(見出しは ZONE のような形になると考えられます)が加わり、どのノードでも空になると考えられます。値が入っていても、この回の手順は kubernetes.io/hostname で数えるので、そのまま進めて構いません。
前の回のクラスタを使い続けている場合は、default の Namespace に Pod が残っていないことを確かめます。
$ kubectl get pods --context kind-multi-node
見つからないことを知らせる 1 行(第8回で推測した No resources found in default namespace. のような表示)が出れば、そのまま進めます。Pod が残っていたら、自分で作ったものかを確かめてから消します。
Pod を監視する
ここからは端末を 2 つ使います。第31回と同じく、1 つ目の端末で default の Namespace の Pod を追い続け、2 つ目の端末でマニフェストを渡します。2 つ目の端末を開いたら、cd でマニフェストを保存するディレクトリに移動しておきます。
1 つ目の端末で、Pod が置かれたノードの名前(NODE 列)も表示しながら監視します。この監視は片付けの前まで止めずに続けます。
$ kubectl get pods --watch -o wide --context kind-multi-node
Pod がまだ無いので、見つからないことを知らせる 1 行が出たあと、止まったように見えると考えられます。
この回は Pod を Deployment(第12回)で作ります。第6回で見たとおり、Deployment の Pod の名前は、ReplicaSet の名前(Deployment の名前とハッシュをつないだもの)で始まります。ここからは「web-anti の Pod」のように、Deployment の名前で Pod を呼びます。
podAffinity でまとめる
まず、まとめる先になる Pod を 1 つ作ります。以下はラベル app=cache の Pod を 1 つ動かす Deployment のマニフェストです。中身は nginx で、名前だけキャッシュの役にしています。cache.yaml という名前で保存します。
apiVersion: apps/v1
kind: Deployment
metadata:
name: cache
spec:
replicas: 1
selector:
matchLabels:
app: cache
template:
metadata:
labels:
app: cache
spec:
containers:
- name: nginx
image: nginx
imagePullPolicy: IfNotPresent2 つ目の端末で、マニフェストを渡します。
$ kubectl apply -f cache.yaml --context kind-multi-node
deployment.apps/cache が作られたことを知らせる 1 行が出るはずです。1 つ目の端末には cache の Pod の行が書き足されていき、STATUS 列は Pending、ContainerCreating を経て Running になるはずです。NODE 列にはワーカーのどちらかの名前が入ると考えられます。コントロールプレーンのノードは、第15回で見たとおり、kind のマルチノードのクラスタでもあるはずの NoSchedule の Taint で外れると考えられます。クラスタを作ったばかりなら、イメージの取得に時間がかかるので、ContainerCreating のまましばらく止まって見えることがあります(第9回)。
参考:Pod Lifecycle | Kubernetes、Taints and Tolerations | Kubernetes、Implementation details | Kubernetes
成功の目安は、cache の Pod が Running になり、NODE 列にワーカーの名前が入っていることです。cache の Pod が Running になるまで待ってから次に進みます。
次に、以下は cache の Pod と同じノードに置く Pod を 3 つ動かす Deployment のマニフェストです。公式ドキュメントの Web サーバーの例をもとに、podAffinity だけを残し、ラベルセレクタを matchLabels で書いた形です。web-together.yaml という名前で保存します。
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-together
spec:
replicas: 3
selector:
matchLabels:
app: web-together
template:
metadata:
labels:
app: web-together
spec:
affinity:
podAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
app: cache
topologyKey: kubernetes.io/hostname
containers:
- name: nginx
image: nginx
imagePullPolicy: IfNotPresent参考:Assigning Pods to Nodes | Kubernetes、Pod | Kubernetes API Reference
labelSelector は app: cache のラベルの Pod を選び、topologyKey: kubernetes.io/hostname でドメインをノードにしています。「app=cache の Pod が動いているノードで動く」という required の規則です。
2 つ目の端末で、マニフェストを渡します。
$ kubectl apply -f web-together.yaml --context kind-multi-node
deployment.apps/web-together が作られたことを知らせる 1 行が出るはずです。1 つ目の端末には web-together の Pod の行が 3 つ書き足されていき、どれも NODE 列が cache の Pod と同じワーカーになるはずです。required のアフィニティは絞り込みで働き、app=cache の Pod がいるノードは 1 つだけだからです。
成功の目安は、web-together の 3 つの Pod が Running になり、NODE 列がすべて cache の Pod と同じワーカーの名前になっていることです。
podAntiAffinity で分散させる
以下は自分と同じラベルの Pod がいるノードには置かない Pod を 3 つ動かす Deployment のマニフェストです。公式ドキュメントのキャッシュの例と同じく、自分の Deployment のラベル(ここでは app: web-anti)をアンチアフィニティの相手にしています。web-anti.yaml という名前で保存します。
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-anti
spec:
replicas: 3
selector:
matchLabels:
app: web-anti
template:
metadata:
labels:
app: web-anti
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
app: web-anti
topologyKey: kubernetes.io/hostname
containers:
- name: nginx
image: nginx
imagePullPolicy: IfNotPresent参考:Assigning Pods to Nodes | Kubernetes、Pod | Kubernetes API Reference
2 つ目の端末で、マニフェストを渡します。
$ kubectl apply -f web-anti.yaml --context kind-multi-node
deployment.apps/web-anti が作られたことを知らせる 1 行が出るはずです。1 つ目の端末には web-anti の Pod の行が 3 つ現れるはずです。2 つは 1 つ目のワーカーと 2 つ目のワーカーに 1 つずつ置かれ、やがて Running になるはずです。残りの 1 つは STATUS 列が Pending のままで、NODE 列にもノードの名前が入らない(<none> のような表示になる)と考えられます。web-anti は自分のラベルを相手にしているので、web-anti の Pod はワーカー 1 つに 1 つまでで、Pod を置けるノードはワーカーの 2 つしかないからです。
参考:Pod Topology Spread Constraints | Kubernetes、Kubernetes Scheduler | Kubernetes
理由を確かめるため、2 つ目の端末で Pending の Pod を詳しく表示します。<Pending の Pod の名前> は 1 つ目の端末に出ている名前に置き換えます。
$ kubectl describe pod <Pending の Pod の名前> --context kind-multi-node
表示の末尾の Events: には第31回と同じく FailedScheduling(From が default-scheduler)の行があると考えられます。メッセージの文面の例はこのメディアで確かめた範囲の公式ドキュメントにはありません。
成功の目安は、web-anti の 2 つの Pod が別々のワーカーで Running になり、残りの 1 つが Pending のままであることです。
Topology Spread で分散させる
以下はノードごとの数の差を 1 までに抑える Pod を 3 つ動かす Deployment のマニフェストです。公式ドキュメントの例(one-constraint.yaml)の制約を、topologyKey: kubernetes.io/hostname とラベル app: web-spread に変えて、Deployment のひな形に入れた形です。nodeTaintsPolicy は書かず、既定の Ignore のままにします。web-spread.yaml という名前で保存します。
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-spread
spec:
replicas: 3
selector:
matchLabels:
app: web-spread
template:
metadata:
labels:
app: web-spread
spec:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: web-spread
containers:
- name: nginx
image: nginx
imagePullPolicy: IfNotPresent参考:Pod Topology Spread Constraints | Kubernetes、Pod | Kubernetes API Reference
labelSelector は、「skew と maxSkew」の節で見たとおり、ひな形の app: web-spread のラベルに合わせています。
2 つ目の端末で、マニフェストを渡します。
$ kubectl apply -f web-spread.yaml --context kind-multi-node
deployment.apps/web-spread が作られたことを知らせる 1 行が出るはずです。「nodeAffinityPolicy と nodeTaintsPolicy」の節の図の上の枠のとおり、1 つ目の端末では、2 つの Pod がワーカーに 1 つずつ置かれて Running になり、3 つ目は Pending のままになると考えられます。
web-anti のときと同じく、2 つ目の端末で Pending の Pod を詳しく表示します。
$ kubectl describe pod <Pending の Pod の名前> --context kind-multi-node
Events: には FailedScheduling の行があり、メッセージには web-anti のときとは違う、Topology Spread の制約を指す語が出ると考えられます(文面は公式ドキュメントでは確かめていません)。2 つの Pending の Pod のメッセージを見比べてみてください。
成功の目安は、web-spread の 2 つの Pod が別々のワーカーで Running になり、残りの 1 つが Pending のままであることです。3 つとも Running になった場合は、第15回と同じコマンドでノードの Taint のキーを表示します。
$ kubectl get nodes -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.taints[*].key}{"\n"}{end}' --context kind-multi-node
参考:JSONPath Support | Kubernetes、Implementation details | Kubernetes
コントロールプレーンのノードの行に node-role.kubernetes.io/control-plane が表示されるはずです。表示されなければ、この記事の前提(コントロールプレーンのノードに Taint がある)と違います。表示されているのに 3 つとも Running なら、この記事の予想が外れていると考えられます。
nodeTaintsPolicy を Honor にする
以下は web-spread.yaml の名前とラベルを web-spread-honor に変え、制約に nodeTaintsPolicy: Honor を足したマニフェストです。web-spread-honor.yaml という名前で保存します。
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-spread-honor
spec:
replicas: 3
selector:
matchLabels:
app: web-spread-honor
template:
metadata:
labels:
app: web-spread-honor
spec:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: web-spread-honor
nodeTaintsPolicy: Honor
containers:
- name: nginx
image: nginx
imagePullPolicy: IfNotPresentweb-spread を直さずに別の Deployment にしたのは、2 つの結果を並べて比べるためです。labelSelector が違うので、2 つの Deployment の Pod は互いの数に入りません。
2 つ目の端末で、マニフェストを渡します。
$ kubectl apply -f web-spread-honor.yaml --context kind-multi-node
deployment.apps/web-spread-honor が作られたことを知らせる 1 行が出るはずです。1 つ目の端末には web-spread-honor の Pod の行が 3 つ書き足されていき、3 つとも Running になるはずです。NODE 列は、片方のワーカーが 2 つ、もう片方が 1 つになると考えられます。Honor では、Pod が許容しない Taint のあるコントロールプレーンのノードが計算から外れ、ワーカーの 2 つだけで数えるからです。どちらのワーカーが 2 つになるかは、公式ドキュメントの説明からは決められません。
成功の目安は、web-spread-honor の 3 つの Pod がすべて Running になり、NODE 列が 2 つと 1 つに分かれていることです。
配置を比べる
1 つ目の端末の表示が落ち着いたら、2 つ目の端末で、ノードの名前と一緒に一覧します。
$ kubectl get pods -o wide --context kind-multi-node
Pod は 13 個表示されるはずです。Deployment ごとに、次の表のようになると考えられます。
| Deployment | NODE 列 | 理由 |
|---|---|---|
cache | どちらかのワーカー | 制約を書いていない。コントロールプレーンのノードは Taint で外れる |
web-together | 3 つとも cache と同じワーカー | required の podAffinity で、app=cache の Pod がいるノードに絞られた |
web-anti | ワーカーに 1 つずつ。1 つは Pending | 自分のラベルを相手にした required の podAntiAffinity で、1 つのノードに 1 つまで |
web-spread | ワーカーに 1 つずつ。1 つは Pending | 既定の Ignore で、コントロールプレーンのノードも 0 個として数えられた |
web-spread-honor | ワーカーに 2 つと 1 つ | Honor で、ワーカーの 2 つだけで差を 1 までに抑えた |
web-anti と web-spread-honor を比べると、どちらも Pod を分散させていますが、ドメインの数より Pod が多いときの動きが違います。アンチアフィニティは 3 つ目を置けず、Topology Spread は差が maxSkew に収まる範囲で、3 つ目をすでに 1 つあるワーカーに置くはずです。既定のままの web-spread は、web-anti と同じ置き方になるはずです。
片付け
1 つ目の端末で Ctrl+C を押して、監視を止めます。
2 つ目の端末で、作った Deployment をマニフェストを指定して削除します。Deployment を消すと、その Pod も消えます(第12回)。
$ kubectl delete -f cache.yaml --context kind-multi-node
$ kubectl delete -f web-together.yaml --context kind-multi-node
$ kubectl delete -f web-anti.yaml --context kind-multi-node
$ kubectl delete -f web-spread.yaml --context kind-multi-node
$ kubectl delete -f web-spread-honor.yaml --context kind-multi-node
残っていないかを確かめます。
$ kubectl get pods --context kind-multi-node
Pod が Terminating で表示されていたら、少し待ってからもう一度実行します。準備のときと同じ、見つからないことを知らせる 1 行が出れば、Pod は削除できています。この回の手順ではノードにラベルを付けていないので、ラベルを外す手順はありません。
最後に、クラスタを削除し、消えたことを確かめます。
$ kind delete cluster --name multi-node
$ kind get clusters
kind delete cluster は存在しないクラスタを指定してもエラーを返しません。kind get clusters に multi-node が表示されなくなったことを確かめます。kubeconfig に kind-multi-node のコンテキストが残っていたら、第3回の片付けと同じく kubectl config delete-context kind-multi-node で消します。
注意点
Pod 間のアフィニティ・アンチアフィニティと Topology Spread でつまずきやすい点と、後から変えられない設定を、次の表にまとめます。
| 項目 | 内容 |
|---|---|
| アフィニティとアンチアフィニティは処理が重い | 大きなクラスタではスケジュールを大きく遅くすることがある。数百ノードを超えるクラスタで使うことは勧められていない |
アンチアフィニティでは、すべてのノードに topologyKey のラベルが要る | ラベルが無いノードがあると、意図しない動きにつながることがある |
nodeTaintsPolicy の既定は Ignore | 書かないと、Pod が許容しない Taint のあるノードも偏りの計算に入る。nodeAffinityPolicy の既定は逆の Honor |
| 数を減らすと偏ることがある | Pod が消えるときは、制約が保たれる保証が無い。Deployment の Pod の数を減らすと、偏って残ることがある |
labelSelector は自分のラベルに合わせる | 合わない Pod は自分を数に入れず、同じドメインに積み重なることがある |
| Pod の制約は、作ったあとは通常の更新では変えられない | v1.37 時点では、affinity と topologySpreadConstraints は Pod の更新で変えられるフィールドの一覧に無い。Deployment ではひな形を変えると Pod が作り直される(第12回) |
| 同じグループの Pod の制約はそろえる | 同じグループのすべての Pod に、同じ Topology Spread の制約を書くべき(should)とされる。ばらばらだと動きがわかりにくくなり、原因を調べにくくなる |
参考:Assigning Pods to Nodes | Kubernetes、Pod Topology Spread Constraints | Kubernetes、Pod Topology Spread Constraints | Kubernetes、Pod Topology Spread Constraints | Kubernetes、Pods | Kubernetes
関連記事
この回で nodeTaintsPolicy とコントロールプレーンのノードの扱いにかかわった、ノードの側から Pod を遠ざける Taint と Toleration については、以下の記事で解説しています。
注意点の表で触れた「数を減らすと偏ることがある」にかかわる、負荷に応じて Pod の数を増やしたり減らしたりする Horizontal Pod Autoscaler については、以下の記事で解説しています。
Horizontal Pod Autoscaler を動かして理解する負荷に合わせて Pod の数を自動で増減させる Horizontal Pod Autoscaler(HPA)の仕組みを図で説明します。metrics-server との関係、requests に対する CPU 使用率から目標の数を求める式、増やすときと減らすときの決まりを押さえ、kind のクラスタで負荷をかけて Pod が増える様子を観察する手順を紹介します。