This page looks best with JavaScript enabled

Pod Health Checks Drained /run Storage Space and Nearly Sent Me Packing

 ·  ☕ 3 min read

If you use nvidia-container-runtime, this is worth your attention — especially if JuiceFS is also in the picture.

1. An Alert Out of Nowhere, and I Panicked

Over the weekend I was studying TensorRT LLM, and while I was at it I installed Dragonfly on the largest production cluster. Then I went out to buy groceries.

In the afternoon I noticed that the Dragonfly daemon on one node had not started and was alerting continuously, so I went to that node and restarted the Kubelet.

About 10 minutes later, production alerts started coming in.

1
2
3
4
5
6
7
8
日志分析告警
时间:2024-01-20 15:26:25
标题:过去20s 状态码503数量超过阈值 547 >= 3
详情:
前三失败接口:
path: /api/xxx/v2/models/xxx/versions/1/infer	数量: 237
path: /api/xxx/v2/models/xxx/versions/1/infer	数量: 188
path: /api/xxx/v2/models/xxx/versions/1/infer	数量: 122

And the alerts kept coming. I had not been in the AI department for long, had not even trained a model yet, and then this happened. I had not done anything, hmph!

Still, solve the problem first. I took a look at the Kubelet log.

1
kubelet[4031671]: E0120 15:31:57.357711 4031671 remote_runtime.go:209] "RunPodSandbox from runtime service failed" err="rpc error: code = Unknown desc = failed to start sandbox container for pod \"dragonfly-dfdaemon-wznfl\": Error response from daemon: write /run/containerd/io.containerd.runtime.v2.task/moby/2b6a70e32f77707d88b73d28054bb83aed34d9ac90c0993df9d1209bd5402b84/config.json: no space left on device: unknown"

Check the disk situation.

1
2
3
df -h

tmpfs       51G   51G     0 100% /run

Found that /run was out of space, so I looked for large files under the /run/containerd/io.containerd.runtime.v2.task/moby directory.

1
2
3
4
5
6.3G	/run/containerd/io.containerd.runtime.v2.task/moby/6be974f9293ab553bf86f0eda38b7813f315a98e63895eddaccb6b290ef6a1ac/log.json
6.3G	/run/containerd/io.containerd.runtime.v2.task/moby/811c9a0aabb50f5ca73e6ee529f41745c2e18568a160f42314caaba142562c6b/log.json
7.9G	/run/containerd/io.containerd.runtime.v2.task/moby/398ecf3c1488b4f8ec0f0ad12ac0a1080355fbd8102f6ed21980c7d7637ec7d2/log.json
8.1G	/run/containerd/io.containerd.runtime.v2.task/moby/ab7253b7bbd05c8fe017008de2ec4494b4c11f2d55b980927d2fdcb3b306c924/log.json
9.6G	/run/containerd/io.containerd.runtime.v2.task/moby/6030f2ad8532162cfa0effb479a9cd3f31c894c2152dfe34ddc59244b53f6241/log.json

Case solved. I emptied the contents of these files directly, and the service recovered immediately.

2. Reproducing the log.json Growth

2.1 Inspecting log.json and Tracing Its Source

First, look at the log contents.

1
{"level":"info","msg":"Running with config:\n{\n  \"AcceptEnvvarUnprivileged\": true,\n  \"NVIDIAContainerCLIConfig\": {\n    \"Root\": \"\"\n  },\n  \"NVIDIACTKConfig\": {\n    \"Path\": \"nvidia-ctk\"\n  },\n  \"NVIDIAContainerRuntimeConfig\": {\n    \"DebugFilePath\": \"/dev/null\",\n    \"LogLevel\": \"info\",\n    \"Runtimes\": [\n      \"docker-runc\",\n      \"runc\"\n    ],\n    \"Mode\": \"auto\",\n    \"Modes\": {\n      \"CSV\": {\n        \"MountSpecPath\": \"/etc/nvidia-container-runtime/host-files-for-container.d\"\n      },\n      \"CDI\": {\n        \"SpecDirs\": null,\n        \"DefaultKind\": \"nvidia.com/gpu\",\n        \"AnnotationPrefixes\": [\n          \"cdi.k8s.io/\"\n        ]\n      }\n    }\n  },\n  \"NVIDIAContainerRuntimeHookConfig\": {\n    \"Path\": \"/usr/bin/nvidia-container-runtime-hook\",\n    \"SkipModeDetection\": false\n  }\n}","time":"2024-01-23T09:43:36+08:00"}

It does not look like an application log, and in fact it is not — it is a runc log, or more precisely a log from nvidia-container-runtime.

The info log level here should be changeable in nvidia-container-runtime’s config file; simply removing info-level logging is enough.

But does this problem exist only with nvidia-container-runtime? No — this is a general problem that has been overlooked.

2.2 Building a Test Workload

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
cat << EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: demo-whomai
spec:
  replicas: 1
  selector:
    matchLabels:
      app: demo-whomai
  template:
    metadata:
      labels:
        app: demo-whomai
    spec:
      containers:
      - name: whomai
        image: shaowenchen/demo:whomai
        readinessProbe:
          exec:
            command:
            - sh
            - -c
            - '[ -e /random/ ]'
          initialDelaySeconds: 1
          periodSeconds: 1
EOF

After repeated testing, I came up with the workload above. You only need to pay attention to two things to trigger continuous growth of the log.json file.

  1. Configure a probe
  2. The probe command must fail to execute

Running sh in the shaowenchen/demo:whomai image returns an error. Probing once per second, a log.json file accumulated over two months can reach several GB.

2.3 Creating the Workload to Test

  • Create the workload

Create a workload from the YAML above.

  • View the workload
