kakakakakku blog

Weekly Tech Blog : Keep on Learning 👍

Descheduler for Kubernetes : 戦略に違反する Pod を他のノードに移動する

Kubernetes を使っていると,運用面で起動中の Pod を他のノードに移動(再スケジューリング)したくなる場面がある.以下に具体的な例を挙げる.理由としては,Kubernetes では kube-scheduler によって Pod を起動する前にノードが決まる仕組み(スケジューリング)になっている.よって,スケジューリング後の Pod は再スケジューリングされず,起動され続けることになる.

  • ノードを追加した後に起動中の Pod を移動したい
  • 一部ノードの使用率が高いので起動中の Pod を移動したい
  • 後からノードに taint を追加したから条件に合わない Pod を移動したい

Descheduler for Kubernetes 🧩

今回紹介する「Descheduler for Kubernetes」を使うと,設定した「戦略」によって起動中の Pod を再スケジューリングする仕組みを Kubernetes クラスタに導入できる.正確に言うと「Descheduler」によって Pod が「再スケジューリング」されるのではなく,「Descheduler」によって「Eviction(排出)」された Pod が kube-scheduler によって「再スケジューリング」される.

github.com

セットアップ 🧩

Quick Start を読むと複数のセットアップ方法が紹介されている.今回は Helm を使う.Helm を使うとデフォルトでは CronJobDescheduler が起動されるけど,設定によって Deployment も選べるようになっている.

  • Run As A Job
  • Run As A CronJob
  • Run As A Deployment
  • Install Using Helm
  • Install Using Kustomize

手順は Artifact Hub に載っている.helm install コマンドを実行すれば簡単にセットアップできる.ConfigMap を確認すると,デフォルトでは以下の5個の「戦略」が有効化されていた.戦略に関しては後述する!

  • LowNodeUtilization
  • RemoveDuplicates
  • RemovePodsViolatingInterPodAntiAffinity
  • RemovePodsViolatingNodeAffinity
  • RemovePodsViolatingNodeTaints

artifacthub.io

$ helm repo add descheduler https://kubernetes-sigs.github.io/descheduler/

$ helm install descheduler --namespace kube-system descheduler/descheduler

$ kubectl get cronjobs -n kube-system
NAME          SCHEDULE      SUSPEND   ACTIVE   LAST SCHEDULE   AGE
descheduler   */2 * * * *   False     1        40s             45s

$ kubectl get configmaps -n kube-system descheduler -o yaml
apiVersion: v1
data:
  policy.yaml: |
    apiVersion: "descheduler/v1alpha1"
    kind: "DeschedulerPolicy"
    strategies:
      LowNodeUtilization:
        enabled: true
        params:
          nodeResourceUtilizationThresholds:
            targetThresholds:
              cpu: 50
              memory: 50
              pods: 50
            thresholds:
              cpu: 20
              memory: 20
              pods: 20
      RemoveDuplicates:
        enabled: true
      RemovePodsViolatingInterPodAntiAffinity:
        enabled: true
      RemovePodsViolatingNodeAffinity:
        enabled: true
        params:
          nodeAffinityType:
          - requiredDuringSchedulingIgnoredDuringExecution
      RemovePodsViolatingNodeTaints:
        enabled: true
kind: ConfigMap
(中略)

Descheduler 戦略 🧩

GitHub に載っているドキュメントを読むと非常に多機能で「戦略」だけでも「計10種類」ある.今回は RemovePodsViolatingTopologySpreadConstraintPodLifeTime にフォーカスして紹介する.

  • RemoveDuplicates
  • LowNodeUtilization
  • HighNodeUtilization
  • RemovePodsViolatingInterPodAntiAffinity
  • RemovePodsViolatingNodeAffinity
  • RemovePodsViolatingNodeTaints
  • RemovePodsViolatingTopologySpreadConstraint ✅ 今回試す
  • RemovePodsHavingTooManyRestarts
  • PodLifeTime ✅ 今回試す
  • RemoveFailedPods

検証環境 🧩

今回は検証環境として eksctl を使って構築した Amazon EKS クラスタ (Kubernetes 1.21) を使う.以下のように 3 Availability Zone に 6 ノードを構築してある.IP アドレスは説明のためにわかりやすく書き換えてある(第3オクテットを .10 / .50 / .90 に合わせた).

