CPU核心数增加后性能不升?用NUMA与内存带宽定位瓶颈
CPU核心数从8核增加到16核、32核后,性能仍可能几乎不变,甚至响应时间变长。原因通常不是“新核心没有工作”,而是任务受到了内存带宽、NUMA远端访问、串行代码、磁盘或网络等待的限制。核心数代表并行执行能力,不等于单线程速度,也不等于应用能够持续获得更多有效吞吐。
排查时不要只看整机CPU利用率。应在同一个时间窗口内同时观察每核利用率、运行队列、IPC、内存带宽、NUMA远端访问、磁盘延迟、网络重传、应用响应时间和错误率,再通过“增加核心”“绑定CPU与内存节点”“限制并发”等对照测试确认瓶颈位置。
先建立可比较的观察窗口
性能测试最容易出现的问题,是前后两次测试并不具备可比性。例如第一次使用热缓存,第二次重新加载数据;第一次只有一个数据库连接,第二次连接池已经排队;或者扩容核心的同时,后台任务、日志压缩、定时备份也开始运行。此时即使结果不同,也无法判断变化来自CPU核心数。
测试前至少记录以下环境信息:
- CPU型号、物理插槽数、物理核心数、逻辑线程数。
- NUMA节点数量、每个节点可见的CPU和内存。
- 内存容量、通道是否对称、是否发生交换或内存回收。
- 操作系统内核、虚拟化状态、CPU频率策略、SMT是否启用。
- 应用版本、编译选项、工作线程数、连接池大小和数据集规模。
- 数据是冷缓存还是热缓存,测试前是否完成预热。
- 磁盘类型、文件系统、网络带宽,以及测试期间是否有其他任务。
一个可复现的观察窗口可以这样设置:先预热2分钟,再连续采样5分钟,重复3次;每次只改变一个变量,例如只改变CPU核心数,或者只改变NUMA绑定策略。这个时长不是固定标准,但需要让短时突发、缓存填充和频率变化基本稳定下来。
测试结果至少要同时记录四类指标:
| 指标类别 | 需要记录的指标 | 主要用途 |
|---|---|---|
| 业务结果 | 吞吐量、平均响应时间、P95/P99响应时间、错误率 | 判断用户实际感知是否改善 |
| CPU | 每核利用率、用户态/内核态、iowait、steal、运行队列、上下文切换 | 判断是否真正缺少计算能力 |
| 内存与NUMA | 使用量、交换、带宽、缓存未命中、每节点访问量、远端访问比例 | 判断容量、带宽和内存位置问题 |
| I/O与网络 | 磁盘await、队列、利用率、网络吞吐、丢包、重传、软中断 | 排除CPU之外的等待来源 |
整机CPU使用率只有一个平均值,可能掩盖单核瓶颈。比如32个逻辑CPU平均利用率为35%,并不代表还有大量可用计算能力;如果一个线程已经跑满单个核心,其他线程又在等待锁,整体平均值仍然会很低。
核心数增加前,先确认增加的是哪种能力
单线程速度、并行度和指令集不是一回事
一个任务的处理能力,可以粗略理解为:
有效吞吐 ≈ 单核执行效率 × 可用频率 × 实际并行线程数 × 内存和I/O供给能力
增加核心数主要提升“实际并行线程数”。如果应用只有一个主线程,或者大部分代码都必须按顺序执行,那么更多核心不会自动提高这部分代码的速度。
指令集也会改变单核执行效率。相同核心数下,使用向量指令处理批量数据的程序,可能比只使用标量指令的程序快很多;但前提是:
- 二进制程序确实使用了目标CPU支持的指令集。
- 数据布局适合向量化。
- 计算过程不是被内存访问或分支预测拖慢。
- 使用宽向量指令后,CPU没有因为功耗和温度限制而显著降频。
因此,出现“核心数翻倍但性能只增加很少”时,需要先区分三种情况:
- 程序没有足够并行任务:线程数增加后,大量线程等待锁、队列或串行阶段。
- 程序已经从计算密集型变成内存密集型:更多核心只是同时向内存控制器发起访问。
- 程序受I/O或外部服务限制:CPU只是等待磁盘、网络、数据库锁或下游响应。
可以用每核利用率和线程状态初步判断。计算密集型任务通常表现为多个核心持续忙碌、iowait较低、运行队列随并发增加;锁竞争任务可能表现为少数核心接近100%,其余核心利用率不高,但上下文切换和线程等待明显增加。
不要把SMT线程当成等价物理核心
逻辑线程可以提高资源利用率,但它们通常共享部分执行资源、缓存和内存通道。对于计算密集型程序,从物理核心增加到逻辑线程,收益可能明显低于增加同数量的物理核心;对于等待内存或I/O的程序,SMT有时可以填补执行空隙,但也可能加重缓存和带宽竞争。
测试中建议分别记录:
- 仅使用物理核心时的吞吐量。
- 使用全部逻辑线程时的吞吐量。
- 每个物理核心上的线程数量。
- 增加逻辑线程后,内存带宽、缓存未命中和P99延迟是否同步恶化。
如果逻辑线程增加后CPU利用率上升,但吞吐量几乎不变、P99延迟变长,通常说明共享执行资源或内存系统已经成为限制。
内存瓶颈不只有“内存不够”
容量不足、带宽不足和延迟过高要分开判断
内存问题至少包括三类:
- 容量不足:工作集无法放入内存,发生交换、直接回收或频繁缺页。
- 带宽不足:数据总量已经超过内存控制器和内存通道能够持续传输的能力。
- 访问延迟过高:线程频繁访问远端NUMA节点、随机地址或共享数据,单次访问等待时间变长。
这三类问题的处理方式不同。增加内存容量不能直接解决内存带宽饱和;增加CPU核心也不能降低远端NUMA访问延迟。
判断容量问题时,重点看内存使用量、swap、major page fault、内存回收和应用错误率。若交换活动持续存在,更多CPU核心通常不会改善性能,反而可能让更多线程同时产生内存压力。
判断带宽问题时,不能只看内存使用百分比。内存使用99%不一定代表带宽饱和,内存使用60%也可能已经达到当前平台的持续带宽上限。应观察单位时间内实际读写数据量、缓存未命中、CPU停顿周期和业务吞吐是否同步变化。
例如,一个内存流式处理任务在8核时使用70 GB/s,在16核时使用150 GB/s,在32核时使用155 GB/s。如果32核时CPU平均利用率反而下降、缓存未命中上升,而吞吐量不再增加,通常不是核心不够,而是内存带宽已经接近可用上限。
NUMA为什么会让更多核心带来更差结果
多插槽服务器通常会把CPU核心和本地内存划分为多个NUMA节点。每个节点拥有自己的内存控制器和内存通道。线程访问本节点内存时属于本地访问,访问其他节点内存时属于远端访问,需要经过节点之间的互连链路。

