KubernetesのPodのResource設定の考え方

PodのResource設定(`spec.containers.resources`)はこうあるべき、みたいなふんわりとしたイメージが自分の中であるのですが言語化できていないのでしていきたいと思います。

なお、この記事の内容は運用するKubernetes上には自分たちが運用するサービスに関するコンテナのみが載っている前提です。
不特定多数のユーザーのコンテナが載る際はまた別の考えになると思います。

あとこの記事の内容は僕個人の考えです。

# TL;DR
* CPU
    * requests
        * 必要。コンテナを動かすために必要な値を設定してね。低すぎちゃだめよ。
    * limits
        * 不要。でも場合によっては必要だよ。
* Memory
    * requests
        * 必要。コンテナを動かすために必要な値より少し余裕を持って設定してね。
    * limits
        * 必要。requestsと同じ値にしてね。

# 1. CPU
## 1-1. requests
* 必ず設定しなくてはいけないもの
* コンテナを動かすのに必要な値を設定する
    * このくらいは常に使うよねっていう値にする。低くしすぎてはいけない。

### 1-1-1. なぜ必ず設定が必要なのか？
この数値は以下の用途で使用されるためです。
* PodをNodeにスケジューリングするとき
* HPAのCPU使用率の計算元

詳しく見ていきます。

#### 1-1-1-1. PodをNodeにスケジューリングするとき
PodはどこかのNodeに配置しないといけません。
その配置先のNodeを決める際に `requests.cpu` の数値を使用します。
CPUに割り当て可能な容量（CPU使用率のことではない）があるNodeに対してPodを配置することができます。

逆にこの数値を設定しない場合、Nodeに割り当て可能な容量があろうがなかろうがどこにでも配置することができます。
その代わりNodeのCPU使用率が高まってきたときに、パフォーマンスが悪くなる場合があります。

##### 1-1-1-1-1. cpu_share
`cpu_share` とは cgroupの1機能で、コンテナ内のプロセスが使用できるCPU時間を制御するものです。
以下の記事が詳しいです。

https://www.batey.info/cgroup-cpu-shares-for-docker.html

`requests.cpu` で指定された値は `cpu_share` の値に変換されます。
`cpu_share` の値はコンテナ間で相対的な数値です。

たとえば1CPUあるNodeに展開される場合、それぞれのコンテナが得られる保証されたCPU時間は以下のようになります。

コンテナ名 | requests.cpu | cpu_share | 得られるCPU時間
--|--|--|--
A | 250m | 256 | 0.5cpu
B | 125m | 128 | 0.25cpu
C | 125m | 128 | 0.25cpu

Nodeにいるコンテナが1つの場合は、そのコンテナはNodeにあるCPUをフル活用できるわけです。

`requests.cpu` が指定されていない場合、 **`cpu_share` は0になります。** 
0だと保証されたCPU時間も0なので、NodeのCPU使用率が高まるとCPUの利用ができなくなりパフォーマンスが悪くなります。

#### 1-1-1-2. HPAのCPU使用率の計算元
HPAでCPU使用率によってPodの増減をしたい場合は `requests.cpu` の指定は必須になります。
計算式はこのようになっています。

```go
currentUtilization = int32((metricsTotal * 100) / requestsTotal)
```
https://github.com/kubernetes/kubernetes/blob/v1.27.0/pkg/controller/podautoscaler/metrics/utilization.go#L49

## 1-2. limits
* 基本的に設定は不要だが必要な場合もある

### 1-2-1. なぜ不要なのか？
まずHPAがある場合を考えてみます。
CPU使用率によってPodが増減する設定をしている場合は、CPU使用率が高まればPodが増える（≒Nodeが増える）のでNode自体のCPUスロットリングは継続して生じません。
また急なトラフィックが来た場合も空いたCPUをフルで使えるため一時しのぎができます。
limitsが設定されているとこのような一時しのぎができません。（limitsを十分大きな値にしている場合は可能です。でもそれって意味あるんでしょうか？)

他のメトリクスでPodの増減をする場合でもCPU使用が継続して高くなりすぎることは考えづらいです。そうなったら増減のパラメータの設定がおかしいことを疑ってください。
よってHPAがある場合は不要と考えています。

HPAがないPodの場合も、Kubernetes上にある他のPodに `resources.requests.cpu` が設定されていれば `cpu_share` によって最低限そのCPU時間はもらえるのが保証されているため、PodのCPU使用が多くなったとしても他のPodに影響を与えることは考えづらいです。（そのために `resources.requests.cpu` は低すぎてはいけない)
また短時間で高負荷を与えるようなバッチのようなものの場合は、NodeのCPUに余裕があればどんどんCPUを使ってほしいのでそういった場合、limitsは邪魔になります。

### 1-2-2. 必要なケースは？
`limits.cpu` が必要なケースを考えてみます。

