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

cpuset.cpus 和 cpuset.mems 必须同时设置才生效
linux 内核要求 cpuset.cpus 和 cpuset.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.cpus 和 cpuset.mems,导致所有子 cgroup 实际被锁死在默认掩码下(通常是全 0 节点)。
- 检查父级掩码:
cat /sys/fs/cgroup/cpuset/cpuset.cpus和cat /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 关闭和进程级 mbind 或 set_mempolicy 调用。
- 检查是否启用了自动 NUMA 平衡:
cat /proc/sys/kernel/numa_balancing,生产环境建议设为0(echo 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 配置。