上周三凌晨两点半,一个做订单系统的朋友把我从床上薅起来:白天好好的接口,到半夜 P99 延迟从 4 毫秒直接飙到 160 毫秒,整整慢了 40 倍,还时不时报一句 Lock wait timeout。我第一反应是「是不是被 DDoS 了」「是不是有人跑大查询」,让他先看 sar 和慢日志。慢日志干干净净,QPS 平稳,CPU 也没跑满,流量入口那几层都查了个遍,没毛病。
最后定位到的东西有点反直觉:这台机器是双路的,128 核,内存 512G,而 mysqld 的工作线程跑得到处都是,有的在 0 号 NUMA 节点上,有的在 1 号节点上,numastat -p 一看 numa_miss 高得离谱。也就是说,进程在节点 A 上要访问的内存,大部分物理上装在节点 B 的内存条上,每次访问都得穿过 CPU 之间的互连总线。这东西平时不显山不露水,一旦访问集中起来,尾延迟就被成倍放大。
如果你手上也是多路大核心的服务器、跑的是对延迟敏感的数据库或者高并发服务,可以先看看 iwebfusion(全球云服务器,含独享主机与托管方案) 和 vpckr(AI 云基础设施,高性能计算集群) 这两家的硬件规格。NUMA 这类问题说到底是硬件拓扑决定的CPU 跨两路、内存插在哪个节点、机器是不是真的按 NUMA 节点划了通道,选机器的时候问清楚,比上线后再用脚本补救省心得多。

NUMA 全称 Non-Uniform Memory Access,直译是「非统一内存访问」。这句话听起来玄,其实就一句话:离哪个 CPU 近,访问哪个内存块就快。
现在的服务器 CPU 早就不是单路独占了。一颗物理 CPU(socket)上面挂着若干颗核心,这些核心再通过片内总线连成一颗「NUMA 节点」(node),每个节点有自己那一半的内存控制器和自己的 DDR 通道。一台双路机器就是两个节点:node 0 挂 socket 0 和它的内存,node 1 挂 socket 1 和它的内存。
问题出在跨节点访问上。node 0 的核心要访问 node 1 的内存,数据得先走进片内的缓存一致性协议,再经过 UPI / QPI 这条 CPU 之间的互连总线出去一趟。单次访问的延迟可能从 80 纳秒涨到 130 纳秒,看起来只多了 50 纳秒,但 CPU 访存是流水线的,一颗核心每秒要做上百亿次访存,这点额外延迟被摊薄之后就是实打实的吞吐下降;更糟的是工作线程在两个节点之间来回漂,缓存行反复失效,那延迟就不是几十纳秒的量级了。
所以 NUMA 优化的核心思路只有一句话:让线程待在它常访问的那块内存所在的节点上。这就是 CPU 亲和性(CPU affinity)存在的意义。

别猜,用命令看。第一步看拓扑:
numactl --hardware
输出里 available: 告诉你有几个 NUMA 节点、每个节点多少颗 CPU、各有多少内存。比如 2 nodes, 64 CPUs each 就说明是双路 128 核。如果只有 1 node,那你大概率不用操心 NUMA,可以直接跳过这一篇后面的内容。
第二步看你的进程实际访问了哪些节点:
numastat -p $(pidof mysqld)
重点看 numa_hit 和 numa_miss 两列。numa_hit 是「本地内存命中」,numa_miss 是「本地没有、要跑远端去取」。健康状态应该是 hit 远大于 miss;如果 miss 占比有几成甚至更高,那基本可以确诊了。系统级的整体情况可以用 numastat(不带 -p)看,能判断是这台机器普遍 NUMA 失衡,还是就你这个进程的事。
第三步看内存页到底被放在哪:
cat /proc/$(pidof mysqld)/numa_maps | head -30
每一行开头那串十六进制是虚拟地址,后面跟着 N0、N1 这样的标记,数字就是该页在对应节点上的份数。如果一个内存池的页几乎都标着同一个节点,说明它整体压在一侧,那压力就来了。
第四步是最狠的一招,定位到具体的缓存行争抢:
perf c2c record -- pid $(pidof mysqld) # 采样记录
perf c2c report # 出报告
报告里会列出被频繁争抢的缓存行、涉及哪些线程、分布在哪些 NUMA 节点上。这一步能直接告诉你「是哪个线程、哪个变量在跨节点打架」,而不是让你凭感觉调参数。