#### 1-2-2-1. Node(Pod)の数を簡単に増やせない
環境がクラウドではなかったり予算に上限があるなど、Nodeを簡単に増やせないとPodの数を増やすのも難しいと思います。
その場合は制限を設け、アプリケーションのパフォーマンス劣化もやむなしと判断できます。

#### 1-2-2-2. 可能な限り生き続けてほしいPod
長時間動くバッチやステートフルなアプリケーションの場合、急にPodが削除（Evict）されると困ることがあります。
PodがEvictされるケースは以下のようなケースです。

* NodeがDrainされた
* [Preemption](https://kubernetes.io/docs/concepts/scheduling-eviction/pod-priority-preemption/#preemption)
* [Node-pressure Eviction](https://kubernetes.io/docs/concepts/scheduling-eviction/node-pressure-eviction/)

Drainに関してはオートスケールとは関係ないNodeGroup上のNodeにそのようなPodを配置するなどで回避することができます。

PreemptionはEvictされないように優先度の高いPriorityClassをPodにつけましょう。

Node-pressure Evictionに関してはEvictionの対象にならないようにする必要があります。
そのためにQuality of Service (QoS)という仕組みがあり、最優先にするために `limits.cpu` を設定します。（これだけではだめです。詳しくは次の項目を読んでください)

##### 1-2-2-2-1. Quality of Service (QoS)
KubernetesはPodのスケジュールやEvictをする際にQoSを参照します。
https://kubernetes.io/docs/tasks/configure-pod-container/quality-service-pod/

上から順に優先度が高く、Evictされづらいです。

* すべてのコンテナでCPUのlimitsとrequestsが同一かつMemoryのlimitsとrequestsが同一の場合は `Guaranteed`
* いずれかのコンテナでCPUまたはMemoryにrequestsが設定されていれば `Burstable`
* それ以外は `BestEffort` となります。

Nodeになにか問題があり（例えばメモリ逼迫）PodをEvictしなければならないときにこの設定が参照されます。

# 2. Memory
## 2-1. requests
* 必ず設定しなくてはいけないもの
* コンテナを動かすのに必要な値を設定する
    * このくらいは常に使うよねっていう値よりも少し余裕のある数値にする

### 2-1-1. なぜ必ず設定が必要なのか？
この数値は以下の用途で使用されるためです。
* PodをNodeにスケジューリングするとき
* HPAのメモリ使用率の計算元（あまり使わない気がするけど・・・）

理屈はCPUと同じなので割愛します。

## 2-2. limits
* 必ず設定しなくてはいけないもの
* `resources.requests.memory` と同じ値にする

### 2-2-1. なぜ必ず設定が必要なのか？
メモリの使いすぎを防ぐためです。
CPUとは違いメモリは絶対的な数値なため、使いすぎると他のPodに影響があります。

たとえばメモリ1GBのNodeがあるとして、
requestが200MBのコンテナがある状態でそのコンテナが1GB使ってしまったらどうなるでしょうか？

新たなPodをNodeにスケジューリングするとき、そのNodeのメモリに割り当て可能な容量（メモリ使用率ではない）は空いてても
実際にスケジュールしたら起動に必要なメモリが足りずCrashLoopBackOffしてしまうことになります。

このようなことを避けるために制限は必要です。

### 2-2-2. なぜ `resources.requests.memory` と同じ値にするの？
これも同じ話でメモリで指定する値は絶対的な数値なので、Nodeのメモリの空き状況とメモリ使用率は近い状態にしておかないと、Podをスケジュールした際に起動できないことが考えられるからです。

たとえば以下のようなコンテナがメモリ4GBのNode上にいるとしましょう。

コンテナ名|requests|limits
--|--|--
A|1GB|2GB
B|1GB|2GB

AとBが制限いっぱいまでメモリを使うとこの時点でNodeは4GBフルで使ってしまっていますが、Nodeのメモリに割り当て可能な容量はまだ2GBあります。
同様のリソースを求めるコンテナCがこのNodeにスケジュールされると起動するために必要なメモリを確保できないためPodが起動できなくなる可能性があります。

# 参考資料
https://sysdig.jp/blog/kubernetes-cpu-requests-limits-autoscaling/

https://home.robusta.dev/blog/stop-using-cpu-limits

https://home.robusta.dev/blog/kubernetes-memory-limit

https://www.batey.info/cgroup-cpu-quota-for-docker.html

https://www.batey.info/cgroup-cpu-shares-for-kubernetes.html

https://gavin-zhou.medium.com/kubernetes%E3%81%A7cpu%E5%88%B6%E9%99%90%E3%82%92%E4%BD%BF%E3%81%84%E7%B6%9A%E3%81%91%E3%82%8B%E3%81%B9%E3%81%8D%E7%90%86%E7%94%B1-f661db0e2c1e

http://blog.father.gedow.net/2019/11/14/eks-kubernetes-resource/