k8s Deschedulerを使いこなしてスムーズなオートスケールを実現する

この記事は基本的にGKEでの利用を想定して解説していますが、AWS等他のプロバイダでも大きくは変わりません。適時読み替えてもらえると。

## クラスタオートスケーラーの課題点

kubernetesでNodeのオートスケールは、ざっくり説明すると以下のような流れになる訳ですが・・・

1. Podの負荷が上がり、HPAによって新しいPodがスケジューリングされる
2. PodをスケジューリングさせるためのNodeが不足して、新しいNodeが立ち上がる
3. 新しいNodeにPodが立ち上がる

このスケーリングには少々問題がありまして、新しいNodeがすぐに有効活用されるかと言えば、そうでも無いんですよね・・・

時間が経つに連れて新しいNodeにもPodが増えますし、負荷も分散されて行くでしょう。しかし、すでに負荷が上がりきっている既存Nodeに配置されているPodが再スケジューリングされる訳では無いので、急なスパイク等には対応できません。GKEだとNodeの立ち上がり自体は早いんですけどね・・・

## deschedulerで負荷が高いNodeのPodを再スケジューリングする

そこでdeschedulerの出番です。deschedulerは条件に応じてPodを削除するためのリソースですね。

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

上手く利用することで、負荷の高いNodeから負荷の低いNodeにPodを移動させることが可能になります。

## 導入

基本的にはGithubのREADMEに書いてある通りに進めればOKです

### Docker Imageの作成

以下にImageの作成方法が説明してあります。

https://github.com/kubernetes-sigs/descheduler#running-descheduler-as-a-job-inside-of-a-pod

Imageが完成したら、GCPならGCR、AWSだったらECR等に適当にアップロードします。

### 権限周りの設定

各クラスタに応じて、権限周りの設定を行います。適宜必要な設定を済ませておいて下さい。

https://github.com/kubernetes-sigs/descheduler#create-a-cluster-role

## ConfigMapによるDeschedulerPolicyの設定

deschedulerにはいくつかの機能が搭載されていますが、今回利用するのは**LowNodeUtilization**という機能です。Nodeの消費済みリソースを閾値としてPodを再スケジューリングさせるものですね。

https://github.com/kubernetes-sigs/descheduler#lownodeutilization

**LowNodeUtilization**には、二つの設定項目があります。一つ目は`thresholds`で、再スケジューリング先Nodeの条件を指定するものです。もう一つは`targetThresholds`で、これはPodを削除するNodeの条件を指定します。以下は設定の一例です。

```yaml
apiVersion: "descheduler/v1alpha1"
kind: "DeschedulerPolicy"
strategies:
"LowNodeUtilization":
   enabled: true
   params:
     nodeResourceUtilizationThresholds:
       thresholds:
         "cpu" : 20
         "memory": 30
         "pods": 10
       targetThresholds:
         "cpu" : 60
         "memory": 80
         "pods": 15
```

ちょっと分かりにくいので細分化して説明します。

### thresholdsの詳しい説明

まず、上の例で`thresholds`は`"cpu": 20`、`"memory": 30`、`"pods": 10`となっていますね。
これは日本語で説明すると、「**リクエスト済みのリソースがNodeの総リソースに対してCPUが20%以内、メモリが30%以内、尚且つ配置済みのPodが10台以内のNodeを再スケジューリング対象とする**」と、なります。ここで注意して欲しいのは、**cpuとmemoryは割合**であること、**podsは台数**であること、そして、**全ての条件を満たすNodeだけが対象**になるということです。

### targetThresholdsの詳しい説明

次に、`targetThresholds`は`"cpu": 60`、`"memory": 80`、`"pods": 15`となっていますね。これも日本語で説明すると、「**リクエスト済みのリソースがNodeの総リソースに対してCPUが60%以上、メモリが80%以上、あるいは配置済みのPodが15台以上のNodeをPod削除対象とする**」と、なります。注意点としては、cpu、memory、pods、3つの項目の内、**いずれか一つの項目を満たせば対象になる**という点です。例えばリクエスト済みのcpu率が60%を超えていれば、memoryが20％とかでも対象になります。

### ConfigMapとして登録する

DeschedulerPolicyをConfigMapとしてクラスタに登録します。

```yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: descheduler-policy-configmap
  namespace: kube-system
data:
  policy.yaml: |
    apiVersion: "descheduler/v1alpha1"
    kind: "DeschedulerPolicy"
    strategies:
      "LowNodeUtilization":
         enabled: true
         params:
           nodeResourceUtilizationThresholds:
             thresholds:
               "cpu" : 20
               "memory": 30
               "pods": 10
             targetThresholds:
               "cpu" : 60
               "memory": 80
               "pods": 15
```

## 再スケジューリング処理を実行するためのCronJobを定義

公式のGithubにはJobを使った方法が紹介されていますが、実際に運用する場合はCronJobで実行させることになるでしょう。以下CronJob定義の一例です。

```yaml
apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: descheduler-job
  namespace: kube-system
spec:
  schedule: "*/30 * * * *"
  jobTemplate:
    spec:
      template:
        metadata:
          name: descheduler-pod
          annotations:
            scheduler.alpha.kubernetes.io/critical-pod: ""
        spec:
          containers:
            - name: descheduler
              image: gcr.io/project/descheduler:v1 # アップロードしたimageを指定
              volumeMounts:
              - mountPath: /policy-dir
                name: policy-volume
              command:
                - "/bin/descheduler"
              args:
                - "--policy-config-file"
                - "/policy-dir/policy.yaml"
                - "--v"
                - "3"
          restartPolicy: "Never"
          serviceAccountName: descheduler-sa
          volumes:
          - name: policy-volume
            configMap:
              name: descheduler-policy-configmap
```

## たくさんのPodが同時に再スケジューリングされるのを防止する

deschedulerのJobを実行すると、対象となるPodが複数ある場合に多くのPodが同時に再スケジューリングされてしまう可能性があります。そこで、`PodDisruptionBudget`を定義して同時に再スケジューリングされるPodを制限します。以下の例だと、対象のdeploymentに属するPodは同時に2台までしか再スケジューリングされなくなります。

```yaml
apiVersion: policy/v1beta1
kind: PodDisruptionBudget
metadata:
  name: sample-pdb
spec:
  maxUnavailable: 2
  selector:
    matchLabels:
      app: sample # deployment等のlabelを指定
```

## まとめ

クラスタオートスケーラーでの利用を想定したdeschedulerの運用方法や、設定項目の詳細に関して紹介させてもらいました。
設定項目に関しては、ちょっと分かりづらい上に、公式のドキュメントにも詳細な挙動が説明されていないので自前で色々検証しました。

個人的な不満点としては、deschedulerの対象とするリソースを選択できない点でしょうか。例えば、特定のdeploymentだけdeschedulerによる再スケジューリングの対象にしたい、kube-systemに属するPodは対象にしたくないといった用途に対応できないですね。一応、`Critical pods`やDeamonSetは対象外になるなど、ある程度制限はされているみたいですが、こちら側で柔軟な選別は現状出来なさそうです。

https://github.com/kubernetes-sigs/descheduler#pod-evictions

絶賛開発中みたいなので、今後にも期待したいです。

https://github.com/kubernetes-sigs/descheduler#roadmap