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,造成非预期的跨节点访问。
cpuset 绑定与 NUMA 节点关系 NUMA Node 0 CPU 0-3 Memory 0 (本地内存) cpuset.cpus=0-3 cpuset.mems=0 NUMA Node 1 CPU 4-7 Memory 1 (本地内存) cpuset.cpus=4-7 cpuset.mems=1 容器 A (绑定 Node 0) 容器 B (绑定 Node 1) cpuset.mems 与 cpuset.cpus 应匹配同一 NUMA 节点,避免远端内存访问

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 与内存分配策略的配合。以下是一个典型配置流程:

  1. 节点拓扑发现: 通过 lscpu 或 numactl --hardware 获取 NUMA 节点拓扑。
  2. cpuset 绑定: 将容器 CPU 绑定到同一 NUMA 节点的物理核心,设置 cpuset.mems 为对应节点。
  3. 内存策略设置: 在容器 entrypoint 中调用 numactl --membind=<node> 或通过 mbind() 系统调用设置策略。
  4. 验证: 使用 numastat -p <pid> 或 /proc/<pid>/numa_maps 检查实际内存分布。
local vs interleave 内存分配对比 local 分配 CPU 0 请求 → 分配在 Node 0 内存 CPU 1 请求 → 分配在 Node 1 内存 特点:低延迟,无跨节点访问 interleave 分配 CPU 0 请求 → 分配在 Node 0 CPU 0 请求 → 分配在 Node 1(轮询) 特点:带宽聚合,可能远端访问 选择策略时需权衡:local 适合延迟敏感型,interleave 适合带宽密集型 可通过 numactl --show 查看当前进程策略 对比

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 感知容量调优奠定基础。