3. 容器运行时内存模型
容器运行时内存模型是理解内存带宽竞争与 NUMA 感知调优的基础。Linux 内核通过 cgroup v1/v2 的 memory 子系统 和 cpuset 子系统 对容器内存进行隔离与绑定,而 内存分配策略(local / interleave)则决定了容器内进程在 NUMA 节点间分配物理页面的行为。本节将逐一拆解这三层机制,并给出典型配置示例。
3.1 cgroup memory 子系统:容器内存边界与回收控制
cgroup memory 子系统为每个容器定义独立的内存使用边界,包括物理内存上限、内存+交换上限、内核内存上限等。当容器内存使用超过限制时,内核触发直接回收或 OOM Killer。
| 控制文件(cgroup v1) | 作用 | 典型值 |
|---|---|---|
memory.limit_in_bytes |
容器物理内存硬上限 | 4G / 8G / 16G |
memory.soft_limit_in_bytes |
软上限,内存压力时优先回收 | 3G / 6G / 12G |
memory.memsw.limit_in_bytes |
物理内存 + swap 总上限 | 5G / 10G / 20G |
memory.kmem.limit_in_bytes |
内核 slab 内存上限 | 1G / 2G |
memory.oom_control |
OOM 行为控制(disable 则暂停进程) | 0(启用 OOM) / 1(暂停) |
关键指标: 在 NUMA 环境下,
memory.limit_in_bytes 只控制总量,不控制页面在哪个 NUMA 节点分配。若容器内存上限远大于单节点内存,内核可能跨节点分配,导致远端内存访问延迟增加。
cgroup v2 将 memory 与 io、pids 等子系统统一挂载,控制文件更简洁:
# cgroup v2 内存控制示例
/sys/fs/cgroup/<container-id>/
├── memory.max # 物理内存硬上限(字节)
├── memory.high # 内存软上限,超过后触发回收
├── memory.current # 当前内存使用量
├── memory.numa_stat # 按 NUMA 节点的内存分布统计
└── memory.oom.group # 组级别 OOM 行为
调优建议: 在 NUMA 环境中,建议将
memory.high 设置为单 NUMA 节点容量的 80%~90%,避免容器因跨节点分配而触发频繁回收。
3.2 cpuset 绑定:CPU 与内存节点的亲和性
cpuset 子系统允许将容器绑定到特定的 CPU 核心和内存节点。通过 cpuset.cpus 和 cpuset.mems 两个控制文件,可以实现 CPU 与内存的 NUMA 亲和性绑定。
| 控制文件 | 作用 | 示例值 |
|---|---|---|
cpuset.cpus |
允许容器使用的 CPU 核心列表 | 0-3,8-11 |
cpuset.mems |
允许容器分配内存的 NUMA 节点列表 | 0 或 0-1 |
cpuset.memory_migrate |
当 cpuset.mems 变化时是否迁移已有页面 |
0(不迁移) / 1(迁移) |
cpuset.cpu_exclusive |
CPU 是否独占(其他 cgroup 不可使用) | 0 / 1 |
典型绑定场景:
- 单 NUMA 节点绑定:
cpuset.cpus=0-3+cpuset.mems=0,容器所有内存分配都在 NUMA 节点 0 上完成,避免跨节点访问。 - 跨 NUMA 节点绑定:
cpuset.cpus=0-7+cpuset.mems=0-1,容器可在两个节点上分配内存,但需配合内存分配策略控制。 - CPU 与内存节点不匹配: 若
cpuset.cpus包含节点 0 的 CPU,但cpuset.mems只允许节点 1,则所有内存访问均为远端访问,性能严重下降。
常见陷阱: 容器编排平台(如 Kubernetes)在设置 cpuset 时,若未显式设置
cpuset.mems,默认值为所有 NUMA 节点(如 0-1)。此时即使 CPU 绑定在节点 0,内存仍可能分配到节点 1,造成非预期的跨节点访问。
3.3 内存分配策略:local 与 interleave
Linux 内核提供两种 NUMA 内存分配策略,通过 numactl 或 mbind() 系统调用设置。容器运行时可通过 cpuset.mems 与进程级策略配合,实现精细控制。
| 策略 | 分配行为 | 适用场景 | 性能特征 |
|---|---|---|---|
| local(默认) | 优先在发起分配请求的 CPU 所在 NUMA 节点分配 | 单节点容器、延迟敏感型应用 | 低延迟、高带宽(本地访问) |
| interleave | 在允许的 NUMA 节点间轮询分配页面 | 多线程大内存应用、内存带宽密集型 | 带宽聚合、延迟均衡但可能增加远端访问 |
| bind | 强制在指定 NUMA 节点分配,失败则返回错误 | 硬实时、确定性内存访问 | 严格本地、无远端 |
| preferred | 优先在指定节点分配,失败则回退到其他节点 | 优先本地但允许降级 | 灵活、容错 |
容器内设置示例:
numactl --membind=0 --cpunodebind=0 ./myapp — 强制容器进程在 NUMA 节点 0 上分配内存和运行 CPU。numactl --interleave=all ./myapp — 在所有 NUMA 节点间轮询分配内存,适用于大页内存或带宽密集型工作负载。
在容器编排中,Kubernetes 通过 TopologyManager 和 CPUManager 实现 NUMA 感知调度,但底层仍依赖 cpuset 与内存分配策略的配合。以下是一个典型配置流程:
- 节点拓扑发现: 通过
lscpu或numactl --hardware获取 NUMA 节点拓扑。 - cpuset 绑定: 将容器 CPU 绑定到同一 NUMA 节点的物理核心,设置
cpuset.mems为对应节点。 - 内存策略设置: 在容器 entrypoint 中调用
numactl --membind=<node>或通过mbind()系统调用设置策略。 - 验证: 使用
numastat -p <pid>或/proc/<pid>/numa_maps检查实际内存分布。
3.4 综合配置示例:NUMA 感知容器启动
以下是一个完整的容器启动脚本,展示如何结合 cgroup memory、cpuset 与内存分配策略:
#!/bin/bash
# NUMA 感知容器启动示例
CONTAINER_ID="my-numa-app"
NUMA_NODE=0
CPU_LIST="0-3"
MEM_LIMIT="4G"
# 1. 创建 cgroup 目录
mkdir -p /sys/fs/cgroup/cpuset/${CONTAINER_ID}
mkdir -p /sys/fs/cgroup/memory/${CONTAINER_ID}
# 2. 设置 cpuset 绑定
echo ${CPU_LIST} > /sys/fs/cgroup/cpuset/${CONTAINER_ID}/cpuset.cpus
echo ${NUMA_NODE} > /sys/fs/cgroup/cpuset/${CONTAINER_ID}/cpuset.mems
# 3. 设置内存上限
echo ${MEM_LIMIT} > /sys/fs/cgroup/memory/${CONTAINER_ID}/memory.limit_in_bytes
# 4. 启动容器进程并加入 cgroup
cgexec -g cpuset,memory:${CONTAINER_ID} \
numactl --membind=${NUMA_NODE} \
/usr/bin/myapp --config /etc/myapp.conf
验证命令:
cat /proc/$(pgrep myapp)/numa_maps | head -20 — 查看进程内存页面的 NUMA 分布。numastat -p $(pgrep myapp) — 查看进程在各 NUMA 节点的内存分配统计。
通过上述配置,容器运行时可以精确控制内存分配行为,避免跨 NUMA 节点访问带来的带宽竞争与延迟抖动,为后续的 NUMA 感知容量调优奠定基础。