Kuberentes クラスタのオートスケール

この記事では Kubernetes クラスタのオートスケールについてまとめていきます。

# Cluster Autoscaler とは

[Cluster Autoscaler](https://github.com/kubernetes/autoscaler/tree/master/cluster-autoscaler) は Kubernetes クラスタを自動的に最適なサイズに調整するためのツールです。
このオートスケーラは Kubernetes のリソース(Podに指定される `pod.spec.containers.resource.requests`)ベースで行うようになっていて、Kuberentes クラスタがリソース不足で Pod をスケジュールできないときに Node を増やし、Node のリソースが長時間利用されていないときに Node を減らすといった挙動をします。

# Cluster Autoscaler の利用方法

Cluster Autoscaler は Kubernetes クラスタを動作させている IaaS に依存するため各クラウドプロバイダごとに利用方法が異なります。[こちら](https://github.com/kubernetes/autoscaler/tree/master/cluster-autoscaler#deployment) のリンクを参照すると各プロバイダの利用方法のドキュメントへのリンクがありますので、そちらを参照して環境を構築し、セットアップできます。

# Cluster Autoscaler を利用する上での注意事項

## Pod の Requests を設定する必要がある

リソースベースのオートスケーラなため Pod の `spec.containers.resource.requests` を設定しなければ機能しません。Requests は HorizontalPodAutoscaler[^hpa] にも必須ですし Pod が正常に動作するためのリソースを保証するには設定しなければならない設定なのでプロダクションで動作させる場合は設定したほうがよいでしょう。

[^hpa]: Pod の水平スケールを行うオートスケール機能です。詳細は https://qiita.com/shmurata/items/8ebaf74028365fc950c9 を参照ください。

## Node の削除に備える必要がある

Node の削除が行われる際、その Node で動作している Pod があればその Pod は別の Node へ移動します。この移動は `kubectl drain node`[^drain] コマンドと同じ挙動で指定した Node を Pod のスケジュール対象にならないようにして、その Node で動作している Pod を退去(evict)することで行われます。そのため、 すべての Pod は evict されても問題ないように 必要があれば PodDisruptionBudget を設定し、グレースフルシャットダウンされるようにすべきでしょう。
なお、Pod の退去が許容できない場合などは削除を行わないようにすること(`--scale-down-enabled`)や動作している Pod があれば削除しない(`--scale-down-non-empty-candidates-count`)ことなどの細かい設定も可能です。どうしても許容できない場合は ClusterAutoScaler の設定を調整すると良いでしょう。

[^drain]: Node のメンテナンスなどのために用意されているコマンドです。指定した Node を Pod のスケジュール対象にならないようにして、その Node で動作している Pod を退去(evict)します。 https://kubernetes.io/docs/tasks/administer-cluster/cluster-management/#maintenance-on-a-node

## 次の Pod がある場合は Node の削除が行われない

オートスケーラが Node の削除を行う際に以下の Pod が存在しないことを確認し、それらがなかった場合に Node を削除します。これらの Pod は別の Node に移動できない Pod と定義されているためこのような対応になっています。

- PodDisruptionBudget(PDB) により制限されている Pod
- kube-system の Pod
    - PDB が設定されていない Pod
- コントローラ(e.g., ReplicaSet, Job, StatefullSet, ...)によって管理されていない Pod
- LocalStorage を持っている Pod
- 様々な制約(e.g., NodeAfinity, NodeSelector, ...)によって移動できる Node がない Pod
- `"cluster-autoscaler.kubernetes.io/safe-to-evict": "false"` annotation が設定されている Pod

# Cluster Autoscaler の開発状況

現在(2018/12/04) v1.13.0 がリリースされていて、v1.0.0 から GA Product と[記載](https://github.com/kubernetes/autoscaler/blob/master/cluster-autoscaler/FAQ.md#is-cluster-autoscaler-an-alpha-beta-or-ga-product)されています。v1.1 では[優先度(priority)によるスケジューリング](https://github.com/kubernetes/autoscaler/blob/master/cluster-autoscaler/FAQ.md#how-does-cluster-autoscaler-work-with-pod-priority-and-preemption)なども追加され必要な機能が揃ってきた印象を受けます。
Cluster Autoscaler のバージョンは Kubernetes のバージョンと対応していて、以下の対応表に従い利用すると良いようです。
https://github.com/kubernetes/autoscaler/tree/master/cluster-autoscaler#releases

# Cluster Autoscaler の仕組み

Cluster Autoscaler（CA） は一つの [Deployment](https://github.com/kubernetes/autoscaler/tree/dacce05da2a304ae4adb69740a59e4b70026949f/cluster-autoscaler#deployment) で動作します。CA は Master Node で動作するようにデザインされていますが、Worker Node に配置することも可能です。
この Pod から Kubernetes API Server を通してクラスタのリソース使用状況を監視し、Node の upscale/downscaleを行います。Node の追加、削除処理自体は[各クラウドプロバイダ](https://github.com/kubernetes/autoscaler/tree/master/cluster-autoscaler/cloudprovider)ごとに処理が異なるためクラウドプロバイダごとに実装されています。そのため、残念ながら現在実装されているプロバイダ以外では利用することができません。 現在は GKE/GCP/AWS/Azure/AliCloud で利用できるようです。

大まかな仕組みとしては以上ですが、これだけだと物足りない人もいると思うので少し実装を見てみましょう。CA の v1.13.0 のソースを見ていきます。

CA はこの [runOnce](https://github.com/kubernetes/autoscaler/blob/cluster-autoscaler-1.13.0/cluster-autoscaler/core/static_autoscaler.go#L119
) の関数を定期的に実行することでオートスケールしています。

Node の追加・削除の判定がどのように行われているか見てみましょう。[このあたり](https://github.com/kubernetes/autoscaler/blob/cluster-autoscaler-1.13.0/cluster-autoscaler/core/static_autoscaler.go#L217)から Node の追加を行うかどうかの判定が始まっています。この部分から判定に係る部分だけを抜粋すると以下のようになります。

```golang
// スケジュールされていない Pod の一覧を取得
allUnschedulablePods, err := unschedulablePodLister.List()

// TPU (Googleの機械学習向け自社開発プロセッサ) のリクエストによってスケジュールされていない Pod を除く
unschedulablePodsWithoutTPUs := tpu.ClearTPURequests(allUnschedulablePods)

// ここでは次の2つの処理をしています
// 低 priority（--expendable-pods-priority-cutoff(default: -10) フラグに指定されたPriority以下）の Podを除く
// Nodeの空き状況を見て、Podがスケジュール可能かどうか予測し、スケジュール可能なPodを除く
filterOutSchedulableStart := time.Now()
unschedulablePodsToHelp := FilterOutSchedulable(unschedulablePods, readyNodes, allScheduled, unschedulableWaitingForLowerPriorityPreemption, a.PredicateChecker, a.ExpendablePodsPriorityCutoff)

// --new-pod-scale-up-delay(default: 0)フラグに指定された期間を経過していない Pod を除く
// finally, filter out pods that are too "young" to safely be considered for a scale-up (delay is configurable)
unschedulablePodsToHelp = a.filterOutYoungPods(unschedulablePodsToHelp, currentTime)

// Node の数が Max 以上になっている場合は Node を追加しない
if a.MaxNodesTotal > 0 && len(readyNodes) >= a.MaxNodesTotal {
  return
}

// 最も古い Pod がこのループの開始より2秒前だったら次のループまで待つ
if allPodsAreNew(unschedulablePodsToHelp, currentTime) {
  return
}
```

これらの判定を通って unschedulablePodsToHelp が 0 以上だった場合は Node の追加を行います。

次は Node の削除を見ていきましょう。Node の削除の処理は[こちら](https://github.com/kubernetes/autoscaler/blob/cluster-autoscaler-1.13.0/cluster-autoscaler/core/static_autoscaler.go#L323)から始まります。スケールダウンの処理は複雑でまだ読み切れていませんが、次の 2 つのフェーズで削除候補の Node を算出しています。

- DaemonSet や MirrorPod(Node上に静的に配置されたマニフェストから起動するPod) を除く Pod が配置されていないの Node を収集するフェーズ
- 移動させることができる Pod がある Node から Pod の状況を収集して削除候補をシミュレータを使って算出するフェーズ

これらのフェーズで算出された削除候補 Node の削除を試みるという流れになっています。詳細は別の機会に追いたいと思います。

# CloudProvider の追加の方法

提供されている CloudProvier 以外で ClusterAutoScaler を利用したい場合は CloudProvier を独自で実装する必要があります。この実装方法の触りだけ見てみましょう。

CloudProvider を追加するには[こちら](https://github.com/kubernetes/autoscaler/blob/cluster-autoscaler-1.13.0/cluster-autoscaler/cloudprovider/cloud_provider.go#L34)のインターフェイスを満たす実装を追加する必要があります。これを追加したあと[builder](https://github.com/kubernetes/autoscaler/blob/cluster-autoscaler-1.13.0/cluster-autoscaler/cloudprovider/builder/builder_all.go)で追加したCloudProviderをビルド対象に追加することで追加できます。インターフェイスで定義されている関数のうち必須のものは半分程度なので、必須の部分だけを追加するのであればそこまで手間がかからないかもしれません。

# まとめ

この記事では Kubernetes クラスタのオートスケーラについて紹介しました。ClusterAutosclaer はすでに GA フェーズということもあり、ドキュメントなども豊富で対応されている CloudProvider 上で Kubernetes クラスタを構築している場合はすぐにでも導入できるように思います。
弊社ではオンプレがメインなこともありまだ導入できていませんが、独自 CloudProvider を実装して導入したいと考えています。Node の削除周り実装が弊社の Kubernetes as a Service と競合している部分があることなどいくつか問題がありそうで、そのまま導入することは難しいかもしれませんが来年に挑戦してみたいと思います。

このエントリは、弊社 Z Lab のメンバーによる Z Lab Advent Calendar 2018 の8日目として業務時間中に書きました。