Carpe Diem

備忘録

KubernetesでPodを複数のZoneに分散させる

概要

Podの冗長化をする上でマルチゾーン構成にしたい場合

Pod Topology Spread Constraints | Kubernetes

上記のPod Topology Spread Constraintsを使うと実現できます。

環境

Pod Topology Spread Constraints

Pod Topology Spread ConstraintsはPodをスケジュール(配置)する際に、zoneやhost名毎に均一に分散できるようにする制約です。

ちなみにkubernetesのスケジューラーの詳細はこちらの記事が非常に分かりやすいです。

engineering.mercari.com

パラメータ

主に使うパラメータは以下です。

パラメータ 説明 デフォルト値
topologyKey スケジュールの制約条件に使う Node Label -
maxSkew zone間のPod数の差の上限
0より大きい数値でないといけない
-
whenUnsatisfiable DoNotSchedule: maxSkewを満たさない場合、そのトポロジーにスケジュールしない
ScheduleAnyway: maxSkewを満たさない場合、skewを減らすようにスケジュールする
DoNotSchedule
labelSelector スケジュール対象の Pod Label -

これら以外のパラメータはドキュメントを参照して下さい。

ref: Pod Topology Spread Constraints | Kubernetes

skewとは

skew(歪度)の計算式は以下の通りです。

skew = 現トポロジーにマッチするPod数 - 全トポロジーでの最小Pod数

以下の図を元に考えると、

ref: https://kubernetes.io/blog/2020/05/introducing-podtopologyspread/

maxSkew=1として

zone1に配置しようとした時

  1. 現状zone1にPodが2つある。
  2. 1つスケジュールすると3つになる。
  3. 各zoneで最小のPodはzone2の0。
  4. 計算式3−0=3でmaxSkewに違反(3>1)する

zone2に配置しようとした時

  1. 現状zone2にPodはない
  2. 1つスケジュールすると1つになる。
  3. 各zoneで最小のPodは同じくzone2の1。
  4. 計算式1−1=0でmaxSkewを満たす(0≦1)

となります。

既に均一だったら?

既にzone1とzone2が均一(例えば2 Podずつ)の場合は、

  1. zone1にスケジュールすると3
  2. 各zoneで最小のPodは同じくzone2の2。
  3. 計算式3−2=1でmaxSkewを満たす(1≦1)

となります。
なのでmaxSkewは0より大きな数値でないといけません。

デフォルト設定

Kubernetes v1.24からはクラスタレベルで次の制約がデフォルト設定されています。

defaultConstraints:
  - maxSkew: 3
    topologyKey: "kubernetes.io/hostname"
    whenUnsatisfiable: ScheduleAnyway
  - maxSkew: 5
    topologyKey: "topology.kubernetes.io/zone"
    whenUnsatisfiable: ScheduleAnyway

ref: https://kubernetes.io/docs/concepts/scheduling-eviction/topology-spread-constraints/#internal-default-constraints

設定方法

具体的な設定方法です。

Deploymentに次のようにtopologySpreadConstraintsを設定します。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
spec:
  replicas: 6
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
      - name: nginx
        image: nginx:1.25
        ports:
          - containerPort: 80
      topologySpreadConstraints:
        - maxSkew: 1
          topologyKey: topology.kubernetes.io/zone
          whenUnsatisfiable: DoNotSchedule
          labelSelector:
            matchLabels:
              app: my-app

またラベルが増えるのでこんがらがる人は以下の記事を参考にして下さい。

christina04.hatenablog.com

注意点

調査時や導入して気になった点を挙げます。

デプロイ時の旧Podも計算対象に入る

DeploymentのstrategyによってはAZ分散がうまく行かないパターンがあります。

例えば

  • replica: 3 (zone-a: 1, zone-b: 1, zone-c: 1)
  • Deploymentのstrategy.rollingUpdate.maxSurge: 50%

