[Sidecar Containers] Pod Eviction 時のメッセージの改善
はじめに
先日 Kubernetes で報告されていたバグを修正する PR を送りました。その時に、今後 Kubernetes へのコントリビュートを考えている方の参考になればと思い、どう取り組んだか (Issue の読み解き方やローカル環境での再現、コードの修正、テストの追加などの一通りの流れ) を脳内ダンプして言語化してみました。それを社内向けに共有していたのですが、PR も無事にマージされたので、一部加筆修正して記事として公開します。
- Issue: [Sidecar Containers] Eviction message should account for the sidecar containers
- PR: [Sidecar Containers] Consider init containers in eviction message
背景
SIG-Node の Chair で WG-Sidecar のリードでもある SergeyKanzhelev が Sidecar Containers 関連の Issue を 3 つ作成していました。どういうバグかと本来どうあるべきかが説明されていたので、その中から興味のある Issue を選びました。Issue ですぐに /assign
はせず、他の人に取られても良いかなくらいの気持ちで取り組み始めました。
- [Sidecar Containers] Pods comparison by maxContainerRestarts should account for sidecar containers
- [Sidecar Containers] Sidecar containers finish time needs to be accounted for in job controller
- [Sidecar Containers] Eviction message should account for the sidecar containers
KEP-753: Sidecar Containers は Kubernetes 1.31 時点でベータの機能です。Sidecar Containers の機能は SergeyKanzhelev と matthyx と gjkim42 と tzneal (最近見かけない) が中心となって開発しています。Kubernetes 1.32 では Sidecar Containers の機能は GA 昇格せず、GA に向けて既知のバグや課題を消化・整理し、Sidecar Containers の KEP からスピンオフした KEP-4438: Restarting sidecar containers during Pod termination の実装も進めていく予定です。そのため、今回 Issue として上がった細かい修正に関しては人手が足りない状態のようです。
Issue を読み解く
k/k#124938 は Pod の Eviction 時のメッセージの中に Restartable Init Containers (Sidecar Containers) が考慮されていないというバグです。
-
evictionMessage()
の中で Restartable Init Containers が考慮されていない - Pod が Evict された時に Restartable Init Containers のリソース要求も annotation の中に含めるべき
Kubernetes において Pod の Eviction (退避) が発生する要因は以下のとおりです。
- kube-scheduler が Node 上にこれ以上 Pod を起動できない場合に、優先度の低い Pod を Preemption して立ち退かせる (Pod Priority and Preemption)
- kubelet が Node のリソース逼迫を理由に Pod を立ち退かせる (Node-pressure Eviction)
- Pod に ephemeral-storage 上限を設定していて使用量が超過した場合に Pod を立ち退かせる(正確には 2. と同じ)
- kubectl drain などが内部的に呼び出している Eviction API によって Pod を立ち退かせる (API-initiated Eviction)
Kubernetes のコードベースを evictionMessage
で調べる (https://github.com/search?q=repo%3Akubernetes%2Fkubernetes evictionMessage&type=code) と kubelet の処理が引っ掛かります。ということで、今回修正したい Eviction は 2. の Node のリソース逼迫を理由とした Pod の立ち退きだと分かります。
次に evictionMessage() の処理を軽く眺めます。Node のリソース逼迫による Eviction が発生した理由と Eviction の情報を含んだ annotation を返す処理です。この処理は後から詳細に見ていくので、まずはこの情報がどう利用されているのかを追いかけます。Eviction の理由と annotation を evictPod() に渡しています。evictPod()
の処理を見てみるとこれらの情報を Kubernetes Event を通してユーザーに通知していることが分かります。
ここまで分かったら evictionMessage()
に戻って処理を見ていきます。stats()
で Pod のメトリクスを取得 しています。stats()
は makeSignalObservations()
から取得 しています。cachedStatsFunc()
を覗くと kubelet の Summary API から取得したメトリクスの情報を Pod 毎 (正確には Pod UID 毎) に詰め直していることが分かります。kubelet の Summary API に関しては Node metrics data に記載があり、kubectl コマンドでも情報が取得できます。
kubectl get --raw "/api/v1/nodes/kind-worker/proxy/stats/summary"
evictionMessage()
に戻って続きを見ていきます。kubelet の Summary API で取得した Pod のメトリクスの情報をコンテナ毎に見ていき、PodSpec の Regular Containers で定義しているコンテナ名と一致しているか を見ています。一致している場合は、PodSpec からコンテナのリソース要求を参照 し、超過したリソース (Ephemeral Storage / Memory) の使用量を Summary API から取得 します。で、超過したリソースの使用量とリソース要求を比較して、リソース要求よりも多くリソースを使用しているコンテナの情報を Event のメッセージに記載 します。最後に annotation にもリソース要求よりも使用量の高いコンテナ名と使用量、リソースの種類の情報を格納 しています。
ここまでの情報から以下のことが分かりました。
- Node のリソース逼迫の際に Pod の Eviction が発生し、その原因などを Kubernetes の Event を通してユーザーに通知している
- リソース逼迫した際に Pod 内の各コンテナのリソースの使用量は kubelet の Summary API から取得している
- リソース逼迫が発生した際に、Pod 内のコンテナ毎にリソースの使用量とリソース要求を比較して、超過しているコンテナの情報を Kubernetes の Event のメッセージや annotation に記載している
- Restartable Init Containers が含まれる Pod で Eviction が発生しても、Event の情報に Restartable Init Containers は含まれない
- PodSpec の Regular Containers に関してはリソース要求の比較を行っているが、Restartable Init Containers に関しては比較を行っていないのが原因
また、以下の点が気になりました。
- Restartable Init Containers がリソース超過した場合も Node のリソース逼迫による Eviction は発生するか?
- Restartable Init Containers のリソース使用量の情報は Summary API に含まれているか?
Pod の Eviction 時の挙動の再現
気になる点をローカル環境で実験しながら潰していきました。まず、kind を利用して Node ののメモリ逼迫の再現を目指しました。これが一番時間が掛かりました。
Node のメモリ逼迫の挙動を確認するための E2E テストを参考に、最初は kubelet の kube-reserved
の設定を増やして Node の割り当て可能なリソースを制限する方法を試しました。
cat <<EOF | kind create cluster --config -
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
image: kindest/node:latest
- role: worker
image: kindest/node:latest
kubeadmConfigPatches:
- |
kind: JoinConfiguration
nodeRegistration:
kubeletExtraArgs:
# ノードの割り当て可能な CPU 数を減らすためのハック
kube-reserved: "memory=4000Mi"
EOF
E2E テストで利用している PodSpec を参考 に stress-ng
コマンドでメモリ使用量を徐々に増やす Pod を作成します。E2E テストの場合、Node の割り当て可能なメモリの容量を見て動的に設定していますが、ローカル環境で再現する時には決めうちしました。
cat <<'EOF' | kubectl create -f -
apiVersion: v1
kind: Pod
metadata:
generateName: alloc-
spec:
terminationGracePeriodSeconds: 1
restartPolicy: Never
initContainers:
- name: el-sidecar1
image: registry.k8s.io/e2e-test-images/agnhost:2.52
args: ["stress", "--mem-alloc-size", "10Mi", "--mem-alloc-sleep", "1s", "--mem-total", "200Mi"]
restartPolicy: Always
- name: el-sidecar2
image: registry.k8s.io/e2e-test-images/agnhost:2.52
command: ["sleep", "infinity"]
restartPolicy: Always
- name: el-sidecar3
image: registry.k8s.io/e2e-test-images/agnhost:2.52
command: ["sleep", "infinity"]
restartPolicy: Always
containers:
- name: el1
image: registry.k8s.io/e2e-test-images/agnhost:2.52
command: ["sleep", "infinity"]
- name: el2
image: registry.k8s.io/e2e-test-images/agnhost:2.52
command: ["sleep", "infinity"]
- name: el3
image: registry.k8s.io/e2e-test-images/agnhost:2.52
command: ["sleep", "infinity"]
EOF
Pod が起動できたら kind の Node に exec してメモリの空き容量を確認します。
docker exec -it kind-worker bash
/# watch free -h
Every 2.0s: free -h kind-worker: Wed May 29 02:48:09 2024
total used free shared buff/cache available
Mem: 5.1Gi 1.3Gi 176Mi 29Mi 4.0Gi 3.8Gi
Swap: 7.1Gi 0B 7.1Gi
ただ、この方法だと stress-ng
コマンドで消費するメモリ使用量の調整が難しく、必ず Pod Eviction を発生させることができませんでした。
-
stress-ng
コマンドを実行しても OOMKill が発生しない -
stress-ng
コマンドを実行する Pod が Node のメモリ逼迫が発生する前に OOMKill される - ローカルマシンの他のプロセスのメモリ使用量にも左右されるため安定しない
この方法だと上手くいかないので、安定して Pod Eviction を発生させる別の方法を探すことにしました。今度は kubelet の設定で Eviction の閾値を変えるという方法を取りました。Node のリソース逼迫は eviction-soft
と eviction-hard
の 2 つの設定で調整できます。eviction-soft
と eviction-soft
は追い出す Pod の停止処理に違いがあります。
-
eviction-soft
: Pod の各コンテナに対して SIGTERM を送り、Pod のterminationGracePeriodSeconds
か kubelet のeviction-max-pod-grace-period
の設定のうち短い時間だけ待って、それでも停止しない場合に SIGKILL を送り強制的にプロセスを停止します。 -
eviction-hard
: Pod の各コンテナに対してgracePeriodSeconds
を 0 秒で上書きして強制的にコンテナを停止します。
eviction-soft
はデフォルトで設定されておらず、eviction-hard
のみ設定されています。メモリの空き容量の閾値はデフォルトで 100MiB となっています。このメモリの空き容量の閾値を高く設定します。
cat <<EOF | kind create cluster --config -
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
kubeadmConfigPatches:
- |
kind: KubeletConfiguration
evictionHard:
memory.available: "1.5Gi"
nodes:
- role: control-plane
image: kindest/node:v1.30.0@sha256:047357ac0cfea04663786a612ba1eaba9702bef25227a794b52890dd8bcd692e
- role: worker
image: kindest/node:v1.30.0@sha256:047357ac0cfea04663786a612ba1eaba9702bef25227a794b52890dd8bcd692e
EOF
stress-ng
コマンドでメモリ使用量を徐々に増やすコンテナを起動します。
cat <<'EOF' | kubectl create -f -
apiVersion: v1
kind: Pod
metadata:
generateName: alloc-
spec:
terminationGracePeriodSeconds: 1
restartPolicy: Never
initContainers:
- name: el-sidecar1
image: registry.k8s.io/e2e-test-images/agnhost:2.52
args: ["stress", "--mem-alloc-size", "100Mi", "--mem-alloc-sleep", "1s", "--mem-total", "4000Mi"]
restartPolicy: Always
- name: el-sidecar2
image: registry.k8s.io/e2e-test-images/agnhost:2.52
command: ["sleep", "infinity"]
restartPolicy: Always
- name: el-sidecar3
image: registry.k8s.io/e2e-test-images/agnhost:2.52
command: ["sleep", "infinity"]
restartPolicy: Always
containers:
- name: el1
image: registry.k8s.io/e2e-test-images/agnhost:2.52
command: ["sleep", "infinity"]
- name: el2
image: registry.k8s.io/e2e-test-images/agnhost:2.52
command: ["sleep", "infinity"]
- name: el3
image: registry.k8s.io/e2e-test-images/agnhost:2.52
command: ["sleep", "infinity"]
EOF
数十秒後に Pod Eviction が発生します。
Event の詳細
apiVersion: v1
count: 1
eventTime: null
firstTimestamp: "2024-05-29T05:35:26Z"
involvedObject:
apiVersion: v1
kind: Pod
name: alloc-xndq5
namespace: default
resourceVersion: "501"
uid: 17d1f6d1-b2a9-4226-bfc2-31137afd74d4
kind: Event
lastTimestamp: "2024-05-29T05:35:26Z"
message: 'The node was low on resource: memory. Threshold quantity: 1536Mi, available:
1516448Ki. Container el1 was using 48Ki, request is 0, has larger consumption
of memory. Container el2 was using 48Ki, request is 0, has larger consumption
of memory. Container el3 was using 48Ki, request is 0, has larger consumption
of memory. '
metadata:
annotations:
offending_containers: el1,el2,el3
offending_containers_usage: 48Ki,48Ki,48Ki
starved_resource: memory
creationTimestamp: "2024-05-29T05:35:26Z"
name: alloc-xndq5.17d3deddb8dd5016
namespace: default
resourceVersion: "601"
uid: 65c745da-0cae-4c59-b37d-1e98baca2f26
reason: Evicted
reportingComponent: kubelet
reportingInstance: kind-worker
source:
component: kubelet
host: kind-worker
type: Warning
Pod の詳細
❯ kubectl describe pods alloc-xndq5
Name: alloc-xndq5
Namespace: default
Priority: 0
Service Account: default
Node: kind-worker/192.168.228.2
Start Time: Wed, 29 May 2024 14:34:34 +0900
Labels: <none>
Annotations: <none>
Status: Failed
Reason: Evicted
Message: The node was low on resource: memory. Threshold quantity: 1536Mi, available: 1516448Ki. Container el1 was using 48Ki, request is 0, has larger consumption of memory. Container el2 was using 48Ki, request is 0, has larger consumption of memory. Container el3 was using 48Ki, request is 0, has larger consumption of memory.
IP: 10.244.1.2
IPs:
IP: 10.244.1.2
Init Containers:
el-sidecar1:
Container ID:
Image: registry.k8s.io/e2e-test-images/agnhost:2.52
Image ID:
Port: <none>
Host Port: <none>
Args:
stress
--mem-alloc-size
100Mi
--mem-alloc-sleep
1s
--mem-total
4000Mi
State: Terminated
Reason: ContainerStatusUnknown
Message: The container could not be located when the pod was terminated
Exit Code: 137
Started: Mon, 01 Jan 0001 00:00:00 +0000
Finished: Mon, 01 Jan 0001 00:00:00 +0000
Last State: Terminated
Reason: ContainerStatusUnknown
Message: The container could not be located when the pod was deleted. The container used to be Running
Exit Code: 137
Started: Mon, 01 Jan 0001 00:00:00 +0000
Finished: Mon, 01 Jan 0001 00:00:00 +0000
Ready: False
Restart Count: 1
Environment: <none>
Mounts:
/var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-cmk5f (ro)
el-sidecar2:
Container ID: containerd://2fbc36327f16ffc4303d5e6ea31eca2fa1d7fe2dc2ba5147ac06092b7dc3fd50
Image: registry.k8s.io/e2e-test-images/agnhost:2.52
Image ID: registry.k8s.io/e2e-test-images/agnhost@sha256:b173c7d0ffe3d805d49f4dfe48375169b7b8d2e1feb81783efd61eb9d08042e6
Port: <none>
Host Port: <none>
Command:
sleep
infinity
State: Terminated
Reason: Error
Exit Code: 137
Started: Wed, 29 May 2024 14:34:45 +0900
Finished: Wed, 29 May 2024 14:35:29 +0900
Ready: False
Restart Count: 0
Environment: <none>
Mounts:
/var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-cmk5f (ro)
el-sidecar3:
Container ID: containerd://7f605897f7eb10021accb5e879c9dc5249b37b596dc8c25d30835d8bc0f5269f
Image: registry.k8s.io/e2e-test-images/agnhost:2.52
Image ID: registry.k8s.io/e2e-test-images/agnhost@sha256:b173c7d0ffe3d805d49f4dfe48375169b7b8d2e1feb81783efd61eb9d08042e6
Port: <none>
Host Port: <none>
Command:
sleep
infinity
State: Terminated
Reason: Error
Exit Code: 137
Started: Wed, 29 May 2024 14:34:46 +0900
Finished: Wed, 29 May 2024 14:35:29 +0900
Ready: False
Restart Count: 0
Environment: <none>
Mounts:
/var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-cmk5f (ro)
Containers:
el1:
Container ID:
Image: registry.k8s.io/e2e-test-images/agnhost:2.52
Image ID:
Port: <none>
Host Port: <none>
Command:
sleep
infinity
State: Terminated
Reason: ContainerStatusUnknown
Message: The container could not be located when the pod was terminated
Exit Code: 137
Started: Mon, 01 Jan 0001 00:00:00 +0000
Finished: Mon, 01 Jan 0001 00:00:00 +0000
Last State: Terminated
Reason: ContainerStatusUnknown
Message: The container could not be located when the pod was deleted. The container used to be Running
Exit Code: 137
Started: Mon, 01 Jan 0001 00:00:00 +0000
Finished: Mon, 01 Jan 0001 00:00:00 +0000
Ready: False
Restart Count: 1
Environment: <none>
Mounts:
/var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-cmk5f (ro)
el2:
Container ID:
Image: registry.k8s.io/e2e-test-images/agnhost:2.52
Image ID:
Port: <none>
Host Port: <none>
Command:
sleep
infinity
State: Terminated
Reason: ContainerStatusUnknown
Message: The container could not be located when the pod was terminated
Exit Code: 137
Started: Mon, 01 Jan 0001 00:00:00 +0000
Finished: Mon, 01 Jan 0001 00:00:00 +0000
Last State: Terminated
Reason: ContainerStatusUnknown
Message: The container could not be located when the pod was deleted. The container used to be Running
Exit Code: 137
Started: Mon, 01 Jan 0001 00:00:00 +0000
Finished: Mon, 01 Jan 0001 00:00:00 +0000
Ready: False
Restart Count: 1
Environment: <none>
Mounts:
/var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-cmk5f (ro)
el3:
Container ID:
Image: registry.k8s.io/e2e-test-images/agnhost:2.52
Image ID:
Port: <none>
Host Port: <none>
Command:
sleep
infinity
State: Terminated
Reason: ContainerStatusUnknown
Message: The container could not be located when the pod was terminated
Exit Code: 137
Started: Mon, 01 Jan 0001 00:00:00 +0000
Finished: Mon, 01 Jan 0001 00:00:00 +0000
Last State: Terminated
Reason: ContainerStatusUnknown
Message: The container could not be located when the pod was deleted. The container used to be Running
Exit Code: 137
Started: Mon, 01 Jan 0001 00:00:00 +0000
Finished: Mon, 01 Jan 0001 00:00:00 +0000
Ready: False
Restart Count: 1
Environment: <none>
Mounts:
/var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-cmk5f (ro)
Conditions:
Type Status
DisruptionTarget True
PodReadyToStartContainers False
Initialized False
Ready False
ContainersReady False
PodScheduled True
Volumes:
kube-api-access-cmk5f:
Type: Projected (a volume that contains injected data from multiple sources)
TokenExpirationSeconds: 3607
ConfigMapName: kube-root-ca.crt
ConfigMapOptional: <nil>
DownwardAPI: true
QoS Class: BestEffort
Node-Selectors: <none>
Tolerations: node.kubernetes.io/not-ready:NoExecute op=Exists for 300s
node.kubernetes.io/unreachable:NoExecute op=Exists for 300s
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 92s default-scheduler Successfully assigned default/alloc-xndq5 to kind-worker
Normal Pulling 92s kubelet Pulling image "registry.k8s.io/e2e-test-images/agnhost:2.52"
Normal Pulled 81s kubelet Successfully pulled image "registry.k8s.io/e2e-test-images/agnhost:2.52" in 10.213s (10.213s including waiting). Image size: 50208832 bytes.
Normal Created 81s kubelet Created container el-sidecar1
Normal Started 81s kubelet Started container el-sidecar1
Normal Pulled 81s kubelet Container image "registry.k8s.io/e2e-test-images/agnhost:2.52" already present on machine
Normal Created 81s kubelet Created container el-sidecar2
Normal Started 81s kubelet Started container el-sidecar2
Normal Pulled 80s kubelet Container image "registry.k8s.io/e2e-test-images/agnhost:2.52" already present on machine
Normal Created 80s kubelet Created container el-sidecar3
Normal Started 80s kubelet Started container el-sidecar3
Normal Pulled 79s kubelet Container image "registry.k8s.io/e2e-test-images/agnhost:2.52" already present on machine
Normal Created 79s kubelet Created container el1
Normal Started 79s kubelet Started container el1
Normal Pulled 79s kubelet Container image "registry.k8s.io/e2e-test-images/agnhost:2.52" already present on machine
Normal Created 79s kubelet Created container el2
Normal Started 79s kubelet Started container el2
Normal Pulled 79s kubelet Container image "registry.k8s.io/e2e-test-images/agnhost:2.52" already present on machine
Normal Created 78s kubelet Created container el3
Normal Started 78s kubelet Started container el3
Warning Evicted 40s kubelet The node was low on resource: memory. Threshold quantity: 1536Mi, available: 1516448Ki. Container el1 was using 48Ki, request is 0, has larger consumption of memory. Container el2 was using 48Ki, request is 0, has larger consumption of memory. Container el3 was using 48Ki, request is 0, has larger consumption of memory.
Normal Killing 40s kubelet Stopping container el-sidecar1
Normal Killing 40s kubelet Stopping container el3
Normal Killing 40s kubelet Stopping container el1
Normal Killing 40s kubelet Stopping container el2
Normal Killing 40s kubelet Stopping container el-sidecar2
Event のメッセージ
message: 'The node was low on resource: memory. Threshold quantity: 1536Mi, available:
1516448Ki. Container el1 was using 48Ki, request is 0, has larger consumption
of memory. Container el2 was using 48Ki, request is 0, has larger consumption
of memory. Container el3 was using 48Ki, request is 0, has larger consumption
of memory. '
Event の annotation
metadata:
annotations:
offending_containers: el1,el2,el3
offending_containers_usage: 48Ki,48Ki,48Ki
starved_resource: memory
上記から確かに Event のメッセージや annotation の情報の中に Restartable Init Containers の情報が含まれていないことが分かりました。
Summary API の挙動の実験
Pod Eviction を常に発生することができる手順を手に入れたので、実験を繰り返しながら Pod Eviction の挙動を確認します。知りたいのは以下の挙動です。
Restartable Init Containers のリソース使用量の情報は Summary API に含まれているか?
仮に Summary API に Restartable Init Containers のメトリクスが含まれているなら、コードの修正は少なくて済みます。
Restartable Init Containers を含んだ Pod を作成します。今回は stress-ng
コマンドで負荷を掛ける必要がないのでコマンドを変えています。
cat <<'EOF' | kubectl create -f -
apiVersion: v1
kind: Pod
metadata:
generateName: alloc-
spec:
terminationGracePeriodSeconds: 1
restartPolicy: Never
initContainers:
- name: el-sidecar1
image: registry.k8s.io/e2e-test-images/agnhost:2.52
command: ["sleep", "infinity"]
restartPolicy: Always
- name: el-sidecar2
image: registry.k8s.io/e2e-test-images/agnhost:2.52
command: ["sleep", "infinity"]
restartPolicy: Always
- name: el-sidecar3
image: registry.k8s.io/e2e-test-images/agnhost:2.52
command: ["sleep", "infinity"]
restartPolicy: Always
containers:
- name: el1
image: registry.k8s.io/e2e-test-images/agnhost:2.52
command: ["sleep", "infinity"]
- name: el2
image: registry.k8s.io/e2e-test-images/agnhost:2.52
command: ["sleep", "infinity"]
- name: el3
image: registry.k8s.io/e2e-test-images/agnhost:2.52
command: ["sleep", "infinity"]
EOF
Summary API を叩くと Restartable Init Containers のメトリクスも含まれていることが分かります。
kubectl get --raw "/api/v1/nodes/kind-worker/proxy/stats/summary"
Summary API の一部
{
(...)
"pods": [
(...)
{
"podRef": {
"name": "alloc-d4w7d",
"namespace": "default",
"uid": "a6e41259-1afc-4d08-8525-50f453c08f47"
},
"startTime": "2024-05-29T05:49:00Z",
"containers": [
{
"name": "el3",
"startTime": "2024-05-29T05:49:02Z",
"cpu": {
"time": "2024-05-29T05:49:13Z",
"usageNanoCores": 54709,
"usageCoreNanoSeconds": 41096000
},
"memory": {
"time": "2024-05-29T05:49:13Z",
"usageBytes": 49152,
"workingSetBytes": 49152,
"rssBytes": 49152,
"pageFaults": 1581,
"majorPageFaults": 0
},
"rootfs": {
"time": "2024-05-29T05:49:18Z",
"availableBytes": 16804392960,
"capacityBytes": 70218711040,
"usedBytes": 4096,
"inodesFree": 0,
"inodes": 0,
"inodesUsed": 8
},
"logs": {
"time": "2024-05-29T05:49:21Z",
"availableBytes": 16804392960,
"capacityBytes": 70218711040,
"usedBytes": 0,
"inodesFree": 0,
"inodes": 0,
"inodesUsed": 1
},
"swap": {
"time": "2024-05-29T05:49:21Z",
"swapAvailableBytes": 0,
"swapUsageBytes": 0
}
},
{
"name": "el-sidecar1",
"startTime": "2024-05-29T05:49:00Z",
"cpu": {
"time": "2024-05-29T05:49:14Z",
"usageNanoCores": 570891,
"usageCoreNanoSeconds": 43136000
},
"memory": {
"time": "2024-05-29T05:49:14Z",
"usageBytes": 2220032,
"workingSetBytes": 1953792,
"rssBytes": 49152,
"pageFaults": 1584,
"majorPageFaults": 7
},
"rootfs": {
"time": "2024-05-29T05:49:18Z",
"availableBytes": 16804392960,
"capacityBytes": 70218711040,
"usedBytes": 4096,
"inodesFree": 0,
"inodes": 0,
"inodesUsed": 8
},
"logs": {
"time": "2024-05-29T05:49:21Z",
"availableBytes": 16804392960,
"capacityBytes": 70218711040,
"usedBytes": 0,
"inodesFree": 0,
"inodes": 0,
"inodesUsed": 1
},
"swap": {
"time": "2024-05-29T05:49:21Z",
"swapAvailableBytes": 0,
"swapUsageBytes": 0
}
},
{
"name": "el-sidecar2",
"startTime": "2024-05-29T05:49:00Z",
"cpu": {
"time": "2024-05-29T05:49:20Z",
"usageNanoCores": 17135,
"usageCoreNanoSeconds": 25895000
},
"memory": {
"time": "2024-05-29T05:49:20Z",
"usageBytes": 49152,
"workingSetBytes": 49152,
"rssBytes": 49152,
"pageFaults": 1692,
"majorPageFaults": 0
},
"rootfs": {
"time": "2024-05-29T05:49:18Z",
"availableBytes": 16804392960,
"capacityBytes": 70218711040,
"usedBytes": 4096,
"inodesFree": 0,
"inodes": 0,
"inodesUsed": 8
},
"logs": {
"time": "2024-05-29T05:49:21Z",
"availableBytes": 16804392960,
"capacityBytes": 70218711040,
"usedBytes": 0,
"inodesFree": 0,
"inodes": 0,
"inodesUsed": 1
},
"swap": {
"time": "2024-05-29T05:49:21Z",
"swapAvailableBytes": 0,
"swapUsageBytes": 0
}
},
{
"name": "el-sidecar3",
"startTime": "2024-05-29T05:49:01Z",
"cpu": {
"time": "2024-05-29T05:49:15Z",
"usageNanoCores": 27182,
"usageCoreNanoSeconds": 28664000
},
"memory": {
"time": "2024-05-29T05:49:15Z",
"usageBytes": 45056,
"workingSetBytes": 45056,
"rssBytes": 45056,
"pageFaults": 1571,
"majorPageFaults": 0
},
"rootfs": {
"time": "2024-05-29T05:49:18Z",
"availableBytes": 16804392960,
"capacityBytes": 70218711040,
"usedBytes": 4096,
"inodesFree": 0,
"inodes": 0,
"inodesUsed": 8
},
"logs": {
"time": "2024-05-29T05:49:21Z",
"availableBytes": 16804392960,
"capacityBytes": 70218711040,
"usedBytes": 0,
"inodesFree": 0,
"inodes": 0,
"inodesUsed": 1
},
"swap": {
"time": "2024-05-29T05:49:21Z",
"swapAvailableBytes": 0,
"swapUsageBytes": 0
}
},
{
"name": "el1",
"startTime": "2024-05-29T05:49:02Z",
"cpu": {
"time": "2024-05-29T05:49:21Z",
"usageNanoCores": 0,
"usageCoreNanoSeconds": 22773000
},
"memory": {
"time": "2024-05-29T05:49:21Z",
"workingSetBytes": 49152
},
"rootfs": {
"time": "2024-05-29T05:49:18Z",
"availableBytes": 16804392960,
"capacityBytes": 70218711040,
"usedBytes": 4096,
"inodesFree": 0,
"inodes": 0,
"inodesUsed": 8
},
"logs": {
"time": "2024-05-29T05:49:21Z",
"availableBytes": 16804392960,
"capacityBytes": 70218711040,
"usedBytes": 0,
"inodesFree": 0,
"inodes": 0,
"inodesUsed": 1
},
"swap": {
"time": "2024-05-29T05:49:21Z",
"swapAvailableBytes": 0,
"swapUsageBytes": 0
}
},
{
"name": "el2",
"startTime": "2024-05-29T05:49:02Z",
"cpu": {
"time": "2024-05-29T05:49:18Z",
"usageNanoCores": 24561,
"usageCoreNanoSeconds": 22993000
},
"memory": {
"time": "2024-05-29T05:49:18Z",
"usageBytes": 49152,
"workingSetBytes": 49152,
"rssBytes": 49152,
"pageFaults": 1685,
"majorPageFaults": 0
},
"rootfs": {
"time": "2024-05-29T05:49:18Z",
"availableBytes": 16804392960,
"capacityBytes": 70218711040,
"usedBytes": 4096,
"inodesFree": 0,
"inodes": 0,
"inodesUsed": 8
},
"logs": {
"time": "2024-05-29T05:49:21Z",
"availableBytes": 16804392960,
"capacityBytes": 70218711040,
"usedBytes": 0,
"inodesFree": 0,
"inodes": 0,
"inodesUsed": 1
},
"swap": {
"time": "2024-05-29T05:49:21Z",
"swapAvailableBytes": 0,
"swapUsageBytes": 0
}
}
],
"cpu": {
"time": "2024-05-29T05:49:13Z",
"usageNanoCores": 14648961,
"usageCoreNanoSeconds": 194774000
},
"memory": {
"time": "2024-05-29T05:49:13Z",
"usageBytes": 3014656,
"workingSetBytes": 2732032,
"rssBytes": 327680,
"pageFaults": 10797,
"majorPageFaults": 8
},
"network": {
"time": "2024-05-29T05:49:19Z",
"name": "eth0",
"rxBytes": 0,
"rxErrors": 0,
"txBytes": 656,
"txErrors": 0,
"interfaces": [
{
"name": "tunl0",
"rxBytes": 0,
"rxErrors": 0,
"txBytes": 0,
"txErrors": 0
},
{
"name": "eth0",
"rxBytes": 0,
"rxErrors": 0,
"txBytes": 656,
"txErrors": 0
}
]
},
"volume": [
{
"time": "2024-05-29T05:49:16Z",
"availableBytes": 3894149120,
"capacityBytes": 3894161408,
"usedBytes": 12288,
"inodesFree": 671960,
"inodes": 671969,
"inodesUsed": 9,
"name": "kube-api-access-7bf44"
}
],
"ephemeral-storage": {
"time": "2024-05-29T05:49:21Z",
"availableBytes": 16804392960,
"capacityBytes": 70218711040,
"usedBytes": 28672,
"inodesFree": 0,
"inodes": 0,
"inodesUsed": 55
},
"process_stats": {
"process_count": 0
},
"swap": {
"time": "2024-05-29T05:49:13Z",
"swapUsageBytes": 0
}
}
]
}
Summary API の実験結果からコードの修正箇所が想像できました。Summary API から取得した各コンテナのリソース使用量を Regular Containers だけでなく、InitContainers も含めて比較するように書き換えれば良さそうです。
// 疑似コード
for _, containerStats := range podStats.Containers {
for _, container := range [pod.Spec.Containers, pod.Spec.InitContainers] {
if container.Name == containerStats.Name {
(...)
}
}
}
コードの修正
コードの修正方針が決まったので、対応していきます。今回は以下の流れでコードの修正と動作確認を行います。Kubernetes の組織に参加していて PR 上でテストを手動でトリガーできる場合は、そちらで確認した方が早いかもしれません。
- ローカル環境にフォークした Kubernetes のリポジトリを用意
- コードを修正
- kind でコードの修正を含んだ Node のイメージをセルフビルド
- 再現手順を元に手動で動作確認
Pod Eviction や Summary API の挙動の実験から Pod Eviction に Restartable Init Containers 専用の処理は不要だと分かりました。修正はシンプルですが、一つ気になる点があります。Issue には Eviction のメッセージに Restartable Init Containers が考慮されていないとしか書かれていないですが、どうやら Init Containers も考慮されていなさそうです。迷いましたが、最初は Restartable Init Containers のみを考慮する方針でコードを書きました。(ただ、後から Init Containers も含めて修正した方が良さそうと思い直しました。)
Restartable Init Containers だけを考慮するなら、Sidecar Containers の FeatureGates が有効な時にだけ InitContainers の中で定義された Restartable Init Containers を含めます。
for _, containerStats := range podStats.Containers {
containers := pod.Spec.Containers
if utilfeature.DefaultFeatureGate.Enabled(features.SidecarContainers) {
for _, initContainer := range pod.Spec.InitContainers {
if initContainer.RestartPolicy != nil && *initContainer.RestartPolicy == v1.ContainerRestartPolicyAlways {
containers = append(containers, initContainer)
}
}
}
for _, container := range containers {
(...)
同様に annotation に関してもコンテナ名と使用量を考慮するようにコードを修正しました。
Kubernetes のソースコードに変更を加えたら、kind の Node のイメージを新しくビルドして動作確認します。
kindest/node:latest
のタグでコンテナイメージをビルド
cd path/to/kubernetes
kind build node-image .
セルフビルドした Node のコンテナイメージを利用して kind でローカルクラスタを起動
cat <<EOF | kind create cluster --config -
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
kubeadmConfigPatches:
- |
kind: KubeletConfiguration
evictionHard:
memory.available: "1.5Gi"
nodes:
- role: control-plane
image: kindest/node:latest
- role: worker
image: kindest/node:latest
EOF
Restartable Init Containers を起動して Pod の Eviction メッセージが期待通りに変化したか確認
cat <<'EOF' | kubectl create -f -
apiVersion: v1
kind: Pod
metadata:
generateName: alloc-
spec:
terminationGracePeriodSeconds: 1
restartPolicy: Never
initContainers:
- name: el-sidecar1
image: registry.k8s.io/e2e-test-images/agnhost:2.52
args: ["stress", "--mem-alloc-size", "100Mi", "--mem-alloc-sleep", "1s", "--mem-total", "4000Mi"]
restartPolicy: Always
- name: el-sidecar2
image: registry.k8s.io/e2e-test-images/agnhost:2.52
command: ["sleep", "infinity"]
restartPolicy: Always
- name: el-sidecar3
image: registry.k8s.io/e2e-test-images/agnhost:2.52
command: ["sleep", "infinity"]
restartPolicy: Always
containers:
- name: el1
image: registry.k8s.io/e2e-test-images/agnhost:2.52
command: ["sleep", "infinity"]
- name: el2
image: registry.k8s.io/e2e-test-images/agnhost:2.52
command: ["sleep", "infinity"]
- name: el3
image: registry.k8s.io/e2e-test-images/agnhost:2.52
command: ["sleep", "infinity"]
EOF
修正後の Event
apiVersion: v1
count: 1
eventTime: null
firstTimestamp: "2024-05-19T08:14:14Z"
involvedObject:
apiVersion: v1
kind: Pod
name: alloc-lm9sq
namespace: default
resourceVersion: "19979"
uid: 786af1f6-3b04-4385-b61f-acfca78ddfd6
kind: Event
lastTimestamp: "2024-05-19T08:14:14Z"
message: 'The node was low on resource: memory. Threshold quantity: 1536Mi, available:
1208704Ki. Container el1 was using 44Ki, request is 0, has larger consumption
of memory. Container el2 was using 48Ki, request is 0, has larger consumption
of memory. Container el3 was using 48Ki, request is 0, has larger consumption
of memory. Container el-sidecar1 was using 3818464Ki, request is 0, has larger
consumption of memory. Container el-sidecar2 was using 1452Ki, request is 0, has
larger consumption of memory. Container el-sidecar3 was using 48Ki, request is
0, has larger consumption of memory. '
metadata:
annotations:
offending_containers: el1,el2,el3,el-sidecar1,el-sidecar2,el-sidecar3
offending_containers_usage: 44Ki,48Ki,48Ki,3818464Ki,1452Ki,48Ki
starved_resource: memory
creationTimestamp: "2024-05-19T08:14:14Z"
name: alloc-lm9sq.17d0d5ba642ef84c
namespace: default
resourceVersion: "20072"
uid: 521c02b7-4935-4def-be2c-53a76c32f4f7
reason: Evicted
reportingComponent: kubelet
reportingInstance: kind-worker
source:
component: kubelet
host: kind-worker
type: Warning
期待通りの挙動に修正できていることが確認できました。
テストの追加
Kubernetes に機能を追加したり、バグ修正する場合はテストを書く必要があります。テストを書いていない変更はレビュー時に必ず指摘されます。Kubernetes では複数の種類のテストが存在します。
- ユニットテスト
- 結合テスト
- E2E テスト
- Conformance テスト
- ファジングテスト
- パフォーマンステスト
- ソークテスト
変更箇所にもよるのですが、機能追加やバグ修正を行う場合、ユニットテスト、結合テスト、E2E テストを書く可能性があります。全ての挙動をテストでカバーするのが理想ですが、現実はそうではありません。ユニットテスト < 結合テスト < E2E テスト の順番でコストが高くなるので、テストケースもカバー率も減る傾向にあります。ユニットテストはほぼ必須ですが、それ以外は変更の大きさやテストのし易さによって変わってきます。
今回の変更はユニットテストだけを書くことにしました。最初は E2E テストを書く or 修正するつもりでいました。ただ、(Restartable) Init Containers 向けの Pod Eviction の E2E テストが存在せず、Pod Eviction の E2E テストは Serial (他のテストと並列実行できない)、Disruptive (Kubernetes クラスタの機能に影響する設定を注入したり、挙動を調べる)、Slow (時間の掛かるテスト?) とマークされていてコストの高いテストになります。Pod Eviction に関連する処理に変更は加えていないので、このバグ修正のために新たに E2E テストを追加するのはコスト対効果が見合わないと判断しました。
Kubernetes のテストでは Pod や Node などのオブジェクトをテストの入力や期待する結果として指定することが多いです。そのため、ユニットテストはテーブル駆動テストとして書くことが多いです。テーブル駆動テストで Pod の構造体を全て書くとテストケースが見辛くなります。Tim Hockin も k/k#116260 でテーブル駆動テストのリファクタリングを提案しているくらいです。
以下のように書くのではなく、
restartPolicyAlways := v1.ContainerRestartPolicyAlways
testcase := []struct {
name string
initContainers []v1.Container
containers []v1.Container
containerMemoryStats []containerMemoryStat
expectedContainerMessage string
expectedAnnotations map[string]string
}{
{
name: "No container exceeds memory usage",
initContainers: []v1.Container{
{
Name: "testcontainer-init",
Resources: v1.ResourceRequirements{
Requests: v1.ResourceList{
v1.ResourceMemory: resource.MustParse("300Mi"),
},
},
},
{
Name: "testcontainer-sidecar",
Resources: v1.ResourceRequirements{
Requests: v1.ResourceList{
v1.ResourceMemory: resource.MustParse("200Mi"),
},
},
RestartPolicy: &restartPolicyAlways,
},
},
containers: []v1.Container{
{
Name: "testcontainer",
Resources: v1.ResourceRequirements{
Requests: v1.ResourceList{
v1.ResourceMemory: resource.MustParse("200Mi"),
},
},
},
},
containerMemoryStats: []containerMemoryStat{
{name: "testcontainer-sidecar", usage: "150Mi"},
{name: "testcontainer", usage: "100Mi"},
},
expectedContainerMessage: memoryExceededEvictionMessage(nil),
}
}
以下のように書けるようにしました。
testcase := []struct {
name string
initContainers []v1.Container
containers []v1.Container
containerMemoryStats []containerMemoryStat
expectedContainerMessage string
expectedAnnotations map[string]string
}{
{
name: "No container exceeds memory usage",
initContainers: []v1.Container{
newContainer("testcontainer-init", newResourceList("", "300Mi", ""), newResourceList("", "", "")),
newRestartableInitContainer("testcontainer-sidecar", newResourceList("", "200Mi", ""), newResourceList("", "", "")),
},
containers: []v1.Container{
newContainer("testcontainer", newResourceList("", "200Mi", ""), newResourceList("", "", "")),
},
containerMemoryStats: []containerMemoryStat{
{name: "testcontainer-sidecar", usage: "150Mi"},
{name: "testcontainer", usage: "100Mi"},
},
expectedContainerMessage: memoryExceededEvictionMessage(nil),
}
}
テストにはヘルパー関数が用意されています。自分の書きたいテストに合わせて新しいヘルパー関数を追加したり、既存のヘルパー関数を拡張するのが良いです。今回のテストでは Restartable Init Containers を作成するヘルパー関数が欲しかったので追加しました。既存の newContainer をラップした単純な関数です。
func newRestartableInitContainer(name string, requests v1.ResourceList, limits v1.ResourceList) v1.Container {
restartAlways := v1.ContainerRestartPolicyAlways
container := newContainer(name, requests, limits)
container.RestartPolicy = &restartAlways
return container
}
ユニットテストを実行します。今回追加した TestEvictionMessageWithInitContainers
に対して実行します。
❯ go test -timeout 30s -count=1 -race -run ^TestEvictionMessageWithInitContainers$ k8s.io/kubernetes/pkg/kubelet/eviction -v
=== RUN TestEvictionMessageWithInitContainers
=== RUN TestEvictionMessageWithInitContainers/No_container_exceeds_memory_usage
=== RUN TestEvictionMessageWithInitContainers/Init_container_exceeds_memory_usage
=== RUN TestEvictionMessageWithInitContainers/Restartable_init_container_exceeds_memory_usage
=== RUN TestEvictionMessageWithInitContainers/Regular_container_exceeds_memory_usage
=== RUN TestEvictionMessageWithInitContainers/Regular_and_init_containers_exceed_memory_usage_due_to_missing_memory_requests
=== RUN TestEvictionMessageWithInitContainers/Regular_and_restartable_init_containers_exceed_memory_usage_due_to_missing_memory_requests
=== RUN TestEvictionMessageWithInitContainers/All_types_of_containers_exceed_memory_usage_due_to_missing_memory_requests
=== RUN TestEvictionMessageWithInitContainers/Multiple_regular_and_(non-)restartable_init_containers_exceed_memory_usage
--- PASS: TestEvictionMessageWithInitContainers (0.00s)
--- PASS: TestEvictionMessageWithInitContainers/No_container_exceeds_memory_usage (0.00s)
--- PASS: TestEvictionMessageWithInitContainers/Init_container_exceeds_memory_usage (0.00s)
--- PASS: TestEvictionMessageWithInitContainers/Restartable_init_container_exceeds_memory_usage (0.00s)
--- PASS: TestEvictionMessageWithInitContainers/Regular_container_exceeds_memory_usage (0.00s)
--- PASS: TestEvictionMessageWithInitContainers/Regular_and_init_containers_exceed_memory_usage_due_to_missing_memory_requests (0.00s)
--- PASS: TestEvictionMessageWithInitContainers/Regular_and_restartable_init_containers_exceed_memory_usage_due_to_missing_memory_requests (0.00s)
--- PASS: TestEvictionMessageWithInitContainers/All_types_of_containers_exceed_memory_usage_due_to_missing_memory_requests (0.00s)
--- PASS: TestEvictionMessageWithInitContainers/Multiple_regular_and_(non-)restartable_init_containers_exceed_memory_usage (0.00s)
PASS
ok k8s.io/kubernetes/pkg/kubelet/eviction 1.865s
一部のユニットテストは連続で実行すると失敗する Flaky な要素を持っている可能性があります。テストを並列で複数回実行して Flaky なテストを検知するために、stress を利用します。
go install golang.org/x/tools/cmd/stress@latest
stress を利用しながらユニットテストを実行します。
go test k8s.io/kubernetes/pkg/kubelet/eviction -race -c
stress ./eviction.test -test.run TestEvictionMessageWithInitContainers
実行例
❯ stress ./eviction.test -test.run TestEvictionMessageWithInitContainers
5s: 24 runs so far, 0 failures
10s: 56 runs so far, 0 failures
15s: 96 runs so far, 0 failures
20s: 128 runs so far, 0 failures
25s: 160 runs so far, 0 failures
30s: 200 runs so far, 0 failures
35s: 232 runs so far, 0 failures
40s: 264 runs so far, 0 failures
45s: 304 runs so far, 0 failures
50s: 336 runs so far, 0 failures
55s: 376 runs so far, 0 failures
1m0s: 408 runs so far, 0 failures
(...)
レビュー対応
PR を作成した際に Sidecar Containers の関係者に自ら /cc
してレビューを依頼していたため、最初のレビューは比較的すぐに受けられました。
/cc @SergeyKanzhelev @matthyx @gjkim42
まず、Sidecar Containers の実装者である Gunju さんから指摘を受けました。Pod Eviction で Restartable Init Containers 向けにロジックを変更したつもりがないので、Pod Eviction 時のメッセージに Restartable Init Containers を考慮しても良いのか分からないというものでした。この指摘に関しては、既にローカル環境で挙動を確認していたので、エビデンスとして再現手順を共有することでパスできました。
次に、Node E2E テストを追加すべきではないかと指摘を受けました。こちらに関しても Pod Eviction 関連の Node E2E テストが重いこと、Init Containers 向けに Pod Eviction のロジックに変更がないこと、追加すべきか迷ったけど追加しなかったことを正直に回答しました。この辺りの判断も PR の概要に記載しておく方が親切だったなと反省しました。
どちらの指摘も Node E2E テストを追加していれば問題にならなかったはずです。テストが書けるならさまざまなケースを想定してテストを書く方がレビューもすんなり進みます。レビュワーの時間を無駄に奪うこともなくなり Win-Win です。
Init Containers の考慮
Init Containers で Node のリソース逼迫が起きるケースがどの程度あるのか分かりませんが、Eviction メッセージで Restartable Init Containers のみを考慮するのはやはりおかしいという気持ちになりました。実際に Init Container で Pod Eviction を発生させてみました。
cat <<'EOF' | kubectl create -f -
apiVersion: v1
kind: Pod
metadata:
generateName: alloc-
spec:
terminationGracePeriodSeconds: 1
restartPolicy: Never
initContainers:
- name: el-init1
image: registry.k8s.io/e2e-test-images/agnhost:2.52
args: ["stress", "--mem-alloc-size", "100Mi", "--mem-alloc-sleep", "1s", "--mem-total", "4000Mi"]
containers:
- name: el1
image: registry.k8s.io/e2e-test-images/agnhost:2.52
command: ["sleep", "infinity"]
EOF
Init Containers でも Pod Eviction は発生しましたが、当然 Event のメッセージや annotation に Init Containers の情報はありません。
apiVersion: v1
count: 1
eventTime: null
firstTimestamp: "2024-05-29T09:20:56Z"
involvedObject:
apiVersion: v1
kind: Pod
name: alloc-v972z
namespace: default
resourceVersion: "573"
uid: a7e67c6e-4838-4ce8-a656-c31f9fb42831
kind: Event
lastTimestamp: "2024-05-29T09:20:56Z"
message: 'The node was low on resource: memory. Threshold quantity: 1536Mi, available:
1453492Ki. '
metadata:
annotations:
offending_containers: ""
offending_containers_usage: ""
starved_resource: memory
creationTimestamp: "2024-05-29T09:20:56Z"
name: alloc-v972z.17d3eb2bd8f92aca
namespace: default
resourceVersion: "655"
uid: b3aa8512-9383-4f8c-9794-89a77f1d6602
reason: Evicted
reportingComponent: kubelet
reportingInstance: kind-worker
source:
component: kubelet
host: kind-worker
type: Warning
そのため、Init Containers が空でない時に Regular Containers と Init Containers の全てのコンテナと Summary API の情報を突き合わせてEviction メッセージを生成する方法に変更しました。
containers := pod.Spec.Containers
if len(pod.Spec.InitContainers) != 0 {
containers = append(containers, pod.Spec.InitContainers...)
}
for _, containerStats := range podStats.Containers {
for _, container := range containers {
(...)
}
}
修正後の Pod Eviction の Event
apiVersion: v1
count: 1
eventTime: null
firstTimestamp: "2024-05-29T09:20:56Z"
involvedObject:
apiVersion: v1
kind: Pod
name: alloc-v972z
namespace: default
resourceVersion: "573"
uid: a7e67c6e-4838-4ce8-a656-c31f9fb42831
kind: Event
lastTimestamp: "2024-05-29T09:20:56Z"
message: 'The node was low on resource: memory. Threshold quantity: 1536Mi, available:
1453492Ki. '
metadata:
annotations:
offending_containers: ""
offending_containers_usage: ""
starved_resource: memory
creationTimestamp: "2024-05-29T09:20:56Z"
name: alloc-v972z.17d3eb2bd8f92aca
namespace: default
resourceVersion: "655"
uid: b3aa8512-9383-4f8c-9794-89a77f1d6602
reason: Evicted
reportingComponent: kubelet
reportingInstance: kind-worker
source:
component: kubelet
host: kind-worker
type: Warning
k/k#124947 (comment) で PR のスコープを Restartable Init Containers から Init Containers に拡大しても良いか確認しつつ、コミットメッセージや PR タイトルと概要も Init Containers を考慮する形に書き換えました。
Ephemeral Containers の考慮
その後しばらく間が空いて、Sergey さんから Ephemeral Containers を考慮しなくて良いのかという指摘を受けました。また、Sergey さんから自己解決的に Ephemeral Containers が Pod のリソース割り当てを指定できないから考慮しなくても良いとコメントも貰っていました。Ephemeral Containers のことは完全に頭になかったので、リソース割り当てを指定できるかを確認するところから始めました。
Ephemeral Containers には既存の Pod の中にデバッグ用のコンテナを作る機能と、Pod を複製してデバッグ用のコンテナを差し込む機能があります。どちらの場合も、差し込む Ephemeral Containers にリソース割り当てが設定できるかが鍵になります。実際に kind のローカルクラスタで挙動を確認してみました。
kind での動作確認
❯ cat <<EOF | kind create cluster --config -
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
image: kindest/node:v1.31.0@sha256:53df588e04085fd41ae12de0c3fe4c72f7013bba32a20e7325357a1ac94ba865
- role: worker
image: kindest/node:v1.31.0@sha256:53df588e04085fd41ae12de0c3fe4c72f7013bba32a20e7325357a1ac94ba865
EOF
Creating cluster "kind" ...
✓ Ensuring node image (kindest/node:v1.31.0) 🖼
✓ Preparing nodes 📦 📦
✓ Writing configuration 📜
✓ Starting control-plane 🕹️
✓ Installing CNI 🔌
✓ Installing StorageClass 💾
✓ Joining worker nodes 🚜
Set kubectl context to "kind-kind"
You can now use your cluster with:
kubectl cluster-info --context kind-kind
Have a nice day! 👋
❯ kubectl version
Client Version: v1.31.1
Kustomize Version: v5.4.2
Server Version: v1.31.0
❯ cat <<'EOF' | kubectl create -f -
apiVersion: v1
kind: Pod
metadata:
name: alloc
spec:
terminationGracePeriodSeconds: 1
restartPolicy: Never
containers:
- name: el1
image: registry.k8s.io/e2e-test-images/agnhost:2.52
command: ["sleep", "infinity"]
EOF
pod/alloc created
❯ cat <<EOF > custom.json
{
"resources": {
"requests": {
"cpu": "0.2",
"memory": "256Mi"
}
}
}
EOF
❯ kubectl debug alloc -it --custom custom.json --image=registry.k8s.io/e2e-test-images/agnhost:2.52 --target=el1 -- sh
Targeting container "el1". If you don't see processes from this container it may be because the container runtime doesn't support this feature.
Defaulting debug container name to debugger-ch2t6.
The Pod "alloc" is invalid: spec.ephemeralContainers[0].resources: Forbidden: cannot be set for an Ephemeral Container
Ephemeral Containers でリソース割り当てを指定すると API サーバのバリデーションエラーとなることが分かります。実際にローカル環境にクローンした Kubernetes のソースコードの中で cannot be set for an Ephemeral Container
で検索すると、該当のコード箇所に辿り着きました。allowedEphemeralContainerField
の中に Resources の指定が許可されていないことが分かります。
Ephemeral Containers でリソース割り当てが指定できないということは、実際の使用量とリソース割り当てを比較する際にリソース割り当てが 0
として扱われます。そのため、Pod Eviction が発生した際に Ephemeral Containers が必ず違反者として報告されることになります。この挙動は紛らわしいため、Ephemeral Containers は考慮しない方が良いです。
考慮する必要がないことが分かったので、Sergey さんから指摘を受けていたコードコメントを追加して、調べた結果も同様に共有しました。また、その後で Ephemeral Containers を指定した Pod のテストケースも追加して欲しいと言われたので、そちらも対応しました。
停止済みの Init Containers の扱い
最後に Sergey さんから停止時の Init Containers も Pod Eviction 時のメッセージに含まれるのかという質問を受けました。stats()
で取得した Pod のメトリクスの中に停止済みの Init Containers が含まれるかという問いと同義です。実際にローカル環境で挙動を確認してみました。
まず、コードを一部修正してデバッグログを書き出すようにしました。
diff --git a/pkg/kubelet/eviction/helpers.go b/pkg/kubelet/eviction/helpers.go
index 4e684a5b902..e6ed91cbabe 100644
--- a/pkg/kubelet/eviction/helpers.go
+++ b/pkg/kubelet/eviction/helpers.go
@@ -1249,8 +1249,10 @@ func evictionMessage(resourceToReclaim v1.ResourceName, pod *v1.Pod, stats stats
containers = append(containers, pod.Spec.InitContainers...)
}
for _, containerStats := range podStats.Containers {
+ klog.InfoS("Eviction manager: check if the container's resource usage exceeds the resource requests", "container", containerStats.Name)
for _, container := range containers {
if container.Name == containerStats.Name {
requests := container.Resources.Requests[resourceToReclaim]
if utilfeature.DefaultFeatureGate.Enabled(features.InPlacePodVerticalScaling) &&
(resourceToReclaim == v1.ResourceMemory || resourceToReclaim == v1.ResourceCPU) {
kind のコンテナイメージをセルフビルドして kind のローカルクラスタを起動します。その後で、停止した Init Containers を含む Pod が Eviction される状況を再現します。
cat <<'EOF' | kubectl create -f -
apiVersion: v1
kind: Pod
metadata:
generateName: alloc-
spec:
terminationGracePeriodSeconds: 1
restartPolicy: Never
initContainers:
- name: init1
image: registry.k8s.io/e2e-test-images/agnhost:2.52
command: ["sleep", "10"]
- name: el-sidecar1
image: registry.k8s.io/e2e-test-images/agnhost:2.52
args: ["stress", "--mem-alloc-size", "100Mi", "--mem-alloc-sleep", "1s", "--mem-total", "4000Mi"]
restartPolicy: Always
- name: el-sidecar2
image: registry.k8s.io/e2e-test-images/agnhost:2.52
command: ["sleep", "infinity"]
restartPolicy: Always
- name: el-sidecar3
image: registry.k8s.io/e2e-test-images/agnhost:2.52
command: ["sleep", "infinity"]
restartPolicy: Always
containers:
- name: el1
image: registry.k8s.io/e2e-test-images/agnhost:2.52
command: ["sleep", "infinity"]
- name: el2
image: registry.k8s.io/e2e-test-images/agnhost:2.52
command: ["sleep", "infinity"]
- name: el3
image: registry.k8s.io/e2e-test-images/agnhost:2.52
command: ["sleep", "infinity"]
EOF
kubelet のログを確認します。
kind export logs
kubelet のログの中からデバッグ用のメッセージを確認します。
Sep 15 00:07:37 kind-worker kubelet[215]: I0915 00:07:37.162521 215 eviction_manager.go:369] "Eviction manager: attempting to reclaim" resourceName="memory"
Sep 15 00:07:37 kind-worker kubelet[215]: I0915 00:07:37.162993 215 eviction_manager.go:380] "Eviction manager: must evict pod(s) to reclaim" resourceName="memory"
Sep 15 00:07:37 kind-worker kubelet[215]: I0915 00:07:37.163022 215 eviction_manager.go:398] "Eviction manager: pods ranked for eviction" pods=["default/alloc-wpt9g","kube-system/kube-proxy-mhj88","kube-system/kindnet-mvnq8"]
Sep 15 00:07:37 kind-worker kubelet[215]: I0915 00:07:37.168883 215 helpers.go:1252] "Eviction manager: check if the container's resource usage exceeds the resource requests" container="el1"
Sep 15 00:07:37 kind-worker kubelet[215]: I0915 00:07:37.169204 215 helpers.go:1252] "Eviction manager: check if the container's resource usage exceeds the resource requests" container="el3"
Sep 15 00:07:37 kind-worker kubelet[215]: I0915 00:07:37.169221 215 helpers.go:1252] "Eviction manager: check if the container's resource usage exceeds the resource requests" container="el-sidecar1"
Sep 15 00:07:37 kind-worker kubelet[215]: I0915 00:07:37.169241 215 helpers.go:1252] "Eviction manager: check if the container's resource usage exceeds the resource requests" container="el-sidecar2"
Sep 15 00:07:37 kind-worker kubelet[215]: I0915 00:07:37.169257 215 helpers.go:1252] "Eviction manager: check if the container's resource usage exceeds the resource requests" container="el-sidecar3"
Sep 15 00:07:37 kind-worker kubelet[215]: I0915 00:07:37.169270 215 helpers.go:1252] "Eviction manager: check if the container's resource usage exceeds the resource requests" container="el2"
上記のログから停止済みの Init Containers は Pod Eviction で考慮されないことが分かりました。調べたことを Sergey さんに共有すると、PR を approve してくれました。そして、無事に PR がマージされました。
さいごに
大規模な OSS プロジェクトはどこも同じだと思いますが、Kubernetes も慢性的なレビュワー不足に陥っています。コントリビューターや新規の方が作成した大量の PR を数少ないメンテナがレビューする構図のためです。どうしてもレビューがボトルネックとなってしまいます。自分の場合も PR を作成してからマージされるまでに 4 ヶ月ほど掛かりました。その間、指をくわえて待つのではなく、SIG-Node 関連の PR のレビューをお手伝いしていました。コードの体裁など表面的な部分のレビューというよりは、実装に踏み込んだレビューを行うよう心掛けました。自分のレビューの後ですんなり PR がマージされると、役に立てたかなという気持ちになります。この活動のおかげで自分の PR のレビュがー早くなったかは謎ですが、楽しいし勉強にもなるので今も続けています。
この記事が、Kubernetesのコミュニティに貢献したいけれど、何から始めればいいか分からない方の助けになれば嬉しいです。
Discussion