KubernetesのPodでメモリ関連のevictionが発生するタイミングについて

# はじめに
たまにクラスタでこんな感じのevictionが発生していたのですが、Podが再起動した後は正常に動いていたので原因を調べなきゃと思いながらも放置していました。
```log
The node was low on resource: memory. Container xxx was using xxxKi, which exceeds its request of xxGi.
```

原因調査をしたのですが、その際に改めてevictionってどんな時に起こるんだっけということを確認したのでその備忘録として記事にしておきます。
なお、今回は上記にある通りメモリに関するevictionについてが主な内容となっています。

# evictionが発生するパターン
evictionはPodが削除される、ということが発生します。
メモリに関連したPodのevictionは以下のようなパターンで発生します。
1. Nodeの空きメモリが、設定してあるメモリ閾値(eviction-hardのmemory.available)より少なくなった時
2. Priorityの高いPodがスケジュールされる際にNodeのリソースを超えたリクエストになった時  
※k8s.ioのドキュメントでは`preempt (evict) `と記載され、`Preemption`と呼ばれています。

上記についてそれぞれどんな感じで発生するのかを見ていきたいと思います。
なお、EKS・AKSはどちらもバージョン`1.24`で確認しました。

# 1. Nodeの空きメモリが、設定してあるメモリ閾値(eviction-hardのmemory.available)より少なくなった時
こちらが今回evictionが発生した原因でした。evictionされたPodのメモリ利用率はLimitsに到達していなかったのですが、Node全体のメモリ利用率が上がり空きメモリが閾値以下になりevictionされてしまいました。

