Linux内核揭秘:NUMA节点探测背后的故事

系统 Linux
Linux 内核中的 NUMA 节点探测技术,就像是一把神奇的钥匙,能够帮助我们打开了解系统内存架构的大门。它让我们清楚地知道每个 CPU 与内存之间的亲和关系,以及数据在不同节点之间的流动情况。

在当今的计算机世界里,随着硬件性能的不断提升,Linux 内核也在持续进化以充分发挥硬件的潜力。而今天,我们要深入探讨的是 Linux 内核中一个至关重要但又常常被忽视的领域 ——NUMA 节点探测。你是否曾好奇,当我们的服务器拥有多个 CPU 和大量内存时,系统是如何高效地管理和分配这些资源的呢?这背后的功臣之一就是 NUMA 架构。想象一下,在一个庞大的数据中心里,众多服务器协同工作,如果不能精准地掌握 NUMA 节点的情况,就如同在一个巨大的仓库中盲目地寻找货物,效率低下且容易出错。

Linux 内核中的 NUMA 节点探测技术,就像是一把神奇的钥匙,能够帮助我们打开了解系统内存架构的大门。它让我们清楚地知道每个 CPU 与内存之间的亲和关系,以及数据在不同节点之间的流动情况。无论是优化服务器性能,还是解决复杂的系统故障,NUMA 节点探测都能为我们提供关键的线索和方向。接下来,就让我们一起踏上这场探索 Linux 内核 NUMA 节点探测的奇妙之旅,揭开其神秘的面纱,看看它是如何在幕后默默工作,保障我们系统高效稳定运行的。

一、什么是NUMA

传统的SMP对称多处理器中,所有处理器都共享系统总线,因此当处理器的数目增大时,系统总线的竞争冲突加大,系统总线将成为瓶颈,所以目前SMP系统的CPU数目一般只有数十个,可扩展能力受到极大限制;NUMA技术有效结合了SMP系统易编程性和MPP(大规模并行)系统易扩展性的特点,较好解决了SMP系统的可扩展性问题,已成为当今高性能服务器的主流体系结构之一。

基于NUMA架构的高性能服务器有HP的Superdome、SGI的Altix 3000、IBM的 x440、NEC的TX7、AMD的Opteron等;NUMA(Non Uniform Memory Access)技术可以使众多服务器像单一系统那样运转,同时保留小系统便于编程和管理的优点。

在早期,对于x86架构的计算机,那时的内存控制器还没有整合进CPU,所有内存的访问都需要通过北桥芯片来完成。此时的内存访问如下图所示,被称为UMA(uniform memory access, 一致性内存访问)。这样的访问对于软件层面来说非常容易实现:总线模型保证了所有的内存访问是一致的,不必考虑由不同内存地址之前的差异。

图片

之后的x86平台经历了一场从“拼频率”到“拼核心数”的转变,越来越多的核心被尽可能地塞进了同一块芯片上,各个核心对于内存带宽的争抢访问成为了瓶颈;此时软件、OS方面对于SMP多核心CPU的支持也愈发成熟;再加上各种商业上的考量,x86平台也顺水推舟的搞了NUMA(Non-uniform memory access,非一致性内存访问)。在这种架构之下,每个Socket都会有一个独立的内存控制器IMC(integrated memory controllers,集成内存控制器),分属于不同的socket之内的IMC之间通过QPI link通讯。

图片

然后就是进一步的架构演进,由于每个socket上都会有多个core进行内存访问,这就会在每个core的内部出现一个类似最早SMP架构相似的内存访问总线,这个总线被称为IMC bus。

图片

于是,很明显的,在这种架构之下,两个socket各自管理1/2的内存插槽,如果要访问不属于本socket的内存则必须通过QPI link 。也就是说内存的访问出现了本地/远程(local/remote )的概念,内存的延时是会有显着的区别的。这也是为什么在NUMA架构下有些应用性能反而更差的原因。

回到当前世面上的CPU,工程上的实现其实更加复杂了。以来看,两个Socket之之间通过各自的一条9.6GT/s的QPI link互访。而每个Socket事实上有2个内存控制器。双通道的缘故,每个控制器又有两个内存通道(channel),每个通道最多支持3根内存条(DIMM)。理论上最大单socket支持76.8GB/s的内存带宽,而两个QPI link,每个QPI link有9.6GT/s的速率(~57.6GB/s)事实上QPI link已经出现瓶颈了。

图片

NUMA in Linux

对于NUMA系统来说,Linux会为每一个NUMA节点创建一套内存管理对象的实例,每个节点包含DMA, DMA32, NORMAL等Zone。当某个节点下的某个Zone无法满足内存分配请求时,系统会咨询zonelist进而决定后备Zone的选择顺序。当本地Zone NORMAL内存不足时,黙认顺序是从本地的Zone DMA32和DMA尝试,然后再尝试其它的节点。此顺序可以由numa_zonelist_order参数更改,比如先去尝试远程节点的Zone NORMAL以节省比较稀缺的Zone DMA32和DMA内存(当然,非黙认NUMA policy有可能偏好远程节点)。

