2. 容器内存限制:cgroup v1/v2 内存子系统架构、memory.limit_in_bytes 与 memory.high 的区别、容器内存上限设置
2.1 cgroup 内存子系统架构概述
容器运行时(containerd / runc / cri-o)通过 Linux cgroup 对容器进程施加内存上限。cgroup 有两个主要版本:v1 和 v2。理解两者的内存子系统架构是调优 OOM 优先级和回收行为的前提。
2.1.1 cgroup v1 内存子系统
cgroup v1 将内存控制器独立挂载在 /sys/fs/cgroup/memory 下。每个容器对应一个子目录,内核通过 memory.limit_in_bytes 和 memory.soft_limit_in_bytes 控制硬限制和软限制。
- memory.limit_in_bytes:硬限制,容器进程总内存(RSS + Page Cache + Swap)超过此值即触发 OOM Killer。
- memory.soft_limit_in_bytes:软限制,仅在系统内存紧张时优先回收该 cgroup 的内存。
- memory.memsw.limit_in_bytes:内存 + Swap 总硬限制(需内核开启 CONFIG_MEMCG_SWAP)。
- memory.usage_in_bytes:当前内存使用量。
- memory.stat:详细的内存统计信息,包括 cache、rss、pgfault 等。
memory.limit_in_bytes 设置的值会立即生效,但内核可能不会立刻回收内存,导致实际使用量短暂超过限制。OOM Killer 的触发存在延迟。
2.1.2 cgroup v2 内存子系统
cgroup v2 将内存控制器集成在统一层级 /sys/fs/cgroup/ 下,不再有独立的 memory 挂载点。v2 引入了更细粒度的控制参数:
- memory.max:对应 v1 的
memory.limit_in_bytes,硬限制。 - memory.high:软限制,当内存使用超过此值时,内核会主动进行内存回收,但不会触发 OOM。
- memory.low:保护性下限,保证该 cgroup 在内存竞争时至少保留此内存量。
- memory.min:硬性保护下限,即使系统内存不足也不会回收此部分内存(需内核支持 PSI)。
- memory.current:当前内存使用量。
- memory.events:内存压力事件统计,包括 low、high、max、oom 等。
memory.high 和 memory.max。前者用于主动回收,后者作为硬性兜底,避免容器进程突然被 OOM 杀死。
2.2 memory.limit_in_bytes 与 memory.high 的区别
这是容器内存调优中最容易混淆的两个参数。下表从多个维度对比两者的行为差异:
| 对比维度 | memory.limit_in_bytes (v1) / memory.max (v2) | memory.high (v2 特有) |
|---|---|---|
| 类型 | 硬限制(Hard Limit) | 软限制(Soft Limit) |
| 触发行为 | 超过后触发 OOM Killer,杀死容器内进程 | 超过后触发同步内存回收,进程可能被 throttle(限流) |
| 回收时机 | 被动回收,仅在 OOM 前尝试回收 | 主动回收,内核在分配内存时同步执行 |
| 对进程影响 | 进程可能被直接杀死,无降级机会 | 进程可能因回收而变慢,但不会被杀 |
| 适用场景 | 严格的内存隔离,防止容器占用过多内存 | 内存弹性控制,允许短时突发但限制长期使用 |
| 默认值 | 无限制(max) | 无限制(max) |
| 与 Swap 的关系 | 可与 memory.swap.max 配合限制 Swap 使用 | 不影响 Swap,仅控制物理内存回收 |
memory.high 是 cgroup v2 引入的“优雅降级”机制。设置合理的 memory.high 可以让容器在内存压力下通过回收和限流来避免 OOM,适合对延迟不敏感但需要稳定性的工作负载。而 memory.limit_in_bytes / memory.max 是最后的防线,必须设置以防止内存泄漏导致宿主机崩溃。
2.3 容器内存上限设置实践
2.3.1 在 containerd 中设置内存限制
containerd 通过 CRI(Container Runtime Interface)接收 kubelet 下发的资源限制,然后传递给 runc。在 Pod 的 YAML 中设置:
apiVersion: v1
kind: Pod
metadata:
name: memory-demo
spec:
containers:
- name: mem-alloc
image: polinux/stress
resources:
requests:
memory: "64Mi"
limits:
memory: "128Mi"
command: ["stress"]
args: ["--vm", "1", "--vm-bytes", "150M", "--vm-hang", "1"]
kubelet 会将 limits.memory 转换为 cgroup v2 的 memory.max,将 requests.memory 转换为 memory.low(v2)或 memory.soft_limit_in_bytes(v1)。
2.3.2 手动设置 cgroup v2 内存上限
在宿主机上直接操作 cgroup 文件系统:
# 创建容器 cgroup 目录
mkdir -p /sys/fs/cgroup/container-demo
# 设置硬限制:最大 256MB
echo 268435456 > /sys/fs/cgroup/container-demo/memory.max
# 设置软限制:超过 192MB 开始主动回收
echo 201326592 > /sys/fs/cgroup/container-demo/memory.high
# 将容器进程 PID 加入 cgroup
echo 12345 > /sys/fs/cgroup/container-demo/cgroup.procs
# 查看当前内存使用
cat /sys/fs/cgroup/container-demo/memory.current
2.3.3 在 runc 配置中设置
runc 的 config.json 中通过 linux.resources.memory 字段设置:
{
"linux": {
"resources": {
"memory": {
"limit": 268435456,
"reservation": 134217728,
"swap": 536870912,
"kernel": 0,
"kernelTCP": 0
}
}
}
}
在 cgroup v2 环境下,runc 会自动将 limit 映射为 memory.max,将 reservation 映射为 memory.low。
2.4 cgroup v1 vs v2 内存架构对比图
下图展示了 cgroup v1 和 v2 在内存子系统上的架构差异:
2.5 内存回收流程对比图
下图展示了设置 memory.high 和 memory.max 后,容器内存分配与回收的完整流程:
2.6 调优建议与避坑指南
- 始终设置
memory.max(v2)或memory.limit_in_bytes(v1),防止内存泄漏影响宿主机。 - 在 cgroup v2 中,额外设置
memory.high为memory.max的 80%~90%,让内核提前回收。 - 对于延迟敏感型容器(如数据库),适当调高
memory.high阈值,减少回收导致的抖动。 - 对于批处理任务,可以设置较低的
memory.high,允许短时突发但强制回收。
- 只设置
memory.high而不设置memory.max:当回收速度跟不上分配速度时,容器可能无限制使用内存,最终导致宿主机 OOM。 - 在 cgroup v1 中误用
memory.soft_limit_in_bytes作为硬限制:软限制仅在内存竞争时生效,无法防止单容器内存暴涨。 - 忽略 Swap 限制:如果未设置
memory.swap.max(v2)或memory.memsw.limit_in_bytes(v1),容器可能通过 Swap 绕过内存限制。
2.7 总结
容器内存限制的核心在于理解 cgroup v1 和 v2 的架构差异,以及 memory.limit_in_bytes / memory.max 与 memory.high 的协同关系。v2 的 memory.high 提供了主动回收机制,是避免容器 OOM 的关键参数。在实际调优中,应结合容器的工作负载类型(延迟敏感型 vs 吞吐型)来设置合理的阈值,并始终保留硬限制作为安全兜底。