Inplace pod vertical Scalingについて調べる

### 概要

この記事ではKubernetes 1.27で新たに導入されたIn-place pod vertical Scalingに関して調べたことを共有します

In-place pod vertical ScalingによってPodのリソース割り当ての変更をPodの再起動を行わずにできるようになります。
Podの再起動を行わずにリソースの変更をできるようにすると、ステートフルワークロードやバッチワークロードなどの再起動することが可用性の低下や実行コストの増加につながるケースでは有効です。

# ソースコードから読み取る

実際にソースコードから調べた内容を共有します。

1.27.4のソースコードや実際のPR, KEPから調べました。

- https://github.com/kubernetes/kubernetes/tree/v1.27.4
- https://github.com/kubernetes/kubernetes/pull/102884
- https://github.com/kubernetes/enhancements/tree/master/keps/sig-node/1287-in-place-update-pod-resources

## 主な変更点

主な変更点はAPIの変更、Kubeletに関する変更、Schedulerに関する変更です。

## Kubeletの変更

今回の主な変更ではKubeletでPodを更新する時の処理に変更が加えられています。

Podの再起動の判定のロジックに変更が加えられ、PodSpecのリソースのリクエストと制限に関して、再起動せずに変更可能となっていることがわかります。

### SyncPod
https://github.com/kubernetes/kubernetes/blob/v1.27.4/pkg/kubelet/kuberuntime/kuberuntime_manager.go#L1011

Podをkube-api-serverからの情報に従って更新するときに、kubeGenericRuntimeManagerのSyncPod関数を利用してコンテナランタイムにコンテナ作成起動などの処理を指示するのですが、この関数にdoPodResizeActionという関数を実行する処理が追加されています。

また、Podの現在の状態を計算する関数であるcomputePodActionsにも変更が入っています。

```golang
// SyncPod syncs the running pod into the desired pod by executing following steps:
//
//  1. Compute sandbox and container changes.
//  2. Kill pod sandbox if necessary.
//  3. Kill any containers that should not be running.
//  4. Create sandbox if necessary.
//  5. Create ephemeral containers.
//  6. Create init containers.
//  7. Resize running containers (if InPlacePodVerticalScaling==true)
//  8. Create normal containers.
func (m *kubeGenericRuntimeManager) SyncPod(ctx context.Context, pod *v1.Pod, podStatus *kubecontainer.PodStatus, pullSecrets []v1.Secret, backOff *flowcontrol.Backoff) (result kubecontainer.PodSyncResult) {
	// Step 1: Compute sandbox and container changes.
	podContainerChanges := m.computePodActions(ctx, pod, podStatus)
  ....

}
```

### doPodResizeAction

https://github.com/kubernetes/kubernetes/blob/fa3d7990104d7c1f16943a67f11b154b71f6a132/pkg/kubelet/kuberuntime/kuberuntime_manager.go#L654

こちらの関数で実施している主な処理は以下です。

- ResourceConfigForPod
    - 現在のPodのリソースの状態を取得する
- updatePodContainerResources
    - CRI APIでUpdateContainerResourcesを実行し、コンテナの状態を更新します。
        
        ```golang
        err := c.cc.Invoke(ctx, "/runtime.v1.RuntimeService/UpdateContainerResources", in, out, opts...)
        ```
        
- setPodCgroupConfig
    - cpuやmemoryの要求に応じてcgroupの設定を変更します。
- 上記の関数の実行順序は以下の通りとなります。
    - まず、ResourceConfigForPodを実行してPodのリソースの状態を取得する。
    - リソースのサイズ変更によりリソース タイプ (CPU またはメモリ) が増加した場合、まずsetPodCgroupConfigを実行してcgroupの制限を更新し、その後updatePodContainerResourcesを実行してコンテナリソースの制限を更新します。
    - リソースのサイズ変更によりリソース タイプ (CPU またはメモリ) が減少した場合、まずupdatePodContainerResourcesを実行してコンテナリソースの制限を更新し、その後、setPodCgroupConfigを実行してcgroupの制限を更新します。

### computePodActions

