LabHub
学习 学习路径 课程

容器内部原理

cgroup v2,以及限制看上去没生效的时候

在 LabHub 中继续学习

一句话总结

命名空间负责能看到什么,cgroup 负责能使用多少。而本课程中最常欺骗人的事实是: cgroup 虽然会施加限制,却不会隐藏 /proc

概念图: 命名空间负责能看到什么,cgroup 负责能使用多少 · 却不会隐藏 /proc · 资源控制 · 超过 5%,就视为问题

为什么需要它

cgroup 于 2006 年起源于 Google,并在 2007 年合入内核 2.6.24。它最初名为 “process container”,后来因为容易与当时已广泛使用的 container 一词混淆, 改名为 control group,简称 cgroup。这个名称的历史本身就解释了它的职责—— 它不是隔离机制,而是资源控制

一台主机上运行三十个容器,其中一个耗尽全部内存,其余二十九个也会一同死亡。 命名空间完全无法解决这个问题,因为它们只是彼此不可见,仍共享同一套物理资源。

它如何运作

首先确认版本。如果 stat -fc %T /sys/fs/cgroup 返回 cgroup2fs,就是 v2; 返回 tmpfs,就是 v1。v2 的主要文件如下。

文件 含义
cpu.max quota period 两个值。50000 100000 表示 0.5 CPU
cpu.weight 1~10000 之间的相对权重,默认值为 100
cpu.stat nr_periodsnr_throttledthrottled_usec
memory.min/low/high/max 保护线与上限
memory.current 当前使用量
memory.events 触及限制的次数
pids.max 进程数量上限

在 v1 中,对应文件分别是 cpu.cfs_quota_usmemory.limit_in_bytes

区分 memory.highmemory.max 很重要。**超过 high 会产生回收压力并减慢分配, 但进程不会死亡;超过 max 则会触发 OOM Kill。**因此 high 表示“从这里开始施加痛苦”, max 表示“这里就是墙”。

CPU 不会杀死进程,而是让它休眠。若 cpu.stat 中的 nr_throttled / nr_periods 超过 5%,就视为问题。1000 个周期中有 350 次被限流,就是 35%。此时会出现典型现象: CPU 使用率图看起来很空闲,只有响应时间突然升高。

常用字节值值得记住:64MiB = 67108864、192MiB = 201326592、 256MiB = 268435456、512MiB = 536870912。

退出码 137 = 128 + 9 = SIGKILL。OOM kill 不会给应用任何异常, 无法用 try/except 捕获,也不会留下应用日志。必须在内核日志中看到 Memory cgroup out of memory: Killed process ... 才能确认, 在运行时侧则要用 State.OOMKilled 区分。

在实际工作中会遇到的情况

这是最重要的陷阱。在用 --memory=256m --cpus=0.5 启动的容器中运行 free -m,会看到 64228MB;运行 nproc,会看到 32。原因是 cgroup 会限制资源, 却不会隐藏 /proc。JVM 通过 UseContainerSupport 直接读取 cgroup 文件解决了这个问题, 但 Node 的 os.cpus() 与 Go 的 runtime.NumCPU() 仍然看到主机值。 所以必须为 Go 服务显式设置 GOMAXPROCS。分配 0.5 个核,却启动 32 个工作线程, 结果只会不断累积限流。

Kubernetes 的 QoS 类别也源于这里。Guaranteed 的 OOM score 为 -997, Burstable 为 2~999,BestEffort 为 1000。BestEffort 最先死亡并非一句策略描述, 而是 cgroup 与 OOM 分数直接作用的结果

还有一点:在 rootless 环境中,即使设置了限制,也可能并未真正强制执行。 如果没有 systemd delegation(Delegate=cpu memory pids io),请求会被记录, 却不会反映到内核。因此后续实验会同时检查 docker inspect 的 HostConfig 请求值, 以及容器实际看到的值。两者若不同,这种差异本身就是要学习的内容。

v1 与 v2 表现不同的地方

cgroup v1 与 v2 同时存在,文件名和含义也不同。如果不先确认使用哪一种, 就会去寻找根本不存在的文件。

stat -fc %T /sys/fs/cgroup      # cgroup2fs 면 v2, tmpfs 면 v1
内容 v1 v2
内存限制 memory/memory.limit_in_bytes memory.max
当前使用量 memory.usage_in_bytes memory.current
CPU 限制 cpu.cfs_quota_us / cpu.cfs_period_us cpu.max(两个值写在同一行)
限流统计 cpu.stat cpu.stat
OOM 记录 (无) memory.events

**v2 的 memory.events 尤其有用。**v1 没有该文件,因此过去只能通过内核日志判断 “这个容器是否曾因 OOM 死亡”。

**没有限制时显示为 max。**它不是数字,而是字符串 max,因此解析代码会抛出异常。 开发工具时经常会碰到这个问题。

**内存限制也会计算页缓存。**大量读取文件的容器会不断积累缓存并触及上限, 但内核会回收缓存,所以通常不会死亡。因此,memory.current 本身紧贴上限并不代表问题。 只有 memory.events 中的 oom_kill 增加,才是真正的问题

**CPU 限制不会体现在使用率上。**进程每 100ms 用完配额后便等待剩余时间, 因此平均使用率很低,响应却很慢。cpu.statnr_throttledthrottled_usec 就是证据。

**过小的限制反而有害。**把 CPU 限制设为 0.1 核,意味着每 100ms 只能运行 10ms, 连短任务也要跨越多个周期才能完成,延迟会显著增加。因此对响应时间而言, request 设置得低一些,limit 则宽松设置或不设更有利。

后续实验要做什么

确认 cgroup 版本和控制器列表,分别设置内存、CPU、PID 限制, 然后对照请求值与观测值。亲自用 SIGKILL 制造退出码 137, 并整理如何将它与 OOM kill 区分开来。