前两年我拿到一台双路服务器十六核起步、内存两百多G本以为跑什么业务都绰绰余。结果把一个有状态服务部署上去压测时线程处理耗时忽高忽低整体吞吐远低于硬件配置应有的水平。排查到最后发现问题根本不在代码逻辑而是NUMA架构下的“本地内存”和“远端内存”在我完全不知情的情况下被混用了。那次之后我才认真把NUMA架构翻了个底朝天也踩了不少坑今天就把其中真正有用、能落地的东西一次性说清楚。NUMA架构在日常开发里经常被一句话带过但真正决定多核性能上限的往往是线程跑在哪个核、内存分配在哪个节点、跨节点访问占比高不高这些看起来琐碎的事。你不需要成为硬件专家但如果你在做数据库、网络网关、高并发中间件、JVM服务或者容器化调度理解NUMA并对进程做合理绑定往往能拿到非常可观的性能提升。我尽量把原理、实操、坑位一起讲透。1. 先看懂NUMA到底解决了什么问题1.1 从“内存远近”这个朴素问题说起很早之前的对称多处理器架构里多个CPU核心共享同一条前端总线去访问内存CPU往内存发起请求不存在“远近”概念大家访问的是一份统一地址空间速度也基本一致。这种叫统一内存访问架构。问题是总线带宽很快成为瓶颈核数越多竞争越激烈加内存条也救不回来。后来硬件厂商把内存控制器从北桥搬进CPU内部每个CPU插槽直接挂接自己的一部分物理内存。CPU访问自己插槽上的内存最快访问其他插槽上的内存则要经过芯片间互联链路延迟和带宽都明显变差。这种“访问不同位置的物理内存开销不一样”的架构就是非统一内存访问架构简称NUMA架构。你可以把它想成仓库自己楼下的仓库取件很快旁边那栋楼里的仓库也能取件但要绕一段路高峰期还要排队取件成本自然不一样。现实中的多路服务器几乎都是NUMA架构部分单路CPU内部也会划分出多个NUMA节点。操作系统和上层应用如果感知不到这种差异内存分配和线程调度就会很随意就会导致本该快起来的线程频繁访问远端内存多核性能被白白消耗掉。1.2 NUMA节点、本地内存和远端内存NUMA架构里最基本的概念是“节点”。一个节点通常由一组CPU核心和一部分物理内存组成这些核心访问本节点内存就是“本地内存访问”访问其他节点的内存就是“远端内存访问”。节点之间通过高速互联链路交换数据。上层软件在做内存分配时内核会按策略决定把物理页放在哪个节点。默认策略通常是“先掉本地节点本地节点充足就避免远端分配”但实际场景会因为线程迁移、内存碎片、多线程并发分配、临时内存波动等因素把不少分配推到了远端节点。更隐蔽的是线程自己可能被调度器从CPU A挪到CPU B一旦跨了节点原来的本地内存就变成了远端内存缓存局域性瞬间崩塌。1.3 跨节点访问的代价有多大不同硬件的远端访问开销差异挺大但一个比较通用的感知是本地内存延迟和远端内存延迟通常能差到1.5到2倍以上内存带宽差距更明显跨节点访问会占用宝贵的互联链路带宽。还有一个容易被忽略的点跨节点共享数据时硬件缓存一致性协议要在不同节点之间发送监听和确认报文接不通时系统会花费大量额外周期。所以NUMA架构本身没有好坏它是硬件为了支撑更多核心、更大内存容量而不得不走的路。问题在于软件到底有没有主动适配它。一句话总结想要榨干多路服务器的多核性能必须让“数据尽量在离线程最近的内存上落地”这就是所有NUMA调优的核心逻辑。2. 读懂机器拓扑是NUMA调优的第一步2.1 用numactl看清节点和距离不要凭经验猜拓扑直接看机器自己报告的数据。Linux下最常用的命令是numactl --hardware它能列出可用节点、节点内CPU范围、物理内存大小以及节点之间的距离。节点距离表里数字越小代表访问越近同一节点到自身一般是10跨节点可能是20、30甚至更高。我自己拿到一台新机器第一件事就是执行numactl --hardware输出大致是available: 2 nodes (0-1) node 0 cpus: 0-15 node 0 size: 128201 MB node 1 cpus: 16-31 node 1 size: 128208 MB node distances: node 0 1 0: 10 21 1: 21 10这说明机器有两个节点0号节点持有CPU 0到15和128GB内存1号节点持有CPU 16到31和另外128GB内存。节点距离表里0到1和1到0都是21说明两边是对称的。这种拓扑下如果CPU 3上的线程去访问节点1内存访问代价明显高于访问节点0内存。2.2 查看进程当前跑在哪个节点光知道机器拓扑还不够还得知道真正的业务进程现在是什么状态。numastat可以看系统整体的NUDA命中情况cat /proc/pid/numa_maps可以看某个进程的内存页分布在哪些节点。举个实际排查例子我发现某服务的内存页大部分落在node 1但线程却经常在node 0的CPU上运行。这就说明线程和数据已经跨节点了。每次内存访问都要绕一次互联链路长时间跑下来性能自然不理想。后来我把进程通过CPU亲和性固定到node 1的CPU上再配合内存节点绑定内存页才慢慢集中回node 1。2.3 注意容器和虚拟机的伪拓扑如果你用的是容器或者虚拟机里面的NUMA拓扑往往会被屏蔽成“单节点”或者展示成宿主机透传的某种抽象。容器里执行lscpu看到的CPU列表可能只是宿主机某个NUMA节点的一部分也可能是跨节点的核心集合。我在某些容器平台上看到过“8核单NUMA节点”的展示但底层8个核实际上分散在两个物理节点上性能表现和真正的单节点完全不同。做容器内NUMA优化时要额外小心优先看宿主机是否开启了NUMA感知调度或者在容器编排层配置CPU pinning时明确指定物理节点。不然你在容器里绑定得再漂亮底层调度也可能给你挪到别的节点上去。3. 把线程和内存绑定在一起实操方法要熟练3.1 numactl常用参数先背熟这几个numactl是Linux下NUMA调优的瑞士军刀我实际用到的参数其实就那么几个核心是三个维度绑定CPU节点、绑定内存节点、绑定具体CPU核。最常用的一组命令是# 让程序在node 0上跑内存也只从node 0分配 numactl --cpunodebind0 --membind0 ./myapp # 让程序只使用节点1的CPU但内存分配优先本节点本节点不够时允许远端 numactl --cpunodebind1 --preferred1 ./myapp # 多个节点间轮询分配内存适合共享数据多、随机访问多的大内存应用 numactl --interleaveall ./myapp这里要注意--membind0是强制只在节点0分配内存如果节点0内存不够分配会失败而不是自动溢出到节点1。--preferred1是优先从节点1分配不够时内核会自动去其他节点业务上更温和。--interleaveall则是将物理页按顺序轮询放在各个节点上适合线程本身就多、访问模式又分散的应用。3.2 绑定CPU核比绑定节点更精细有些场景只绑节点还不够还需要精确指定逻辑核心。比如一个8核进程在节点0的16个核上跑调度器依然可能在CPU 0和CPU 15之间来回迁移虽然不跨节点但缓存热点会丢失。这时可以用taskset或者numactl --physcpubind指定CPU列表# 将进程绑定到0到7号CPU核 taskset -c 0-7 ./myapp # numactl也可以直接指定物理CPU numactl --physcpubind4,5,6,7 ./myapp物理CPU编号和拓扑里的逻辑编号要仔细对应。超线程开启后同一物理核心的两个逻辑核排序往往相邻绑定CPU 0到7时可能只覆盖了4个物理核心。真要追求数据局部性得先通过lscpu确认CPU编号和物理核心的映射关系必要时用“通过超线程绑核”的方式。3.3 用cgroup做更稳妥的容器内绑核容器化环境下我更推荐用cgroup的cpuset子系统来做绑核因为它在容器重启后依然能保持配置也方便集群管理。给某个容器设置cpuset只需要修改两个控制文件cpuset.cpus控制允许使用的CPU列表cpuset.mems控制允许使用的内存节点列表。比如把某容器固定到node 1的CPU和内存节点上# 假设容器cg路径为/sys/fs/cgroup/cpuset/mycontainer echo 16-31 /sys/fs/cgroup/cpuset/mycontainer/cpuset.cpus echo 1 /sys/fs/cgroup/cpuset/mycontainer/cpuset.mems如果你在编排平台里看到支持CPU pinning最好同时指定内存节点。很多平台只绑了CPU没绑内存节点容器里的进程照样会把内存放到远端节点等于只完成了一半。4. 应用层应该怎么做才能吃到NUMA红利4.1 JVM大型堆要谨慎开启NUMA特性有些新人在JVM里看到-XX:UseNUMA就直接加上觉得能自动优化内存分配。实际上这个开关在某些分代收集器里才有明确效果主要针对老年代大对象分配。对于线程本地分配缓冲区频繁的场景开启NUMA反而可能增加分配开销。我给一个朋友的JVM服务做过压测对比业务是大量短生命周期对象、堆内存只有8GB开了UseNUMA后吞吐反而下降了几个百分点。后来换成专门给某内存计算框架做的大堆应用起步就是64GB堆对象存活时间长UseNUMA才带来了可感知的收益。所以JVM层面的NUMA优化要基于监控数据来做不是无脑加的配置项。如果确认要用JVM的NUMA特性还要看垃圾收集器是否支持。不支持的话可以简化到只在宿主机层面对整个JVM进程做CPU和内存绑定效果通常比JVM内部开关更直接。4.2 数据库和缓存服务适合绑定节点数据库、缓存类服务的特征是线程很多、共享内存大、对延迟敏感而且往往有明确的本地化访问模式。这类应用我强烈建议做进程级NUMA绑定。比如某个单实例数据库如果只工作在部分分片数据上绑定到对应节点后页缓存和锁结构都集中在一个节点性能提升非常明显。我在某一套模拟业务系统里做过对比绑定到单一节点后事务平均延迟降低了大约五分之一尾延迟改善更大。原因很简单锁竞争、日志缓冲、数据页缓存的访问都变成了本地访问互联链路不再承载高频内存请求。但要注意数据库如果做了主从复制、跨实例同步或者本身就是全量数据在内存里随机访问绑定节点未必合适。最好先观察业务的内存访问到底偏向“本地集中”还是“全局分散”再决定绑不绑。4.3 网络中断处理应该贴近业务所在节点网络收包中断的处理很容易被忽略。服务进程绑在node 0但网卡中断却默认落在node 1的CPU上那么报文数据在内存里的放置位置、协议栈处理的缓存、应用读取数据的路径都要跨节点延迟直接受影响。处理办法是把网卡中断绑定到业务进程所在的节点。Linux下可以通过修改/proc/irq/irq编号/smp_affinity来设置中断CPU亲和性。实操时先找网卡对应的中断号再把掩码设置为业务节点上某个空闲CPU最后让应用线程也固定在同一节点。这样从网卡队列到协议栈再到用户态处理整个链路都在本地节点内完成。需要注意别把多个中断都挤在同一个CPU上否则大量小包场景下中断处理本身就会成为瓶颈。多队列网卡可以合理散列到节点内的多个核心既保留NUMA局部性又避免单核过载。5. 排障实录那些常见的NUMA深坑5.1 现象CPU使用率不高吞吐却上不去有次压测某个服务在32核机器上CPU整体使用率不到60%可请求延迟波动很大锁竞争看起来也不严重。用numastat一查发现“跨节点内存分配”和“跨节点访问”两个指标高得吓人。再配合perf观察大量时间消耗在内存访问和缓存一致性协议上这就是典型的远端内存访问拖累。排查思路可以按这个顺序先numastat看整体节点失衡情况哪个节点被塞得特别满再通过numastat -p pid看具体进程的内存分布接着cat /proc/pid/status | grep Cpus_allowed_list确认进程的CPU亲和性最后看numactl --hardware确认距离表判断跨节点代价是否严重。如果内存分布和CPU亲和性明显不一致优先把两者调整到同一节点往往能立竿见影。5.2 现象加了NUMA绑定后性能反而下降绑定不是万灵药。有个案例是服务本身线程数非常多工作集也大我把进程绑到一个节点上后内存带宽瞬间饱和因为所有内存访问都汇聚到同一个内存控制器。结果远不如两个节点一起分担。这种情况更适合用--interleaveall让内存页分散在两个节点间确保内存带宽充分利用。还有一种副作用是绑核太死操作系统负载均衡失效。某几个核心跑满其他核心空闲但进程因为绑死不能迁移造成长尾延迟。这时应该放宽到节点级绑定而不是物理核级绑定给调度器留一点弹性。5.3 现象容器热迁移之后性能雪崩虚拟机和容器迁移会带来一个隐蔽问题。迁移前进程内存分布在一个物理节点迁移后CPU已经落到新节点的物理核上但内存页可能还留在旧节点甚至旧节点已经从操作系统的CPU列表中移除。访问这些远端残留页面代价比普通跨节点还高。在支持在线迁移的平台上要么配置迁移后自动重建内存页到本地节点要么提前规划好CPU和内存节点的绑定关系避免迁移导致拓扑错乱。如果业务确实无法接受迁移导致的不稳定最稳妥的方案是把关键实例固定在物理机节点上禁止热迁移。排查这种问题时我常用一个小技巧写一段持续读写内存的测试程序分别用本地节点和远端节点内存跑一遍得到基线差值。之后如果线上服务性能无缘无故波动就把波动幅度和远本访问差值做对比能很快定位是不是跨节点内存访问导致。5.4 不要忽略巨页和NUMA的交互开启透明巨页或者显式大页后内存分配是按更粗粒度进行的。这时候如果NUMA策略不当一个2MB或者1GB的巨页被分配到远端节点浪费的不仅是访问速度还占用了大量互联带宽。更麻烦的是巨页回收和迁移比普通页更困难问题会持续更久。做大规模内存分配时优先考虑通过numactl --membind显式指定内存节点再配合巨页使用。对某些随机访问型应用interleave也能让巨页均匀分布在多个节点避免单一节点内存带宽被压满。6. 给想上手NUMA优化的你几条能立刻用的建议6.1 先做压测基线再谈优化什么都不改先跑一轮基线压测记录延迟平均值、百分位延迟、吞吐量、CPU使用率。然后只做绑定优化其他条件不变对比两次数据。没有基线就调优等于闭眼开车后面出了问题很难说清楚到底是哪一步导致的。压测时要特别留意数据的随机波动。机器刚启动时内存页分配策略和长时间运行后完全不一样最好用稳定业务跑一段时间后再采集数据。最少对比三轮取相对稳定的结果做结论。6.2 哪些应用适合绑定哪些适合交错根据我自己的经验可以把常见应用简单分个类应用特征推荐策略理由单实例数据库、缓存引擎CPU节点绑定 内存节点绑定数据访问集中在本地减少跨节点开销大型内存计算、向量检索多节点交错分配充分利用多个内存控制器的带宽高并发短连接网络服务节点绑定 中断绑定减少报文路径跨节点通用业务微服务尽量不动进程规模小绑错了反而增加负载不均风险“尽量不动”也是一种有效策略。现代操作系统调度器在NUMA感知方面已经做了不少工作对很多中小型应用来说默认策略足够用。强行绑定不一定带来收益还可能出现核心空闲却无法利用的问题。6.3 分享一个实用的监控小脚本我习惯在后台常驻一个小循环监控各NUMA节点的内存和CPU负载变化while true; do clear numactl --hardware | head -n 8 echo ---------------- numastat | head -n 8 sleep 2 done这个脚本不解决具体问题但能帮你快速感知到节点之间有没有出现严重倾斜。所谓“严重倾斜”就是某一个节点的内存被占得差不多其他节点还有不少富余。这种情况通常意味着内存分配策略出了问题值得继续往下查。我个人的体会是NUMA优化最大的门槛不是命令不熟而是对业务内存访问模式不够了解。每个服务到底偏重内存带宽、内存延迟、还是在缓存一致性上卡住只有通过监控和对照实验才能判断清楚。先学会看拓扑、看进程分布再考虑改配置比我当初直接上手就绑要靠谱得多。