https://github.com/kubernetes/kubernetes/blob/fa3d7990104d7c1f16943a67f11b154b71f6a132/pkg/kubelet/kuberuntime/kuberuntime_manager.go#L812

computePodActionsはPodの現在の状態と希望の状態を比較し、再起動など必要な変更を決定します。

必要な変更がある場合はSyncPodの各ステップ(step1~step8)で変更が実施されることになります。

例えば、コンテナの再起動を行うかどうかを以下の条件などで判定しています

- Podの定義の変更
- Podの再起動ポリシー(Always, OnFailure, Never)に応じて再起動を行う
- liveness probeの失敗
- startup probeの失敗

今回のInPlacePodVerticalScalingでの修正は主に以下の部分です。

この修正により、PodのCPUやメモリの変更を行なっても再起動を実施しないように実装されていることがわかります。

- Podのスペックの変更
    - 該当のリソースは以下のようになっています。
    - 以前はこの部分の判定はcontainerChangedの関数の実行だけでした。containerChangedにより、再起動が必要な条件は以下の通りです。
        - コンテナの現在の状態と希望の状態をHash化して比べて、異なる場合は再起動が必要と判定
    - InPlacePodVerticalScalingの変更により、再起動の判定は以下のようになっています。
        - containerChangedの判定がtrueである
        - InPlacePodVerticalScalingのfeaturegateが無効である 且つ
        - コンテナのResourcesフィールドを除いた部分のspecが異なる
    - つまり以下の場合は再起動が実施されないことになります
        - InPlacePodVerticalScalingのfeaturegateが有効である
        - コンテナの希望の状態と現在の状態がResourcesフィールドを除いて等しい
        
        ```golang
        if _, _, changed := containerChanged(&container, containerStatus); changed &&
        			(!isInPlacePodVerticalScalingAllowed(pod) ||
        				kubecontainer.HashContainerWithoutResources(&container) != containerStatus.HashWithoutResources) {
        			message = fmt.Sprintf("Container %s definition changed", container.Name)
        			// Restart regardless of the restart policy because the container
        			// spec changed.
        			restart = true
        ....
        ```
        

## APIの変更

以下のフィールドが追加されています

- pod.spec.containers[].resources[].resizePolicy
    - resourceName: cpuかmemoryを指定します
    - policy: NotRequiredまたはRestartContainerを指定します
    - ２つをあわせて、cpu or memoryを更新したときに、再起動するかどうかを指定します。
- pod.status.containerstatuses[].ResourcesAllocated
    - Podとコンテナに割り当てられたリソースを表示します
- pod.status.containerStatuses[].Resources
    - 実行中のコンテナのリソースを表示します
- pod.status.resize
    - リサイズの進行状況が表示されます
        - Proposed
            - リサイズ変更が要求された状態
        - InProgress
            - リサイズが進行中の状態
        - Deferred
            - リサイズが現在はできないが、後で再試行する状態
        - Infeasible
            - リサイズが実施できず、再評価できない状態

### validation

API Serverにリクエストを送るときのバリデーションに変更があります。

- リサイズポリシーが設定されていない場合、applyされているyamlのresourcesを更新しても、validationが適用されて、更新することはできません。(spec: Forbidden: pod updates may not change fields…)
- リサイズポリシーが設定されている場合、resourcesを更新しても、上記のvalidationが適用されることはありません。

## Schedulerの変更

- calculateResource関数に変更があります。詳細は把握していませんが、この関数でリソースの割り当てを計算しているようです。
    - InPlacePodVerticalScalingが有効な場合リソース割り当ての計算に以下の値を利用します。
        - PodResizeのStatusがInfeasibleの場合、ResourcesAllocatedを利用します。
        - PodResizeのStatusがInfeasibleではない場合、ResourcesAllocatedかResources.Requestsの大きい方を利用します。

## まとめ

Inplace pod vertical Scalingの機能について、Kubeletの処理やAPI,Schedulerにどのような変更が行われたのか調べてみました。特にKubeletの部分の処理を追ってみると、今までどのように再起動処理が行われていたかなどがある程度把握できました。