$ kubectl get nodes \
>   -l 'eks.amazonaws.com/nodegroup=nodegroup' \
>   -o=custom-columns=NAME:.metadata.name,ZONE:metadata.labels."topology\.kubernetes\.io/zone",STATUS:status.conditions[-1].type \
>   --sort-by metadata.labels."topology\.kubernetes\.io/zone"
NAME                                                ZONE              STATUS
ip-192-168-10-100.ap-northeast-1.compute.internal   ap-northeast-1a   Ready
ip-192-168-10-101.ap-northeast-1.compute.internal   ap-northeast-1a   Ready
ip-192-168-50-100.ap-northeast-1.compute.internal   ap-northeast-1c   Ready
ip-192-168-50-101.ap-northeast-1.compute.internal   ap-northeast-1c   Ready
ip-192-168-90-100.ap-northeast-1.compute.internal   ap-northeast-1d   Ready
ip-192-168-90-101.ap-northeast-1.compute.internal   ap-northeast-1d   Ready

1. RemovePodsViolatingTopologySpreadConstraint

さっそく RemovePodsViolatingTopologySpreadConstraint を試す.これは「Pod Topology Spread Constraints」に違反している Pod を探して Eviction(排出)してくれる.「Pod Topology Spread Constraints」に関しては以下の記事にまとめてある.

kakakakakku.hatenablog.com

まず spec.topologySpreadConstraints を設定した Deployment で Pod を「6個」起動する.

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

期待通りに各 Availability Zone に 2 Pod 起動している.

$ kubectl get pods \
>   -o=custom-columns=NAME:.metadata.name,NODE:.spec.nodeName \
>   -l app=topology-spread-constraints \
>   --sort-by .spec.nodeName
NAME                                           NODE
topology-spread-constraints-7bcb664dc6-b6wt4   ip-192-168-10-100.ap-northeast-1.compute.internal
topology-spread-constraints-7bcb664dc6-jlwrx   ip-192-168-10-101.ap-northeast-1.compute.internal
topology-spread-constraints-7bcb664dc6-774lt   ip-192-168-50-100.ap-northeast-1.compute.internal
topology-spread-constraints-7bcb664dc6-tk7rd   ip-192-168-50-100.ap-northeast-1.compute.internal
topology-spread-constraints-7bcb664dc6-92fx8   ip-192-168-90-101.ap-northeast-1.compute.internal
topology-spread-constraints-7bcb664dc6-rt46n   ip-192-168-90-101.ap-northeast-1.compute.internal

次に kubectl drain コマンドを使って Availability Zone 1d のノードを意図的にメンテナンスにする.kubectl drain コマンドに関しては以下の記事にまとめてある.

kakakakakku.hatenablog.com

$ kubectl drain ip-192-168-90-100.ap-northeast-1.compute.internal --ignore-daemonsets
$ kubectl drain ip-192-168-90-101.ap-northeast-1.compute.internal --ignore-daemonsets

結果的に 6 Pod は Availability Zone 1a と Availability Zone 1c に偏った.

$ kubectl get pods \
>   -o=custom-columns=NAME:.metadata.name,NODE:.spec.nodeName \
>   -l app=topology-spread-constraints \
>   --sort-by .spec.nodeName
NAME                                           NODE
topology-spread-constraints-7bcb664dc6-b6wt4   ip-192-168-10-100.ap-northeast-1.compute.internal
topology-spread-constraints-7bcb664dc6-jlwrx   ip-192-168-10-101.ap-northeast-1.compute.internal
topology-spread-constraints-7bcb664dc6-l77cf   ip-192-168-10-101.ap-northeast-1.compute.internal
topology-spread-constraints-7bcb664dc6-nmwpd   ip-192-168-50-100.ap-northeast-1.compute.internal
topology-spread-constraints-7bcb664dc6-774lt   ip-192-168-50-100.ap-northeast-1.compute.internal
topology-spread-constraints-7bcb664dc6-tk7rd   ip-192-168-50-101.ap-northeast-1.compute.internal

次にメンテナンスを完了したことにして Availability Zone 1d のノードを kubectl uncordon コマンドを使って戻す.しかし,起動中の Pod は移動されずそのままになるため,Availability Zone 1d のノードにスケジューリングされずに「偏ったまま」となる.

$ kubectl uncordon ip-192-168-87-26.ap-northeast-1.compute.internal
$ kubectl uncordon ip-192-168-70-254.ap-northeast-1.compute.internal

$ kubectl get pods \
>   -o=custom-columns=NAME:.metadata.name,NODE:.spec.nodeName \
>   -l app=topology-spread-constraints \
>   --sort-by .spec.nodeName
NAME                                           NODE
topology-spread-constraints-7bcb664dc6-b6wt4   ip-192-168-10-100.ap-northeast-1.compute.internal
topology-spread-constraints-7bcb664dc6-jlwrx   ip-192-168-10-101.ap-northeast-1.compute.internal
topology-spread-constraints-7bcb664dc6-l77cf   ip-192-168-10-101.ap-northeast-1.compute.internal
topology-spread-constraints-7bcb664dc6-nmwpd   ip-192-168-50-100.ap-northeast-1.compute.internal
topology-spread-constraints-7bcb664dc6-774lt   ip-192-168-50-100.ap-northeast-1.compute.internal
topology-spread-constraints-7bcb664dc6-tk7rd   ip-192-168-50-101.ap-northeast-1.compute.internal

