Linux cpuset cgroup 的 CPU 隔离与 NUMA 节点绑定生产实践

1次阅读

cpuset.cpus 和 cpuset.mems 必须同时非空设置才生效,否则写入任一文件均报 invalid argument;若 cgroup 中有任务需先清空 tasks;父 cgroup 掩码限制子 cgroup 可用资源;numa 绑定需配合关闭 numa_balancing 和进程级内存策略;v2 下路径和行为不同,需确认版本并按新规则配置。

Linux cpuset cgroup 的 CPU 隔离与 NUMA 节点绑定生产实践

cpuset.cpus 和 cpuset.mems 必须同时设置才生效

linux 内核要求 cpuset.cpuscpuset.mems 在同一个 cgroup 中必须都非空,否则写入任意一个都会失败(报错 Invalid argument)。这和直觉相反——很多人以为先设 CPU、再设内存节点是可行的。

常见错误现象:echo 0-1 > cpuset.cpus 成功,但紧接着 echo 0 > cpuset.mems 报错;或者反过来,cpuset.mems 写入成功后,cpuset.cpus 却拒绝写入。

  • 必须用原子方式同时设置:比如 echo 0-1 > cpuset.cpus && echo 0 > cpuset.mems(注意顺序不重要,但不能有间隔)
  • 如果 cgroup 已存在任务(tasks 非空),必须先清空 tasks 才能修改这两个值
  • cpuset.mems 的值必须是当前系统实际存在的 NUMA 节点 ID,可通过 numactl --hardware 查看;写入不存在的节点(如 echo 99 > cpuset.mems)也会报 Invalid argument

绑定进程前必须确认其不在父 cgroup 的 cpuset 掩码中

cpuset 是严格继承的:子 cgroup 只能使用父 cgroup 允许的 CPU 和内存节点。如果父 cgroup(比如 /sys/fs/cgroup/cpuset/ 根目录)的 cpuset.cpus 是空的或限制过窄,子 cgroup 再怎么配也没用。

典型场景:在 kubernetes 中用 cpusets 限制 Pod,但 Node 上 kubelet 启动时没显式配置根 cgroup 的 cpuset.cpuscpuset.mems,导致所有子 cgroup 实际被锁死在默认掩码下(通常是全 0 节点)。

  • 检查父级掩码:cat /sys/fs/cgroup/cpuset/cpuset.cpuscat /sys/fs/cgroup/cpuset/cpuset.mems
  • 生产环境建议在系统启动早期(如 systemd service 或 init.d 脚本中)就初始化根 cgroup,例如:echo 0-63 > /sys/fs/cgroup/cpuset/cpuset.cpus && echo 0-3 > /sys/fs/cgroup/cpuset/cpuset.mems
  • 修改父 cgroup 掩码会立即影响所有未显式覆盖的子 cgroup,需评估对已有负载的影响

NUMA 绑定失效的三个隐蔽原因

即使 cpuset.mems 正确设置了 NUMA 节点,进程仍可能跨节点分配内存,本质是内核内存策略未同步约束。

关键点在于:cpuset 只控制「可访问哪些节点」,不控制「优先从哪个节点分配」。要真正实现本地内存分配,还需配合 numa_balancing 关闭和进程级 mbindset_mempolicy 调用。

  • 检查是否启用了自动 NUMA 平衡:cat /proc/sys/kernel/numa_balancing,生产环境建议设为 0echo 0 > /proc/sys/kernel/numa_balancing
  • 进程启动时若未调用 set_mempolicy(MPOL_BIND, ...),malloc 默认仍走系统全局策略,可能 fallback 到其他节点
  • 某些语言运行时(如 jvm)有自己的内存管理器,需额外参数支持 NUMA 感知,例如 OpenJDK 的 -XX:+UseNUMA,且仅在启用 cpuset.mems 后才有效

cpuset v2 下路径和行为差异必须注意

如果你用的是较新内核(5.11+)且启用了 cgroup v2(systemd 默认),cpuset 控制器的行为和路径完全不同:没有独立的 cpuset.cpus 文件,而是统一通过 cgroup.procs + cpuset.cpus.effective + cpuset.mems.effective 管理,且父子继承逻辑更严格。

错误现象:在 v2 下仍尝试写 cpuset.cpus,得到 No such file or Directory;或发现 cpuset.cpus.effective 显示为空,其实是被父级限制为 0。

  • 确认版本:mount | grep cgroup —— 若挂载点含 unified,就是 v2
  • v2 中设置 CPU 掩码应写入 cgroup.subtree_control 启用 cpuset,再写 cpuset.cpus(注意:v2 的 cpuset.cpus 是可写的,但需先启用控制器)
  • v2 下 cpuset.mems 同样必须与 cpuset.cpus 同时设置,且子 cgroup 的 effective 值由父级 cpuset.cpus/cpuset.mems 与自身共同决定,不可越界

最常被忽略的一点:cpuset 对线程粒度无效。一个进程绑定了 CPU 0-1 和 NUMA 节点 0,它的某个线程仍可能被调度到其他 CPU(除非用 sched_setaffinity 单独绑核),而内存分配策略也只作用于进程首次 malloc 的上下文。真要隔离,得在应用层做细粒度控制,不能只靠 cgroup 配置。

text=ZqhQzanResources