
本文教你如何在 NUMA(Non-Uniform Memory Access,非一致内存访问)架构的服务器上做内存绑定与 CPU 亲和性调优,覆盖识别 NUMA 拓扑、用 numactl 绑定进程、cpuset 隔离与 vm.zone_reclaim_mode 调整四类核心动作。读完你会得到三件产出:一份 NUMA 现状诊断清单;针对 MySQL、JVM、HPC 三种典型负载的绑定模板;针对跨节点访存抖动、内存碎片、kswapd 高负载的排查路径。
一、为什么 NUMA 是双刃剑
现代多插槽服务器每颗 CPU 自带一组本地内存,CPU 访问本地节点(local node)内存只需要几十纳秒,跨节点(remote node)访问要再加 30%-50% 的延迟。Linux 内核默认会把内存分配优先放到当前进程运行的 CPU 所在节点,听上去很合理,但常出问题:进程被调度器迁移到另一节点后,原节点的内存就变成”远端”,性能立刻下降。
如何看自己机器有几个 NUMA 节点?最直观的命令是 numactl -H,会列出每个节点的 CPU、内存大小、节点间距离矩阵。lscpu 也能看 NUMA node 列。生产服务器双路 Intel 一般 2 个节点,AMD EPYC 单路也可能因 chiplet 设计出现 4 个甚至 8 个节点。如果你看到节点间距离 21 或更高,跨节点访存代价就非常显著了。云服务器(基于虚拟化的弹性计算资源)的虚拟机一般会暴露 1 个或 2 个 NUMA 节点,按宿主机配置而定,相关选型可参考 云服务器选购指南。
二、用 numactl 绑定进程
numactl 是最直接的工具,可以在启动命令前指定 CPU 与内存策略:
numactl --cpunodebind=0 --membind=0 mysqld --defaults-file=/etc/my.cnf
numactl --interleave=all java -Xms64g -Xmx64g -jar app.jar
--cpunodebind 限制进程只跑在指定节点的 CPU 上;--membind 强制只从指定节点分配内存,分不到就 OOM;--interleave 在多个节点上交替分配内存,缓存性能要求高、不能轻易 OOM 时常用。MySQL 大实例一般用 --interleave=all 避免 swap insanity;JVM 也可加 -XX:+UseNUMA 让 JVM 自己感知。
如何确认绑定生效?numastat -p <pid> 会按节点列出该进程的内存分布,主要全在 node0 就说明绑定成功。相关数据库性能调优可继续看 美国 VPS 部署教程 中的内存与 swap 章节。
三、cpuset 与 cgroup 的精细隔离
numactl 控制的是单个进程,要做大规模隔离需要 cpuset(CPU set,CPU 集合,cgroup 的一个子系统)。在 cgroup v2 下创建一个 slice:
mkdir /sys/fs/cgroup/db.slice
echo "0-15" > /sys/fs/cgroup/db.slice/cpuset.cpus
echo "0" > /sys/fs/cgroup/db.slice/cpuset.mems
echo <pid> > /sys/fs/cgroup/db.slice/cgroup.procs
这套配置把进程限定在 node0 的 0-15 号 CPU 上,并强制内存只在 node0 分配。systemd 的 unit 文件可以直接写 AllowedCPUs 与 AllowedMemoryNodes,更优雅。容器场景下 Docker 与 Kubernetes 都支持 --cpuset-cpus 与 --cpuset-mems,Kubernetes 还有 CPU Manager 的 static 策略可自动按 NUMA 拓扑分配。
注意 cpuset 与 numactl 不冲突,但优先级是 cgroup 限定优先。如果你既用 cpuset 限定了节点 0,又在 numactl 里写 --membind=1,进程会拿不到内存直接 OOM。CDN(Content Delivery Network,内容分发网络)回源代理的高并发 Nginx 通常按 worker 数对齐 NUMA 节点,相关边缘策略可参考 W3 Total Cache 调优实战 与 WordPress 香港主机选购指南。
四、内核参数:zone_reclaim_mode 与自动平衡
两个关键 sysctl 必须知道:
vm.zone_reclaim_mode:默认 0,意味着内存不够时优先跨节点借用而不是回收本地;改为 1 让它先回收本地,对吞吐敏感型负载有时有用,但容易引发短暂 stallkernel.numa_balancing:默认 1,内核会定期采样并把热点内存迁移到当前 CPU 所在节点;MySQL/JVM 这类大内存且自己已做绑定的进程,建议关闭以避免无效迁移vm.swappiness:与 NUMA 有交叉影响,跨节点压力大时内核更倾向 swap,需要一并审视vm.numa_zonelist_order:现代内核已废弃,旧内核(≤ 3.10)仍可见
设置示例(写到 /etc/sysctl.d/99-numa.conf):
vm.zone_reclaim_mode = 0
kernel.numa_balancing = 0
关掉自动平衡后必须自己做好绑定,否则进程被调度器迁移就成了瞎跑。
五、常见诊断命令与场景
排查 NUMA 相关性能问题的一组组合拳:
numastat 看 numa_hit、numa_miss、local_node、other_node 四列,miss 与 other_node 持续上涨说明跨节点访存严重;perf stat -e node-loads,node-load-misses 跑业务命令一段时间,miss 比例 >10% 就要警觉;pcm-numa.x(Intel PCM 工具)可以更细看每个 socket 的 QPI/UPI 带宽利用率,超过 60% 就接近瓶颈。
进程层面用 cat /proc/<pid>/numa_maps 看每段虚拟内存的实际分布,N0=xxx N1=xxx 数字相差太大就说明绑定没生效。这套诊断步骤在 MySQL 大实例上特别常用:实例运行一段时间后 buffer pool 的内存分布会发生漂移,重启加上 --interleave=all 一般能立即恢复。更多服务器侧维护可继续阅读 WordPress 分类下的相关文章。
六、何时不要用 NUMA 绑定
NUMA 绑定不是越多越好。短生命周期的进程、容器中的批处理任务、内存使用远小于单节点容量的服务,绑定带来的收益远小于运维复杂度。绑定一旦做了就意味着每次部署、扩缩容、迁移都要重新评估,建议只对长生命周期的高内存负载(数据库、缓存、JVM 应用、HPC 计算)才显式绑定。
另外要警惕单节点物理内存不够的情况:如果 MySQL 配置了 100GB buffer pool 但单 NUMA 节点只有 96GB,强制 --membind=0 会直接 OOM。这种情况下要么扩内存、要么用 --interleave=all 接受跨节点访存,要么把实例垂直拆分到两台机器。
七、容器化与 Kubernetes 的 NUMA 集成
Kubernetes 在 1.18 起把 NUMA 拓扑感知调度做进了 CPU Manager 与 Topology Manager 两个组件。开启方法:kubelet 启动参数加 --cpu-manager-policy=static --topology-manager-policy=best-effort,然后给关键 Pod 设置 Guaranteed QoS 并指定整数 CPU。kubelet 会自动按 NUMA 拓扑分配 CPU 与内存,无需在容器内再手动 numactl。
但要注意 Topology Manager 的 strict 策略会让无法满足拓扑要求的 Pod 直接调度失败,生产环境多用 best-effort 兼顾灵活性与性能。监控侧可以用 node-exporter 的 numa stat collector 抓数据,配合 Grafana 看每个节点的本地访存比例。这套机制对延迟敏感的 NFV、AI 推理、HFT 类工作负载有显著收益。
总结与建议
NUMA 调优的核心思路是先识别拓扑、再绑定关键进程、最后用监控指标验证收益。建议你按本文顺序逐步落地:从 numactl -H 看清现状,到对单个长寿命大内存进程做绑定,再考虑用 cpuset 做大规模隔离。如果你需要在生产做规模化调优,可以考虑把 numactl 启动命令统一收纳到 systemd 单元文件,并用 Prometheus 抓 numastat 的指标做长期监控。总结一下,NUMA 绑定收益最大的永远是大内存高吞吐的关键业务,对其他负载建议保留默认让内核自己处理,节奏对了才能稳。