分类:
2008-04-07 18:43:56
非统一内存架构(Non-Uniform Memory Architecture,NUMA)在现代系统架构中得到了广泛应用。某些应用程序在这类架构中进行扩展时会遇到一些困难,因为 CPU 也需要同步升级。即便引入多核CPU也无法让应用程序的运行效率成比例地增长。
本文将阐述如何通过降低计算机的“NUMAness”效用,来提高应用程序在 Sun SPARC E6900 上的性能。本文还将讨论如何对 AMD Opteron X4600 上的另一个应用程序实现类似效果。我将解释本文中所使用的性能分析方法,此方法已经被定性为性能分析的科学方法。此外,我将让大家了解业务指标的重要性,并简要介绍一下 中的几种 NUMA 感知工具。
性能分析的一种实用方法是,将与性能相关的所有指标与我们真正关心的内容联系起来。在本例中,这些内容包括每时间单元的调用次数,每时间单元的事务个数,每时间单元处理的记录数等等。我将这种指标称为 业务指标。
性能指标只在它们如何与业务指标关联的上下文中才有意义。通过考虑作用于业务指标的效果,我们将调查重点从找出未加以限定的指标转移到只找出与业务指标相关的指标。例如,如果当 tcp 重传输率提高时实现业务指标变得很困难,改进 tcp 重传输率可能会改进业务指标。类似地,如果缓慢的磁盘 I/O 与业务指标的退步毫无关系,那么改进磁盘 I/O 就不可能改进业务指标。
本文中的图将就业务指标和与特定性能相关指标的测量进行比较。
规划性能调查时,在开始之前指定试验参数很有用处。首先,我们要建立一个目标,指出要了解或实现的内容。而目标的表达形式就是业务指标,而且目标指定了成功的标准。
然后,我们确定试验的用例。对于生产应用程序,用例的定义就是一个工作负载周期,负载在此周期内有升有降。能够在周期内捕捉到带时间戳的业务指标测量,这一点很重要。在实验室里,我们想建立可重复的负载步进。例如,将每小时一百万,两百万,三百万的繁忙呼叫作为一台虚拟电话交换机定义的3个负载步进。在分析过程中,我使用了一些图来指出性能指标与业务指标之间的关联。
非统一内存架构(NUMA)在现代系统中十分常见。在 NUMA 架构中,CPU 并非都与内存的视图相同。考虑图 1 中的图例。
|
CPU1 访问位于自己主板上内存的时间要少于访问 CPU2 所在主板上内存的时间,反之亦然。
当一个应用程序线程运行在 NUMA 架构中的不同 CPU 上时,此运行环境中的负载开销和/或存储器可能会根据线程在哪个 CPU 上执行而有所不同。因此,应用程序在每个单元时间内能够完成的业务指标量取决于当应用程序运行时,它与其内存之间的距离有多“远”。
一种 SUN SPARC 计算机的架构是主板附加到一条中心总线,这与图 1 类似。计算机可以有多块主板,每块主板上都配有内存和 CPU。CPU 访问其自己主板上内存的速度比访问另一主板上内存的速度更快。
在基于 AMD Opteron 的计算机上,每个 CPU 插槽都用过 HyperTransport 与其他 CPU 插槽相连。访问内存组(memory bank)的速度由必须经过的 HyperTransport 链接的个数决定。表 1 列出了一些例子,时间单位为纳秒(十亿分之一秒)。
表 1. 内存访问时间示例 |
|
|
Sun Fire 3800-6800 服务器 |
Sun Fire 12K/15K 服务器 |
---|---|---|
访问同一主板上的内存 |
180-207 |
180-197 |
访问远程主板上的内存 |
240 |
333-440 |
Sun 基于 Opteron 的平台访问本地内存花费了大约 80 纳秒,而访问每个 HyperTransport 链接则另外花费了 50 纳秒。双插槽平台的平均访问时间大约是 105 纳秒,四插槽系统的平均访问时间则是 124 纳秒,而八插槽系统的平均访问时间则高达 155 纳秒。
有 6 块主板,而每块主板都有 4 个插槽。在我们的配置中,每个插槽都配有一个双核UltraSPARC IV+ CPU,它包含 64 KB L1 指令缓存,64 KB L1 数据缓存,2 MB 共享的 L2 缓存,和 32 MB 的共享 L3 缓存。
应用程序是虚拟电话交换机的一个组成部分。尽管业务指标是以每秒处理的呼叫或每小时的繁忙呼叫来表示的,我们在图中一般使用每秒事务数。应用程序是由大量基于 Java 技术的和 C++ 进程组成的。
问题: CPU 利用率的增长与负载之间是非线性的。特别地,当客户将单主板配置与六主板配置进行比较时,应用程序使用六主板解决方案所能处理的负载不过是使用单主板解决方案的 3 倍,而不是客户期待的 6 倍。换句话说,CPU 的数量变为 6 倍,并不代表能够处理 6 倍的负载。
目标:
计划: 首先,要找出 4 种负载情形:15,000, 30,000, 45,000和 60,000 个事务。接着,按照次序运行以上每种负载情形并收集性能指标。针对数据收集,我们使用了一个 ksh
shell 脚本,该脚本主要使用 Solaris OS 提供的工具收集带时间戳的性能指标。图 2 比较了不同 CPU 利用率下的业务指标或负载。
|
在图 2 中,100 代表 100% 的系统 CPU 容量。业务指标以每秒事务数表示。我们关心的内容是,CPU 利用率的增长在负载方面有何对应反映。结果是,CPU 的使用超过对第三个负载步进的预计,是对第四个负载步进预期的两倍。
找出利用率增高的理由的第一步是,看一看其他可能的性能抑制因素。为此,我们使用了一个名为 mpstat
的工具。关于这个和其他工具的更多信息,可以参考
下面是输出示例:
CPU minf mjf xcal intr ithr csw icsw migr smtx srw syscl usr sys wt idl |
有趣的指标包括但不限于线程迁移 (migr
),非主动上下文切换(icsw
),互斥锁旋转(smtx
),和交叉调用 (xcal
)。
线程迁移 发生的原因是,一个线程这次运行所基于的 CPU 核心与上次不同。 非主动上下文切换 发生的原因是,由于更高优先级的线程要运行,或者线程耗尽了时间片,线程脱离了 CPU。互斥锁旋转 可用于统计为了在内核中获得互斥锁而进行的不成功尝试的数量。 交叉调用 就是当为了满足某个请求,一个 CPU 核心不得不中断另一个 CPU 核心时出现的软件中断。交叉调用的原因包括信号和页面映射行为。
非主动上下文切换与处于较高负载步进时的 CPU 利用率增高有关,线程迁移也是如此,但是最有力的证据来自每秒的系统调用数。参见图 3。
|
通常,我们希望每个工作单元的系统调用次数是确定的。图 3 显示了每个工作单元的系统调用正在变少。这很不寻常,但对于使用诸如轮询和定时等待条件变量这样的定时事件的系统来说,当负载增加时使用的系统调用变少是可能的。
我们使用 dTrace 对正被使用的系统调用进行抽样,然后得出结论,定时事件并非速度降低的原因。我们还排除了互斥锁上旋转的增加是系统调用变慢的原因。最后,我们推理出,特殊用户指令的运行时间实际上变长,从而导致每秒的系统调用次数比我们期望的要少。
我们决定集中精力确定,由于板外 CPU 迁移和 L3 缓存不中,加载和存储是不是会花费更长的时间,事实上也就是每条指令使用的周期更多。
幸运的是,Solaris OS 提供一种可用于访问硬件性能计数器的工具,叫做 cpustat
。对于 E6900,我们可以使用以下命令来测量由于不中引起的开销:
cpustat -c pic0=Cycle_cnt,pic1=Re_L3_miss 10 100 (10 seconds between samples, 100 samples) |
下面给出了一个输出的例子,其中 pic0
是周期数,而 pic1
则是周期中的 L3 不中开销 (Re_L3_miss
):
time cpu event pic0 pic1 |
通过在从 (Re_L3_miss
) 到 Cycle_cnt
的周期中使用 L3 不中开销,我们看到了服务于 L3 缓存不中的 CPU 周期的百分比。图 4 显示了此图。
|
结果表面,41% 的系统时间用于服务 L3 不中的情形。
我们给出了如下补救措施:处理器亲和性和 Lgroups(位置组,Locality Group)。
Solaris OS 有一种内置的 处理器亲和性,可以让线程倾向于在它之前运行过的地方运行。调度器考虑线程自上次运行以来的时间长短,以及线程运行之前所在运行队列的长度。简而言之,如果线程运行的时间过长,或者目标 CPU 上的运行队列过深,线程就会发生迁移。显然,处理器亲和性在这里不足以保证线程会倾向于重复地运行在同一个 CPU 核心上。
Solaris OS 中有一个概念叫做 Lgroup。 Lgroup 就是一组拥有相同内存视图的 CPU。在 Among these tools are the following:?/p>
madvise(3C)
应用建议的 proc 工具
下面给出了使用这些 NUMA 感知工具所得到输出的一些示例。
下面是 plgrp -l 12502
的输出:
PID/LWPID HOME |
HOME
列中的数字代表着每个轻量进程的 home lgroup(lwp
)。
下面是 pmap -x -L 5962
的输出:
5962: xxxx |
6, 3 和 4 代表分配内存的 lgroup。
使用 pmap -xL
时,可以看到有关进程让内存分配分散在整个内存中。此外,使用 plgrp
时,还可以看到有关进程的线程在不同主板上都有自己的 home lgroups。
要评估的第一个补救措施是,把进程的所有线程都指派给同一个 home lgroup。我们将进程指派向外扩展到所有主板上,并期望 lgroup 亲和性可以减少板外迁移和 L3 不中开销。但这对性能的提升帮助微不足道,不过从 10% 升到了 15%。显然还需要更多有效手段。
接下来考虑的是处理器组。处理器组是排他的,而且我们目前的知识也不足以将所有进程都指派给处理器组,同时不会让磁盘负载失去平衡。
最后考虑的是处理器绑定。我们可以把进程中对延迟敏感的线程绑定到它们自己的 CPU。处理器绑定带来的风险是,一旦某个线程绑定到了一个 CPU,它就只能运行在该 CPU 上。还有一个风险在于,它可能无法迅速运行,甚至可能无法获得 CPU 周期。减小上述风险的方法是,使用一个较大的时间片,在拥有高优先级的固定优先级(Fixed Priority,FX)调度类中运行被绑定的线程。这种解决方案是最终答案。
我们使用了如下命令:
lwp
(在 Solaris OS 9 和 10 中,线程和 lwps
之间存在 1:1 的对应关系) : pbind -b processor_id pid/lwpid |
priocntl -s -c FX -m 59 -p 59 -t 300 -i pid "PID" |
图 5 显示了经过努力改进后的可伸缩性。
|
每秒 99,000 个事务对 60,000 而言已是相当大的改进。这种可伸缩性在很大程度上可归结于 L3 不中开销的稳定性。尽管减少了非主动上下文切换和线程迁移,性能提高的主要原因还是 L3 不中开销的减少,如图 6 所示。
|
Sun Fire X4200 有两个插槽,每个插槽有两个核心。通向内存的 HyperTransport 数量最大为 1。 V40z 有四个插槽,每个插槽有两个核心。通向内存的最大数量为 2。但是 有八个插槽,每个插槽有两个核心。现在,通向内存的数量可以高达 3。 X4600 的平均内存访问时间是 155 纳秒,与之相对的是双插槽系统的时间为 105 纳秒。
现在所分析的应用程序是基于 Java 技术的。下面是对问题的描述。X4200 服务器能够在 214 分钟内处理完一大块工作,这是借助一台 Java 虚拟机(JVM)和四个工作线程实现的。* 当使用一台 JVM 和 16 个工作线程把应用程序部署在 X4600 服务器上时,执行时间是 195 分钟。我们的目标是把运行在 X4600 上时的负荷变为在 X4200 运行时的四分之一。
图 7 显示了 X4600 上插槽的布局情况。
|
使用一种可以显示近似的实时 CPU 使用情况的 Sun 内部工具 perfbar
之后,我们注意到,在某些期间内 Java 进程变为了单线程的。通过打开 verbose garbage collection 选项,我们证实这些单线程的期间用于了垃圾收集。这会将应用程序划分为多个 Java 进程。因为负荷在本质上是批量的,平衡多个进程之间的工作就显得直截了当。因为 X4600 的核心数量是 X4200 的四倍,我们决定使用四个 Java 进程。
记住每个插槽都包含一个带有两个核心的 CPU。我们推出结论,让一台 X4600 服务器(16个核心)执行的效率相当于四台 X4200 服务器(4个核心)的关键在于,将 X4600 划分为几个处理器集合,这样每个集合就能模拟出一台 X4200 的效果。图 7 说明,我们想将插槽 0 和 1,插槽 2 和 4,插槽 3 和 5,插槽 6 和 7分别配对。当然,其他组合也是可以的。
完成上述划分的命令如下:
#delete any existing processor sets |
我们进行上述修改的目的是,确保内存分配发生在处理器集合内。
通过把一个大的 Java 进程划分为四个进程,将划分后的每个进程指派给上述处理器集合之一,以及使用 FX 调度和处理器集合感知的内存分配,我们就能够在不到 80 分钟时间内完成以前需要 195 分钟才能完成的工作。我们并未达到既定目标,尽管如此,80 分钟仍然是一个可观的提高。
访问本地内存时,集成的内存管理单元让现代处理器拥有了极大的性能优势。当内存访问变得不成比例得远程时,这种优势就成了一种责任。
现代系统致力于通过三种方式来使执行效率最大化:赋予线程对它最近运行在其上的 CPU 以亲和性,赋予线程对距离线程要访问的内存“最近的”处理器(在 Solaris OS 中表现为 lgroups)以亲和性,以及使用较大的 L2 和 L3 缓存。有时,这些策略还不足以确保执行变得高效和可扩展。
基于智能策略的调度可能会有所帮助,但操作系统所能做的也就这么多了。经验丰富且对负荷有着充分了解的称职分析师,可以让一个不可扩展的、执行情况不稳定的系统变为运转平稳的高效系统。
_______
与此 Web 站点上的使用方法一样,术语 “Java 虚拟机” 或 “JVM”是指用于 Java 平台的虚拟机。
以上文章转自于 : http://developers.sun.com.cn/