前置きが長くなった!やっとここで「Descheduler」の本領発揮となる.まずは RemovePodsViolatingTopologySpreadConstraint を有効化するために ConfigMap を以下のように更新する.

apiVersion: v1
data:
  policy.yaml: |
    apiVersion: "descheduler/v1alpha1"
    kind: "DeschedulerPolicy"
    strategies:
      RemovePodsViolatingTopologySpreadConstraint:
        enabled: true
        params:
          includeSoftConstraints: true
kind: ConfigMap
(中略)

少し待つと(Descheduler のデフォルト設定では 2 min 間隔)Pod が再スケジューリングされた.おおー👏

$ kubectl get pods \
>   -o=custom-columns=NAME:.metadata.name,NODE:.spec.nodeName \
>   -l app=topology-spread-constraints \
>   --sort-by .spec.nodeName
NAME                                           NODE
topology-spread-constraints-7bcb664dc6-b6wt4   ip-192-168-10-100.ap-northeast-1.compute.internal
topology-spread-constraints-7bcb664dc6-jlwrx   ip-192-168-10-101.ap-northeast-1.compute.internal
topology-spread-constraints-7bcb664dc6-nmwpd   ip-192-168-50-100.ap-northeast-1.compute.internal
topology-spread-constraints-7bcb664dc6-774lt   ip-192-168-50-100.ap-northeast-1.compute.internal
topology-spread-constraints-7bcb664dc6-l74k2   ip-192-168-90-100.ap-northeast-1.compute.internal
topology-spread-constraints-7bcb664dc6-d7tfb   ip-192-168-90-101.ap-northeast-1.compute.internal

2. PodLifeTime

今度は PodLifeTime を試す.これは設定した「Pod の起動時間上限」に違反している Pod を探して Eviction(排出)してくれる.継続的に Pod を作り直してリフレッシュしたいときに使える.PodLifeTime を有効化するために ConfigMap を以下のように更新する.

  • podLifeTime.maxPodLifeTimeSeconds
    • 「300秒(5分)」まで
  • matchLabels
    • 対象をラベルで指定する
$ kubectl get configmaps -n kube-system descheduler -o yaml
apiVersion: v1
data:
  policy.yaml: |
    apiVersion: "descheduler/v1alpha1"
    kind: "DeschedulerPolicy"
    strategies:
      PodLifeTime:
        enabled: true
        params:
          podLifeTime:
            maxPodLifeTimeSeconds: 300
          matchLabels:
            app: pod-life-time
kind: ConfigMap
(中略)

さっそく Deployment で Pod を「3個」起動する.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: pod-life-time
spec:
  replicas: 3
  selector:
    matchLabels:
      app: pod-life-time
  template:
    metadata:
      labels:
        app: pod-life-time
    spec:
      containers:
      - name: nginx
        image: nginx:1.21
        ports:
          - containerPort: 80

少し待つと Pod が作り直された.おおー👏

$ kubectl get pods -l app=pod-life-time
pod-life-time-5bf5bc8dcf-bqbwc                1/1     Running   0          24s
pod-life-time-5bf5bc8dcf-jc45x                1/1     Running   0          25s
pod-life-time-5bf5bc8dcf-n7xb5                1/1     Running   0          24s

$ kubectl get pods -l app=pod-life-time
pod-life-time-5bf5bc8dcf-bqbwc                1/1     Running   0          5m35s
pod-life-time-5bf5bc8dcf-jc45x                1/1     Running   0          5m36s
pod-life-time-5bf5bc8dcf-n7xb5                1/1     Running   0          5m35s

$ kubectl get pods -l app=pod-life-time
pod-life-time-5bf5bc8dcf-8pbbc                1/1     Running   0          18s
pod-life-time-5bf5bc8dcf-bbpnv                1/1     Running   0          17s
pod-life-time-5bf5bc8dcf-jzrlk                1/1     Running   0          18s

アンインストール 🧩

検証が終わったら Helm で削除しておく.

$ helm uninstall descheduler --namespace kube-system
release "descheduler" uninstalled

まとめ 🧩

今回は「Descheduler for Kubernetes」を試して,起動中の Pod を他のノードに移動(再スケジューリング)できることを確認した.記事では RemovePodsViolatingTopologySpreadConstraintPodLifeTime にフォーカスしたけど,他にも LowNodeUtilization(使用率が低いノードを有効活用する)と RemovePodsViolatingNodeTaints(taint に違反する Pod を Eviction する)も試して,理解を深められた.Kubernetes クラスタの耐障害性などを考慮すると「Descheduler for Kubernetes」を導入すると良さそう!

github.com