この記事は Wantedly Advent Calendar 2023 の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 というものがあります。そういえばこのクラスタにもインストールしていました。今思い出しました。
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).
なるほど。Limit Ranges や VPA Policy と設定がかち合わない限り、もともとの requests と limit の幅を維持してスケールするようですね。
CPU だと、もともとの Deployment の 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 が表示されるようになっていました。
犯人はわかったが計算が合わない
ところで現状 Pod には 200Pi
が設定されているので、理論上 2000Pi
が設定されるのだとすると 0 が一個足りません。なんででしょう。考えてみます。
まず Kubernetes で設定できるメモリの最大数はどれくらいなんでしょう?
試しに Pod に 10EiB
のメモリを limit に指定して apply したところ、実際には 9223372036854775807
(=8EiB
) が設定されました。
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 を使っています。
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
に丸まってる気配を感じます。動かして確認してみます。
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.
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)がこの値を基に(仕様通りに)自動的にスケーリングを行った結果でした。
謎が解けてスッキリしました。