LabHub
学习 学习路径 课程

容器内部原理

加上限制,再和观测值对照

在 LabHub 中继续学习

本实验在真正的 VM 中运行

这个环境不是 Pod,而是由 KubeVirt 启动的虚拟机。Linux 内核独立运行,systemd 真正管理服务,docker 也不是模拟工具,而是真正的 Docker 引擎。通过 docker run 启动的容器会成为实际进程,docker execdocker logs 也都能照常工作。

过去,这个实验在 Pod 中运行。由于该环境放弃了所有内核权限,启动容器的步骤无法进行,因此只能学习直接解开镜像归档的变通方法。现在不再需要绕路了。

有两点需要了解。

目标

亲手设置内存、CPU、PID 限制,并将请求的值docker inspect 的 HostConfig)与容器实际看到的值/sys/fs/cgroup/*)并排比较。亲自制造退出码 137,并整理如何将其与 OOM kill 区分。

为什么重要

运维中遇到的问题几乎总是“明明设了限制,为什么没有生效”,或者“CPU 明明很空闲,为什么还是很慢”。前者源于不了解请求值可能与强制执行值不同(在 rootless 环境中,如果没有 systemd delegation,可能只记录请求);后者则源于不了解 CPU cgroup 不会杀死进程,而是让它休眠。而且,这两个问题都不能只靠 docker stats 或仪表盘确认,必须直接读取 cgroup 文件。因此,本实验旨在养成从两个位置读取值并进行对照的习惯。只读其中一处,必然会得出错误结论。

步骤

  1. 创建 /root/int2 目录,将 stat -fc %T /sys/fs/cgroup 的输出原样保存到 /root/int2/version.txtcgroup2fs 表示 v2,tmpfs 表示 v1)。
  2. /sys/fs/cgroup/cgroup.controllers 文件的内容原样保存到 /root/int2/controllers.txt
  3. 使用 alpine:3.20 镜像,在后台运行 dk-mem 容器,并设置**64MiB(67108864 字节)**内存限制。评分时它必须仍处于 running 状态。
  4. dk-mem 内部读取 /sys/fs/cgroup/memory.max,将该值原样保存到 /root/int2/memmax.txt
  5. 使用 alpine:3.20 镜像,在后台运行 dk-cpu 容器,并设置 CPU 0.5 核限制。
  6. 启动 dk-killed 容器后,亲自发送 SIGKILL,使其以退出码 137 结束(不能是 OOM)。然后在 /root/int2/exit137.txt 中同时写明:137 由 128 + 9 组成,以及 OOM kill 也会产生相同的 137。
  7. 使用 alpine:3.20 镜像运行 dk-pids 容器,并设置 PID 限制 64
  8. /root/int2/limits.md 中写三行。dk-mem 行包含实际字节值 67108864dk-cpu 行包含 0.5(或 500m),dk-pids 行包含 64

参考

判断 cgroup 版本

创建 /root/int2 目录,将 stat -fc %T /sys/fs/cgroup 的输出原样保存到 /root/int2/version.txtcgroup2fs 表示 v2,tmpfs 表示 v1)。

stat 有一个选项可以查询已挂载文件系统的类型。不要解释结果后写成 v1/v2,而要将得到的字符串原样保存。

列出可用的控制器

/sys/fs/cgroup/cgroup.controllers 文件的内容原样保存到 /root/int2/controllers.txt

cgroup v2 会在根层级放置一个文件,用一行列出可用控制器的名称。不要手动抄写,请直接复制该文件内容。空格差异可以接受,但不能缺少项目。

请求 64MiB 内存限制

使用 alpine:3.20 镜像,在后台运行 dk-mem 容器,并设置**64MiB(67108864 字节)**内存限制。评分时它必须仍处于 running 状态。

内存限制标志接受 mg 等后缀。64MiB 正好是 67108864 字节,评分会检查这个数字。评分时容器必须仍在运行,因此不能使用很快结束的命令启动。

查看容器实际看到的 memory.max

dk-mem 内部读取 /sys/fs/cgroup/memory.max,将该值原样保存到 /root/int2/memmax.txt

请在容器内部读取 /sys/fs/cgroup/memory.max。其值可能不是 67108864,而是 max;这并非错误,正是本步骤的重点——请求值和强制执行值可能不同。请原样保存看到的内容。

限制 CPU 为 0.5 核

使用 alpine:3.20 镜像,在后台运行 dk-cpu 容器,并设置 CPU 0.5 核限制。

有一个标志可以用小数指定核心数。在内部,quota 会是 period 的一半;这不是“把核心切成两半”,而是“每个周期只运行一半时间”。

制造并解释退出码 137

启动 dk-killed 容器后,亲自发送 SIGKILL,使其以退出码 137 结束(不能是 OOM)。然后在 /root/int2/exit137.txt 中同时写明:137 由 128 + 9 组成,以及 OOM kill 也会产生相同的 137。

不能通过耗尽内存杀死容器,而必须亲自发送 SIGKILL(如果 OOMKilled 为 true,本步骤会失败)。此外,还要在备忘文件中说明 137 可拆分为 128 与 9,并写明 OOM kill 也同样会产生 137。

使用 PID 限制阻止 fork 炸弹

使用 alpine:3.20 镜像运行 dk-pids 容器,并设置 PID 限制 64

有一个专门限制进程数量上限的标志。内存或 CPU 限制无法阻止 fork 炸弹——虽然每个进程都很小,但数量会耗尽内核资源。

编写限制汇总表

/root/int2/limits.md 中写三行。dk-mem 行包含实际字节值 67108864dk-cpu 行包含 0.5(或 500m),dk-pids 行包含 64

三行都必须填写通过 docker inspect 确认的值,而不是猜测值。内存保留原始字节数字,CPU 要能识别为 0.5,PID 保留原始数字。