Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1315496
  • 博文数量: 554
  • 博客积分: 10425
  • 博客等级: 上将
  • 技术积分: 7555
  • 用 户 组: 普通用户
  • 注册时间: 2006-11-09 09:49
文章分类

全部博文(554)

文章存档

2012年(1)

2011年(1)

2009年(8)

2008年(544)

分类:

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)在现代系统中十分常见。在 NUMA 架构中,CPU 并非都与内存的视图相同。考虑图 1 中的图例。

NUMA 架构
图 1. NUMA 架构
 

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 纳秒。

案例研究一:Sun Fire E6900 服务器

有 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 倍的负载。

目标:

  • 即时:让六主板配置能够支持的负载提高 15%。
  • 6 个月内:让六主板配置能够支持的负载提高 20%。
  • 1 年内:让六主板配置能够支持的负载提高 50%。

计划: 首先,要找出 4 种负载情形:15,000, 30,000, 45,000和 60,000 个事务。接着,按照次序运行以上每种负载情形并收集性能指标。针对数据收集,我们使用了一个 ksh shell 脚本,该脚本主要使用 Solaris OS 提供的工具收集带时间戳的性能指标。图 2 比较了不同 CPU 利用率下的业务指标或负载。

业务指标对 CPU
利用率
图 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
0 217 0 925 312 206 344 4 133 3024 0 1196 9 3 0 87
1 43 0 1146 36 8 1476 19 492 201 0 1816 10 4 0 86
2 55 0 1108 18 6 1285 5 486 171 0 1665 10 4 0 86
3 45 0 1069 18 7 1238 5 467 169 0 1614 10 4 0 86
4 283 0 1615 18 6 1521 5 517 206 0 2000 11 5 0 84
 

有趣的指标包括但不限于线程迁移 (migr),非主动上下文切换(icsw),互斥锁旋转(smtx),和交叉调用 (xcal)。

线程迁移 发生的原因是,一个线程这次运行所基于的 CPU 核心与上次不同。 非主动上下文切换 发生的原因是,由于更高优先级的线程要运行,或者线程耗尽了时间片,线程脱离了 CPU。互斥锁旋转 可用于统计为了在内核中获得互斥锁而进行的不成功尝试的数量。 交叉调用 就是当为了满足某个请求,一个 CPU 核心不得不中断另一个 CPU 核心时出现的软件中断。交叉调用的原因包括信号和页面映射行为。

非主动上下文切换与处于较高负载步进时的 CPU 利用率增高有关,线程迁移也是如此,但是最有力的证据来自每秒的系统调用数。参见图 3。

每个 CPU 核心每秒的系统调用数对业务指标
Figure 3. 每个 CPU 核心每秒的系统调用数对业务指标
单击 此处 可观看大图
 

通常,我们希望每个工作单元的系统调用次数是确定的。图 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
10.008 8 tick 264339684 64702523
10.008 16 tick 379033424 82264350
10.009 17 tick 139120474 43937429
10.009 2 tick 188557829 40718416
10.008 1 tick 196036061 51324303
10.009 9 tick 147137952 31141793
10.008 512 tick 179496352 47598315
10.009 514 tick 213559632 51053565
10.009 10 tick 146540546 29848051
10.009 18 tick 197444045 54358956
10.009 3 tick 241315996 59839941
10.009 515 tick 169493146 37817954
10.009 11 tick 206307589 48544746
10.009 19 tick 139896698 38977705
 

通过在从 (Re_L3_miss) 到 Cycle_cnt 的周期中使用 L3 不中开销,我们看到了服务于 L3 缓存不中的 CPU 周期的百分比。图 4 显示了此图。

花在等待 L3 不中上的全部系统时间的百分比
Figure 4. 花在等待 L3 不中上的全部系统时间的百分比
Click here for a larger image
 

结果表面,41% 的系统时间用于服务 L3 不中的情形。

我们给出了如下补救措施:处理器亲和性和 Lgroups(位置组,Locality Group)。

Solaris OS 有一种内置的 处理器亲和性,可以让线程倾向于在它之前运行过的地方运行。调度器考虑线程自上次运行以来的时间长短,以及线程运行之前所在运行队列的长度。简而言之,如果线程运行的时间过长,或者目标 CPU 上的运行队列过深,线程就会发生迁移。显然,处理器亲和性在这里不足以保证线程会倾向于重复地运行在同一个 CPU 核心上。

Solaris OS 中有一个概念叫做 Lgroup。 Lgroup 就是一组拥有相同内存视图的 CPU。在 Among these tools are the following:?/p>

  • : 用于显示 Lgroup 层次、内容和特性的 Perl 脚本
  • : 用于观察和影响 home lgroup 和 lgroup 相似性的 proc 工具
  • : 使用 madvise(3C) 应用建议的 proc 工具
  • 显示包含了在特定过程中给定虚拟地址的物理内存的 lgroup 的扩展
  • 用于 lgroups 的扩展
  • 用于 lgroups 的扩展

下面给出了使用这些 NUMA 感知工具所得到输出的一些示例。

下面是 plgrp -l 12502 的输出:

PID/LWPID    HOME
12502/1 1
12502/2 3
12502/3 4
12502/4 5
12502/5 2
12502/6 6
12502/7 1
12502/8 4
12502/9 5
 

HOME 列中的数字代表着每个轻量进程的 home lgroup(lwp)。

下面是 pmap -x -L 5962的输出:

