目录
概述
CPU拓扑是用来描述CPU的组成、向kernel调度器提供重要信息来合理调度CPU的重要因素。首先了解一下几个CPU结构。
CPU架构
SMP架构
SMP(Symmetric multiprocessing,对称多处理),SMP架构由多个具有对称关系的处理器组成。所谓对称,即处理器之间是水平的镜像关系,无主从之分。
每个处理器都有自己的L1缓存,共享L2缓存,通过某种互联方式(如系统总线)共享资源如内存和I/O。
SMP结构的特征就是多个处理器共享一个集中式存储器,每个处理器访问存储器的时间片一致,使工作负载能够均匀的分配到所有可用的处理器上,极大提到了整个系统的数据处理能力。
虽然系统具有多个处理器,但由于共享一个集中式存储器,所以只会运行一个操作系统和数据库的副本(实例),能够保持单机的特性,同时也要求系统需要保持共享存储器的数据一致性。如果多个处理器同时请求访问这些共享资源,就会引发资源竞态,需要软硬件实现加锁机制来解决这个问题。按照上面的情况,所以SMP是典型的UMA(Uniform Memory Access,一致性内存访问)架构。所谓一致性就是在UMA架构中:
- 在任意时刻,多个处理器只能为存储器的每个数据保存或共享一个唯一的数值。
- 每个处理器访问存储器所需要的时间都是一致的
很显然,这样的架构设计注定没法拥有良好的处理器数量扩展性,因为缓存一致性和共享对象。综合来说,SMP架构广泛的适用于PC和移动设备领域,能显著提升并行计算性能。但SMP却不适合超大规模的服务器端场景,例如:云计算。
NUMA架构
现代的计算机系统中,处理器的处理速度远快于主存的速度,所以限制计算机性能的瓶颈在存储器带宽上。SMP架构因为限制了处理器访问存储器的频次,所以处理器可能会经常处于对数据访问的饥渴状态。
NUMA(Non-Uniform Memory Access,非一致性内存访问)架构优化了SMP架构扩展性差以及存储器带宽瓶颈的问题。NUMA的设计理念就是将处理器和存储器划分到不同的节点(NUMA Node),使它们都拥有几乎相同的资源。在NUMA节点内部会通过自己的存储总线访问内部的本地内存,而所有NUMA节点都可以通过主板上的共享总线来访问其他节点的远程内存。
显然,处理器访问本地内存和远程内存的耗时并不一致,NUMA非一致性内存访问因此得名。而且因为节点划分并没有实现真正意义上的存储隔离,所以NUMA同样只会保存一份操作系统和数据库系统的副本。
NUMA多节点的结构设计也在一定程度上解决SMP存储器带宽瓶颈的问题。假设有一个4 NUMA节点的系统,每个NUMA节点内部具有1GB/s的存储带宽,外部共享总线也具有1GB/s的带宽。理想状态下,如果所有的处理器总是访问本地内存的话,那么系统就拥有了4GB/s的存储带宽,此时每个节点可以近似看成一个SMP;相反,在最不理想的情况下,如果所有处理器处理器总是跨节点访问远程内存的话,那么系统就只能有1GB/s的存储带宽了。
除此之外,使用外部共享总线时,可能会触发NUMA节点间的Cache同步异常,这会严重影响内存密集型工作负载的性能。当I/O性能至关重要时,共享总线上的Cache资源浪费,会让连接到远程PCIe总线上的设备(不同NUMA节点间通信)作业性能急剧下降。
由于这个特性,基于NUMA开发的应用程序应该尽可能避免跨节点的远程内存访问。因为,跨节点内存访问不仅通信速度慢,还可能需要处理不同节点间内存和缓存的数据一致性。多线程在不同节点间的切换,是需要花费很大成本的。
虽然NUMA相比于SMP具有更好的处理器扩展性,但因为NUMA没有实现彻底的主存隔离。所以NUMA远没有达到无限扩展的水平,最多可支持几百个CPU。这是为了追求更高的并发性能所作出的妥协,一个节点未必就能完全满足多并发需求,多节点间线程切换实属一个折中的方案。这种做法使得NUMA具有一定的伸缩性,更加适合应用在服务器端。
MPP架构
MPP(Massive Parallel Processing,大规模并行处理),既然NUMA扩展性的限制是没有完全实现资源(存储器、互联模块)的隔离性,那么MPP的解决思路就是为处理器提供彻底的独立资源。
MPP拥有多个真正意义上的独立SMP单元,每个SMP单元独占并且只会访问自己本地的内存、I/O资源,SMP单元间通过节点互联网络进行连接(Data Redistribution,数据重分配),是一个完全无共享(Share Nothing)的CPU计算平台结构。
MPP的典型特征就是多个SMP单元组成,单元之间完全无共享。除此之外,MMP结构还有以下特点:
- 每个SMP单元都可以包含一个操作系统副本,所以每个SMP单元都可以运行自己的操作系统。
- MPP需要一种复杂的机制来调度和平衡各个节点的负载和并行处理过程,目前一些基于MPP技术的服务器往往通过系统级软件(数据库)来屏蔽这种复杂性。
- MPP架构的本地区域内访存延迟低于远地访存延迟,因此Linux会自定采用局部节点分配策略,当一个任务请求分配内存时,首先在处理器自身节点内寻找空闲页,如果没有则到相邻的节点寻找空闲页,如果还没有再到远地节点中寻找空闲页,在操作系统层面就实现了访存性能优化。
因为完全的资源隔离特性,所以MPP的扩展性是最好的,理论上可以无限扩展,目前的技术可以实现512个节点互联,数千个CPU,多应用于大型机。
SMT架构
SMT(Simultaneous Multithreading,同步多线程)是一种将硬件多线程与超标量处理器技术相结合的处理器设计。SMT把CPU的每个物理内核拆分为虚拟内核,这些虚拟内核被称为线程,这样做是为了提高性能并允许每个内核一次运行两个指令流。
SMT是SMP架构的设计补充。SMP架构中的CPU共享一条总线和存储,而SMT架构中CPU共享更多的组件。共享组件的CPU被称为Siblings。所有的CPU在系统上都显示为可用CPU,并且可以执行工作负载。但是,与NUMA一样,都会有线程竞争共享资源的情况。
Intel Hyper-Threading Technolog(超线程技术)和SMT完全一样,都是允许在每个内核上运行多个线程。
NUMA和UMA的异同
NUMA和UMA(SMP)的异同。
- NUMA和SMP中的处理器都可以访问整个系统的物理存储器。
- NUMA采用了分布式存储,提供了分离的存储器给各个节点,避免了SMP中多个处理器无法同时访问单一存储器的问题。
- NUMA节点的处理器访问内部存储器所需的时间,要比访问其他节点的远程存储器要快得多。
- NUMA即保持了SMP单一操作系统备份、简单应用程序编程以及易于管理的特点,又继承了大规模并行处理MPP的可扩展性,是一个折中的方案。
NUMA和MPP的异同
NUMA和MPP的相同点。
- 它们都是由多个节点组成。
- 每个节点都有自己的CPU、内存、I/O。
- 节点之间都可以通过节点互联机制进行信息交互。
NUMA和MPP的不同点。
- 节点互联机制不同。
- NUMA节点互联机制是在同一台物理服务器内部实现的,当某个CPU需要进行异地内存访问时,它必须等待,这也是NUMA服务器无法实现CPU增加时性能线性扩展的主要原因。
- MPP节点互联机制是在不同SMP服务器外部通过I/O实现的,每个节点只访问本地内存和存储,节点之间的信息交互与节点本身的处理是并行进行的。因此,MPP节点在增加节点时,其性能基本上可以实现线性扩展。
- 内存访问机制不同。
- 在NUMA服务器内部,任何一个CPU都可以访问整个系统的内存,但异地内存访问的性能远低于本地内存访问,因此,在开发应用时应该尽量避免异地内存访问。
- 在MPP服务器中,每个节点只访问本地内存,不存在异地内存访问的问题。
Linux中的NUMA
NUMA的基本概念
- Node:包含有若干个物理CPU的组。
- Socket:表示一颗物理CPU的封装(物理CPU插槽),简称插槽。为了避免将逻辑处理器和物理处理器混淆,Intel将物理处理器称为插槽。
- Core:Socket内含有的物理核。
- Thread:在具有Intel超线程技术的处理器上,每个Core可以被虚拟为若干个(通常为2个)逻辑处理器,逻辑处理器会共享大多数内核资源(内存缓存、功能单元)。逻辑处理器被统称为Thread。
- Processor:处理器的统称,可以区分为物理处理器(Physical Processor)和逻辑处理器(Virtual Processor)。对于大多数应用程序而言,它们并不关心处理器是物理的还是逻辑的。
- Siblings:相同物理封装(Socket)中的逻辑处理器(Virtual Processor)的数量。Siblings的个数和CPU是否打开超线程有关。如果打开超线程,Siblings = 2 * Core(物理核心)的个数;关闭超线程,Siblings = Core(物理核心)的个数。
包含关系:NUMA Node > Socket > Silblings >= Core > Thread
调度策略
Linux的每个进程或线程都会延续父进程的NUMA策略,优先会将其约束在同一个NUMA Node内。当然,如果NUMA策略允许的话,进程也可以调用其他Node上的资源。
NUMA的CPU分配策略有下列两种:
- cpunodebind:规定进程运行在指定的若干个node内。
- physcpubind:规定进程运行在指定的若干个物理CPU内。
NUMA的内存分配策略有下列4种:
- localalloc:规定进程只能从当前Node(本地)请求分配内存。
- preferred:宽松地为进程指定一个优先Node获取内存,如果优先Node上没有足够的内存资源,那么进程允许尝试别的Node。
- membind:规定进程只能从指定的若干个Node上请求分配内存。
- interleave:规定进程可以使用RR(Round Robin 轮询调度)算法轮转地从指定的若干个Node中请求分配内存。
因为NUMA默认的内存分配策略是localalloc,优先在进程所在CPU的本地内存中分配,会导致CPU节点之间内存分配不均衡,当某个CPU节点的内存不足时,会导致Swap产生,而不是从远程节点分配内存。这就是所谓的Swap Insanity现象。
获取宿主机的NUMA拓扑
判断系统是否支持NUMA
1 | dmesg | grep -i numa |
如果输出上述内容则表示支持NUMA,如果输出No NUMA configuration found则表示不支持。
Linux命令lscpu
1 | lscpu |
Linux命令numactl
查看NUMA拓扑
1 | numactl --hardware |
查看NUMA策略
1 | numactl --show |
Bash脚本
1 |
|
运行结果。
1 | Socket 0: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 |
Nova中的NUMA
在Icehouse版本之前,Nova定义的libvirt.xml,不会考虑宿主机NUMA的情况。导致Libvirt在默认情况下,有可能发生跨NUMA Node获取CPU/Memory资源的情况,导致Guest性能下降。Openstack在Juno版本中新增NUMA特性,用户可以通过将Guest的vCPU/Memory绑定到宿主机NUMA Node上,以此来提升Guest的性能。
基本概念
除了上文中提到的NUMA的基本概念之外,Nova还自定义一些对象概念。
- Cell:NUMA Node的同义词,供Libvirt API使用。
- vCPU:虚拟机的CPU,根据虚拟机NUMA拓扑的不同,一个虚拟CPU可以是一个Socket、Core或者Thread。
- pCPU:宿主机的CPU,根据宿主机NUMA拓扑的不同,一个物理CPU可以是一个Socket、Core或者Thread。
- Siblings Tread:兄弟线程,即由同一个Core超线程出来的Thread。
- Host NUMA Topology:宿主机的NUMA拓扑。
- Instance NUMA Topology(Guest NUMA Topology):虚拟机的NUMA拓扑。
Nova中实现NUMA亲和
背景
操作系统发行版许可证(Licensing)
根据不同的操作系统发行版许可证,可能会严格约束操作系统能够支持的最大Sockets数量,同时也就约束了服务器上可运行虚拟机的数量。所以,此时应该更加偏向于使用Core来作为vCPU,而不是Socket。
OpenStack管理员应该遵从操作系统许可需求,限制虚拟机使用的CPU拓扑(e.g. max_sockets=2)。设置默认的CPU拓扑参数,在保证GuestOS镜像能够满足许可证的同时,又不必让每个用户都单独去设置镜像属性。
CPU拓扑对宿主机性能的影响
宿主机CPU拓扑的方式对其自身性能(Performance)具有很大影响。
- 单Socket单Core拓扑(单核结构):一个Socket只集成了一个Core。对于多线程程序,主要是通过时间片轮转来获得CPU的执行权,实际上是串行执行,没有做到并行执行。
- 单Socket多Core拓扑(多核结构):一个Socket集成了多个水平对称(镜像)的Core,Core之间通过CPU内部数据总线通信。对于多线程程序,可以通过多Core实现真正的并行执行。不过对于并发数或线程数要大于Core数的程序而言,多核结构存在线程(上下文)切换问题。这会带来一定的开销,但好在使用的是CPU内部数据总线,所以开销会比较低。除此之外,还因为多Core是水平镜像的,所以每个Core都有着自己的Cache,在某些需要使用共享数据(共享数据很可能会被Cache住)的场景中,存在多核Cache数据一致性的问题,这也会带来一些开销。
- 多Socket单Core拓扑:多Socket之间通过主板上的总线进行通信,集成为一个统一的计算平台。每一个Socket都拥有独立的内部数据总线和Cache。对于多线程程序,可以通过多Socket来实现并行执行。不同于单Socket多Core拓扑,多Socket单Core拓扑的线程切换以及Socket间通信走的都是外部总线,所以开销会比使用CPU内部数据总线高得多、延时也更长。当然,在使用共享数据的场景中,也同样存在多Socket间Cache一致性的问题。多Socket拓扑的性能瓶颈在于Socket间的I/O通讯成本。
- 超线程拓扑(Hyper-Threading):将一个Core虚拟为多个Thread(逻辑处理器),实现一个Core也可以并行执行多个线程。Thread拥有自己的寄存器和中断逻辑,不过Thread之间会共享执行单元(ALU逻辑运算单元)和Cache,所以性能提升是比较有限的,但也非常极致了。
- 多Socket多Core超线程拓扑:具有多个Socket,每个Socket又包含多个Core,每个Core又虚拟出多个Thread。是上述拓扑类型的集大成者,拥有最好的性能和最先进的工艺,常见于企业级的服务器产品,例如:MPP,NUMA计算平台系统。
多Socket单Core拓扑的多线程,Socket间协作要通过外部总线通信,在不同Socket上执行的线程间的共享数据可能会同时存放在不同的Socket Cache上,所以要保证不同Cache的数据一致性。具有通信开销大,线程切换开销大,Cache数据一致性难维持,多Socket占位面积大,集成布线工艺难等问题。
单Socket多Core拓扑的多线程,每个Core处理一个线程,支持并发。具有多Core之间通信开销小,Socket占位面积小等优势。但是,当需要运行多个“大程序”(一个程序就可以将内存、Cache、Core 占满)的话,就相当于多个大程序需要通过分时切片来使用CPU。此时,程序间的上下文(指令、数据替换)切换消耗将会是巨大的。所以单Socket多Core拓扑在多任务、高并发、高消耗内存的程序运行环境中效率会变得非常低下(大程序会独占一个Socket)。
综上,对于程序规模小的应用场景,建议使用单Socket多Core拓扑,例如个人PC;对于多大规模程序的应用场景(云计算服务器端),建议使用多Socket单Core甚至是多Socket多Core超线程的组合,为每个程序分配到单个CPU,为每个程序的线程分配到单个CPU中的Core。
CPU架构对性能的影响
CPU架构对并发程序设计而言,主要需要考虑两个问题,一个是内存可见性问题,一个是Cache一致性问题。前者属于并发安全问题,后者则属于性能范畴的问题。
- 内存可见性问题:该问题在单处理器或单线程情况下是不会发生的。但在多线程环境中,因为线程会被分配到不同的Core上执行,所以会出现Core1和Core2可能会同时把主存中某个位置的值load到自己的一级缓存中,而Core1修改了自己一级缓存中的值后,却不更新主存中对应的值,这样对于Core2来说,将永远看不到Core1对值的修改,从而导致不能保证并发安全性。
- Cache一致性问题:假如Core1和Core2同时把主存中的值load到自己的一级缓存,Core1将值修改后,会通过BUS总线让Core2中的值失效。Core2发现自己一级缓存中的值失效后,会再通过BUS总线从主存中得到最新的值。但是,总线的通信带宽是固定的,通过总线来进行各CPU一级缓存数据同步的动作会产生很大的流量,从而总线成为了性能的瓶颈。可以通过减小数据同步竞争来减少Cache一致性的流量。
超线程对性能的影响
需要注意的是,超线程技术并非万能药。从Intel和VMware对外公开的资料看,开启超线程后,Core的总计算能力是否提升以及提升的幅度和业务模型相关,平均提升在20%-30%左右。但超线程对Core的执行资源的争抢,业务的执行时延也会相应增加。当超线程相互竞争时,超线程的计算能力相比不开超线程时的物理核甚至会下降30%左右。所以,超线程应该关闭还是开启,主要还是取决于应用模型。
现在很多应用,比如Web App,大多会采用多Worker设计,在超线程的帮助下,两个被调度到同一个Core下不同Thread的Worker,由于Threads共享 Cache和TLB(Translation Lookaside Buffer,转换检测缓冲区),所以能够大幅降低Workers线程切换的开销。另外,在某个Worker不忙的时候,超线程允许其它的Worker先使用物理计算资源,以此来提升Core的整体吞吐量。
- 对于时延敏感型任务,比如用户需要及时响应任务运行结果的场景,在节点负载过高,引发超线程竞争时,任务的执行时长会显著增加,导致影响用户体验。所以,不推荐计算密集型和时延敏感型任务使用超线程技术。
- 对于后台计算型任务,它不要求单个任务的响应速度,比如超算中心上运行的后台计算型任务(一般要运行数小时或数天),就建议开启超线程来提高整个计算节点的吞吐量。
即便在对虚拟机性能要求不高的场景中,除非我们将虚拟机的CPU和宿主机的超线程一一绑定,否则并不建议应该使用超线程技术,pCPU应该被映射为一个Socket或Core。换句话说,如果我们希望开启Nova Compute Node的超线程功能,那么我会建议你使用CPU绑定功能来将虚拟机的vCPU绑定到某一个 pCPU(此时pCPU映射为一个Thread)上。
NUMA Topology
现在的服务器基本都支持NUMA拓扑,上文已经提到过,主要驱动NUMA体系结构应用的因素是NUMA具有的高存储访问带宽、有效的Cache效率以及灵活PCIe I/O 设备的布局设计。但由于NUMA跨节点远程内存访问不仅延时高、带宽低、消耗大,还可能需要处理数据一致性的问题。因此,虚拟机的vCPU和内存在NUMA节点上的错误布局,将会导宿主机资源的严重浪费,这将抹掉任何内存与CPU决策所带来的好处。所以,标准的策略是尽量将一个虚拟机完全局限在单个NUMA节点内。
Guest NUMA Topology
将虚拟机的vCPU/Mem完全局限在单个NUMA节点内是最佳的方案,但假如分配给虚拟机的vCPU数量以及内存大小超过了一个NUMA节点所拥有的资源呢?此时必须针对大资源需求的虚拟机设计出合适的策略,Guest NUMA Topology的概念也是为此而提出。
这些策略或许禁止创建超出单一NUMA节点拓扑的虚拟机,或许允许虚拟机跨多NUMA节点运行。并且在虚拟机迁移时,允许更改这些策略。也就是说,在对宿主机(Compute Node)进行维护时,接收临时降低性能而选择次优的NUMA拓扑布局。当然了,NUMA 拓扑布局的问题还需要考虑到虚拟机的具体使用场景,例如,NFV虚拟机的部署就会强制的要求严格的NUMA拓扑布局。
如果虚拟机具有多个Guest NUMA Node,为了让操作系统能最大化利用其分配到的资源,宿主机的NUMA拓扑就必须暴露给虚拟机。让虚拟机的Guest NUMA Node与宿主机的Host NUMA Node进行关联映射。这样可以映射大块的虚拟机内存到宿主机内存,和设置vCPU与pCPU的映射。
Guest NUMA Topology实际上是将一个大资源需求的虚拟机划分为多个小资源需求的虚拟机,将多个Guest NUMA Node分别绑定到不同的Host NUMA Node。这样做是因为虚拟机内部运行的工作负载同样会遵守NUMA节点原则,最终的效果实际上就是虚拟机的工作负载依旧有效的被限制在了一个Host NUMA Node内。也就是说,如果虚拟机有4 vCPU需要跨两个Host NUMA Node,vCPU 0/1 绑定到Host NUMA Node 1,而vCPU 2/3绑定到Host NUMA Node 2上。然后虚拟机内的DB应用分配到vCPU 0/1,Web应用分配到vCPU 2/3,这样实际就是DB应用和Web应用的线程始终被限制在了同一个 Host NUMA Node上。但是,Guest NUMA Topology并不强制将vCPU与对应的Host NUMA Node中特定的pCPU进行绑定,这可以由操作系统调度器来隐式完成。只是如果宿主机开启了超线程,则要求将超线程特性暴露给虚拟机,并在NUMA Node内绑定vCPU与pCPU的关系。否则vCPU会被分配给Siblings Thread,由于超线程竞争,性能远不如将vCPU分配到Socket或Core的好。
- 如果Guest的vCPU/RAM分配大于单个Host NUAM Node,那么应该划分为多个Guest NUMA Topology,并分别映射到不同的Host NUMA Node上。
- 如果Host开启了超线程,那么应该在单个Host NUMA Node上进行vCPU和pCPU的绑定,否则vCPU会被分配给Siblings Thread,性能不如物理Core好。
大页内存
绝大多数现代CPU支持多种内存页尺寸,从4KB到2MB/4MB,最大可以达到1GB;所有处理器都默认使用最小的4KB页。如果大量的内存可以使用大页进行分配,将会明显减少CPU页表项,因此会增加页表缓存的命中率,降低内存访问延迟。
如果操作系统使用默认的小页内存,随着运行时间,系统会出现越来越多的碎片,以至于很难申请到大页的内存。在大页内存大小越大时,该问题越严重。因此,如果有使用大页内存的需求,最好的办法是在系统启动时就预留好内存空间。
当前的Linux内核不允许针对特定的NUMA节点进行这样的设定,不过,在不久的将来这个限制将被取消。更进一步的限制是,由于MMIO空洞的存在,内存开始的1GB不能使用1GB的大页。Linux内核已经支持透明巨型页(THP,Transparent Huge Pages)特性。该特性会尝试为应用程序预分配大页内存。依赖该特性的一个问题是,虚拟机的拥有者,并不能保证给虚拟机使用的是大页内存还是小页内存。
内存块是直接指定给特定的NUMA节点的,这就意味着大页内存也是直接存在于NUMA节点上的。因此在NUMA节点上分配虚拟机时,计算服务需要考虑在NUMA节点或者主机上可能会用到的大页内存(NUMA Node或Host存在哪一些大页内存类型和数量状况)。为虚拟机内存启用大页内存时,可以不用考虑虚拟机操作系统是否会使用。
- 有使用大页内存的需求,需要在系统启动时就预留好内存空间,Linux内核使用THP来实现,但也存在着问题。
- 如果希望让虚拟机使用大页内存,那么应该收集NUMA节点所拥有的内存页类型和数量信息。
专用资源绑定
计算节点可以配置CPU与内存的超配比例,例如,16个物理CPU可以允许使用成256个虚拟CPU,16GB内存可以允许使用24GB虚拟机内存。
超配的概念可以扩展到基本的NUMA布局,但是一旦提到大页内存,内存便不能再进行超配。当使用大页内存时,虚拟机内存页必须与主机内存页一一映射,并且主机操作系统能通过交换分区分配大页内存,这也排除了内存超配的可能。但是大页内存的使用,意味着需要支持内存作为专用资源的虚拟机类型。尽管设置专用资源时,不会超配内存与CPU,但是CPU与内存的资源仍然需要主机操作系统提前预留。如果使用大页内存。必须在主机操作系统中明确预留。
对于CPU则有一些灵活性。因为尽管使用专用资源绑定CPU,主机操作系统依然会使用这些CPU的一些时间。不管怎么样,需要预留一定的物理CPU专门为宿主机操作系统服务,以避免操作系统过多占用虚拟机CPU,而造成对虚拟机性能的影响。Nova可以保留一部分CPU专门为操作系统服务,这部分功能将会在后续的设计中加强。
允许内存超配时,超出主机内存的部分将会使用到Swap。Swap将会影响主机整体I/O性能,所以尽量不要把需要专用内存的虚拟机与允许内存超配的虚拟机放在同一台物理主机上。
如果专用CPU的虚拟机与允许超配的虚拟机竞争CPU,由于Cache的影响,将会严重影响专用CPU的虚拟机的性能,特别在同一个NUMA单元上时。因此,最好将使用专用CPU的虚拟机与允许超配的虚拟机放在不同的主机上,其次是不同的 NUMA 单元上。
- 确定虚拟机支不支持使用大页内存。
- 大页内存需要明确的在物理主机中预留。
- 为了虚拟机能够更加好的“独占”物理CPU,一般的,也会预留一些物理CPU资源给宿主机使用。
- 尽量不要将占用专用内存的虚拟机与使用内存超配的虚拟机放到同一个物理主机中运行。
- 尽量不要将占用专用CPU的虚拟机与使用CPU超配的虚拟机放到同一个物理主机中运行,其次是不要放到同一个NUMA Node中运行。
内存共享
Linux内核有一项特性,叫做内核共享存储(KSM),该特性可以使得不同的处理器共享相同内容的内存页。内核会主动扫描内存,合并内容相同的内存页。如果有处理器改变这个共享的内存页时,会采用CoW的方式写入新的内存页。
当一台主机上的多台虚拟机使用相同操作系统或者虚拟机使用很多相同内容内存页时,KSM可以显著提高内存的利用率。因为内存扫描的消耗,使用KSM的代价是增加了CPU的负载,并且如果虚拟机突然做写操作时,会引发大量共享的页面,此时会存在潜在的内存压力峰值。虚拟化管理层必须因此积极地监控内存压力情况并做好现有虚拟机迁移到其他主机的准备,如果内存压力超过一定的水平限制,将会引发大量不可预知的Swap操作,甚至引发OOM。ZSwap特性允许压缩内存页被写入Swap设备,这样可以大量减少Swap设备的I/O执行,减少了交换主机内存页面中固有的性能下降。
虚拟化管理层应该积极的监控内存压力,适时的将虚拟机迁移到其他物理主机。
PCI设备
PCI设备与NUMA单元关系密切,PCI设备的DMA操作使用的内存最好在本地NUMA节点上。因此,在哪个NUMA单元上分配虚拟机,将会影响到PCI设备的分配。
实现
从上述背景知识我们能够清晰的认识到,为了最大化利用主机资源,好好利用NUMA与大页内存等工具显得尤为重要。即使使用默认配置,Nova也能够做到NUMA布局的优化以及考虑到大页内存的使用。显式的配置(通过配置虚拟机套餐类型 Flavor)只是为了满足性能优化或者虚拟机个性化需求,亦或者云平台提供商希望为不同的价格方案设置认为的设置。显示配置还能够限制用户可使用的拓扑,以防止用户使用非最优NUMA拓扑方案。
只有当虚拟机的虚拟CPU与主机的物理CPU一一绑定时,配置超线程参数(threads != 1)才有意义。这不是一个最终用户需要考虑的东西,但是云平台管理员希望能够通过设置虚拟机类型明确避免使用主机超线程(如果vCPU和pCPU没有绑定,那么应该过滤物理主机的超线程ID)。这可以通过使用主机聚合调度的方式实现。
CPU绑定
CPU绑定:将虚拟机的vCPUs绑定到pCPUs,vCPU只会在指定的pCPU上运行,避免pCPU间线程切换(上下文切换,内存数据转移)带来的性能开销。
openstack命令。
1 | openstack flavor set <FLAVOR-NAME> \ |
CPU-POLICY有2种参数类型。
- shared(默认的):允许vCPUs跨pCPU浮动,尽管vCPUs受到的NUMA Node的限制也是如此。
- dedicated:Guest的vCPUs会严格的pinned到pCPUs的集合中。在没有明确vCPU拓扑的情况下,Drivers会将所有vCPU作为Sockets的一个Core或一个Thread(如果启动超线程)。如果已经明确的将vCPUs Topology Pinned到CPUs Topology中时,会严格执行CPU Pinning,将Guest内部的CPU的拓扑匹配到已经Pinned的宿主机的CPUs的拓扑中。此时的overcommit ratio 为 1.0。例如:虚拟机的两个vCPU被pinned到了一个宿主机的Core的两个Thread 上,那么虚拟机内部将会获得一个Core(对应的两个Thread)的拓扑。
CPU-THREAD-POLICY有下列3种参数类型。
- prefer(默认的):主机也许是SMT架构,如果是SMT架构,那么将会优先将一个vCPU绑定到一个宿主机的Thread Siblings上,否则按照一般的方式将vCPU绑定到Core上。
- isolate:主机不应该是SMT架构,或者能够识别Thread Siblings并从逻辑上屏蔽它。每一个vCPU都将会被pinned到一个物理CPU的Core上(如果是多核CPU)。如果物理机是SMT架构支持超线程,那么物理Cores就具有Thread Siblings,这样的话,如果一个Guest不同的vCPU被pinned到不同的物理Core上,那么这个物理Core将不会再继续接受其他Guest的vCPU。所以,需要保证物理Core上没有Thread Siblings。
- require:宿主机必须是SMT架构,每一个vCPU都分配给Thread Siblings。但如果没有足够的Thread Siblings,则会调度失败。如果主机不是 SMT架构,则配置无效。
NUMA亲和
NUMA亲和:将虚拟机绑定NUMA Node,Guest vCPUs/RAM 都分配在同一个NUMA Node 上,充分使用NUMA Node Local Memory,避免访问Remote Memory的性能开销。
1 | openstack flavor set <FLAVOR-NAME> \ |
FLAVOR-NODES:整数,设定Guest NUMA Nodes的个数。如果不指定,则Guest vCPUs可以运行在任意可用的Host NUMA Nodes上。
N:整数,Guest NUMA nodes ID,取值范围在[0, FLAVOR-NODES-1]。
FLAVOR-CORES:逗号分隔的整数,设定分配到Guest NUMA Node N上运行的vCPUs列表。如果不指定,vCPUs在Guest NUMA Nodes之间平均分配。
FLAVOR-MEMORY:整数,单位MB,设定分配到Guest NUMA Node N上Memory Size。如果不指定,Memory在Guest NUMA Nodes之间平均分配。
只有在设定了hw:numa_nodes后hw:numa_cpus.N和hw:numa_mem.N才会生效。另外,只有当Guest NUMA Node存在非对称访问CPUs/RAM时(一个 Host NUMA Node无法满足虚拟机的vCPUs/RAM资源需求时),才需要去设定这些参数。
N仅仅是Guest NUMA node 的索引,并非实际上的Host NUMA Node的ID。例如,Guest NUMA Node 0可能会被映射到Host NUMA Node 1。类似的,FLAVOR-CORES的值也仅仅是vCPU的索引。因此,Nova的NUMA特性并不能用来约束Guest vCPUs/RAM绑定到某一个Host NUMA node 上。要完成 vCPU 绑定到指定的 pCPU,需要借助CPU Pinning Policy和Nova底层隐式实现的CPU Binding(映射)机制。
如果 hw:numa_cpus.N 和 hw:numa_mem.N 设定的值大于虚拟机本身可用的CPUs/Memory的话,则触发异常。
举例:Flavor定义Guest有4个vCPU,4096MB内存,设定Guest的NUMA Topology为2个NUMA Node,vCPU 0、1运行在 NUMA Node 0上,vCPU 2、3运行在NUMA Node 1上。并且占用NUMA Node 0的Memory 2048MB,占用 NUMA Node 1 的Memory 2048MB。
1 | openstack flavor set aze-FLAVOR \ |
使用该flavor创建的虚拟机,将会具有上述Guest NUMA Topology,并由Libvirt Driver隐射到Host NUMA Node上。
Nova分配NUMA的两种方式:
- 自动分配NUMA的约束和限制:仅指定Guest NUMA Nodes的个数,然后由Nova根据Flavor的规格平均将vCPU/Memory分布到不同的Host NUMA Nodes上(默认从 Host NUMA Node 0 开始分配,依次递增)。这将最大程度的降低配置参数的复杂性。如果没有NUMA节点的定义,管理程序可以在虚拟机上自由使用NUMA拓扑。
- 不能设置numa_cpus 和numa_mem。
- 自动从0节点开始平均分配。
- 手动指定NUMA的约束和限制:不仅指定Guest NUMA Nodes的个数,还指定了每个Guest NUMA Nodes上分配的vCPU ID和 Memory Size。设定了Guest NUMA topology,由Nova来完成Guest NUMA Nodes和Host NUMA Nodes的映射关系。
- 设定的vCPU总数需要和虚拟机flavor中的CPU总数一致。
- 设定的Memory大小需要和虚拟机flavor中的memory大小一致。
- 必须设置numa_cpus和numa_mem。
- 需要从Guest NUMA Node 0开始指定各个NUMA节点的资源占用参数。
除了通过flavor extra-specs来设定Guest NUMA Topology之外,还可以通过image metadata来设定。
1 | openstack image set <IMAGE-NAME> \ |
当用户镜像的NUMA约束与flavor的NUMA约束冲突时,以flavor中的约束为准。
调度器使用虚拟机类型的参数numa_nodes决定如何布置虚拟机。如果没有设置numa_nodes参数, 调度器将自由决定在哪里运行虚拟机,而不关心单个NUMA节点是否能够满足虚拟机类型中的内存设置,尽管仍然会优先考虑一个NUMA节点可以满足情况的主机。
- 如果参数numa_nodes设置为1,调度器将会选择单个NUMA节点能够满足虚拟机类型中内存设置的主机。
- 如果参数numa_nodes设置大于1,调度器将会选择NUMA节点数量与NUMA节点中内存能够满足虚拟机类型中numa_nodes参数与内存设置的主机。
ComputeNode会暴露它们的NUMA拓扑信息(例如:每个NUMA节点上有多少CPU和内存),以及当前的资源利用率。这些数据会被加入到计算节点的数据模型(compute_nodes)中。
应用场景。
hw:huma_nodes=1,应该让Guest的vCPU/Memory从一个固定的Host NUMA Node中获取,避免跨NUMA Node的Memory访问,减少不可预知的通信延时,提高Guest性能。
hw:huma_nodes=N,当Guest的vCPU/Memory超过了单个Host NUMA Node占有的资源时,手动将Guest划分为多个Guest NUMA Node,然后再与 Host NUMA Node对应起来。这样做有助于Guest OS感知到Guest NUMA并优化应用资源调度。(数据库应用)
hw:huma_nodes=N,对于Memory访问延时有高要求的Guest,即可以将vCPU/Memory完全放置到一个Host NUMA Node中,也可以主动将Guest划分为多个Guest NUMA Node,再分配到Host NUMA Node。以此来提高总的访存带宽。(NFV/搜索引擎)
- 如果N == 1,表示Guest的vCPU/Memory固定从一个Host NUMA Node获取。
- 如果N != 1,表示为Guest划分N个Guest NUMA Node,并对应到N个Host NUMA Node上。
大页内存
大页内存:使用大页来进行内存分配,那么将会明显减少CPU页表项,因此会增加页表缓存的命中率,降低内存访问延迟。
与NUMA不同,如果虚拟机类型中声明了大页内存,则需要主机能够进行预留该内存块。因为这些内存同时也作为该主机上的NUMA节点专用内存,所以必须提前显式声明。例如,如果主机配置了大页内存,也应该从NUMA节点中分配。
透明巨型页技术允许主机出现内存超配,并且调度程序可以使用该特性。如果主机支持内存预分配,主机将会上报是否支持保留内存或者THP,甚至在严格条件下,可以上报剩余可用内存页数。如果虚拟机使用的主机类型中将huge_pages参数设置为strict时,并且没有主机在单NUMA节点中拥有足够的大页内存可用,调度器将会返回失败。
1 | openstack flavor set <FLAVOR-NAME> \ |
PAGE_SIZE有下列4种参数类型。
- small(默认):使用最小的page size,例如:4KB,x86架构。
- large:只为Guest使用的larger page size,例如:2MB或1GB,x86 架构
- any:由Nova virt drivers决定,不同的driver具有不同的实现。
:字符串,显式自定义page size,例如:4KB/2MB/2048/1GB。
针对虚拟机的RAM可以启动large page特性,可以有效提供虚拟机性能。
将大页内存分配给虚拟机,可以不考虑GuestOS是否使用。如果GuestOS不使用,则会识别小页内存。相反,如果GuestOS是需要使用大页内存的,则必须要为虚拟机分配大页内存,否则虚拟机的性能将达不到预期。
为专有资源虚拟机使用大页内存。这需要主机拥有足够的可用大页内存,并且虚拟机内存大小是大页内存大小的倍数。在主机配置时,为所有虚拟机创建两个资源组,为两个组分配不同的物理内存区域。使用专有资源的虚拟机与共享资源的虚拟机分别使用两个不同的资源组。专用内存的分配的复杂性还在于,主要虚拟机之外,KVM还有许多不同的内存分配的需求,有些虚拟机处理视频内容,会在KVM过程处理I/O请求时,分配任意大小的内存。有些情况下,这也会影响虚拟CPU的使用,因为KVM模拟程序线程代表的就是虚拟机行为。更进一步讲,主机操作系统也需要内存与CPU资源。
PCI passthrough
可以通过下述属性参数来分配PCI直通设备给虚拟机。
1 | openstack flavor set <FLAVOR-NAME> \ |
ALIAS:字符串,在nova.conf中配置的特定PCI设备的alias。(PCI alias)
COUNT:整数,分配给虚拟机的ALIAS类型的PCI设备数量。
实现流程
- nova-api对flavor metadata或image property中的NUMA配置信息进行解析,生成Guest NUMA Topology,保存为instance[‘numa_topology’]。
- nova-scheduler通过NUMATopologyFilter判断Host NUMA Topology是否能够满足Guest NUMA Topology,进行ComputeNode调度。
- nova-compute再次通过instance_claim检查Host NUMA资源是否满足建立Guest NUMA。
- nova-compute建立Guest NUMA Node和Host NUMA Node的映射关系,并根据映射关系调用libvirt driver生成XML文件。
1 | <domain> |
对 NUMA 相关数据的解析和处理,提供了以下class。
nova/objects/numa.py
- NUMACell
- NUMA Cell,定义了NUMA Cell内的基本数据成员。
- NUMAPagesTopology
- NUMA Page,NUMA Node内存页大小。
- NUMATopology
- Host的NUMA拓扑,NUMA的基本数据成员,即cells[]
- NUMATopologyLimits
- NUMA限制,包括CPU和内存超配和网络元数据。
nova/objects/instance_numa.py
- InstanceNUMACell
- Guest NUMA Cell。
- InstanceNUMATopology
- Guest的NUMA拓扑,NUMA的基本数据成员,即cells[]