资讯详情

资讯详情

NUMA调优实战:多核服务器性能瓶颈与内存访问优化指南

写这篇东西其实是因为我自己在调优时踩过太多NUMA的坑。很多人以为服务器核数越多性能越好结果上了64核、128核的机器跑起业务来CPU利用率稀稀拉拉延迟忽高忽低看监控哪哪都正常就是性能上不去。问题很多时候就出在NUMANon-Uniform Memory Access非统一内存访问上。这篇文章把我对NUMA的理解、排查过程和实用调优手法整理出来适合正在做数据库部署、云原生基础设施、高并发服务调优的朋友。1. 为什么要理解NUMA先搞懂多核时代的内存瓶颈1.1 多核处理器壮大了内存却成了短板过去十几年处理器性能提升主要靠堆核心频率做到一定程度后功耗和发热就顶不住了于是从双核、四核一路加到几十核单颗CPU的核心数量不断翻倍。按理说核心越多并发能力越强但实际跑起来很多人发现核心数量增加到一定程度后性能增长就明显放缓甚至出现“核更多反而更慢”的现象。原因在于内存访问通道。传统架构里所有核心共享一个内存控制器通过一条总线去访问物理内存。核心少的时候这个问题不明显一旦到了16核、32核、64核多个核心同时抢内存带宽总线就成了最堵的瓶颈。你可以想象一个厨房只有一个水龙头厨师越多排队取水的时间就越长炒菜效率反而上不去。CPU核心就是厨师内存控制器就是水龙头大家在等数据从内存搬过来时只能闲着这种现象叫内存延迟隐藏在访存拥堵中性能自然上不去。内存带宽的扩展跟不上核心数量的增长是推动NUMA架构出现的根本原因。1.2 从UMA到NUMA一次架构上的“分治”在老的多路服务器时代系统采用的是UMAUniform Memory Access统一内存访问架构。在UMA架构下所有CPU共享同一个内存控制器任何一个CPU访问任何一块内存延迟和带宽基本一致。听起来很公平但问题也出在这里所有访问都要经过同一个控制器和同一条总线核多之后争抢非常激烈扩展性极差。NUMA的做法则是“分治”把服务器划分成若干个节点每个节点包含一组CPU和一部分物理内存。每个节点有自己的内存控制器CPU访问自己节点上的内存最快访问其他节点的内存则需要经过节点间的互联通道比如Intel的UPI、AMD的Infinity Fabric速度明显变慢。这就是“非统一内存访问”这个名字的由来——不同位置的CPU访问内存花费的时间是不一样的。这种设计本质上是用空间换带宽每增加一个CPU同时也会增加对应的内存控制器和本地内存总带宽跟着涨不再是一条总线扛所有负载。代价是引入了“本地内存”和“远程内存”的区别而所有优化动作也是围绕这个区别展开的。2. 本地访问与远程访问NUMA性能差异的根源2.1 NUMA节点CPU与内存的“对对碰”NUMA节点的概念需要先理清楚。简单说一个NUMA节点就是一组CPU核心和紧挨着它们的一部分物理内存的组合。常见的双路服务器通常有两个NUMA节点节点0包含第一颗CPU及其对应内存条节点1包含第二颗CPU及其对应内存条。操作系统会给每个节点分配一个ID从0开始编号。很多人第一次接触NUMA是在本地开发机上结果运行lscpu一看只有一个NUMA节点压根没意识到NUMA的存在。这个现象很典型单路桌面机和大部分笔记本只有一个节点所有CPU访问所有内存都是“本地访问”不存在远程访问问题。一旦业务部署到双路、四路服务器上两个甚至更多节点同时工作问题就暴露出来了。这也是为什么NUMA问题往往只在生产环境出现开发环境很难复现。在单节点内CPU访问本地内存延迟很低跨节点访问时数据需要经过节点间的互联总线延迟明显上升。这个差异在不同厂商、不同代际的处理器上表现不同但整体趋势是一致的跨节点访问比本地访问慢得多。2.2 本地访问和远程访问差的不只是几纳秒本地内存访问延迟通常在几十纳秒到一百纳秒左右远程访问则要高出一截具体数值跟处理器架构、互联拓扑、内存频率都有关系。如果把两个节点之间的距离换算成相对数值本地通常是10远程可能是20甚至更高这个值直接反映在系统提供的distance矩阵里。更关键的是带宽差异。跨节点访问的带宽受限于节点间互联通道实际可用带宽往往只有本地带宽的一部分甚至不到一半。这意味着如果应用频繁访问远程内存不光每一次读写更慢整体吞吐量也会被削掉一大截。延迟和带宽的差距对实际业务的影响是叠加的数据库的索引扫描、消息队列的排队操作、大文件的页缓存命中这些场景都需要反复读写内存。一旦大量数据落在远程节点上每一次访存都额外增加延迟高并发下延迟波动会被放大P99性能指标尤其难看。用生活化的例子来说本地仓库自提货物几分钟搞定跨仓库调拨还得等运输数据量越大差距越明显。2.3 操作系统默认的内存分配逻辑Linux默认的NUMA内存分配策略是“优先本地分配”进程运行在哪个节点就优先从哪个节点分配内存。这个策略本身很合理能最大程度减少远程访问但它有一个副作用。假设node0上的进程密集node0的内存很快耗尽而node1的内存还有大量空闲。此时进程再申请内存操作系统只能从node1分配。node0上的进程要访问这些新分配的内存就得走远程路径。随着时间推移node0的内存压力越来越大远程访问比例越来越高性能就开始逐步恶化。更麻烦的是如果进程反复申请和释放内存页面在节点之间被频繁访问会产生大量跨节点流量性能波动会变得很随机。这个场景在数据库、Java应用这类内存大户上非常常见。很多运维同学发现服务刚启动时性能很好跑几天后越来越慢重启又恢复正常其中一部分原因就在于此内存分配在节点间逐渐失衡远程访问比例不断上升。理解了这块再看后面的优化手段就顺理成章了。3. 快速看清你的NUMA拓扑这些命令马上能用3.1 lscpu一眼看穿节点分布排查NUMA问题第一步先看清机器长什么样。Linux下最简单的方式是跑lscpu输出中直接有NUMA相关的关键字段。以一台双路32核服务器为例输出大概是这个样子$ lscpu Architecture: x86_64 CPU(s): 32 On-line CPU(s) list: 0-31 Thread(s) per core: 1 Core(s) per socket: 16 Socket(s): 2 NUMA node(s): 2 NUMA node0 CPU(s): 0-15 NUMA node1 CPU(s): 16-31从上面能看到这台机器有2个物理CPU插槽Socket每个插槽16核一共32核有2个NUMA节点节点0和节点1各包含16个CPU核心。这里有一个容易被忽略的点NUMA节点和CPU插槽在大多数情况下是对应的但并非永远如此。有些服务器开启了某些模式后一个物理插槽可能被划分成多个NUMA节点或者多个插槽映射到一个节点。任何时候都要以实际输出的节点列表为准不要想当然。除了lscpu还可以用lscpu -p输出固定格式的字段列表方便脚本解析用lscpu -e按CPU编号列出所属节点、插槽、核心等详细信息。脚本巡检时这两个参数很实用。3.2 numactl --hardware更完整的NUMA视野如果只想针对NUMA做深入检查numactl --hardware是最直接的工具。它会输出每个节点的CPU编号、内存总量和剩余量以及节点间的距离矩阵。虚构一台双路服务器的输出如下$ numactl --hardware available: 2 nodes (0-1) node 0 cpus: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 node 0 size: 32768 MB node 0 free: 20000 MB node 1 cpus: 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 node 1 size: 32768 MB node 1 free: 30000 MB node distances: node 0 1 0: 10 20 1: 20 10最下面的distance矩阵直观展示了节点间的相对距离10表示本地访问20表示远程访问。这个数值不是纳秒而是系统计算出来的相对开销主要用来帮助内核做调度决策。我在实际排查过程中经常用这个命令判断节点间的内存均衡情况。如果node0的free明显小于node1说明node0的内存被消耗得更快很可能是大量进程集中在node0上引起的。配合numastat命令观察节点间内存分配和远程访问的统计值基本就能定位是不是NUMA内存失衡导致的问题。3.3 用图形化工具加深理解如果条件允许安装lstopo来自hwloc工具包可以生成一张完整的系统拓扑图把CPU插槽、核心、NUMA节点、内存以及PCIe设备的关系一图展示出来。lstopo输出的图形对新手特别友好能直观看到哪个核心属于哪个节点、内存条挂在哪个节点下。$ lstopo --of pdf topology.pdf不过说实话日常调优场景下lscpu加numactl --hardware已经够用了。图形化工具更适合做机房硬件梳理、拓扑文档整理时使用。还有个需要注意的地方如果服务器运行在虚拟化环境中lscpu和numactl显示的NUMA信息有可能是虚拟化平台虚拟出来的并不代表真实物理拓扑。这种场景下不能完全依赖命令结果需要跟平台方确认底层物理分配情况。4. 实战优化numactl绑核与内存策略选择指南4.1 numactl绑核绑内存的标准姿势理解了拓扑之后真正的优化手段才刚开始。numactl是Linux上最核心的NUMA控制工具可以控制进程的运行节点和内存分配节点。最常见的用法是把进程绑定到指定节点上运行并且让内存也只从该节点分配# 进程只能运行在节点0的CPU上内存也只能从节点0分配 numactl --cpunodebind0 --membind0 ./myapp--cpunodebind指定允许运行的CPU节点--membind指定允许分配内存的节点。两个参数经常需要配合使用如果只绑CPU不绑内存进程运行在节点0但内存分配时由于策略是localalloc正常情况下会从当前运行节点分配看起来没问题但如果node0内存紧张系统仍然会把内存分配到node1去跨节点访问就出现了。所以明确用--membind把内存也压到同一个节点能避免这种隐性风险。还有一种常见的写法是按CPU编号指定numactl --physcpubind0-7 --membind0 ./myapp这种方式适合只想用某个节点的一部分核心、同时把内存固定在同一节点的情况。我在生产环境里的习惯是把一个节点看成一个独立资源池节点0跑一组实例节点1跑另一组实例两边互不干扰内存带宽各用各的。比如在一个双路服务器上部署两个数据库实例一个绑定节点0一个绑定节点1效果比两个实例随便跑要好得多。4.2 四种内存分配策略怎么选Linux下的NUMA内存策略主要有四种理解它们的区别能让优化更精准而不是盲目下参数。localalloc默认策略从当前CPU所在的节点分配内存。实现简单但容易出现节点间内存使用不均衡。preferred优先策略指定一个优先节点该节点内存充足时就在这个节点分配内存不足时才去其他节点找空闲内存。适合希望优先用某个节点、但又不希望分配失败的场景。bind绑定策略强制只能从指定节点集分配如果这些节点内存不足内存分配就会失败。适合对数据局部性要求极高、且内存需求可控的应用。interleave交错策略在指定节点集合之间轮询分配页面让数据均匀分布在多个节点上。适合进程或线程分布较均衡、访问模式分散的场景。用一个例子帮助理解假设一台双路服务器上有一个进程线程很多且分散在各个节点上运行。此时如果用bind策略把内存全部绑定在node0那么运行在node1的线程访问内存全部要走远程路径性能反而不如localalloc。但如果改用--interleaveall内存页在节点0和节点1之间均匀分布每个线程访问本地内存的概率大约占一半整体延迟反而更均衡。实际操作中并没有一种策略放之四海而皆准。我自己的选择逻辑是单实例且固定在一个节点的服务优先用preferred或bind多线程且线程分散到所有节点的服务可以考虑interleave临时任务和短生命周期进程localalloc够用不必额外折腾。4.3 在代码里做NUMA感知除了系统层面的工具应用代码也能做得更精细。Linux提供了libnuma库可以直接在程序里设置内存策略和CPU亲和性。一个简单的C代码示例是这样的#include numa.h #include numaif.h #include sched.h int main() { // 设置本进程的内存分配策略限制在节点0 struct bitmask *nodes numa_allocate_nodemask(); numa_bitmask_setbit(nodes, 0); numa_set_membind(nodes); numa_free_nodemask(nodes); // 设置CPU亲和性只允许运行在CPU 0~3上 cpu_set_t set; CPU_ZERO(set); for (int i 0; i 4; i) { CPU_SET(i, set); } sched_setaffinity(0, sizeof(set), set); // 业务逻辑... }线程层面可以使用pthread_setaffinity_np来限制每个线程运行的CPU集合更细粒度的内存控制可以用mbind系统调用直接为某块内存指定节点策略。不过我要提醒一句大多数业务代码并不需要直接写NUMA接口。通过numactl启动时设置策略或者借助容器、systemd的CPU亲和配置已经覆盖了绝大多数场景。只有那些追求极致性能、有明确线程节点亲和需求的核心模块才值得在代码层面做NUMA感知设计。过度设计反而增加代码复杂度调优成本不划算。4.4 容器和虚拟化场景的NUMA处理容器时代的NUMA处理又多了一层复杂性。容器通过cpuset可以限制进程能使用的CPU集合这在物理NUMA节点上天然形成了一种隐式绑定。比如给一个容器只分配节点0上的8个CPU那么容器内的进程只能跑在这8个核上内存分配默认也会优先落在节点0。问题在于容器的内存限制如果配置不当容器内进程的内存压力可能导致内核在节点间做内存回收或者迁移反而引入额外的性能抖动。有些平台提供了拓扑感知调度能力比如让Pod的CPU绑定和内存节点分配严格限制在同一个物理节点上这需要底层平台配合才能做到。虚拟机的处理思路类似主要靠虚拟化平台的NUMA亲和配置。以KVM/QEMU为例可以使用numatune参数为虚拟机指定内存亲和节点把vCPU和内存都绑在同一个物理节点上避免虚拟机跑起来后vcpu和内存跨物理节点访问。如果平台没有做这些配置虚拟机内的NUMA信息可能是虚拟的优化效果很难保证。5. 典型场景实践数据库、缓存与高并发服务的实测效果5.1 某查询服务从“卡顿”到“顺滑”的排查记录我之前处理过一个查询服务的性能问题这台服务器是双路64核。日常负载下整体CPU利用率不算高但P99响应时间波动很大而且top里能看到部分核很忙、部分核很闲。一开始怀疑是应用瓶颈结果性能分析工具一跑热点集中在内存访问路径上CPU大量时间在等待数据从内存搬过来。接着做了两个检查一是numactl --hardware看节点内存分布发现node0的free明显低于node1说明内存分配已经出现明显倾斜二是numastat看节点间的访问统计远程访问次数以肉眼可见的速度增长。最终确认是默认的localalloc策略导致的恶性循环node0上的进程多、内存消耗快新内存只能从node1分配运行在node0的线程访问这些新内存全部变成远程访问。最后处理方式是调整服务启动参数把进程按节点分组运行一半进程绑定到节点0另一半绑定到节点1内存分配也分别固定到各自节点。改完之后远程访问比例迅速下降P99响应时间恢复到稳定水平整体吞吐量提升了约三成。这次排查给我的启发是CPU没有全满不代表系统没问题跨节点访问带来的延迟波动会让性能变得非常不可预测。5.2 缓存类应用的NUMA经验缓存类应用对延迟的敏感程度比普通业务更高。以Redis这类单线程模型为例它本身的吞吐能力取决于单核性能和内存访问延迟一旦进程被调度到不同节点之间切换性能损耗非常直观。在一台多路服务器上部署多个Redis实例时我建议每个实例固定绑定一组CPU同时将内存绑定到对应的节点numactl --cpunodebind8-15 --membind0 /usr/local/redis/bin/redis-server /etc/redis/redis_1.conf这样每个实例独占一部分CPU和本节点的内存不会出现多个实例抢同一节点资源的情况。还有一个容易忽略的点CPU 0所在节点往往承担着大量内核线程、软中断和系统管理任务业务实例尽量避开那个节点避免和系统任务抢时间片。5.3 高并发Web服务的NUMA亲和配置高并发Web服务比如Nginx、API网关这类多worker模型NUMA的影响主要体现在进程调度的不确定性上。如果worker进程没有做CPU绑定内核可能把它们调度到不同节点每个worker访问内存的路径各不相同导致节点间流量不均衡延迟抖动明显。常见的做法是给每个worker配置CPU亲和性让一个worker尽量固定在一个节点内。Nginx的worker_cpu_affinity指令就是干这个的worker_processes 4; worker_cpu_affinity 0001 0010 0100 1000;这样4个worker分别绑在4个不同的CPU上每个worker的内存访问路径相对固定。另一个细节是网络中断处理如果网卡支持RSS多队列最好把不同队列的中断绑定到不同CPU核心上同时配合网络驱动配置避免中断都挤在同一个节点否则即使业务绑核了中断处理也会成为新的瓶颈。6. 问题排查与避坑指南6.1 常见问题速查表把这几年遇到的NUMA相关高频问题整理成一张表方便排查时对照。问题现象可能原因排查命令解决方案CPU使用率不均衡部分核满载部分闲置进程未绑定调度随机top查看各核、pidstat -t对进程做CPU亲和性绑定整体性能不高且内存带宽占用高大量远程内存访问numastat、perf mem绑定内存策略优化数据布局服务跑一段时间后性能下降默认本地分配导致节点间内存失衡numactl --hardware看节点free变化使用preferred或interleave策略容器内应用延迟波动明显容器未感知物理NUMA拓扑查看cpuset、平台调度配置使用平台级NUMA感知调度绑定后反而变慢绑定的节点资源不足或系统任务干扰numastat、检查CPU0负载调整绑定节点避开系统繁忙节点6.2 容易被忽略的坑第一只绑CPU不绑内存。很多人对绑核印象深刻但忘了同时限制内存分配节点。绑核带来的收益会被远程内存访问吃掉一大半。我习惯上总是把--cpunodebind和--membind一起写缺一不可。第二盲目使用interleave。interleave适合线程均匀分布于多个节点的场景但如果你有一个明显的本地化数据访问模式interleave反而会让一部分访问变成远程访问得不偿失。用之前先想清楚业务线程跑在哪些节点上。第三忽略系统在CPU 0上的负载。CPU 0承担了大量内核管理任务包括时钟中断、软中断、RCU处理等。如果业务进程绑定到CPU 0所在节点的核心虽然逻辑上是本地访问但实际性能可能被系统任务拖累。分配业务负载时尽量先避开CPU 0附近的核心。第四AutoNUMA不是万能药。内核的AutoNUMA特性会自动迁移内存页到访问它的CPU所在节点听起来很智能但迁移本身需要开销和扫描成本。如果应用的内存热区很大、访问模式变化频繁页迁移反而会造成性能波动。这类场景可以考虑关闭或调整AutoNUMA参数。第五没有把NUMA信息固化到部署流程。手工执行numactl绑一次两次没问题但每次服务重启、扩容都要重新绑定很容易漏掉。建议把numactl参数直接写进systemd服务文件[Service] ExecStart/usr/bin/numactl --cpunodebind0 --membind0 /usr/sbin/myapp这样服务每次启动都会自动应用NUMA配置不会再被人为遗漏。我自己的体会是NUMA调优没有银弹公式它要求你先搞清楚两件事这台机器的节点拓扑长什么样业务工作负载的访存模式是什么。有了这两个基础再用numactl、libnuma、内核配置去匹配多半能收到立竿见影的效果。当你看到numastat上远程访问比例降下来、P99曲线变得平稳那种感觉和解决一个复杂Bug差不多。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →