1、性能问题的现状
通常认为,传统的软件开发过程主要包括4个阶段:分析、设计、编码和测试
- 分析-是开发过程的第一步,用于评估需求、权衡各种架构的利弊以及构思高层抽象
- 设计-依据分析阶段的基本架构和高层抽象,进行更精细的抽象并着手考虑具体实现
- 编码-设计的实现
- 测试-用以验证实现是否合乎应用需求
值得注意的是,测试阶段通常只包括功能测试,即检验应用的执行是否合乎需求规格。一旦测试完成,应用就可以发布给客户了。
遵循这种传统软件开发过程的应用,通常要到测试或即将发布时才会关注性能或扩展性。为了解决这个问题,Wilson和Kesselman对传统软件开发过程做了些补充,在传统开发模型基础上引入了性能测试分析阶段,参见他们的畅销书Java Platform Performance。他们建议在测试阶段之后增加性能测试,并将“性能测试是否通过”设定为产品是否发布的标准。如果达到性能和扩展性标准,应用就可以发布,否则就要转向性能分析,并依据分析结果回到之前的某个或者某些步骤。换句话说,通过性能分析来定位性能问题。
对分析阶段提炼出来的性能需求,Wilson和Kesselman建议以用例(use case)的方式特别标识出来,这有助于在分析阶段制定性能评估指标。不过应用的需求文档中通常都不会明确描述性能或扩展性需求。如果你正在开发的应用还没有明确定义这些需求,那就应该想办法将它们挖掘出来。拿吞吐量和延迟性需求举例,以下清单列举了挖掘这些需求所要考虑的问题。
- 应用预期的吞吐量是多少?
- 请求和响应之间的延迟预期是多少?
- 应用支持多少并发用户或者并发任务?
- 当并发用户数或并发任务数达到最大时,可接受的吞吐量和延迟是多少?
- 最差情况下的延迟是多少?
- 要使垃圾收集引入的延迟在可容忍范围之内,垃圾收集的频率应该是多少?
需求和对应的用例文档应该回答上述问题,并以此制定【基准测试】和性能测试,确保应用能够满足性能和扩展性需求。基准测试和性能测试应该在性能测试阶段执行。评估用例时有些用例的风险过高,难以实现,应该在分析阶段后期,通过一些【原型、基准测试和微基准测试】来降低此类风险。分析结束后再变更决策的代价非常高,这个方法可以让你事先对决策进行评估。软件开发周期中的软件缺陷、低劣设计和糟糕实现发现得越晚,修复的代价就越大。降低用例的高风险有助于避免这些代价昂贵的错误。
现在许多应用在开发过程中都会使用自动构建和测试。Wilson和Kesselman建议改进软件开发过程,在自动构建或测试中进一步添加自动性能测试。自动性能测试可以发出通知,比如用电子邮件将性能测试结果(如性能是衰减还是改善,或性能指标的达成度)发送给干系人。这个过程可以将因不满足应用性能指标而失败的测试,以及测试的统计数据自动记录到追踪系统。
将性能测试集成到自动构建过程中后,每次代码变更提交到源代码库时,都能很容易地追踪因变更而导致的性能变化,也就能在软件开发的早期发现性能衰减。
2、性能分析的两种方法:自顶向下和自顶向上
自顶向下和自底向上是两种常用的性能分析方法。顾名思义,自顶向下(Top Down)着眼于应用顶层,从上往下寻找软件栈中的优化机会和问题。相反,自底向上(Bottom Up)则从软件栈最底层的CPU统计数据(例如CPU缓存未命中率、CPU指令效率)开始,逐渐上升到应用自身的结构或该应用常见的使用方式。应用开发人员常常使用自顶向下的方法,而性能问题专家则通常采用自底向上的方法,用以辨别因不同硬件架构、操作系统或不同的Java虚拟机实现所导致的性能差异。如你所想,不同方法可以用来查找不同类型的性能问题。
2.1自顶向下
使用自顶向下的方法时,通常你需要从干系人发现性能问题的负载开始监控应用。应用的配置变化或日常负荷变化可能导致性能降低,这种情况下,需要持续地监控应用。此外,当应用的性能和扩展性需求发生变化时,应用可能无法满足新的要求,这时也需要监控应用程序的性能。
不管何种原因引起的性能调优,自顶向下的第一步总是对运行在特定负载之下的应用进行监控。监控的范围包括操作系统、Java虚拟机、Java EE容器以及应用的性能测量统计指标。基于监控信息所给出的提示再开展下一步工作,例如JVM垃圾收集器调优、JVM命令行参数调优、操作系统调优,或者应用程序性能分析。性能分析可能导致应用程序的更改,或者发现第三方库或Java SE类库在实现上的不足。
2.2自底向上
在不同平台(指底层的CPU架构和数量不同)上进行应用性能调优时,性能专家常使用自底向上的方法。将应用迁移到其他操作系统上时,也常用这种方法改善性能。在无法更改应用源代码时,例如应用已经部署在生产环境中,或者系统供应商为了在竞争中占得先机而必须将性能发挥到极致,也常常会使用这种方法。
自底向上需要收集和监控最底层CPU的性能统计数据。监控的CPU统计数据包括执行特定任务所需要的CPU指令数(通常称为路径长度,path length),以及应用在一定负载下运行时的CPU缓存未命中率。虽然还有其他重要的CPU统计数据,但这两项是自底向上中最常用的。在一定负载下,应用执行和扩展所需的CPU指令越少,运行得就越快。提高CPU缓存命中率也能改善应用的性能,因为CPU缓存失效会导致CPU为了等待从内存获取数据而浪费若干个周期,而提高CPU缓存命中率,意味着CPU可以减少等待内存数据的时间,应用也就能运行得更快。
自底向上关注的通常是在不更改应用的前提下,改善CPU使用率。假如应用可以更改,自底向上也能为如何修改应用提供建议。这些更改包括应用源代码的变动,如将经常使用的数据移到一起,使得访问同一条CPU缓存行(CPU cache line)就能获取这些数据,而不用等待从内存中获取数据。这个改动可以提高CPU缓存命中率,从而减少CPU等待内存数据的时间。
现代Java虚拟机集成了成熟的JIT编译器,可以在Java应用的执行过程中进行优化,比如依据应用的内存访问模式或应用特定的代码路径,生成更有效的机器码。也可以调整操作系统的设置来改善性能,例如更改CPU调度算法,或者修改操作系统的等待时间(指操作系统在将应用执行线程迁移到其他CPU硬件线程之前所等待的时间)。
如果你觉得可以用自底向上的方法,那应该先从收集操作系统和JVM的统计数据开始。监控这些统计数据可以为下一步应该关注哪些重点提供线索。
3、选择正确的平台并评估系统性能
帮助改善应用性能的专家有时会发现,性能差只是因为应用运行的CPU架构或系统不合适。引入多核和每核多硬件线程(multiple hardware threads per core,也称为CMT,chip multithreading)以后,CPU架构和系统已经发生了天翻地覆的变化,因此为特定应用选择正确的平台和CPU架构就显得尤为重要了。此外,随着CPU架构的演变,评估系统性能的方法也需要与时俱进。
3.1选择正确的CPU架构
Oracle的SPARC T系列处理器引入了芯片多处理(chip multiprocessing)和芯片多线程(chip multithreading)。SPARC T系列处理器设计上的主要亮点是引入了每核多硬件线程(multiple hardware threads per core),以应对CPU缓存失效所带来的问题。第一代SPARC T系列UltraSPARC T1,每个CPU有4、6或8个核,每核有4个硬件线程。从操作系统的角度来看,8核的UltraSPARC T1处理器就像是有32个处理器的系统。即操作系统把每个核中的每个硬件线程都看成一个处理器,所以配置为8核的UltraSPART T1系统,操作系统会把它当成32个处理器。
UltraSPARC T1的独特之处在于每个核有4个硬件线程。在一个时钟周期内,每核4个硬件线程中只有一个可以运行。发生长延迟时,例如CPU缓存失效,如果同一个UltraSARC T1核中还有其他就绪的硬件线程(runnable hardware thread),下一个时钟周期就会让这个硬件线程运行。相比而言,其他每核单硬件线程(即便是超线程)的CPU,就会被诸如CPU缓存失效这样的长延迟事件所阻塞,从而因等待事件完成而浪费时钟周期。对于这些CPU,如果就绪的应用线程已经准备好运行却没有可用的硬件线程,运行前就必须进行线程上下文切换。线程上下文切换通常需要耗费数百个时钟周期。由于SPARC T系列处理器可以在下一时钟周期切换到同核上的另一个就绪线程,因此,对于有许多待执行线程、高度线程化的应用来说,SPARC T系列处理器可以执行得更快。不过,这种每核多硬件线程和下一时钟周期切换的设计代价是CPU的时钟频率比较低。换句话说,像SPARC T系列这样有多硬件线程的CPU,与他每核单硬件线程或者无法在下一周期切换的CPU相比,运行的时钟频率通常较低。
选择硬件系统时,如果预计目标应用有大量的并发线程,那么它在SPARC T系列处理器上的性能和扩展性,就要好于每核硬件线程少的处理器。与之相比,如果应用只需少量线程,特别是预计同时运行的线程数少于SPARC T系列处理器的硬件线程数,那么它在每核硬件线程数不多但时钟速率更高的处理器上的性能,就要好于时钟速率较低的SPARC T系列处理器。简言之,要想发挥SPARC T系列处理器的性能,就需要大量并发的线程,让大量硬件线程保持负荷,从而在发生例如CPU缓存失效这样的事件时,发挥它在下一时钟周期切换到另一硬件线程的能力。如果没有大量的并发线程,SPARC T系列就和传统低时钟速率的处理器差不多了。需要同时用大量线程将许多SPARC T系列硬件线程跑满的观点也表明,传统判断系统性能是否合格的方法或许没有真正展现系统的性能。
3.2评估系统性能
SPARC T系列处理器可以在下一时钟周期切换到同核中其他就绪的硬件线程,所以为了评估它的性能,必须加载大量的并发线程。
通常评估新系统性能的方法是将预期目标负载的一部分加载到系统上,或者执行一个或多个微基准测试(micro-benchmark),然后监控系统性能或单位时间内应用完成的运算量。然而,为了评估SPARC T系列处理器的性能,必须加载足够多的并发线程以便将众多硬件线程跑满。对于长时间延迟(如CPU缓存失效)事件引起的下一周期线程切换,SPARC T系列需要足够大的负载才能从中受益。CPU缓存失效引起的阻塞和等待会耗费许多CPU周期,大约要数百个时钟周期。因此,为了充分利用SPARC T系列处理器,系统需要加载足够多的并发任务,这样下个周期线程切换这种任务所带来的好处才能体现出来。
如果SPARC T系列处理器不能以目标负载的方式运行,系统性能反而不会很好,因为并非所有的硬件线程都满载。记住SPARC T系列处理器的主要设计点就是允许其他硬件线程在下一时钟周期执行,从而应对CPU的长时间延迟。对于每核单硬件线程的处理器来说,长时间延迟(例如CPU缓存失效)意味着需要浪费大量的CPU时钟周期,以等待从内存获取数据。为了切换到另一个线程,必须用其他可运行线程及其状态信息替换当前的线程。这不仅需要时钟周期以便进行上下文切换,也需要CPU缓存为新运行的线程获取不同的状态信息。
因此,评估SPARC T系列处理器性能时,很重要的一点就是在系统上加载足够大的负载,从而充分利用更多的硬件线程,以及在下一时钟周期同一个CPU核内切换到另一个硬件线程的能力。