のような時に、

  1. zone-a, zone-bに1Podずつ、計2Pod追加される
    • zone-a: 2, zone-b: 2, zone-c: 1
  2. zone-bの旧PodがTerminate
    • zone-a: 2, zone-b: 1, zone-c: 1
  3. 新しいPodがzone-bに追加される(maxSkewを違反しないため)
    • zone-a: 2, zone-b: 2, zone-c: 1
  4. zone-aの旧PodがTerminate
    • zone-a: 1, zone-b: 2, zone-c: 1
  5. zone-cの旧PodがTerminate
    • zone-a: 1, zone-b: 2, zone-c: 0

といったことが起きることがあります。

ゾーン障害から復旧してもリバランスはされない

Pod Topology Spread ConstraintsはあくまでPodスケジュール時の制約であるため、

  • replica: 6 (zone-a: 2, zone-b: 2, zone-c: 2)

でバランス良く分散されていた状態で、zone-cにゾーン障害が発生した場合

  • replica: 6 (zone-a: 3, zone-b: 3, zone-c: 0)

となります。

その後zone-cが復旧したとしても、現状のPodをEvictしてzone-cにリバランスしてはくれません。
(もちろんPodの削除やDeploymentが発生すればzone-cにスケジュールされます)

DoNotScheduleだとストックアウト時に配置できなくなる

github.com

にあるように、ストックアウトによって一部のゾーンのノードが増やせない状態に陥った場合に、maxSkew=1だとPodが配置できなくなる状況に陥ります。

  • zone-cでノードのストックアウトが起きる
  • zone-cのノードのリソースがなくなりPodがこれ以上置けない
  • topologySpreadConstraints
    • maxSkew: 1
    • whenUnsatisfiable : DoNotSchedule

という状況の場合、replica: 6だとすると

  1. zone-aにPodをスケジュール→Running
  2. zone-bにPodをスケジュール→Running
  3. zone-cにPodをスケジュールするが配置できるノードが無い→Pending
  4. zone-aにPodをスケジュールしようとするがzone-cとのPod差が1ある状態でDoNotSchedule制約がある→Pending
  5. zone-bにPodをスケジュールしようとするがzone-cとのPod差が1ある状態でDoNotSchedule制約がある→Pending

ということになります。

なので次に紹介するDeschedulerと、whenUnsatisfiable: ScheduleAnywayの組み合わせがベターと言えそうです。

Deschedulerとの組み合わせ

前述の

  • デプロイ時に偏る可能性
  • ゾーン障害復旧後の偏りが直らない

といった問題を解決する方法としてDeschedulerがあります。

github.com

これは条件に違反したPodをEvictさせる仕組みです。
こちらは先程と逆で、PodのEvictのみに責務を持っています。

  • Deployment
  • CronJob
  • Job

の3通りの使い方があり、以下でサンプルコードが提供されています。

https://github.com/kubernetes-sigs/descheduler/tree/master/kubernetes

--policy-config-fileには次のようなポリシーを用意します。ConfigMapなどで用意するのが良いでしょう。

apiVersion: "descheduler/v1alpha2"
kind: "DeschedulerPolicy"
profiles:
  - name: ProfileName
    pluginConfig:
    - name: "RemovePodsViolatingTopologySpreadConstraint"
      args:
        constraints:
          - DoNotSchedule
          - ScheduleAnyway
    plugins:
      balance:
        enabled:
          - "RemovePodsViolatingTopologySpreadConstraint"

ref: https://github.com/kubernetes-sigs/descheduler/blob/master/examples/topology-spread-constraint.yaml

注意点

DeschedulerによってEvict対象のPodが多いことでサービスのキャパシティが足りなくなってしまったり、ダウンタイムが発生する可能性があります。

そうならないためにもPodDisruptionBudgetを設定することで最低限起動するPod数を保証できます。

kubernetes.io

PodDisruptionBudgetはDeschedulerやDeployment strategyよりも優先的に扱われます。

設定例

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: my-app
spec:
  minAvailable: 2
  selector:
    matchLabels:
      app: my-app

まとめ

KubernetesでPodをMulti AZにスケジュールする方法、設定する上での注意点を紹介しました。

参考