Linux内核文档:What is NUMA?

关于NUMA架构是什么的问题,可以同时从硬件、软件的不同角度进行解释。

硬件角度

从硬件来看,NUMA系统是一个组合了多个内部包含多核CPU、本地内存、以及IO总线组件的计算机平台。为了简短表达以及防止歧义,我们在文档中称这些组件为cells

每一个cells都可以看做是NUMA系统下的一个SMP(对称多处理器)子集,同时要注意,有些独立的SMP系统并不位于任何cell上。NUMA系统下的cell通过连接组件连接彼此,比如交叉开关或者点对点连接。这些类型的连接组件都可以进行组合用于创建与其他cell的连接。

在Linux系统上,NUMA主要是指Cache Coherent NUMA 缓存一致NUMA,简称ccNUMA。在系统内部,只要CPU与cell连接,所有内存都是可见并且可访问的。同时,缓存一致性问题会被缓存处理器以及各个连接组件处理。

内存访问时长与有效带宽因CPU访问的不同cell间的距离不同而不同。比如访问同个cell下的内存会明显快于离得远的不同的cell间的内存。NUMA平台架构允许多种距离组合的cells。

硬件平台厂商不直接实现NUMA架构,让软件实现的部分更加有意思了。准确来说,NUMA架构是为了提供一种可拓展的内存带宽。为了达到这个目标,操作系统以及软件就必须要让大部分内存引用都位于本地cell的部分(local memory),或者说,越近越好。

软件角度

上面的描述正好引入了软件部分的视角:

Linux把系统硬件资源都抽象为了软件中的nodes,硬件物理核(physical cells)与nodes间形成mapping映射,这层抽象屏蔽了一些架构上的细节。软件中的nodes(可对应到物理核)这时候就能对应到0-多个CPU、内存、IO设备上。并且,访问离得近的nodes(其实是映射到离得近的cells(物理核))就会比访问远程cells更快(时长更短、带宽更有效)。

比如在X86架构上,Linux会把一些没有内存分配的node(映射到物理核cell上)给隐藏起来,同时会二次分配,把CPU分配到有内存资源的node上。因此,在这种架构下,我们可以看到,分配到某个node上的不同CPU,有可能有着不同的本地内存访问时间与带宽效率。

除此之外,还是以X86为例,Linux支持附加nodes的模拟。Linux会划分已有的nodes(或者是非NUMA系统的内存)到更多的nodes上。每个模拟出来的node都管理者底层cells物业内存的部分。当测试非NUMA架构上的NUMA内核与软件特性时,这个模拟非常实用。并且跟cpusets一起使用时,这是一种内存资源管理机制。

针对每个带内存的node,Linux有一套独立的内存管理子系统,里面包含了free page lists, in-use page lists, usage statistics and locks to mediate access。另外,Linux把每个内存区域(DMA, DMA32, NORMAL, HIGH_MEMORY, MOVABLE)都放到一个叫zonelist的结构里。一个zonelist定义了当选中的zone、node不满足分配请求时可二次访问的zones或者nodes资源。这种情况,我们称之为溢出或者退路。

因为有的nodes中包含了多种内存资源的zones,所以Linux需要决策:不同node但是相同zone类型碰到退路分支时(或者同个node上要分配不同的zone类型),是否要对zonelist进行排序。对于DMA``DMA32这类稀缺资源来说,这是很重要的策略。Linux默认给定一个有序的zonelist。在连接到远程node之前,系统会自动帮你找同个node上的不同zone,以NUMA中的距离排序,先找近的再找远的。

这个内存分配查找的过程大概是这样:

Linux默认先找执行本次请求的CPU资源。

先本地分配:先尝试分配请求源zonelist中的第一个node资源。

如果本地分配不成功,内核会按照顺序(NUMA距离排序)找list中最近满足要求的node。

本地分配倾向于使得后续请求都访问本地的物理资源,并且倾向于远离系统连接组件(这一步需要保证分配的内存后续没有发生migrate动作)。Linux的调度器这里使用NUMA拓扑图策略,可访问[see Documentation/scheduler/sched-domains.txt],调度器的目标是尽可能减少远程调度(task migration)。但是,调度器不能直接拿到一个任务的NUMA细节数据。因此,在足够多的不平衡发生时,多个任务会在nodes间、从原始node到远程node进行migrate。

系统管理员可以限制上述这个migration的发生,以此来提升NUMA架构下的本地效率。比如可使用CPU affinity cli:

  • taskset(1)
  • numactl(1)

也可以使用程序接口:

  • sched_setaffinity(2)

当然也可以修改内核默认分配策略的配置,可参考: [see Documentation/admin-guide/mm/numa_memory_policy.rst.]

我们也可以通过使用control groups以及cpusets来限制非特权用户所使用的CPU以及nodes内存。参考:[see Documentation/cgroup-v1/cpusets.txt]

在不隐藏无内存node的架构上,Linux在zonelist中只保留有内存的zone(node)资源。这也就意味着在一个CPU对应的zonelist中,本地内存第一个node节点将不是它本身,而是创建zonelist时内核选中的有内存的最近的node。因此默认逻辑是,本地分配的结果是分配一个最近可用的内存块。发生退路、溢出等异常情况时,选择逻辑依旧是选当前离得最近的node。

部分内核分配的情况不适用这种退路逻辑(比如当子系统在每个CPU内分配内存资源)。相反,他们想要确定:

  • 当前分配发生在特定node上
  • 同时可以拿到分配失败的结果(分配的node没有内存了)

一个例子:

  • 通过numa_node_id() CPU_to_node()获取当前CPU关联的nodeId
  • 接着通过上述nodeId获取对应内存

当上面这种分配失败的时候,可以退到自定义的逻辑内。