1
2
3
4
kubectl get pod -l app=demo-whomai

NAME                             READY   STATUS    RESTARTS   AGE
demo-whomai-966dd7875-jvvzr      0/1     Running   0          48m

Because the health check fails, the Pod never reaches Ready.

  • Find the container ID
1
CONTAINER_ID=$(kubectl get pod -l app=demo-whomai -ojson | jq -r '.items[0].status.containerStatuses[0].containerID | sub("docker://"; "")')
  • Inspect the container’s log.json file

With Docker this should be under the moby namespace,

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
ls -alh /run/containerd/io.containerd.runtime.v2.task/moby/$CONTAINER_ID

total 1.4M
drwx------   3 root root  240 Jan 23 10:40 .
drwx--x--x 162 root root 3.2K Jan 23 11:29 ..
-rw-rw-rw-   1 root root   89 Jan 23 10:40 address
-rw-r--r--   1 root root 9.5K Jan 23 10:40 config.json
-rw-r--r--   1 root root    7 Jan 23 10:40 init.pid
prwx------   1 root root    0 Jan 23 10:40 log
-rw-r--r--   1 root root 1.4M Jan 23 11:31 log.json
-rw-------   1 root root   23 Jan 23 10:40 options.json
drwxr-xr-x   1 root root 4.0K Jan 23 10:40 rootfs
-rw-------   1 root root    0 Jan 23 10:40 runtime
-rw-------   1 root root   32 Jan 23 10:40 shim-binary-path
lrwxrwxrwx   1 root root  118 Jan 23 10:40 work -> /data/containerd/io.containerd.runtime.v2.task/k8s.io/240ccf68446af4a761273e5db08f6aebc362715f64d51efea785c26900c569c5

With Containerd this should be under the k8s.io namespace, ls -alh /run/containerd/io.containerd.runtime.v2.task/k8s.io/$CONTAINER_ID

  • Inspect the contents of the container’s log.json file

With Docker this should be under the moby namespace,

1
2
3
4
5
cat /run/containerd/io.containerd.runtime.v2.task/moby/$CONTAINER_ID/log.json

{"level":"error","msg":"exec failed: unable to start container process: exec: \"sh\": executable file not found in $PATH","time":"2024-01-23T11:32:01+08:00"}
{"level":"error","msg":"exec failed: unable to start container process: exec: \"sh\": executable file not found in $PATH","time":"2024-01-23T11:32:01+08:00"}
{"level":"error","msg":"exec failed: unable to start container process: exec: \"sh\": executable file not found in $PATH","time":"2024-01-23T11:32:01+08:00"}

With Containerd this should be under the k8s.io namespace, cat /run/containerd/io.containerd.runtime.v2.task/k8s.io/$CONTAINER_ID/log.json

3. Solutions

3.1 Clean Up Directly

  • Install Opscli
1
curl -sfL https://raw.githubusercontent.com/shaowenchen/ops/main/getcli.sh |VERSION=latest sh -

If it is already installed, run opscli upgrade to update it.

  • Find log.json files larger than 100M

Command for Docker:

1
opscli task -f ~/.ops/tasks/clear-biglog.yaml --logpath /run/containerd/io.containerd.runtime.v2.task/moby/ --logname "log.json" --size 100M -i ~/.kube/config --all

Command for Containerd:

1
opscli task -f ~/.ops/tasks/clear-biglog.yaml --logpath /run/containerd/io.containerd.runtime.v2.task/k8s.io/ --logname "log.json" --size 100M -i ~/.kube/config --all
  • Clean up log.json files larger than 100M

Add the --clear flag on top of the inspection command, and it will directly clean up any log.json file larger than 100M.

3.2 Change the Log Level of nvidia-container-runtime

Edit the nvidia-container-runtime config file.

1
2
3
4
vim /etc/nvidia-container-runtime/config.toml

[nvidia-container-runtime]
log-level = "info"

Changing log-level = "info" to log-level = "error" avoids emitting logs like {"level":"info","msg":"Running with config:\n{\n \"AcceptEnvvarUnprivileged\": true,\n \"NVIDIAContainerCLIConfig\": {\n \"Root\": \"\"\n },\n \"NVIDIACTKConfig\": {\n \"Path\": \"nvidia-ctk\"\n },\n \"NVIDIAContainerRuntimeConfig\": {\n \"DebugFilePath\": \"/dev/null\",\n \"LogLevel\": \"info\",\n \"Runtimes\": [\n \"docker-runc\",\n \"runc\"\n ],\n \"Mode\": \"auto\",\n \"Modes\": {\n \"CSV\": {\n \"MountSpecPath\": \"/etc/nvidia-container-runtime/host-files-for-container.d\"\n },\n \"CDI\": {\n \"SpecDirs\": null,\n \"DefaultKind\": \"nvidia.com/gpu\",\n \"AnnotationPrefixes\": [\n \"cdi.k8s.io/\"\n ]\n }\n }\n },\n \"NVIDIAContainerRuntimeHookConfig\": {\n \"Path\": \"/usr/bin/nvidia-container-runtime-hook\",\n \"SkipModeDetection\": false\n }\n}","time":"2024-01-23T09:43:36+08:00"}.

This log shows up under io.containerd.runtime.v2 for both Docker and Containerd, but not under io.containerd.runtime.v1.

3.3 Change Containerd’s state Directory

1
2
3
vim /etc/containerd/config.toml

state = "/run/containerd"

Change state = "/run/containerd" to state = "/data/containerd", and mount the /data directory on an additional large disk, so that even a very large log.json file will not easily fill up the storage space.

4. References


微信公众号
WRITTEN BY
微信公众号