Linux NUMA调优实战:多线程程序加速方法
在当今数据爆炸的时代,多线程程序已成为挖掘硬件潜力、加速应用运行的关键手段。对于运行在 Linux 系统中的多线程程序而言,非统一内存访问(NUMA)架构既带来了机遇,也提出了挑战。NUMA 架构通过将内存划分至不同节点,每个节点关联特定处理器核心,实现本地内存快速访问。这意味着在多线程程序运行时,若线程与内存访问能精准匹配到同一节点,便能大幅提升效率。
然而,实际情况往往复杂得多,线程跨节点访问内存的现象时有发生,从而引发性能瓶颈。今天,我们将深入 Linux NUMA 调优实战领域,聚焦多线程程序加速方法。从底层原理剖析,到实用工具运用,再到具体优化策略实施,带你逐步掌握在 NUMA 架构下释放多线程程序性能的精髓,为开发高效能应用筑牢根基 。
一、NUMA 是什么?
NUMA(非统一内存访问)是一种多处理器系统的内存架构,其中内存访问时间取决于内存相对于处理器的位置。在 NUMA 系统中,处理器可以比访问远程内存更快地访问本地内存。
在传统的统一内存访问(UMA,Uniform Memory Access)架构中,所有处理器通过共享的内存总线访问同一块物理内存,就好像所有的人都要通过同一个门进入一个大仓库取东西 。这种架构下,处理器访问内存的延迟是一致的,好处是内存访问逻辑简单,易于实现和管理,在处理器数量较少时表现良好。但随着处理器数量的不断增加,内存总线就成了性能瓶颈,就好比这个门太小,人多了就会拥堵,导致内存访问延迟增加,内存带宽不足,限制了系统整体性能的提升。
1.1传统SMP架构的问题
在早期的对称多处理器(SMP)系统中:
所有 CPU 通过共享总线访问统一的内存池
随着 CPU 数量增加,总线成为瓶颈
内存带宽无法满足多个 CPU 的需求
为了解决 UMA 架构的局限性,NUMA 架构应运而生。在 NUMA 架构中,系统被划分为多个节点(Node),每个节点包含一个或多个处理器、本地内存以及 I/O 设备 。每个处理器可以访问本地内存和其他节点的内存,但访问本地内存的速度要比访问其他节点(远程)内存的速度快很多。
这就好比每个团队都有自己独立的小仓库,团队成员优先从自己团队的小仓库取东西,速度自然快,如果要去别的团队仓库取东西,就会花费更多时间。例如,在一个双路服务器中,每个 CPU 及其对应的内存可以组成一个 NUMA 节点,当 CPU1 访问自己节点的内存时,延迟较低;而当 CPU1 访问 CPU2 节点的内存时,延迟就会明显增加。
传统 SMP 架构:
CPU1 ─┐
CPU2 ─┼─── 共享总线 ─── 内存
CPU3 ─┤
CPU4 ─┘
1.2 NUMA的解决方案
NUMA 通过将系统划分为多个节点来解决这个问题:
每个节点包含一组 CPU 和本地内存
节点之间通过高速互连网络连接
NUMA 架构:
节点0: CPU0,CPU1 ── 本地内存0│├─── 互连网络 ──——─┤│ │
节点1: CPU2,CPU3 ── 本地内存1
1.3为什么要进行 NUMA 性能调优?
在 NUMA 架构下,多线程程序的性能表现与内存访问的局部性密切相关。如果多线程程序在运行时没有充分考虑 NUMA 架构的特性,就可能出现大量的跨节点内存访问,导致性能急剧下降。
假设你正在运行一个多线程的大数据分析程序,这个程序需要处理海量的数据。每个线程都要频繁地读写内存中的数据,如果没有进行 NUMA 性能调优,线程可能会随机地访问不同节点的内存。比如,一个线程原本在 Node1 上运行,却频繁地去访问 Node2 上的内存数据,这就好比你在自己团队的仓库找不到东西,要频繁地跑去别的团队仓库找,一来一回就浪费了很多时间。这种跨节点内存访问会增加内存访问延迟,降低内存带宽的利用率,使得程序的执行速度大幅下降,原本可能几小时就能完成的数据分析任务,现在可能需要花费数倍的时间。
再以数据库服务器为例,数据库服务器需要处理大量的并发查询请求,每个查询请求可能由一个线程来处理。如果线程没有合理地分配到本地内存资源,不同线程对内存的访问在各个节点上杂乱无章,就会导致大量的跨节点内存访问。这不仅会增加内存访问的延迟,还可能导致内存带宽成为瓶颈,影响数据库服务器的并发处理能力,使得数据库的响应速度变慢,无法满足大量用户的快速查询需求。
对于一些高性能计算场景,如天气预报模型计算、基因测序数据分析等,这些任务通常需要进行大规模的并行计算,对内存访问性能要求极高。如果多线程程序在 NUMA 架构下没有进行优化,跨节点内存访问带来的性能损耗会严重制约计算效率,使得科研人员需要等待更长的时间才能得到计算结果,影响科研进展。
由此可见,进行 NUMA 性能调优对于多线程程序来说至关重要。通过优化,可以让线程尽可能地访问本地内存,减少跨节点内存访问,充分利用 NUMA 架构的优势,提高内存访问效率,从而提升多线程程序的整体性能,使其能够更快速、高效地完成任务,满足不同应用场景对高性能计算的需求。
二、NUMA系统架构
2.1内存管理的 “进化之路”
⑴SMP 架构的困境
在早期的计算机系统中,随着应用对计算性能需求的不断攀升,多处理器技术应运而生,其中对称多处理(SMP)架构备受瞩目。在 SMP 架构下,多个处理器平等地连接到同一条共享内存总线上,共享同一物理内存空间,就像一群小伙伴共同围绕着一个公共的玩具箱,每个人都能平等地从中拿取玩具。这种架构的设计初衷是为了充分利用多个处理器的并行计算能力,通过操作系统的调度,让不同的处理器协同处理各种任务,从而提升系统的整体性能。
然而,随着处理器核心数量的持续增加,SMP 架构逐渐暴露出严重的性能瓶颈。想象一下,当众多小伙伴同时冲向玩具箱想要取出自己心仪的玩具时,共享内存总线就如同那狭窄的玩具箱开口,成为了激烈竞争的焦点。多个处理器频繁地同时访问内存,导致总线争用异常激烈,内存访问延迟急剧上升。在高并发场景下,为了保证数据的一致性,处理器往往需要使用原子指令来访问内存,例如通过锁总线的方式独占内存访问权。这就好比小伙伴们在争抢玩具时,有人直接把玩具箱的开口堵住,不让其他人拿玩具,直到自己拿到为止,使得其他处理器只能干巴巴地等待,造成大量的处理器资源闲置浪费,系统整体性能大打折扣。
以一个典型的数据库服务器为例,在处理大量并发事务时,多个处理器核心需要频繁读写内存中的数据块。由于 SMP 架构下内存总线的争用,处理器常常需要等待很长时间才能获取到所需的数据,导致事务处理的响应时间大幅增加,系统吞吐量急剧下降,无法满足业务对高性能的要求。这种内存访问瓶颈严重制约了 SMP 架构在大规模计算场景下的应用,迫切需要一种新的内存架构来打破这一困境。
⑵NUMA 架构应运而生
为了突破 SMP 架构在内存访问方面的瓶颈,非统一内存访问(NUMA)架构应运而生。它像是给计算机系统重新规划了一个更合理的 “居住布局”,将整个系统划分为多个节点(Node),每个节点都配备了自己的本地内存、处理器以及 I/O 设备,节点之间则通过高速互连网络进行通信,就如同在一个大型社区里,划分出了多个相对独立的小区,每个小区都有自己的配套设施,小区之间有便捷的道路相连。
在 NUMA 架构中,处理器访问本地内存的速度远远快于访问其他节点的远程内存,这是因为本地内存与处理器之间的物理距离更近,数据传输延迟更低,就像小区居民在自家楼下的小超市购物,方便快捷;而访问其他节点的内存则像是要跑到隔壁小区的超市购物,需要经过一段路程,花费更多的时间。这种内存访问的非一致性特性,使得 NUMA 架构能够有效地减少内存总线的争用,提高内存访问的并行性,进而提升系统的整体性能。
与 SMP 架构相比,NUMA 架构最大的不同在于其内存访问的非对称性。SMP 架构下,所有处理器对内存的访问延迟是一致的,就像所有居民到公共玩具箱的距离都一样远;而 NUMA 架构中,不同节点的内存访问延迟存在差异,处理器会优先访问本地内存,以获取更快的数据读写速度。这种差异使得 NUMA 架构在处理大规模数据密集型应用时具有显著优势,能够更好地适应现代计算机系统对高性能、高扩展性的需求。例如,在大规模科学计算、云计算数据中心等场景中,NUMA 架构能够充分发挥各个节点的计算能力,高效地处理海量数据,为用户提供快速、稳定的服务。
2.2Linux 内核中的 NUMA 架构 “画像”
⑴节点的组织与表示
在 Linux 内核的世界里,对于 NUMA 架构的支持可谓是精心设计、精妙绝伦。每个 NUMA 节点在内核中是由结构体 pglist_data(在老版本内核中叫 pg_data_t,本质相同)来进行描述的,它就像是每个节点的 “管家”,掌管着节点内诸多关键信息。这个结构体包含了一个名为 node_zones 的数组,其类型为 struct zone,这便是内存区域的 “收纳盒”,每个节点内不同特性的内存区域都被收纳其中。
为了兼容不同硬件设备五花八门的特性以及应对 32 位、64 位系统各自的需求,Linux 内核将内存划分成了不同的区域类型。常见的有 ZONE_DMA、ZONE_DMA32、ZONE_NORMAL 等。ZONE_DMA 区域,通常是低 16M 的内存范围,它可是专为那些支持直接内存访问(DMA)的设备量身定制的。因为有些老旧的 DMA 控制器,它们只能访问这低 16M 的内存空间,所以内核特意划分出这片区域,以确保这些设备能够顺畅地与内存交互,实现数据的高速传输,就好比为特殊需求的客人预留了特定的通道。
随着硬件的发展,64 位系统登上舞台,一些新的 DMA 设备能够访问更广泛的内存空间,但又达不到完整的 4G 范围,于是 ZONE_DMA32 应运而生,它主要服务于这些较新的、能访问 4G 以内内存的 DMA 设备,为它们提供了专属的 “栖息地”。
而 ZONE_NORMAL 区域,则涵盖了 16M 到 896M(在 32 位系统且开启物理地址扩展 PAE 的情况下)或者更大范围(64 位系统)的内存,这片区域的内存可以直接映射到内核的虚拟地址空间,内核能够直接、高效地对其进行访问,就像是家里的 “常用物品存放区”,取用物品极为便捷,是内核日常运行时频繁使用的内存 “主力军”。
不同的内存区域有着不同的使命,它们紧密协作,与硬件设备默契配合,为整个系统的稳定高效运行奠定了坚实基础。这种精细的内存区域划分,充分展现了 Linux 内核设计的前瞻性与兼容性,使得 Linux 能够在各种硬件平台上纵横驰骋,大放异彩。
⑵内存分配的 “策略蓝图”
当系统需要分配内存时,Linux 内核就像一位精明的调度大师,有着一套严谨且周全的策略。首先,它会依据预先计算好的节点距离信息,为内存分配指引方向。每个节点与其他节点之间的距离都被精准度量,这个距离可是影响内存分配优先级的关键因素。
以 ZONELIST_FALLBACK 策略为例,假设系统中有多个 NUMA 节点,节点 0 在分配内存时,会优先查看自身节点的内存情况。因为访问自身节点的内存就如同在自家院子里取东西,速度最快,延迟最低。要是自身节点内存告急,无法满足需求,内核就会按照节点距离由近及远的顺序,依次去相邻节点 “借” 内存,比如先看向节点 1,再是节点 2、节点 3 等。这就好比先向隔壁邻居求助,若邻居也没办法,再往稍远一点的人家打听。
不过,也有些特殊场景,比如使用 __GFP_THISNODE 标志进行内存分配时,就遵循 ZONELIST_NOFALLBACK 策略,意味着内存分配只能在当前 NUMA 节点内进行,哪怕内存吃紧,也绝不 “外借”,有点像坚守自家资源,自力更生的意思。
确定好节点后,接下来就要挑选具体的内存区域了。这时候,node_zonelists 数组就派上了大用场。它就像是一张详细的 “内存地图”,指引着内核找到合适的内存区域。在 ZONELIST_FALLBACK 策略下,对于节点 0 的 node_zonelists[ZONELIST_FALLBACK],其内部的 zoneref 元素会按照节点距离排序,同时每个节点内的内存区域又依据优先级从高到低排列,通常是 ZONE_NORMAL 优先级较高,优先被考虑,其次是 ZONE_DMA32,最后是 ZONE_DMA。这是因为 ZONE_NORMAL 区域的内存使用最为频繁、便捷,而 ZONE_DMA 区域相对较为特殊,只用于特定的 DMA 设备,不能轻易动用。
当进程申请内存时,内核会从这张 “地图” 的起始位置开始查找,优先锁定距离最近节点中的最高优先级内存区域。若该区域内存不足,才会按照既定顺序,逐步往低优先级区域或者更远节点的内存区域探索,直到找到满足需求的内存为止。如此精细复杂的内存分配策略,确保了在 NUMA 架构下,系统能够充分利用各个节点、各个区域的内存资源,达到性能的最优平衡,让计算机系统在多任务、高负载的复杂环境下依然能够稳健运行,高效处理各种数据与任务。
三、NUMA核心技术
NUMA(Non - Uniform Memory Access Architecture)即非统一内存访问架构,是一种用于多处理器的电脑内存设计技术。其核心内容如下:
内存和 CPU 分组:将 CPU 和内存划分为多个节点(Node),每个节点包含若干 CPU 核心和本地内存。例如,双路服务器通常包含 2 个 NUMA 节点。
访问速度差异:CPU 访问本地节点的内存(本地内存)速度快,延迟通常约为 100ns。而访问其他节点的内存(远程内存)速度慢,需通过跨节点互联,如 AMD 的 Infinity Fabric,延迟可达到 200 - 300ns,带宽可能减半。
NUMA 技术的出现主要是为了解决传统 UMA(Uniform Memory Access,统一内存访问)架构中,随着 CPU 核心数增加,内存总线争用导致性能下降的问题。它通过将系统划分为多个节点,每个节点独立管理本地内存,减少了全局总线竞争,提升了系统的扩展性。
在实际应用中,可通过进程绑定(CPU Pinning)将进程限制在特定 NUMA 节点,强制使用本地内存;也可利用中断亲和性(IRQ Affinity)将网卡中断分配到与进程相同的 NUMA 节点,减少跨节点通信。此外,多线程应用中,确保线程访问的数据位于同一 NUMA 节点,实现数据结构本地化,也能优化性能
3.1UMA技术
UMA是并行计算机中的共享存储架构,即物理存储器被所有处理机均匀共享,对所有存储字具有相同的存取时间。每台处理机可以有私用高速缓存,外围设备也以一定形式共享。UMA技术适合于普通需求和多用户共享时间的应用,在时序要求严格的应用中,被用作加速单一大型程序的执行率。
3.2NUMA技术
NUMA是用于多进程计算中的存储设计,存储读取取决于当前存储器与处理器的关联。在NUMA技术下,处理器访问本地存储器比非本地存储器(另一个处理器的本地存储器或者处理器共享的存储器)更快。
3.4vNUMA
vNUMA消除了VM和操作系统之间的透明性,并将NUMA架构直通到VM的操作系统。值得一提的是,vNUMA在业内与NUMA同样盛名。对于一个广泛VM技术,VM运行的底层架构,VM的NUMA拓扑跨越多个NUMA节点。在启用了vNUMA的VM的初始功能之后,呈现给操作系统的架构是永久定义的,并且不能被修改。这个限制通常是正面的,因为改变vNUMA体系结构可能会导致操作系统的不稳定,但是如果VM通过vMotion迁移到带有不同NUMA架构的管理程序,则可能导致性能问题。值得一提的是,尽管大多数应用程序都可以利用vNUMA,但大多数VM都足够小,可以装入NUMA节点;最近对宽-VM支持或vNUMA的优化并不影响它们。
因此,客户操作系统或它的应用程序如何放置进程和内存会显著影响性能。将NUMA拓扑暴露给VM的好处是,允许用户根据底层NUMA架构做出最优决策。通过假设用户操作系统将在暴露的vNUMA拓扑结构中做出最佳决策,而不是在NUMA客户机之间插入内存。
3.5NUMA的重要性
多线程应用程序需要访问CPU核心的本地内存,当它必须使用远程内存时,性能将会受到延迟的影响。访问远程内存要比本地内存慢得多。所以使用NUMA会提高性能。现代操作系统试图在NUMA节点(本地内存+本地CPU=NUMA节点)上调度进程,进程将使用本地NUMA节点访问核心。ESXi还使用NUMA技术为广泛的虚拟机,当虚拟核心大于8时,将虚拟核心分布在多个NUMA节点上。当机器启动时,虚拟核心将被分发到不同的NUMA节点,它将提高性能,因为虚拟核心将访问本地内存。
四、探寻NUMA节点
4.1探测的 “魔法指令”
在 Linux 系统中,想要揭开 NUMA 节点的神秘面纱,查看其详细信息,我们有一些非常实用的 “魔法指令”。就拿 numactl 来说,它堪称是探索 NUMA 架构的得力助手。当我们在终端输入 “numactl --hardware”,系统就如同一位贴心的导游,为我们展示出系统的 NUMA 拓扑全景图。从这幅图中,我们能清晰知晓系统里究竟有多少个 NUMA 节点,它们就像是分布在计算机世界里的不同 “领地”。
每个节点配备的 CPU 核心数量也一目了然,这些 CPU 核心可是节点的 “主力军”,肩负着处理各种任务的重任。内存总量信息则让我们对系统的存储资源心中有数,清楚每个节点能容纳多少数据 “宝藏”,以及当前还有多少可用内存,为资源分配提供关键参考。另外,节点之间的距离信息也十分关键,它直观地反映了不同节点间内存访问的 “路程远近”,帮助我们理解数据传输的开销成本。
举个例子,在一台配置了双路处理器、拥有两个 NUMA 节点的服务器上执行此命令,可能会得到类似这样的结果:节点 0 拥有 8 个 CPU 核心,内存总量为 16GB,当前空闲内存 2GB,与节点 1 的距离为 20;节点 1 同样有 8 个 CPU 核心,内存总量 16GB,空闲内存 3GB,节点间距离相互对称。有了这些详细信息,我们就能精准把握系统资源布局,为后续的应用部署、性能优化提供有力依据,让系统运行更加高效流畅。
除了 numactl,在 /sys/devices/system/node 目录下也隐藏着诸多关于 NUMA 节点的 “情报”。这里面的每个以 “node” 开头的子目录,都对应着一个具体的 NUMA 节点,仿佛是一个个装满信息的 “宝箱”。进入这些子目录,查看诸如 “cpulist” 文件,就能知晓该节点所关联的 CPU 核心列表,就像拿到了节点的 “兵力部署图”;“meminfo” 文件则详细记录着内存的使用情况,包括已用内存、空闲内存等,是内存资源的 “账本”。这些文件里的数据实时更新,时刻反映着系统运行过程中 NUMA 节点的动态变化,为系统管理员、开发者提供了一手的资源动态信息,便于及时调整策略,保障系统稳定高效运行。
4.2代码中的 “蛛丝马迹”
倘若我们想要深入到 Linux 内核的底层,从代码层面去理解 NUMA 节点探测的原理,那就得走进内核源码的 “神秘世界”。以常见的 64 位多核操作系统为例,在 Linux 内核源码里,有一系列关键的结构体和函数在默默运作。
首先是 numa_node_id() 函数,它就像是一个 “导航仪”,当进程在运行过程中需要获取当前所处的 NUMA 节点编号时,只要调用这个函数,就能快速定位。它的实现原理涉及到对硬件寄存器、内存映射等底层机制的巧妙运用。在一些基于 Intel 架构的系统中,处理器会通过特定的寄存器来记录当前访问内存所对应的 NUMA 节点信息,numa_node_id() 函数则会读取这个寄存器的值,经过简单的转换和校验,将准确的节点编号返回给调用者,确保进程能精准知晓自己的 “归属地”。
再深入探究,struct pglist_data 结构体中的诸多成员变量,为我们全方位揭示了 NUMA 节点的详细信息。node_id 成员明确标识了节点的唯一编号,如同每个人的身份证号,在整个系统中独一无二;node_start_pfn 记录着节点起始物理页帧的编号,这是内存管理的重要基石,通过它可以快速定位节点内存的起始位置,为内存分配、回收等操作划定边界;node_spanned_pages 则精确统计了节点所跨越的物理页帧数量,让我们清楚了解每个节点的内存容量大小,以便合理规划资源。
当系统启动初始化阶段,内核会逐个遍历识别出的 NUMA 节点,就像一位严谨的普查员,对每个节点的硬件信息进行仔细登记。通过读取主板 BIOS 提供的 ACPI(高级配置与电源接口)表,获取节点的 CPU 拓扑结构、内存布局等关键信息,然后将这些信息填充到相应的结构体成员中,完成对 struct pglist_data 结构体的初始化。在后续的系统运行过程中,内核就依据这些初始化后的信息,有条不紊地进行内存管理、进程调度等一系列复杂而关键的任务,确保整个系统在 NUMA 架构下高效协同运行,为用户提供流畅稳定的使用体验。
①查看 NUMA 信息
# 查看 NUMA 拓扑
lscpu | grep NUMA
numactl --hardware
lstopo # 需要安装 hwloc# 查看进程的 NUMA 使用情况
numastat
numastat -p <pid># 查看内存使用情况
cat /proc/meminfo
cat /proc/buddyinfo
②监控 NUMA 性能
# 查看 NUMA 命中率
numastat -c# 使用 perf 监控 NUMA 事件
perf stat -e node-loads,node-load-misses ./program
③调试 NUMA 问题
# 查看进程的内存映射
cat /proc/<pid>/numa_maps# 查看 NUMA 平衡统计
cat /proc/vmstat | grep numa
④性能调优
# 禁用自动 NUMA 平衡(可能提高性能)
echo 0 > /proc/sys/kernel/numa_balancing# 调整 zone_reclaim_mode
echo 1 > /proc/sys/vm/zone_reclaim_mode
五、实战前的准备:工具与环境
在开始 NUMA 性能调优实战之前,需要先准备好相关的工具,并确保运行环境支持 NUMA。下面介绍一些常用的工具及其安装和基本使用方法。
5.1 numactl
numactl 是一个非常实用的命令行工具,用于在 NUMA 架构下控制进程和线程的内存分配与调度 。它可以帮助我们将进程绑定到特定的 NUMA 节点,指定内存分配策略等。
在 Debian/Ubuntu 系统上,可以使用以下命令安装 numactl:
sudo apt - get install numactl
在 CentOS/RHEL 系统上,安装命令如下:
sudo yum install numactl
安装完成后,我们可以使用以下命令查看系统的 NUMA 架构信息:
sudo numactl --hardware
执行上述命令后,会输出系统中每个 NUMA 节点的编号、CPU 列表、内存信息以及内存的距离等。例如:
available: 2 nodes (0 - 1)
node 0 cpus: 0 1 2 3 4 5 6 7 16 17 18 19 20 21 22 23
node 0 size: 131037 MB
node 0 free: 3019 MB
node 1 cpus: 8 9 10 11 12 13 14 15 24 25 26 27 28 29 30 31
node 1 size: 131071 MB
node 1 free: 9799 MB
node distances:
node 0 10: 10 201: 20 10
如果我们希望将某个进程绑定到第 0 个 NUMA 节点上,可以使用如下命令:
sudo numactl --cpunodebind = 0 --membind = 0 your_command
其中,--cpunodebind = 0表示将进程分配到第 0 个 NUMA 节点上的 CPU 列表中,--membind = 0表示将进程分配到第 0 个 NUMA 节点上的内存,your_command是需要启动的进程,比如一个可执行文件或一个命令行。
5.2 numastat
numastat用于监控和显示系统的内存使用情况,特别是各个NUMA节点的内存分配和使用情况,它能帮助我们了解内存的使用分布,以便更好地进行性能优化 。
安装 numastat 同样可以使用包管理器,在 Debian/Ubuntu 系统上:
sudo apt - get install numactl
在 CentOS/RHEL 系统上:
sudo yum install numactl
安装好后,直接运行numastat命令,就可以查看系统的内存使用信息:
numastat
输出结果类似如下:
node0 node1
numa_hit 1775216830 6808979012
numa_miss 4091495 494235148
numa_foreign 494235148 4091495
interleave_hit 52909 53004
local_node 1775205816 6808927908
other_node 4102509 494286252
在这些输出数据中,numa_hit表示命中的,即成功分配本地内存访问的内存大小;numa_miss表示把内存访问分配到另一个 node 节点的内存大小;numa_foreign表示另一个 Node 访问本节点内存的大小;local_node表示本节点的进程成功在本节点上分配内存访问的大小;other_node表示本节点的进程在其它节点上分配的内存访问大小。通常,numa_miss和numa_foreign值越高,就越需要考虑进行 NUMA 绑定优化 。
5.3 perf
perf 是 Linux 内核自带的性能分析工具,它基于事件采样原理,能够对处理器相关性能指标与操作系统相关性能指标进行剖析,可用于查找性能瓶颈和定位热点代码 。由于 perf 是内核自带工具,大多数 Linux 系统无需额外安装。
使用 perf 可以分析程序运行期间发生的硬件事件(如cpu - cycles、instructions 、cache - misses等)和软件事件(如page - faults、context - switches等)。例如,要分析某个进程的 CPU 周期使用情况,可以使用以下命令:
sudo perf stat -p <pid> -e cpu - cycles
其中,-p <pid>指定要分析的进程 ID,-e cpu - cycles表示要统计的事件为 CPU 周期。执行后会输出该进程在运行期间的 CPU 周期相关统计信息。
如果想要查看某个进程的热点函数,使用perf top命令,它会实时显示系统中占用 CPU 周期最多的函数:
sudo perf top -p <pid>
在输出结果中,第一列是符号引发的性能事件的比例,指占用的 CPU 周期比例;第二列是符号所在的 DSO(Dynamic Shared Object),可以是应用程序、内核、动态链接库、模块;第三列是 DSO 的类型,[.]表示此符号属于用户态的 ELF 文件,[k]表示此符号属于内核或模块;第四列是符号名。
在进行 NUMA 性能调优实战前,确保系统安装并熟悉使用这些工具,将为后续的性能分析和优化工作提供有力支持。
六、实战开始:NUMA性能调优步骤
6.1查看系统 NUMA 配置
在开始调优之前,首先要了解系统的 NUMA 配置情况。可以使用lscpu命令查看系统的CPU和NUMA节点信息 。执行lscpu命令后,会输出一系列关于 CPU 和系统架构的详细信息,其中与 NUMA 相关的部分如下:
NUMA node(s): 2
NUMA node0 CPU(s): 0,2,4,6,8,10,12,14,16,18,20,22,24,26,28,30
NUMA node1 CPU(s): 1,3,5,7,9,11,13,15,17,19,21,23,25,27,29,31
从这些输出中,我们能得知系统中有 2 个 NUMA 节点,以及每个节点包含的 CPU 编号。
也可以使用numactl -H命令查看更详细的 NUMA 架构信息,包括每个节点的 CPU 列表、内存大小、空闲内存以及节点之间的距离 :
available: 2 nodes (0 - 1)
node 0 cpus: 0 1 2 3 4 5 6 7 16 17 18 19 20 21 22 23
node 0 size: 131037 MB
node 0 free: 3019 MB
node 1 cpus: 8 9 10 11 12 13 14 15 24 25 26 27 28 29 30 31
node 1 size: 131071 MB
node 1 free: 9799 MB
node distances:
node 0 10: 10 201: 20 10
node distances表示节点之间的距离,距离值越大,说明节点间的访问延迟越高。这里节点 0 访问节点 1 的距离为 20,节点 1 访问节点 0 的距离同样为 20,而节点访问自身的距离为 10,表明访问本地节点的延迟更低。这些信息对于后续的性能调优至关重要,它让我们清楚地了解系统的硬件布局,为合理分配资源提供了依据。
6.2进程绑定到 NUMA 节点
将进程绑定到特定的 NUMA 节点是优化多线程程序性能的重要一步。这样可以确保线程访问的内存是本地内存,减少跨节点内存访问。可以使用taskset和numactl命令来实现进程绑定 。
taskset命令用于设置或查看进程的 CPU 亲和性,也可以用来将进程绑定到特定的 NUMA 节点 。例如,要将进程号为1234的进程绑定到 NUMA 节点 0 上,可以使用以下命令:
taskset -m 0 -p 1234
其中,-m选项表示指定 NUMA 节点,0表示 NUMA 节点 0,-p选项后面跟着要绑定的进程号。
numactl命令功能更强大,不仅可以绑定 CPU,还能指定内存绑定 。例如,要启动一个新的进程并将其 CPU 和内存都绑定到 NUMA 节点 1,可以使用如下命令:
numactl --cpunodebind = 1 --membind = 1 your_command
--cpunodebind = 1表示将进程绑定到节点 1 的 CPU 上,--membind = 1表示将进程的内存分配绑定到节点 1 的内存上,your_command是要执行的命令或程序。比如我们要启动一个test程序,并将其绑定到节点 1,命令如下:
numactl --cpunodebind = 1 --membind = 1 ./test
通过这样的绑定操作,进程在运行时就会优先使用指定 NUMA 节点的 CPU 和内存资源,从而减少跨节点访问带来的性能损耗,提高程序的执行效率。
6.3优化内存分配策略
内存分配策略对多线程程序在 NUMA 架构下的性能有着重要影响。合理调整内存分配策略,可以减少跨节点内存访问,提高内存使用效率 。
Linux 系统提供了多种内存分配策略,其中常用的有首选本地节点(preferred)和交错(interleaving)模式 。首选本地节点策略是指进程优先从本地 NUMA 节点分配内存,这样可以充分利用本地内存访问速度快的优势 。可以通过修改/sys/kernel/mm/transparent_hugepage/enabled文件来设置内存分配策略为首选本地节点。打开该文件,将其内容修改为:
echo 'preferred' > /sys/kernel/mm/transparent_hugepage/enabled
交错模式则是指进程在多个 NUMA 节点间交错地分配内存,这种策略适用于内存访问负载较为均衡,且需要充分利用多个节点内存资源的场景 。设置交错模式的命令如下:
echo 'interleave' > /sys/kernel/mm/transparent_hugepage/enabled
也可以使用numactl命令在启动进程时临时设置内存分配策略 。例如,要以交错模式启动一个test程序,可以使用以下命令:
numactl --interleave=all ./test
interleave=all表示在所有 NUMA 节点间交错分配内存。通过根据程序的内存访问特点选择合适的内存分配策略,可以有效提升程序在 NUMA 架构下的性能。
5.4使用大页(Huge Pages)
大页是一种内存管理机制,它使用比普通内存页更大的页面来管理内存 。在 NUMA 系统中,使用大页可以减少页表项的数量,降低内存管理开销,从而提高内存访问效率,尤其对于内存访问密集型的多线程程序,效果更为显著 。
查看当前系统的大页配置情况,可以使用如下命令:
cat /proc/meminfo | grep HugePages
执行该命令后,会输出类似如下信息:
AnonHugePages: 0 kB
HugePages_Total: 0
HugePages_Free: 0
HugePages_Rsvd: 0
HugePages_Surp: 0
Hugepagesize: 2048 kB
从输出中可以看到当前系统的大页总数(HugePages_Total)、空闲大页数(HugePages_Free)、已保留大页数(HugePages_Rsvd)、超额大页数(HugePages_Surp)以及大页的大小(Hugepagesize)。
要分配大页,可以通过修改/sys/kernel/mm/hugepages/hugepages-<size>/nr_hugepages文件来实现,其中<size>是大页的大小,如2048kB 。例如,要分配 1024 个 2MB 的大页,可以使用以下命令:
echo 1024 > /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages
使用大页启动应用程序时,可以结合numactl命令 。比如,要启动一个test程序,并让其使用大页且将内存绑定到 NUMA 节点 0,可以使用如下命令:
numactl --membind=0 --huge ./test
--membind=0表示将内存绑定到节点 0,--huge表示使用大页。通过合理配置和使用大页,能够有效提升多线程程序在 NUMA 架构下的内存访问性能,进而提高程序的整体运行效率。
6.5性能监控与效果评估
在完成 NUMA 性能调优后,需要对调优效果进行监控和评估,以确定是否达到了预期的性能提升。这就好比一场考试后,要通过检查成绩来判断学习方法是否有效。
(1)监控工具的使用
①top 命令:top 命令是 Linux 系统中常用的实时性能监控工具,通过在终端输入 “top” 即可启动 。它能实时展示系统的关键性能指标,如 CPU 使用率、内存占用、负载平均值等,以及每个进程的详细信息 。在多线程程序运行时,我们可以通过 top 命令查看进程的 CPU 使用率和内存占用情况。如果某个多线程进程的 CPU 使用率在调优后明显降低,且内存占用也更加合理,这可能意味着调优起到了作用。比如,在调优前,一个多线程的数据处理程序的 CPU 使用率一直维持在 80% 以上,内存占用不断攀升;调优后,CPU 使用率稳定在 50% 左右,内存占用也趋于稳定,这就是一个积极的信号。
②perf 工具:perf 是一个功能强大的性能分析工具,基于事件采样原理,能对处理器和操作系统相关性能指标进行剖析,查找性能瓶颈和定位热点代码 。例如,使用 “perf stat -p -e cpu - cycles” 命令,可以分析指定进程的 CPU 周期使用情况 。在 NUMA 性能调优前后,对多线程程序执行此命令,对比调优前后的 CPU 周期数。如果调优后 CPU 周期数明显减少,说明程序的执行效率得到了提高,可能是因为减少了跨节点内存访问,降低了 CPU 等待内存数据的时间。使用 “perf top -p ” 命令可以实时显示系统中占用 CPU 周期最多的函数,帮助我们找出性能热点,以便针对性地进行优化。
③numastat 工具:numastat 用于监控和显示系统的内存使用情况,特别是各个 NUMA 节点的内存分配和使用情况 。运行 “numastat” 命令,它会输出各个 NUMA 节点的内存命中(numa_hit)、内存缺失(numa_miss)、跨节点访问(numa_foreign)等信息 。在调优前,如果 numa_miss 和 numa_foreign 的值较高,说明存在大量的跨节点内存访问。而在调优后,若这些值显著降低,numa_hit 的值升高,就表明内存访问的局部性得到了改善,多线程程序更多地访问了本地内存,从而提高了性能。
(2)性能指标对比
①运行时间:在调优前后,多次运行多线程程序,记录其运行时间 。可以使用 “time” 命令来测量程序的执行时间,例如 “time your_command” 。假设一个多线程的图像渲染程序,调优前完成一次渲染需要 10 分钟,调优后缩短到了 6 分钟,这直观地显示了调优对程序运行效率的提升。
②吞吐量:对于一些数据处理或网络服务类的多线程程序,吞吐量是一个重要的性能指标 。比如,一个多线程的网络服务器,调优前每秒能处理 1000 个请求,调优后每秒能处理 1500 个请求,吞吐量的提升表明调优使得程序能够更高效地处理任务,满足更多的业务需求。
③CPU 使用率:通过 top 命令或其他性能监控工具获取调优前后多线程程序的 CPU 使用率 。如果调优后 CPU 使用率降低,说明程序对 CPU 资源的利用更加高效,可能是因为减少了不必要的计算或等待时间,这在服务器环境中尤为重要,可以为其他任务释放更多的 CPU 资源。
④内存访问延迟:虽然没有直接的工具可以精确测量内存访问延迟,但可以通过 perf 工具结合其他指标来间接评估 。例如,观察 cache - misses(缓存未命中)的情况,缓存未命中次数的减少通常意味着内存访问延迟的降低。因为当内存访问更倾向于本地内存时,数据更容易被缓存命中,从而减少了从内存中读取数据的时间,提高了程序的整体性能。
通过上述性能监控工具和指标对比,我们可以全面、准确地评估 NUMA 性能调优对多线程程序的效果,验证调优策略的有效性,为进一步的优化提供数据支持。