Kubernetes Pod の resources.limits.memory がいつのまにか 200PiB に設定されていた話

この記事は [Wantedly Advent Calendar 2023](https://qiita.com/advent-calendar/2023/wantedly) の17日目の記事にしようと思ったけど早く書きすぎたので普通の記事です。

## 事の始まり
あるとき Kubernetes Node を見ていると、Node のメモリを大幅にオーバーコミットしている Pod を見つけました。以下は `kubectl describe node` したときの出力の一部です。
 
```
Non-terminated Pods:
  Namespace Name                          CPU Requests  CPU Limits  Memory Requests Memory Limits       Age
  --------- ----                          ------------  ----------  --------------- -------------       ---
  irotoris  my-deployment-85df655dc-7cvwq 150m (1%)     300m (3%)   1Gi (1%)        200Pi (344797407%)  3d12h
```

Memory Limits が __`200Pi (344797407%)` ってなに？？__

さすがにびっくりして調べることにしました。

## Pod の設定を見てみる

Pod の設定を `kubectl describe po` で確認してみると、ただの nginx コンテナが動いているだけの Pod ですが、確かにメモリの limit が `200Pi` になっています。CPU や メモリの requests は常識的な値です。

```
Name:             my-deployment-85df655dc-7cvwq
Namespace:        irotoris
Priority:         0
Service Account:  default
Node:             ip-10-3-240-126.ap-northeast-1.compute.internal/10.3.240.126
Start Time:       Tue, 31 Oct 2023 18:05:47 +0900
Labels:           app=my-app
                  pod-template-hash=85df655dc
Annotations:      kubectl.kubernetes.io/restartedAt: 2023-10-31T18:05:47+09:00
                  vpaObservedContainers: nginx
                  vpaUpdates: Pod resources updated by irotoris: container 0: cpu request, memory request, cpu limit, memory limit
Status:           Running
IP:               10.3.238.241
IPs:
  IP:           10.3.238.241
Controlled By:  ReplicaSet/my-deployment-85df655dc
Containers:
  nginx:
    Container ID:   containerd://e47974f78334fe254e2f5e1169f9b95aea21756ed2b25dcb6845bead0d99f8de
    Image:          public.ecr.aws/nginx/nginx:1.25
    Image ID:       public.ecr.aws/nginx/nginx@sha256:56fae3726ce208394da92a3ac447caacfccd59d8325f700b8542e6faf614cc4a
    Port:           80/TCP
    Host Port:      0/TCP
    State:          Running
      Started:      Tue, 31 Oct 2023 18:05:48 +0900
    Ready:          True
    Restart Count:  0
    Limits:
      cpu:     300m
      memory:  200Pi
    Requests:
      cpu:        150m
      memory:     1Gi
    Environment:  <none>
    Mounts:
      /var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-9p9gn (ro)
Conditions:
  Type              Status
  Initialized       True
  Ready             True
  ContainersReady   True
  PodScheduled      True
Volumes:
  kube-api-access-9p9gn:
    Type:                    Projected (a volume that contains injected data from multiple sources)
    TokenExpirationSeconds:  3607
    ConfigMapName:           kube-root-ca.crt
    ConfigMapOptional:       <nil>
    DownwardAPI:             true
QoS Class:                   Burstable
Node-Selectors:              <none>
Tolerations:                 node.kubernetes.io/not-ready:NoExecute op=Exists for 300s
                             node.kubernetes.io/unreachable:NoExecute op=Exists for 300s
Events:                      <none>
```

この Pod の ReplicaSet を見に行くと、どうやら `my-deployment` という Deployment で管理されているようです。この Deployment の設定を見に行きましょう。

## Deployment の設定を見てみる

```
Name:                   my-deployment
Namespace:              irotoris
CreationTimestamp:      Tue, 31 Oct 2023 17:43:32 +0900
Labels:                 app=my-app
Annotations:            deployment.kubernetes.io/revision: 3
Selector:               app=my-app
Replicas:               1 desired | 1 updated | 1 total | 1 available | 0 unavailable
StrategyType:           RollingUpdate
MinReadySeconds:        0
RollingUpdateStrategy:  25% max unavailable, 25% max surge
Pod Template:
  Labels:       app=my-app
  Annotations:  kubectl.kubernetes.io/restartedAt: 2023-10-31T18:05:47+09:00
  Containers:
   nginx:
    Image:      public.ecr.aws/nginx/nginx:1.25
    Port:       80/TCP
    Host Port:  0/TCP
    Limits:
      cpu:     200m
      memory:  200Mi
    Requests:
      cpu:        100m
      memory:     100m
    Environment:  <none>
    Mounts:       <none>
  Volumes:        <none>
Conditions:
  Type           Status  Reason
  ----           ------  ------
  Available      True    MinimumReplicasAvailable
  Progressing    True    NewReplicaSetAvailable
OldReplicaSets:  <none>
NewReplicaSet:   my-deployment-85df655dc (1/1 replicas created)
Events:          <none>
```

Deployment には `200Pi` なんてメモリは設定されてないですね…。ん？__`resources.limits.memory` 以外にも Deployment と Pod のリソースが全体的に少し違います。__

Pod
```
    Limits:
      cpu:     300m
      memory:  200Pi
    Requests:
      cpu:        150m
      memory:     1Gi
```

Deployment
```
    Limits:
      cpu:     200m
      memory:  200Mi
    Requests:
      cpu:        100m
      memory:     100m
```

## Pod の垂直オートスケーリング (VPA) が怪しそう

Kubernetes において Pod の resource をいい感じにしてくれるものと言えば、Pod の垂直オートスケーリングを可能にする VerticalPodAutoscaler というものがあります。そういえばこのクラスタにもインストールしていました。今思い出しました。

https://github.com/kubernetes/autoscaler/tree/master/vertical-pod-autoscaler

Pod の設定をよく見ると annotation に `vpaUpdates: Pod resources updated by irotoris: container 0: cpu request, memory request, cpu limit, memory limit` というものがあります。この VerticalPodAutoscaler (VPA) のリソースを探しに行きます。

```
❯ kubectl -n irotoris describe vpa
Name:         irotoris
Namespace:    irotoris
Labels:       <none>
Annotations:  <none>
API Version:  autoscaling.k8s.io/v1
Kind:         VerticalPodAutoscaler
Metadata:
  Creation Timestamp:  2023-10-31T08:40:29Z
  Generation:          114
  Resource Version:    1237148614
  UID:                 47654dab-fbe0-4139-a937-d0b93bcaec9c
Spec:
  Resource Policy:
    Container Policies:
      Container Name:  nginx
      Min Allowed:
        Cpu:     150m
        Memory:  1Gi
      Mode:      Auto
  Target Ref:
    API Version:  apps/v1
    Kind:         Deployment
    Name:         my-deployment
  Update Policy:
    Update Mode:  Initial
Status:
  Conditions:
    Last Transition Time:  2023-10-31T08:44:28Z
    Status:                True
    Type:                  RecommendationProvided
  Recommendation:
    Container Recommendations:
      Container Name:  nginx
      Lower Bound:
        Cpu:     150m
        Memory:  1Gi
      Target:
        Cpu:     150m
        Memory:  1Gi
      Uncapped Target:
        Cpu:     25m
        Memory:  262144k
      Upper Bound:
        Cpu:     150m
        Memory:  1Gi
Events:          <none>
```

ありました。どうやら nginx コンテナの `resources.requests` の下限 （Min Allowed） を `CPU 150m` `Memory 1Gi` として Pod のリソースをスケールアップしているようです。`Mode: Auto`、`Update Mode:  Initial` になっているので、Pod の作成時にのみこれまでのリソース使用状況を見てスケールされています。

現状この Pod へのアクセスは少なく、リソースもそこまで使用していなかったので `resources.requests` が VPA の Min Allowed までスケールされています。

では VPA において `resources.limits` はどうスケールされるのでしょうか？ドキュメントを読んでみます。

>VPA will try to cap recommendations between min and max of limit ranges. If limit range conflicts and VPA resource policy conflict, VPA will follow VPA policy (and set values outside the limit range).

https://github.com/kubernetes/autoscaler/tree/master/vertical-pod-autoscaler#limits-control

なるほど。[Limit Ranges](https://kubernetes.io/docs/concepts/policy/limit-range/) や VPA Policy と設定がかち合わない限り、もともとの requests と limit の幅を維持してスケールするようですね。

CPU だと、もともとの Deｐloyment の requests が `100m` で Pod が `150m` へスケールしてるので、1.5倍 の変更です。limit はもともと `200m` なのでその 1.5倍の　`300m` になります。

ではメモリはというと、もともとの Deployment の requests が `100m` で…

`100m`…？ 0.1 Byte ってこと…？

0.1 Byte が `1GiB` までスケールするのでその倍率は… `1GiB` は 1073741824 Byte だから…

約107億4千万倍！！！！

limit の方はもともと `200Mi` なのでその107億4千万倍は…

__2000PiB！！！！！！！__

`limits.memory` がとんでもない数値になっていたのは VPA がスケールさせていたからでした。メモリの requests が `100m` なのは `100Mi` の間違いないのか、オーバーコミットのために 0 以外を設定しようとしたのかはもはや覚えていませんが、Kubernetes でメモリ設定する時に間違えて lower case で `m` と書いて問題になることはよくあるらしく、Warining が表示されるようになっていました。

https://github.com/kubernetes/kubernetes/blob/953afbb328939e748edc3c154bb1ab9248ff70e5/pkg/api/pod/warnings.go#L201-L203

https://github.com/kubernetes/kubernetes/issues/49442

https://github.com/kubernetes/kubernetes/issues/79950

## 犯人はわかったが計算が合わない

ところで現状 Pod には `200Pi` が設定されているので、理論上 `2000Pi` が設定されるのだとすると 0 が一個足りません。なんででしょう。考えてみます。

まず Kubernetes で設定できるメモリの最大数はどれくらいなんでしょう？

試しに Pod に `10EiB` のメモリを limit に指定して apply したところ、実際には `9223372036854775807`(=`8EiB`) が設定されました。

```yaml
apiVersion: v1
kind: Pod
metadata:
  name: my-pod
  namespace: irotoris
spec:
  containers:
  - image: public.ecr.aws/nginx/nginx:1.25
    name: nginx
    ports:
    - containerPort: 80
      protocol: TCP
    resources:
      limits:
        cpu: 300m
        memory: 10Ei
      requests:
        cpu: 100m
        memory: 50Mi
```

```
❯ kubectl -n irotoris describe po my-pod
Name:             my-pod

...(省略)...

    Limits:
      cpu:     300m
      memory:  9223372036854775807
    Requests:
      cpu:        100m
      memory:     50Mi

...(省略)...
```

これはメモリ設定が Kubernetes 内部の Go において int64 で表現されるためで、`math.MaxInt64` が上限になっていると思われます。つまり符号付き64ビット整数の最大値である2の63乗-1の `9223372036854775807` です。

Kubernetes で設定できる limits.memory は `2000PiB` 以上ということがわかりました。ということは VPA の方がなにかしてるのでしょうか？ 

VPA で limits.memory を計算している箇所はここです。今更ですが、筆者の環境では EKS 1.26, VPA 0.14 を使っています。 

https://github.com/kubernetes/autoscaler/blob/cd5ed0be530e2313f762e30042cb324b768507f6/vertical-pod-autoscaler/pkg/utils/vpa/limit_and_request_scaling.go#L155-L174

https://github.com/kubernetes/autoscaler/blob/cd5ed0be530e2313f762e30042cb324b768507f6/vertical-pod-autoscaler/pkg/utils/vpa/limit_and_request_scaling.go#L89C20-L89C20

```golang
func scaleQuantityProportionallyMem(scaledQuantity, scaleBase, scaleResult *resource.Quantity, rounding roundingMode) (*resource.Quantity, bool) {
	originalValue := big.NewInt(scaledQuantity.Value())
	scaleBaseValue := big.NewInt(scaleBase.Value())
	scaleResultValue := big.NewInt(scaleResult.Value())
	var scaledOriginal big.Int
	scaledOriginal.Mul(originalValue, scaleResultValue)
	scaledOriginal.Div(&scaledOriginal, scaleBaseValue)
	if scaledOriginal.IsInt64() {
		result := resource.NewQuantity(scaledOriginal.Int64(), scaledQuantity.Format)
		if rounding == roundUpToFullUnit {
			result.RoundUp(resource.Scale(0))
		}
		if rounding == roundDownToFullUnit {
			result.Sub(*resource.NewMilliQuantity(999, result.Format))
			result.RoundUp(resource.Scale(0))
		}
		return result, false
	}
	return resource.NewQuantity(math.MaxInt64, scaledQuantity.Format), true
}
```

うーんどうやら `big.NewInt(scaleBase.Value())` しているときに `100m` が `1` に丸まってる気配を感じます。動かして確認してみます。

```golang
package main

import (
	"fmt"
	"math/big"

	"k8s.io/apimachinery/pkg/api/resource"
)

func main() {
	mem := resource.MustParse("100m")
	memValue := big.NewInt(mem.Value())
	fmt.Printf("%v\n", mem)         // {{100 -3} {<nil>} 100m DecimalSI}
	fmt.Printf("%v\n", mem.Value()) // 1
	fmt.Printf("%v\n", memValue)    // 1
}
```

こいつですね。

>func (q *Quantity) Value() int64
Value returns the unscaled value of q rounded up to the nearest integer away from 0.

https://pkg.go.dev/k8s.io/apimachinery/pkg/api/resource#Quantity.Value

`limits.memory` を計算する際に もともとの `requests.memory` である `100m`(=0.1) が、VPA の内部では 1 と扱われてしまい、そこから VPA が `limits.memory` を計算したもんだから `2000Pi` となるところが `200Pi` となり、0 が一個足りない状態になっていると思われます。

## まとめ

ということで Kubernetes Pod の `resources.limits.memory` がいつのまにか 200PiB に設定されていた話は、Podの `resources.requests.memory` が非常に小さい値(`100m`)とされていたため、VerticalPodAutoscaler（VPA）がこの値を基に（仕様通りに）自動的にスケーリングを行った結果でした。

謎が解けてスッキリしました。