Kubernetes 新機能先取り: InPlacePodVerticalScaling Feature Gate

この機能は 2019/06/18(火) [Kubernetes Meetup Tokyo](https://k8sjp.connpass.com/event/134064/) で注目発表として話してから早3年が経過しましたが、ようやくマージされそうな雰囲気になってきました :tada:
*Kubernetes 1.26 に入るかと思いましたが、残念ながら 1.27 になるようです...*

この記事では最終的と思われる仕様と使い方、仕組みを紹介していきたいと思います。

**2022/12/1 時点ではまだ未マージで Alpha としてもリリースされていません。[1.27 に alpha として追加される予定で現在作業が進められています。](https://github.com/kubernetes/enhancements/pull/3670)**

*注意:　[KEP](https://github.com/kubernetes/enhancements/tree/313ad8b59c80819659e1fbf0f165230f633f2b22/keps/sig-node/1287-in-place-update-pod-resources)や [Feature PR](https://github.com/kubernetes/kubernetes/pull/102884) を見ながら筆者が独自解釈した内容なので誤りがある可能性があります。見つけたらコメントいただけると大変助かります*

## InPlacePodVerticalScaling とは

Pod またはそのコンテナを再起動・再作成することなく、Pod リソースの Request を変更する機能です。
これは現在(1.25)の Kubernetes では Pod のリソースを動的に変更する事はできず、リソースを変更したい場合は Pod を再作成する必要がありました。

### 想定ユースケース

- ステートフルやバッチワークロードなどの Pod の再作成が高コストになる Pod
- [VerticalPodAutoscaler](https://qiita.com/shmurata/items/197b5b722ac7e9dedb90) などでPod の再作成を伴わずにスケールしたいケース

## 使い方イメージ

例としてすでに Pod を作成済みで、その Pod の pod.spec.resources.request.cpu を 1 から 2 に変更したいケースとします。

### [User] リサイズリクエストを投げる

apiserver の Pod オブジェクトに対して以下のような PUT or PATCH リクエストを送ることで変更をリクエストできます。

```
$ kubectl patch pod ${POD_NAME} -p --type='json' -p='[{"op": "replace", "path": "/spec/containers/0/resources/requests/cpu", "value": "2"}]'
```

*beta では権限を細かく分けるために resize サブリソースが作られる計画がありますが現時点では実装はありません。*

正常にスケールアップされる場合はこのまま待っていれば自動的に適応されていきますが、エラーが発生するケースなどこのスケールアップの状況を確認できるステータスが追加されましたので、この後の処理される流れに合わせてそれらを紹介していきます。

### [apiserver] リサイズリクエストを受け付けて、リソースに受付状態を記録する

リクエストを送ると正常に apiserver に受け付けられた場合は以下のように `pod.status` に記録されます。

apiserver はリクエストを受けると `pod.status.resize[cpu]` に `Proposed` を設定します。

```
$ kubectl get pod ${POD_NAME} -o jsonpath='{.status.resize[cpu]}'
Proposed
```

### [kubelet] リサイズリクエストを受け付ける

次に kubelet は 自身に割り当てられた Pod を Watch していますが、resize のリクエストを受け取ったらケースに合わせて以下の挙動をします。

#### Node の Available なリソース量を超えてリクエストされた場合

確保できるリソースがないため、`pod.status.resize[cpu]` に `Infeasible` として記録し、リサイズが失敗したことを通知します。

:memo: このときはリトライしないようです。

#### 何らかの理由で Pod のリサイズのリクエストが受け付けられない状態の場合

一時的にリサイズ操作ができないため、`pod.status.resize[cpu]` に `Deferred` として記録し、リトライすることを通知します。

#### 事前のチェックでリサイズが受理された場合

`pod.status.containerStatuses[i].resourcesAllocated` にその値を適応し、受け付けたことを `pod.status.resize[cpu]` に `InProgress` として記録します。

```
$ kubectl get pod ${POD_NAME} -o jsonpath='{.status.containerStatuses[0].resourceAllocated}'
{"cpu": "2"}
$ kubectl get pod ${POD_NAME} -o jsonpath='{.status.resize[cpu]}'
InProgress
```

:memo: ResourcesAllocated は新しく追加されたフィールドで、Nodeリソースのリソースの割当リクエスト状況を記録します。

### [kubelet] 受け付けたリサイズのリクエストをコンテナに反映する

リサイズのリクエストを受け付けたら UpdateContainerResources CRI API を呼び出してランタイム(containerd など)にリクエストされたスケールが実施します。

無事リサイズできたら `pod.status.containerStatuses[0].resources.requests[cpu]` に実際に確保した値を設定し、`status.resize[cpu]` から値を削除します。

```
$ kubectl get pod ${POD_NAME} -o jsonpath='{.status.containerStatuses[0].resources.requests[cpu]}'
2
$ kubectl get pod ${POD_NAME} -o jsonpath='{.status.resize[cpu]}'
nil
```

これでリサイズが完了します。

## 主要な変更点

大まかな動きは想像がついたと思うので各コンポーネントの実装の変更点を簡単に見ていこうと思います。

変更PR : https://github.com/kubernetes/kubernetes/pull/102884

### Kuberentes API

追加された主なフィールドは以下のとおりです。

- `pod.spec.containers[].resources[].resizePolicy`
  - `resourceName`: cpu, memory など
  - `policy`: RestartNotRequired(default), RestartRequired が指定可能
    - リサイズのときに再起動するかどうかを指定する
- `pod.status.containerStatuses[].resourcesAllocated`
  - ResourceList 形式で kubelet がリソースのリクエストを受け付けたリソース量が記録される
- `pod.status.containerStatuses[].resources`
  - ResourceRequirements 形式で kubelet が確保したリソース量が記録される
- `pod.status.resize`
  - リサイズの進行状況が記録される
    - `Proposed`, `InProgress`, `Deferred`, `Infeasible`

詳細は[こちら](https://github.com/kubernetes/kubernetes/pull/102884/commits/916b6b0445c258b5cc12ee3908c9ac60c6931421#diff-19b27e79a2dcb337f58b0f7f3477a29d29498b5d135fbb67e9f858cf1dae222c)を参照してください。

### apiserver

- [resizePolicy のバリデーション](https://github.com/kubernetes/kubernetes/pull/102884/commits/916b6b0445c258b5cc12ee3908c9ac60c6931421#diff-c713e8919642d873fdf48fe8fb6d43e5cb2f53fd601066ff53580ea655948f0dR3043)
- [リソースの変更時に `pod.status.resize` を自動リセット](https://github.com/kubernetes/kubernetes/pull/102884/commits/916b6b0445c258b5cc12ee3908c9ac60c6931421#diff-ab4be0f6494ee8361eae124a5d1dd82887908c65fd20f2d1c720335ae37260c0R124)
  - `spec.containers.resource.requests` が変更されたときに `Proposed` を設定する

### scheduler

- [ResourcesAllocated を使用してリソース割り当てを計算します](https://github.com/kubernetes/kubernetes/pull/102884/commits/c65c8bcc052477c0617d093ede93ee7609fe0f4e#diff-9b169e646cb953f34c0362a885d844daea86f5bef52012cb3af974f176bb281f)
  - :memo: cheduler はあまりコードを追ったことがないのでこれで置き換わっているかは未確認です

### kubelet

https://github.com/kubernetes/kubernetes/pull/102884/commits/f7844496573c2615e8ebc64037942490cd2e1266 のコミットですが量が多くて Github 上では表示しきれないようです。。。

主に以下のことをやっています。

- `pod.status.resize` および `pod.status.resourcesAllocated` をサイズ変更時に更新する
  - https://github.com/kubernetes/kubernetes/blob/3388ce5528fb4fe658ef879f39ef58d758475f51/pkg/kubelet/kubelet.go#L2571 で実施しています
- ノードに新しい Pod を配置する際に、`pod.status.containerStatuses[i].resources` をコンテナに設定する
  - https://github.com/kubernetes/kubernetes/blob/3388ce5528fb4fe658ef879f39ef58d758475f51/pkg/kubelet/kuberuntime/kuberuntime_manager.go#L735
- リサイズの判定と実行を行う
  - https://github.com/kubernetes/kubernetes/blob/3388ce5528fb4fe658ef879f39ef58d758475f51/pkg/kubelet/kuberuntime/kuberuntime_manager.go#L525
  - https://github.com/kubernetes/kubernetes/blob/3388ce5528fb4fe658ef879f39ef58d758475f51/pkg/kubelet/kuberuntime/kuberuntime_manager.go#L637
- Linux と Windows の両方で機能するように UpdateContainerResources CRI API を変更
  - https://github.com/kubernetes/kubernetes/blob/3388ce5528fb4fe658ef879f39ef58d758475f51/pkg/kubelet/cri/remote/remote_runtime.go#L434

## 注意事項

- 競合を避けるためにすべてのコンポーネントは、Pod が使用するリソースを計算するときに、Pod の Status.ContainerStatuses[i].ResourcesAllocated を使用してください
  - :memo: scheduler は対応しましたが他のコンポーネントはまだ対応できていないと思うのでこれから対応を進めていく必要がありそうです
  - :memo: メトリクス辺りもどの値を利用するか考えていく必要がありそうに思っています
- アプリケーションがページを保持している場合、メモリ制限の引き下げが常にすぐに有効になるとは限りません
  - :memo: ページの開放は強制的は行わずに解放を促しつつ開放されるのを `InProgress` の状態にして待つということだと思われます

## 今後の予定

refs: https://github.com/kubernetes/enhancements/tree/313ad8b59c80819659e1fbf0f165230f633f2b22/keps/sig-node/1287-in-place-update-pod-resources#future-enhancements

- kubelet (またはscheduler) は、ノードから優先度の低い Pod を削除して、リサイズの余地を作ります。kubelet によるプリエンプションはより簡単で、待ち時間が短くなる可能性があります。
- Pod スコープでの resizePolicy の設定を許可し、(一部の) コンテナで独自に設定されていない場合にデフォルトとして機能します
- resizePolicy を拡張して、リソースの増減を個別に制御します
  - :memo: メモリを下げるときのみ再起動するなどの細かい設定ということらしい
- ノード情報 API を拡張して、ノードの CPU Manager ポリシーを報告し、Static CPU Manager  ポリシーを持つノードの統合 CPU サイズ変更の検証を有効にします
- Job、Deployment などのテンプレートリソースの更新を実行中の Pod に伝達します
- ephemeral storage のリサイズ対応
- resouces.limit への対応
- Pod スコープのリソース対応 [#1592](https://github.com/kubernetes/enhancements/pull/1592)

## まとめ

現時点 (2022/12/1) での InPlacePodVerticalScaling の仕様をまとめました。この後大きく変わることはないとは思いますが、まだ alpha でもないため変更がある可能性はありますのでご注意ください。
筆者は逐次変更はウォッチしていなかったので長引いた背景などは把握しきれていませんが、[Implementation History](https://github.com/kubernetes/enhancements/tree/313ad8b59c80819659e1fbf0f165230f633f2b22/keps/sig-node/1287-in-place-update-pod-resources#implementation-history) を見ると構想から約4年間もかかっており、大変だったのかと思います。当初の予定よりもスケジューラが関わる部分が減り、思っていたよりシンプルな仕様に落ち着いたように感じました。
久々に根幹部分に関わる大きな変更だと思うので多数のバグなども出そうですが、昔よりは安全に追加していくための仕組みやルールが整いつつあると思うので無事リリースできることを期待しています。

## 参考文献

- KEP: https://github.com/kubernetes/enhancements/tree/313ad8b59c80819659e1fbf0f165230f633f2b22/keps/sig-node/1287-in-place-update-pod-resources
- enhancement issue: https://github.com/kubernetes/enhancements/issues/1287
- PR: https://github.com/kubernetes/kubernetes/pull/102884