确认了问题,剩下的就是调。核心工具是 numactl,三个常用模式:
1. 严格绑定(--cpunodebind + --membind):把进程锁死在 0 号节点,内存也只用 0 号节点的。适合进程确定只需要一部分 CPU 和内存的场景,比如你确定只用前 32 核,内存用 128G 就够。
numactl --cpunodebind=0 --membind=0 mysqld --innodb-buffer-pool-size=64G
好处是彻底消除跨节点访问,坏处是另一半 CPU 和内存你就用不上了,等于白花钱。这是最常见的误用很多教程一上来就教你 --membind=0,结果 128 核的机器只跑 64 核的性能。
2. 内存交错(--interleave=all):让内核把内存页均匀撒在所有节点上,每个节点各占一半。这是我的 MySQL 场景最后采用的方案:
numactl --interleave=all mysqld
为什么对 Buffer Pool 特别有效?因为 InnoDB 的内存池是所有线程共享的,你没法提前知道哪块页归哪个核用,那就干脆均摊。每个节点各有一份,任何线程从本地命中都是大概率事件,同时所有节点的内存也都被用满了。
顺带说一句版本坑:MySQL 5.7 有个 innodb_numa_interleave 参数专门干这事,8.0 起已经移除,所以别再去网上抄 innodb_numa_interleave=ON,8.0 上这个参数是不存在的,交给 numactl --interleave=all 更省事。
3. 轻量的 CPU 亲和性(taskset):不改内存策略,只限制进程能用哪些 CPU 核。
taskset -c 0-31 mysqld
这条命令不依赖 numactl 工具包,很多精简版镜像里没装 numactl 也能用,是我临时救火最常用的手段。
另外,Linux 内核还有一套「自动 NUMA 均衡」(automatic NUMA balancing)机制,会在后台偷偷搬运内存页来让「谁在哪」更匹配。这个特性在虚拟化环境和容器里经常出问题页迁移本身会制造延迟毛刺。我的处理方式是直接关掉:
# 查看当前状态
cat /proc/sys/kernel/numa_balancing
# 关闭(需 root,会影响整机)
echo 0 | tee /proc/sys/kernel/numa_balancing
坑一:把 CPU 绑得太死,线程互相抢核。MySQL 是多线程模型,如果 --cpunodebind=0 之后所有线程都在 0 号节点的 64 个核上跑,而 1 号节点 64 个核全闲着,等于把一台 128 核的机器当成 64 核用。更糟的情况是绑得太窄,比如 taskset -c 0-3,16 个工作线程挤在 4 个核上,CPU 上下文切换直接开满。绑核的思路是「让线程待在离数据近的那一侧」,不是「让线程都挤在一起」。
坑二:忘了 --membind 的内存其实还在原地。有人以为 taskset 之后内存会自动跟着走,不会的。CPU 亲和性只管调度,内存放哪儿完全是另一套逻辑。所以只做 taskset 而不做内存交错,效果往往只有一半。
坑三:容器里根本没做 NUMA 感知。现在很多服务跑在 K8s 或 Docker 里,容器默认看到的还是宿主机的完整拓扑(除非你显式配置),调度器随手把 Pod 放到哪台机器、哪个节点上,你完全不知道。这时候靠宿主机上跑 numactl 是不现实的,得从调度策略和 cgroup 这层去管。这也是为什么我要强调:能选物理拓扑清晰、NUMA 划分规范的机器,本身就是一种优化。
坑四:只看平均延迟。NUMA 问题几乎不影响平均 QPS,只影响尾部。你要看的是 P95 / P99,不是 avg。用 pt-query-digest 看慢查询分布,或者直接盯监控里的 P99 曲线,不然调了半天没效果还以为方法不对。

按这个顺序走一遍,多核机器上「白天正常半夜抽风」这类问题基本都能查清楚:
numactl --hardware # 1. 几个 NUMA 节点?每节点多少核多少内存
numastat -p $(pidof mysqld) # 2. numa_miss 占比高不高
numastat # 3. 系统级是否整体失衡
cat /proc/$(pidof mysqld)/numa_maps | head # 4. 内存页压在哪一侧
perf c2c record -- pid $(pidof mysqld) && perf c2c report # 5. 定位争抢的缓存行
cat /proc/sys/kernel/numa_balancing # 6. 自动均衡开着没有
numactl --interleave=all mysqld # 7. 内存交错重启后对比 P99
第 2 步是分水岭:numa_miss 占比超过两成,且 P99 明显高于 P50,就该动手了。如果 miss 很低但延迟依旧高,那 NUMA 就不是你的病根,别在这儿耗时间,去查连接池、锁等待和慢查询。
最后说一句实在话。NUMA 是个典型的「硬件决定上限、软件决定发挥」的东西:CPU 怎么分、内存条插在哪个通道、主板是不是按 NUMA 节点走线的,这些在买机器的时候就定死了,后面再怎么用 numactl 调都只是在残局里找补。所以选机器的时候,直接问销售一句「双路是不是每个 NUMA 节点独立走通道、内存是不是按节点配平」,比事后调参管用得多。而你自己手上的活儿,把 numactl --interleave=all 这条命令记牢,关键时刻能救一晚上的命。