远端访问通常会带来更高延迟和额外链路流量。实际延迟和带宽取决于CPU架构、内存频率、节点距离、访问模式和并发量,不能用一个固定数字套用所有服务器。典型现象是:跨节点运行后,整机CPU利用率下降,但内存带宽和互连流量增加,P95/P99延迟变差。
Linux中的内存分配还受到“首次触碰”规则影响。某个线程首次写入一页内存时,这页内存通常会优先分配到该线程所在的NUMA节点。如果后续线程被调度到其他节点,或者主线程在节点0初始化数据、工作线程却在节点1处理数据,就可能形成大量远端访问。
先用以下命令查看拓扑。命令只读取系统信息,不会修改配置:
lscpu
lscpu -e=CPU,CORE,SOCKET,NODE
numactl -H
测试进程运行期间,可以查看进程的NUMA统计。将示例PID替换为目标进程的实际PID:
PID=12345
numastat -p "$PID"
不同内核和工具版本显示的字段可能略有差异,重点观察各节点的内存分配、命中、错配和迁移趋势,而不是只看某一个字段。
如果应用允许通过命令行启动,可以做三组对照:
# 将进程和内存都绑定到NUMA节点0
numactl --cpunodebind=0 --membind=0 /path/to/application --same-workload
# 让进程在所有节点运行,并交错分配内存
numactl --interleave=all /path/to/application --same-workload
# 使用相同参数运行默认策略,作为基线
/path/to/application --same-workload
这不是固定的优化顺序,而是诊断对照。若绑定CPU和内存到同一节点后吞吐提高、远端访问下降,说明NUMA位置是重要因素。若交错分配后带宽提高但P99延迟变差,说明应用更看重总带宽,或者已经无法维持严格的本地性。
绑定策略也有边界。将进程限制在单个节点时,必须确认该节点有足够内存;否则可能触发内存回收或分配失败。对于需要跨节点共享大数据集的程序,强制单节点绑定也可能让容量和带宽变成新的瓶颈。
用一组联动指标解释“加核不加速”
下面是一组用于说明判断方法的模拟数据,并非某台服务器的实测结果。假设同一应用分别使用8核、16核和32核,32核测试再增加一次按NUMA节点拆分的运行方式:

| 配置 | 吞吐量(请求/秒) | CPU平均利用率 | 内存带宽 | 远端访问比例 | P99响应时间 |
|---|---|---|---|---|---|
| 8核,单节点 | 1200 | 82% | 76 GB/s | 5% | 38 ms |
| 16核,单节点 | 1700 | 93% | 138 GB/s | 6% | 41 ms |
| 32核,默认跨节点 | 1760 | 69% | 248 GB/s | 34% | 67 ms |
| 32核,按节点拆分工作线程 | 2050 | 81% | 222 GB/s | 9% | 46 ms |
这组数据可以这样解释:
- 8核到16核时,吞吐量明显增加,CPU利用率和内存带宽同步上升,说明原先仍有较多并行计算需求。
- 16核到32核时,吞吐量几乎不变,但内存带宽接近假设的平台持续带宽上限,远端访问比例从6%升至34%,P99明显变差。
- 32核按NUMA节点拆分后,远端访问下降,虽然总内存带宽不一定更高,但吞吐量和尾延迟改善,说明之前的主要问题不是核心数量,而是数据位置和跨节点访问。
- 如果32核测试中运行队列、上下文切换和锁等待也大幅上升,则还需要排查应用并行度和共享锁,不能只归因于NUMA。
这里的“接近上限”应以同一台服务器、相同内存配置和相近访问模式下的持续带宽基准作为参照,而不是拿厂商标称的理论带宽直接比较。理论带宽通常没有扣除协议、刷新、访问模式和软件开销。
观察CPU、内存、磁盘和网络的组合信号
可以使用系统工具采集一个短时间窗口。以下命令适用于常见Linux环境,具体选项是否可用取决于已安装的sysstat和perf版本:
mpstat -P ALL 1 10
vmstat 1 10
pidstat -p "$PID" -u -r -d -w 1 10
iostat -xz 1 10
sar -n DEV,TCP,ETCP 1 10
如果系统允许使用性能计数器,可以对目标进程进行短时间采样:
perf stat -p "$PID" \
-e cycles,instructions,cache-misses,context-switches,cpu-migrations \
sleep 30
不同CPU支持的事件名称可能不同,不能直接假设所有平台都有相同的内存事件。可以先查看本机可用事件:
perf list | grep -Ei 'cache|memory|stall|uncore'
如果权限不足、虚拟机屏蔽了性能计数器,或事件不可用,应记录这一限制,不要把缺失的指标当成零值。
CPU瓶颈的典型组合
较可信的CPU计算瓶颈通常同时具备以下特征:
- 多个物理核心持续高利用率。
- 用户态CPU时间占比高,iowait较低。
- 运行队列随并发增加,但磁盘和网络没有对应拥塞。
- 增加核心后吞吐量仍能较明显上升。
- P99响应时间没有因为核心增加而明显恶化。
- IPC、频率和上下文切换没有出现异常下降或暴涨。
如果只有一个核心接近100%,其余核心空闲,优先检查单线程限制、主循环、锁竞争和线程调度,而不是继续增加核心数。
内存带宽或NUMA瓶颈的典型组合
以下组合更接近内存系统瓶颈:
- 增加核心后,内存带宽快速上升并接近持续上限。
- CPU利用率不再上升,甚至下降。
- LLC缓存未命中、内存停顿周期增加。
- 远端NUMA访问比例随核心数增加而上升。
- 绑定CPU与内存节点后,吞吐量或P99得到改善。
- 磁盘队列、网络重传和应用错误率没有同步恶化。
如果内存带宽并不高,但远端访问比例很高,可能更偏向NUMA延迟或数据布局问题;如果远端比例不高但带宽已经饱和,则应重点优化数据搬运量、访问模式和并发度。
内存容量瓶颈的典型组合
容量不足常见于以下情况:
- 可用内存持续下降,swap in/out增加。
- major page fault、直接回收或内存压缩活动持续。
- 应用响应时间与内存回收事件同时出现尖峰。
- CPU利用率不高,但线程大量处于等待状态。
- 增加核心数没有改善吞吐,甚至让内存压力更快达到上限。
这种情况下,增加CPU核心不能替代增加可用内存或降低工作集。对于数据库,还要区分操作系统内存不足与数据库缓冲池配置不足。
磁盘I/O瓶颈的典型组合
磁盘瓶颈不应仅凭“CPU不高”判断。更有价值的联动信号包括:
iostat中的await持续升高。- 设备队列长度增加,利用率长期接近饱和。
- 进程级读写速率与业务请求增长同步。
- CPU的iowait上升,但内存带宽没有达到上限。
- 吞吐量不再增加,P95/P99和超时率上升。
需要注意,iowait高也可能来自网络文件系统、内存回收或块设备驱动等待。应将进程级I/O、设备队列和应用响应时间放在同一时间轴中对照。
网络瓶颈的典型组合
网络侧常见判断信号包括:
- 网卡吞吐接近链路或虚拟网卡可用上限。
- 接收丢包、发送丢包、TCP重传或软中断积压增加。
- 网络吞吐下降时,应用P99和超时率同步上升。
- CPU并非全部繁忙,但网络处理线程、软中断或连接队列出现压力。
- 增加CPU核心后,网络吞吐没有增加,说明限制可能在链路、对端或连接处理,而非本机计算能力。
网络吞吐的单位要保持一致。例如,若需要把64 GB数据在10秒内传完,按十进制GB计算:64 × 8 × 1000 ÷ 10 = 51200 Mbps,也就是51.2 Gbps。不要把GB、GiB、MB和Mb混写,否则带宽判断会出现明显误差。
应用和数据库瓶颈的典型组合
有些等待不会直接显示为磁盘或网络拥塞,而是发生在应用内部或数据库内部:
- 线程大量等待互斥锁、条件变量或内部队列。
- 连接池已满,但CPU和磁盘仍有余量。
- GC、对象分配、序列化或压缩占用处理时间。
- 数据库活动会话增加,但主要等待类型是锁、日志刷新、行锁或缓冲池缺页。
- SQL执行时间变长,而数据库主机CPU并没有满载。
- 应用错误率、超时率上升,但系统资源曲线看起来并不饱和。
数据库需要结合自身监控查看活动会话、等待事件、锁冲突、缓冲池命中、日志写入和慢查询。只看数据库主机的CPU利用率,无法判断是查询计算、锁等待还是日志提交限制。
用对照测试排除替代解释
为了避免把所有问题都归因于NUMA,可以按下面的顺序做低风险对照:
- 固定请求量和数据集,改变CPU核心数。
如果吞吐量随核心数增加而增加,且内存带宽未饱和,说明仍存在CPU并行空间。
- 固定核心数,改变线程数。
线程数增加后若运行队列和上下文切换明显增加,但吞吐量不变,可能是过度并发或锁竞争。
- 固定线程数,进行CPU与内存节点绑定。
如果绑定后远端访问下降、P99改善,说明调度位置和数据位置不匹配。
- 比较默认分配、单节点本地分配和交错分配。
本地分配更适合延迟敏感且数据可以按节点拆分的应用;交错分配可能更适合追求总带宽、访问模式均匀的任务,但不一定改善尾延迟。
- 单独观察磁盘和网络。
测试期间若磁盘await、设备队列、网络重传或连接队列同时升高,应先处理对应I/O路径,再判断CPU扩容效果。
- 检查应用和虚拟化约束。
容器的CPU配额、cpuset、内存限制,虚拟机的steal时间,以及宿主机超售,都可能让“增加可见核心”不等于获得更多物理计算资源。
典型测试矩阵可以整理成下面的形式:
| 测试组 | CPU绑定 | 内存策略 | 主要观察点 |
|---|---|---|---|
| A | 默认 | 默认 | 作为基线,记录自然调度结果 |
| B | 固定单节点 | 固定同一节点 | 判断本地访问是否改善延迟 |
| C | 固定多节点 | 默认 | 判断跨节点调度对吞吐的影响 |
| D | 固定多节点 | 交错分配 | 判断总带宽是否提升、尾延迟是否恶化 |
| E | 降低并发 | 保持与A一致 | 判断是否存在带宽、锁或队列饱和 |
每组测试都应使用相同请求序列、相同数据集、相同预热方式和相同采样时长。不要一边更换NUMA策略,一边修改线程数或数据库缓存,否则无法知道是哪项变化产生了结果。
从测试结果形成可执行判断
可以按照“业务结果先行、资源信号佐证”的方式做判断:
- 吞吐量随核心数稳定增长,CPU多核高利用,内存带宽未饱和:更接近CPU计算瓶颈,可以考虑增加物理核心或优化并行度。
- 核心数增加后吞吐量平台化,内存带宽接近上限,缓存未命中和停顿增加:更接近内存带宽瓶颈,应降低无效数据搬运、改善访问模式、控制并发或优化内存通道配置。
- 默认跨节点运行时远端访问升高,绑定后P99改善:更接近NUMA局部性问题,应让工作线程、数据初始化和内存分配尽量保持节点一致。
- CPU整体利用率不高,但单线程满载或锁等待明显:更接近应用串行段或锁竞争,增加核心不会直接解决。
- iowait、磁盘await和队列同步升高:优先处理存储路径,CPU扩容不是主要措施。
- 网络吞吐、重传、连接队列和错误率同步升高:优先检查网络处理和对端服务,不能仅通过增加CPU核心判断。
- 数据库等待事件、锁冲突或日志刷新时间突出:应从SQL、事务、锁和日志路径定位,而不是只看数据库服务器CPU。
对于NUMA调优,通常优先考虑以下原则:
- 按NUMA节点划分工作线程和数据分片。
- 让初始化数据的线程与实际处理数据的线程尽量位于同一节点。
- 减少跨节点共享写入和频繁迁移的对象。
- 为每个节点保留足够的内存余量,避免单节点绑定造成局部内存不足。
- 用实际吞吐量和P99延迟验证调整效果,不以“远端访问更少”作为唯一成功标准。
复测时必须保持哪些条件
一次调优结果只有在条件一致时才有参考价值。下一轮复测至少保持以下内容不变:
- 相同的CPU频率策略、SMT状态和后台任务。
- 相同的数据集、缓存状态和预热时间。
- 相同的请求速率、并发数、线程数和连接池大小。
- 相同的日志级别、压缩方式、批处理大小和超时设置。
- 相同的磁盘、网络和下游依赖状态。
- 相同的采样周期,并同时记录吞吐量、P99、错误率和资源指标。
如果测试中修改了内存通道、BIOS功耗策略、虚拟机拓扑、容器配额或数据库缓存,也应把它们作为独立变量记录。否则即使最终性能变好,也无法确认是核心数、NUMA绑定还是其他环境变化产生了效果。
下一次观察时,不要只截取一张CPU利用率截图。建议把同一时间轴上的“每核CPU利用率、运行队列、IPC或停顿、内存带宽、远端NUMA比例、磁盘await、网络重传、吞吐量、P99响应时间和错误率”放在一起。只有当业务结果与资源指标在时间上相互印证,才能判断服务器CPU核心数增加后究竟是计算能力不足,还是已经被NUMA、内存带宽、磁盘、网络、应用或数据库中的其他环节限制。