二、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核心技术

三种系统架构 & 两种存储器共享方式

从系统架构来看,目前的商用服务器大体可以分为三类:

  • 对称多处理器结构(SMP:Symmetric Multi-Processor)
  • 非一致存储访问结构(NUMA:Non-Uniform Memory Access)
  • 海量并行处理结构(MPP:Massive Parallel Processing)。

共享存储型多处理机有两种技术:

  • 均匀存储器存取(Uniform-Memory-Access,简称UMA)技术
  • 非均匀存储器存取(Nonuniform-Memory-Access,简称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节点实战

5.1进程与节点的 “绑定术”

了解了这么多关于 NUMA 节点的知识,那如何在实际应用中充分发挥它的优势呢?关键就在于将进程合理地绑定到特定的 NUMA 节点或 CPU 核心上。

在 Linux 系统中,我们有一些便捷的工具来实现这一操作。就拿 numactl 来说,假如我们有一个对内存带宽要求极高的计算密集型任务,像是大规模的科学计算模拟程序,为了减少内存访问延迟,提升计算效率,我们可以使用 “numactl --cpunodebind=0 --membind=0 程序名” 这样的命令,将该程序绑定到 NUMA 节点 0 上运行。这意味着程序运行过程中所涉及的 CPU 核心都来自节点 0,内存分配也优先从节点 0 的本地内存获取,如同为这个任务开辟了一条专属的 “高速通道”,让数据的读写能够以最快的速度完成,避免了跨节点访问带来的额外开销。

除了 numactl,taskset 命令也是我们的得力帮手。它专注于 CPU 核心的绑定,例如 “taskset -c 2,3,4 程序名”,就能将指定程序固定在 CPU 核心 2、3、4 上运行。这在一些多线程应用场景中尤为实用,比如网络服务器程序,通过将不同的线程绑定到不同的 CPU 核心,可以有效减少线程上下文切换的开销,提高服务器的并发处理能力,确保每个请求都能得到快速响应,就像为每个任务分配了专属的 “工作间”,互不干扰,高效协作。

合理运用这些绑定技巧,能够让进程与 NUMA 节点、CPU 核心之间形成默契配合,充分挖掘硬件的性能潜力,为各类应用场景带来显著的性能提升,让系统运行得更加流畅、高效。

5.2优化的 “妙手回春”

为了更直观地感受 NUMA 节点优化带来的神奇效果,我们来看一个实际案例。

假设有一台配备了两个 NUMA 节点的服务器,每个节点拥有 8 个 CPU 核心,总内存为 32GB。在未进行 NUMA 优化之前,运行一个多线程的数据库查询应用程序,该程序会频繁地访问内存数据。由于进程默认的内存分配和 CPU 调度是相对随机的,很容易出现跨节点访问内存的情况,导致内存访问延迟增加,系统性能受到制约。

通过使用 numactl 工具,我们将这个数据库查询应用程序绑定到其中一个 NUMA 节点上,例如 “numactl --cpunodebind=0 --membind=0 数据库应用程序名”。绑定之后,再次运行该程序,并使用性能监测工具观察相关指标。

对比绑定前后的数据,我们发现内存访问延迟有了显著降低。原本跨节点访问时,平均延迟可能高达 200 纳秒左右,而绑定后,在节点内部访问内存,延迟大幅下降到 50 纳秒以内,这使得查询操作能够更快地获取所需数据,整体响应时间缩短了约 30%。

从带宽利用率来看,未优化前,由于频繁的跨节点内存访问争用带宽,实际可用带宽利用率仅能达到 50% 左右;优化后,数据基本在本地节点内流转,带宽利用率提升至 80% 以上,数据传输更加顺畅,系统吞吐量明显增加,能够同时处理更多的查询请求,大大提升了数据库服务器的性能表现,为业务的高效运行提供了有力支撑。

责任编辑:武晓燕 来源: 深度Linux
相关推荐

2012-07-26 12:00:50

笔记本

2011-05-06 18:02:32

数据库迁移行业案例DB2

2016-07-07 10:33:53

思科DNA视频

2012-10-17 13:50:25

2012-08-16 16:23:05

2018-12-27 10:56:04

Linux内核现状

2010-11-25 16:14:07

2018-11-06 15:56:25

西门子工业网络智能制造

2011-09-26 14:28:28

水果忍者

2020-10-20 10:39:38

腾讯AI

2014-11-06 10:35:57

程序员

2013-11-29 12:34:50

敏捷运维新浪微博许杨毅

2011-04-06 11:21:25

PHPPython

2014-04-14 10:06:22

.Net 开源

2016-12-12 14:19:59

LLVMClangApple

2017-01-15 11:01:56

2024-03-15 08:54:59

Linux内核NUMA

2009-01-04 09:26:44

架构Google服务器

2015-06-18 11:28:18

谷歌
点赞
收藏

51CTO技术栈公众号