5962:  xxxx
00010000 8K r-x-- 6 xxxx
00020000 8K rwx-- 6 xxxx
00022000 800K rwx-- 6 [ heap ]
000EA000 8K rwx-- - [ heap ]
000EC000 40K rwx-- 6 [ heap ]
000F6000 8K rwx-- - [ heap ]
000F8000 32K rwx-- 6 [ heap ]
00100000 32K rwx-- - [ heap ]
00108000 8K rwx-- 6 [ heap ]
0010A000 32K rwx-- - [ heap ]
00112000 8K rwx-- 6 [ heap ]
00114000 32K rwx-- - [ heap ]
0011C000 216K rwx-- 6 [ heap ]
00152000 32K rwx-- - [ heap ]
0015A000 1416K rwx-- 6 [ heap ]
002BC000 1008K rwx-- 3 [ heap ]
003B8000 32K rwx-- - [ heap ]
003C0000 8K rwx-- 3 [ heap ]
...
FF3B0000 184K r-x-- 6 /lib/ld.so.1
FF3E6000 8K r-x-- 4 /lib/libthread.so.1
FF3E8000 8K r-x-- - /lib/libthread.so.1
FF3EE000 8K rwx-- 6 /lib/ld.so.1
FF3F0000 8K rwx-- 6 /lib/ld.so.1
FF3F8000 16K r-x-- 6 /lib/libpthread.so.1
FF400000 24K ----- - [ anon ]
FFB2C000 848K rwx-- 6 [ stack ]
total 3320488K
 

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
     
  • 指派一个 PID 给 FX 调度类:
     
    priocntl -s -c FX -m 59 -p 59 -t 300 -i pid "PID"
     

    它表示优先级为 59,时间为 300 毫秒。

图 5 显示了经过努力改进后的可伸缩性。

所有 CPU 核心的平均 CPU 利用率对业务指标
图 5. 所有 CPU 核心的平均 CPU 利用率对业务指标
单击 此处 可观看大图
 

每秒 99,000 个事务对 60,000 而言已是相当大的改进。这种可伸缩性在很大程度上可归结于 L3 不中开销的稳定性。尽管减少了非主动上下文切换和线程迁移,性能提高的主要原因还是 L3 不中开销的减少,如图 6 所示。

不中开销占系统 CPU 的百分比
Figure 6. 不中开销占系统 CPU 的百分比
Click here for a larger image
 
案例研究二:Sun Fire X4600 服务器

Sun Fire X4200 有两个插槽,每个插槽有两个核心。通向内存的 HyperTransport 数量最大为 1。 V40z 有四个插槽,每个插槽有两个核心。通向内存的最大数量为 2。但是 有八个插槽,每个插槽有两个核心。现在,通向内存的数量可以高达 3。 X4600 的平均内存访问时间是 155 纳秒,与之相对的是双插槽系统的时间为 105 纳秒。

现在所分析的应用程序是基于 Java 技术的。下面是对问题的描述。X4200 服务器能够在 214 分钟内处理完一大块工作,这是借助一台 Java 虚拟机(JVM)和四个工作线程实现的。* 当使用一台 JVM 和 16 个工作线程把应用程序部署在 X4600 服务器上时,执行时间是 195 分钟。我们的目标是把运行在 X4600 上时的负荷变为在 X4200 运行时的四分之一。

图 7 显示了 X4600 上插槽的布局情况。

X4600 插槽布局
Figure 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
psrset -d 1 2 3
#create processor set 1, assign cores 0,1 and 2,3 to processor set 0 (sockets 0 and 1)
psrset -c -F 0 1 2 3
#create processor set 2, assign cores 4,5 and 8,9 to processor set 1 (sockets 2 and 4)
psrset -c -F 4 5 8 9
#create processor set 3, assign cores 6,7 and 10,11 to processor set 2 (sockets 3 and 5)
psrset -c -F 6 7 10 11
# which leaves Cores 12,13 and 14,15 in the default set (sockets 6 and 7)

Use FX(Fixed Priority scheduling, $$ = pid of interest)
priocntl -s -c FX -m59 -p59 -t300 -i pid $$

in /etc/system,
set lgrp_mem_pset_aware=1
set lgrp_mem_default_policy=3 ( LGRP_MEM_POLICY_RANDOM_PSET)
 

我们进行上述修改的目的是,确保内存分配发生在处理器集合内。

通过把一个大的 Java 进程划分为四个进程,将划分后的每个进程指派给上述处理器集合之一,以及使用 FX 调度和处理器集合感知的内存分配,我们就能够在不到 80 分钟时间内完成以前需要 195 分钟才能完成的工作。我们并未达到既定目标,尽管如此,80 分钟仍然是一个可观的提高。

结束语

访问本地内存时,集成的内存管理单元让现代处理器拥有了极大的性能优势。当内存访问变得不成比例得远程时,这种优势就成了一种责任。

现代系统致力于通过三种方式来使执行效率最大化:赋予线程对它最近运行在其上的 CPU 以亲和性,赋予线程对距离线程要访问的内存“最近的”处理器(在 Solaris OS 中表现为 lgroups)以亲和性,以及使用较大的 L2 和 L3 缓存。有时,这些策略还不足以确保执行变得高效和可扩展。

基于智能策略的调度可能会有所帮助,但操作系统所能做的也就这么多了。经验丰富且对负荷有着充分了解的称职分析师,可以让一个不可扩展的、执行情况不稳定的系统变为运转平稳的高效系统。

_______
与此 Web 站点上的使用方法一样,术语 “Java 虚拟机” 或 “JVM”是指用于 Java 平台的虚拟机。

以上文章转自于 : http://developers.sun.com.cn/

 

阅读(577) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~