## 閾値の設定について
閾値はkubeletの`--evction-hard`オプションのmemory.availableで指定されます。([Eviction Thresholds](https://kubernetes.io/docs/tasks/administer-cluster/reserve-compute-resources/#eviction-thresholds))  

EKSとAKSでは以下のような値で設定されています。  
| 環境 | memory eviction閾値 | 参照 | 
|:-:|:-:| :-- |
| AKS | 750Mi | [Azure Kubernetes Services (AKS) における Kubernetes の基本概念 - Azure Kubernetes Service](https://learn.microsoft.com/ja-jp/azure/aks/concepts-clusters-workloads) |
| EKS | 100Mi | [amazon-eks-ami/bootstrap.sh at master · awslabs/amazon-eks-ami](https://github.com/awslabs/amazon-eks-ami/blob/master/files/bootstrap.sh#L450) |

同じ8GBメモリのNodeであってもevictionが発生するメモリ利用率がAKSでは約7.25GB、EKSでは約7.9GBというように、環境によって異なるので確認するようにしましょう。  
ちなみにメモリ関連でevictionが発生するとNodeの `MemoryPressure` ConditionがtrueになりPodのスケジュールできなくなります。

## Nodeのリソースについて
evictionを理解するためにもNodeのリソースがどうなっているかということも再確認しておきます。  
NodeのメモリやCPUのリソースは以下のようになっています。
![node_resources.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2959481/ec87db46-774c-739a-59ee-bd6f441b81a7.png)
| 項目 | 説明 |
|:-:|:--|
| Capacity | Nodeに搭載されているリソースの全量 |
| kube-reserved | kubeletやcontainer runtimeのために予約されているリソース  |
| system-reserved | OSのデーモンなどのために予約されているリソース |
| eviction-hard | evictionの閾値として確保されたリソース |
| Allocatable | kubernetesが利用可能なリソース<br/>Capacity - (kube-reserve + system-reserved + eviction-hard) |

EKS/AKSにおけるevition-hardの値は固定値ですが、kube-reservedはどうなっているか見てみましょう。  
ちなみにsystem-reservedはどちらも指定されていなかったので0になります。

- EKS
  `11*MAX_POD_NUM + 255 MB` となっていました。([amazon-eks-ami/bootstrap.sh at master · awslabs/amazon-eks-ami](https://github.com/awslabs/amazon-eks-ami/blob/master/files/bootstrap.sh#L212))
MAX_POD_NUMの値はインスタンスサイズにより変化します。デフォルト値一覧は[amazon-eks-ami/eni-max-pods.txt at master · awslabs/amazon-eks-ami](https://github.com/awslabs/amazon-eks-ami/blob/master/files/eni-max-pods.txt)にあります。
なので例えばですが、m5.xlarge(16GBメモリ)では893Mi, m5.large(8GBメモリ)では574Miというようになります。 

- AKS
  [Azure Kubernetes Services (AKS) における Kubernetes の基本概念 - Azure Kubernetes Service](https://learn.microsoft.com/ja-jp/azure/aks/concepts-clusters-workloads#resource-reservations)を見ると以下のようになっていました。
> kubelet デーモンが適切に機能するための予約されているメモリの回帰率 (kube-reserved)。 
・最初の 4 GB のメモリの 25%
・次の 4 GB のメモリの 20% (最大 8 GB)
・次の 8 GB のメモリの 10% (最大 16 GB)
・次の 112 GB のメモリの 6% (最大 128 GB)
・128 GB を超えるメモリの 2%

ということなので、例えばB2MS(8GBメモリ)のVMでは1.8GBというようになります。

EKSと比べ、AKSはkube-reservedが大きくなるようなのでAllocatableが小さくなります。  
8GBのVMをkubectl describe nodeで確認すると以下のようにAllocatableが約5.2GBと搭載メモリよりかなり小さくなっていることが分かります。  
AKSの方はeviction-hardのところでもそうでしたが、リソースに余裕を持たせるようになっていました。
```:AKS - B2MS VM
Capacity:
  cpu:                2
  ・・・
  memory:             8148456Ki
  pods:               30
Allocatable:
  cpu:                1900m
  ・・・
  memory:             5493224Ki
  pods:               30

```

EKSではrequestsが足りていてスケジュールできていたのにAKSでは足りない、みたいなことが起こりえるので注意しましょう。
また、4GBや8GBといったメモリが比較的小さいVMサイズでは搭載メモリに対してAllocatableのメモリの割合が特に小さくなってしまうので特に注意しましょう。

## 閾値はCapacityに対してなのか？
改めてevictionについて確認する前、eviction-hardの閾値はCapacityに対してなのか、Capacityからsystem-reservedとkube-reservedの値を引いたものに対してなのか？マークでした。
「reserved」とあるのでevictionが発生する閾値は前者で以下なのではないかと思っていました。
```
Capacity - (system-reserved + kube-reserved + eviction-hard) => AKSの8GB VMの例: 8 - (0 + 1.8 +0.75) = 5.45GB)
```
 
ですが上記で述べた通りCapacityからeviction値を引いたものが閾値となっています。
 ```
Capacity - eviction-hard => AKSの8GB VMの例: 8 - 0.75 = 7.25GB
```


これはkubeletの`--enforce-node-allocatable`関連の設定によって挙動が異なります。
`--enforce-node-allocatable`に**system-reservedとkube-reservedの両方が指定されている**と前者の
`Capacity - (system-reserved + kube-reserved + eviction-hard)`
が閾値となります。
ですがEKS/AKSどちらも指定はされていないので
`Capacity - eviction-hard`
が閾値となります。


## 補足)evictionされるPodの優先度
以下の優先度でevictionされるPodが選定されます。
**1. リソースをrequests以上に使用している**
**2. PodのQoS(後述)**
**3. requestsに対するリソースの使用量**

Podには以下のようにresourceのrequests/limitsの設定によってQoSクラスが割り当てられます。 
[PodにQuality of Serviceを設定する | Kubernetes](https://kubernetes.io/ja/docs/tasks/configure-pod-container/quality-service-pod/) 
| QoS | 設定 |
|:-:|:--|
| Guaranteed | Pod内すべてのコンテナにメモリ・CPUのrequests/limitsが設定されてあり、その値が同じ  |
| Burstable | Pod内の1つ以上のコンテナがメモリ or CPUのrequestsが設定されてある  |
| BestEffort | メモリ,CPUどちらのrequests/limitsも設定されていない  |

Podがevictionされる優先度は `BestEffort > Burstable > Guaranteed` となっています。

ということで、簡単にまとめると**Nodeのメモリ利用量が閾値を超えるとevictionが発生しPodが落とされる**ということでした。


# 2. Priorityの高いPodがスケジュールされる際にNodeのリソースを超えたリクエストになった時
Podには以下のようにPriorityを設定することができます。
> [Podの優先度とプリエンプション | Kubernetes](https://kubernetes.io/ja/docs/concepts/scheduling-eviction/pod-priority-preemption/)より
Podは priority（優先度）を持つことができます。 優先度は他のPodに対する相対的なPodの重要度を示します。 もしPodをスケジューリングできないときには、スケジューラーはそのPodをスケジューリングできるようにするため、優先度の低いPodをプリエンプトする（追い出す）ことを試みます。

**Priorityの高いPodがスケジュールされる際に、メモリのrequestsがAllocatableの100%を超える場合そのNode上のPriorityの低いPodを追い出してしまう**といったケースです。

優先度はPriorityClassで定義され、Valueの値が大きいほど優先度が大きくなります。
デフォルトで`system-cluster-critical`と`system-node-critical`というPriorityClassがあり、EKSにもAKSにもデフォルトで作成されています。

```bash
$ kubectl describe priorityclasses.scheduling.k8s.io -A
Name:              system-cluster-critical
Value:             2000000000
GlobalDefault:     false
PreemptionPolicy:  PreemptLowerPriority
Description:       Used for system critical pods that must run in the cluster, but can be moved to another node if necessary.
Annotations:       <none>
Events:            <none>


Name:              system-node-critical
Value:             2000001000
GlobalDefault:     false
PreemptionPolicy:  PreemptLowerPriority
Description:       Used for system critical pods that must not be moved from their current node.
Annotations:       <none>
Events:            <none>

```

PreemptionPolicyが`PreemptLowerPriority(デフォルト)`の場合Preemptが発生しますが、`Never`に設定されていると発生しないです。

基本的にkube-system Namespaceで動作しているPodにはどちらかのPriorityClassが設定されていました。
```bash
$ kubectl describe deploy,ds -n kube-system
# 抜粋
Name:                   coredns
  Priority Class Name:  system-cluster-critical

Name:                   ebs-csi-controller
  Priority Class Name:  system-cluster-critical

Name:           aws-node
  Priority Class Name:  system-node-critical

Name:           ebs-csi-node
  Priority Class Name:  system-node-critical

Name:           ebs-csi-node-windows
  Priority Class Name:  system-node-critical

Name:           kube-proxy
  Priority Class Name:  system-node-critical
・・・

```

なのでリソースにあまり余裕がない状態で稼働しているクラスタの場合、**新たに各クラウドサービスの監視エージェント機能を有効にするときやEKSでのアドオンを追加するとき**などは一度リソース状況を確認した方が良いです。



# おまけ(試してみる)
## eviction-hardの動作
Nodeは2GBメモリ、eviction-hardは空きメモリが750MBを下回ったら(memory.available<750Mi)となっています。
stressコマンド用のPodを作成します。
stressコマンドが入ったイメージは[Assign Memory Resources to Containers and Pods | Kubernetes](https://kubernetes.io/docs/tasks/configure-pod-container/assign-memory-resource/)のサンプルを使いました。

```yaml:stress
apiVersion: apps/v1
kind: Deployment
metadata:
  name: stress
  labels:
    app: stress
    role: sample
spec:
  replicas: 1
  selector:
    matchLabels:
      app: stress
      role: sample
  template:
    metadata:
      labels:
        app: stress
        role: sample
    spec:
      containers:
      - name: memory-stress
        image: polinux/stress
        resources:
          requests:
            memory: "50Mi"
          limits:
            memory: "1.5Gi"
        command: ["sh", "-c", "while true; do sleep 10; done"]
        ports:
        - containerPort: 80
```

上記Deployment作成後、stress Podにアクセスしてメモリ負荷をかけていきます。
(今回Nodeのメモリ利用率が約400MBだったので1GBの負荷をかけています)
```bash
$ kubectl exec -it stress-5bcd98cb59-zmppw -- bash
bash-5.0# stress --vm 1 --vm-bytes 1G --vm-hang 2
stress: info: [161] dispatching hogs: 0 cpu, 0 io, 1 vm, 0 hdd
```

kubectl describe nodeコマンドで見てみると、MemoryPressureがtrueになっていることが分かります。
```bash
$ kubectl describe node
Conditions:
  Type                 Status  LastHeartbeatTime                 LastTransitionTime                Reason                         Message
  ----                 ------  -----------------                 ------------------                ------                         -------
・・・
  MemoryPressure       True    Fri, 24 Mar 2023 00:17:19 +0000   Fri, 24 Mar 2023 00:17:19 +0000   KubeletHasInsufficientMemory   kubelet has insufficient memory available
  Ready                True    Fri, 24 Mar 2023 00:17:19 +0000   Thu, 23 Mar 2023 23:51:26 +0000   KubeletReady                   kubelet is posting ready status. AppArmor enabled
・・・
Events:
  Type     Reason                     Age   From     Message
  ----     ------                     ----  ----     -------
・・・
  Warning  EvictionThresholdMet       2m3s  kubelet  Attempting to reclaim memory
  Normal   NodeHasInsufficientMemory  115s  kubelet  Node node01 status is now: NodeHasInsufficientMemory
```
Podのステータスは以下のように遷移していました。
```bash
$ kubectl get po -o wide -w
NAME                                READY   STATUS    RESTARTS   AGE   IP            NODE     NOMINATED NODE   READINESS GATES
stress-5bcd98cb59-zmppw             1/1     Running   0          20m   192.168.1.4   node01   <none>           <none>
stress-5bcd98cb59-zmppw             1/1     Running   0          25m   192.168.1.4   node01   <none>           <none>
stress-5bcd98cb59-zmppw             0/1     Error     0          25m   192.168.1.4   node01   <none>           <none>
stress-5bcd98cb59-6b4vq             0/1     Pending   0          0s    <none>        <none>   <none>           <none>
stress-5bcd98cb59-zmppw             0/1     Error     0          26m   192.168.1.4   node01   <none>           <none>
stress-5bcd98cb59-6b4vq             0/1     Pending   0          4m34s   <none>        node01   <none>           <none>
stress-5bcd98cb59-6b4vq             0/1     ContainerCreating   0          4m34s   <none>        node01   <none>           <none>
stress-5bcd98cb59-6b4vq             0/1     ContainerCreating   0          4m34s   <none>        node01   <none>           <none>
stress-5bcd98cb59-6b4vq             1/1     Running             0          4m35s   192.168.1.5   node01   <none>           <none>
```
空きメモリが少なくなったことによりPodがEvictionされ再スケジュールされています。
しかしNodeがMemoryPressureによりスケジュールできなくなっているためしばらくPendingになっていました。
しばらくするとMemoryPressureが解除されスケジュール可能になりstress Podが起動しました。

## Preemption
evictionのときと同じく2GBメモリのNodeで試しています。
先ほどのstress Podのメモリrequestsを1.5GBに変更し再作成します。

```yaml:stressメモリrequests変更
・・・
        resources:
          requests:
            memory: "1.5Gi"
```

nodeのリソース状況を確認すると以下のようにrequestsの余裕が無くなっています。
```bash
$ kubectl describe node
・・・
Non-terminated Pods:          (4 in total)
  Namespace                   Name                        CPU Requests  CPU Limits  Memory Requests  Memory Limits  Age
  ---------                   ----                        ------------  ----------  ---------------  -------------  ---
  default                     stress-69d5997954-x8fhg     0 (0%)        0 (0%)      1536Mi (81%)     1536Mi (81%)   24s
  kube-system                 canal-nwxks                 25m (2%)      0 (0%)      0 (0%)           0 (0%)         39m
  kube-system                 coredns-68dc769db8-bl7xg    50m (5%)      0 (0%)      50Mi (2%)        170Mi (9%)     28d
  kube-system                 kube-proxy-84r66            0 (0%)        0 (0%)      0 (0%)           0 (0%)         28d
Allocated resources:
  (Total limits may be over 100 percent, i.e., overcommitted.)
  Resource           Requests      Limits
  --------           --------      ------
  cpu                75m (7%)      0 (0%)
  memory             1586Mi (84%)  1706Mi (90%)
  ephemeral-storage  0 (0%)        0 (0%)
  hugepages-2Mi      0 (0%)        0 (0%)
```

この状態で以下のPriorityClassとPodを作成しNodeのrequestsの上限以上になるようにしてみます。
```yaml:priority-test
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: test-priority
preemptionPolicy: PreemptLowerPriority
value: 1000000000
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: high-priority-nginx
  labels:
    app: high-priority-nginx
    role: sample
spec:
  replicas: 1
  selector:
    matchLabels:
      app: high-priority-nginx
      role: sample
  template:
    metadata:
      labels:
        app: high-priority-nginx
        role: sample
    spec:
      priorityClassName: test-priority
      containers:
      - name: nginx
        image: nginx:1.21.6
        resources:
          requests:
            memory: "1Gi"
          limits:
            memory: "1.5Gi"
        ports:
        - containerPort: 80
```

Podのステータスを見てみると以下のように優先度の高いPodによりstress PodがPreemptionされていました。

```bash
$ kubectl get po -o wide -w
NAME                      READY   STATUS    RESTARTS   AGE     IP            NODE     NOMINATED NODE   READINESS GATES
stress-69d5997954-x8fhg   1/1     Running   0          4m54s   192.168.1.3   node01   <none>           <none>
high-priority-nginx-5788c84cd8-swbqz   0/1     Pending   0          0s      <none>        <none>   <none>           <none>
stress-69d5997954-x8fhg                1/1     Running   0          5m51s   192.168.1.3   node01   <none>           <none>
stress-69d5997954-x8fhg                1/1     Terminating   0          5m51s   192.168.1.3   node01   <none>           <none>
stress-69d5997954-ftj5k                0/1     Pending       0          0s      <none>        <none>   <none>           <none>
stress-69d5997954-x8fhg                1/1     Terminating   0          5m51s   192.168.1.3   node01   <none>           <none>
high-priority-nginx-5788c84cd8-swbqz   0/1     Pending       0          0s      <none>        <none>   node01           <none>
stress-69d5997954-ftj5k                0/1     Pending       0          0s      <none>        <none>   <none>           <none>
high-priority-nginx-5788c84cd8-swbqz   0/1     Pending       0          2s      <none>        <none>   node01           <none>
stress-69d5997954-x8fhg                1/1     Terminating   0          6m22s   192.168.1.3   node01   <none>           <none>
stress-69d5997954-x8fhg                0/1     Terminating   0          6m22s   192.168.1.3   node01   <none>           <none>
stress-69d5997954-x8fhg                0/1     Terminating   0          6m22s   192.168.1.3   node01   <none>           <none>
stress-69d5997954-x8fhg                0/1     Terminating   0          6m22s   192.168.1.3   node01   <none>           <none>
high-priority-nginx-5788c84cd8-swbqz   0/1     Pending       0          31s     <none>        node01   node01           <none>
high-priority-nginx-5788c84cd8-swbqz   0/1     ContainerCreating   0          31s     <none>        node01   <none>           <none>
high-priority-nginx-5788c84cd8-swbqz   0/1     ContainerCreating   0          32s     <none>        node01   <none>           <none>
high-priority-nginx-5788c84cd8-swbqz   1/1     Running             0          38s     192.168.1.4   node01   <none>           <none>
```

eventからも以下のようにPreemptionされたことが確認できます。
```bash
$ kubectl get event
・・・
5m8s        Normal    Killing                   pod/stress-69d5997954-x8fhg                 Stopping container memory-stress
5m8s        Normal    Preempted                 pod/stress-69d5997954-x8fhg                 Preempted by a pod on node node01
```

# 参考
- [Reserve Compute Resources for System Daemons | Kubernetes](https://kubernetes.io/docs/tasks/administer-cluster/reserve-compute-resources/)
- [Node-pressure Eviction | Kubernetes](https://kubernetes.io/docs/concepts/scheduling-eviction/node-pressure-eviction/)
- [Pod Priority and Preemption | Kubernetes](https://kubernetes.io/docs/concepts/scheduling-eviction/pod-priority-preemption/)
- [Assign Memory Resources to Containers and Pods | Kubernetes](https://kubernetes.io/docs/tasks/configure-pod-container/assign-memory-resource/)
- [Configure Quality of Service for Pods | Kubernetes](https://kubernetes.io/docs/tasks/configure-pod-container/quality-service-pod/)
- [Azure Kubernetes Services (AKS) における Kubernetes の基本概念 - Azure Kubernetes Service](https://learn.microsoft.com/ja-jp/azure/aks/concepts-clusters-workloads)
- [awslabs/amazon-eks-ami: Packer configuration for building a custom EKS AMI](https://github.com/awslabs/amazon-eks-ami)