Java性能权威指南-总结3
性能测试方法
原则4:尽早频繁测试
这是最后的原则。性能测试应该作为开发周期不可或缺的一部分。理想情况下,在代码提交到中心源代码仓库前,性能测试就应该作为过程的一部分运行,如果代码引入了性能衰减,提交就会被阻止。
好的性能测试包含了许多代码——至少中等规模的介基准测试是这样。它需要在新老代码上重复运行多次,以便确认性能真的有差别而不是随机变动。在大型项目中,这可能需要花费好几天或者一周时间,这使得在提交代码到仓库之间运行性能测试变得不那么现实。
通常的软件开发周期也没使事情变得更容易。项目日程通常会固定特性的发布日期:所有的代码变动必须在发布周期的早些时候就提交到源代码仓库,而剩下的时间则贡献给了将新版本中的缺陷(包括性能问题)抖落干净。这导致了提早测试的两个问题。
- 为了赶上项目进度,开发人员会在时间压力之下提交代码,而一旦有时间修复性能问题时,又变得踯躇不前。早期提交代码所导致的1%的性能衰减,开发人员愿意承受压力,修复问题。而等到功能特性截止夜才提交的代码,如果性能衰减20%,开发人员就只能以后再处理了。
- 代码发生变化,性能也会随之而变。这个道理与测试全应用(以及可能有的模块测试)相同:堆内存的使用情况会改变,代码编译也会改变,等等。
开发过程中无论有多少困难,频繁的性能测试仍然很重要,即便有时候不能立刻解决问题。比如代码性能衰减了5%,随着开发的推进,开发人员或许可以采用以下措施:如果他的代码依赖有待集成的功能特性,那就等该功能可用时,再稍微调整代码,性能衰减的问题或许就解决了。这是合理的情况,即便这意味着性能测试不得不几个星期都伴随着5%的性能衰减(这是不幸的事,却又无法避免,还可能掩盖了其他问题)。
另一方面,如果性能衰减只有等架构更改才能修复的话,那就最好在其余代码开始依赖新代码实现之前,尽早捕获和解决它。这是一种平衡,需要仔细分析。
遵循以下准则,可以使得尽早频繁测试变得最有用:
自动化一切
所有的性能测试都应该脚本化(或者程序化,虽然脚本更简单)。全部环境都必须通过脚本安装和配置新代码(创建数据库连接、建立用户账号等),然后用脚本运行测试集。所谓自动化,还不止这些:脚本必须能够多次运行测试,对结果进行t检验分析,并能生成置信度报告,说明统计结果是相同,还是不同,如果不同,相差多少。
在测试运行前,必须通过自动化技术确保机器处于已知状态:必须检查是否有不希望运行的进程,操作系统配置是否正确,等等。只有每轮运行时保持相同的环境,性能测试才是可重复的。自动化过程中必须考虑这点。
测试一切
必须自动收集能想象到的每一点数据,以便进行后续分析。这些数据包括整个运行过程中采集的系统信息:CPU使用率、磁盘使用率、网络使用率和内存使用率等。数据还包括应用的日志——应用产生的日志,以及垃圾收集器的日志。理想情况下,还应该包括JFR记录的信息,或者对系统影响较小的性能分析(profiling)信息,周期性线程堆栈,以及其他堆分析数据,例如直方图或者全堆的转储信息(尤其是全堆转储,需要占用大量空间,没有必要长期保留)。
如果适用的话,监控信息还必须包括系统其他部分的数据:例如,如果程序使用数据库,就应该包括数据库机器的系统统计数据,以及所有的数据库诊断输出(包括Oracle的Automatic Workload Repository [AWR]这样的性能报告)。
这些数据可以指导所有未被覆盖的回归分析。如果CPU使用率上升,就需要参考性能分析信息,弄清楚是什么花费了这么多时间。如果GC时间变长,就该查阅堆性能分析信息,搞明白是什么消耗了这么多内存。如果CPU和GC时间都减少,某些地方的竞争可能降低了性能:栈数据可以指示特定的同步瓶颈,JFR记录可用来发现应用的延迟,数据库日志也可以发现数据库竞争加剧的线索。
当发现性能衰减源时,需要进一步巡查,找到更多可用数据,更多可以追踪的线索,发生性能衰减的未必是JVM。测量一切,从而确保分析的正确性。
在真实系统上运行
在单核笔记本上运行测试,与在256线程SPARC CPU机器上有很大的不同。从线程效应上来说,原因很清楚:机器规模越大,同时能运行的线程就越多,从而能减少应用线程对CPU的竞争。与此同时,大规模系统也会遇到小型笔记本上会被忽略的同步性能瓶颈。
还有其他重大的性能差异,即便乍一眼看上去不那么明显。许多重要的性能调优标志,它们的默认值是基于JVM运行的底层硬件系统计算出来的。平台和平台之间所编译出来的代码也不同。缓存——软件缓存以及更重要的硬件缓存——在不同系统和不同负载下也是不一样的,等等。
因此,除非在预期的负载和预期的硬件下测试,否则永远无法在测试中完全了解特定生产环境下的性能。可以在配置较低的硬件上运行规模较小的测试,以此来模拟和外推。在现实测试中,复制生产环境相当困难或昂贵。但外推只是简单的预测,即便在最好的情况下,预测也可能是错的。大规模系统远不只是将各部分加起来那么简单,没有什么测试能够代替在真实系统上的负载了。
快速小结
- 虽然频繁的性能测试很重要,但并非毫无代价,在日常的开发周期中需要仔细斟酌。
- 自动化测试系统可以收集所有机器和程序的全部统计数据,这可以为查找性能衰减问题提供必不可少的线索。
小结
性能测试包括各种权衡。面对诸多相互制约的选择,能否做出适当的决策,对于系统性能能否提升至关重要。
性能测试应该先测哪部分,与经验和直觉息息相关。微基准测试在这方面的作用最小,它的用途仅限于为某些操作设立宽泛的指导。这为其他测试留下广泛的施展空间,从小模块的测试到大规模多层的应用环境。所有这些测试都有某方面的价值,如何选择就得依靠经验和直觉了。不过最终部署到生产环境中之后,除了全应用测试,就没什么可选了。只有到那时才能理解所有与性能相关的问题以及全部影响。
与此类似,哪些代码导致或没导致性能衰减,并不总是皂白分明的。程序时不时会表现出随机行为,而一旦引入了随机性,就再也无法100%确定这些数据意味着什么了。使用统计分析有助于使结果变得更客观,但即便如此,仍然免不了主观臆断。理解这些数据背后的概率及其意义,有助于降低主观性。
Java性能调优工具箱
操作系统的工具和分析
实际上性能分析的起点与Java无关:它是一组操作系统自带的基本监控工具。在基于Unix的系统上,有sar(System Accounting Report)及其组成工具,例如vmstat、iostat、prstat等。在Windows上,有图形化资源监视器以及像typeperf这样的命令行工具。
无论何时运行性能测试,都应该收集操作系统的数据,至少需要收集CPU、内存和磁盘使用率的信息。 如果程序使用网络,还应该收集网络使用率。如果是自动化性能测试,还需要依靠命令行工具(即使是Windows系统)。不过,即便可以通过交互方式进行测试,也最好用命令行工具捕获输出,而不是一边盯着GUI,一边琢磨它的意思。在分析的时候可以再次将这些输出图形化。
CPU使用率
先看CPU的监控,以及监控所能告诉我们的关于Java程序的信息。通常CPU使用率可以分为两类:用户态时间和系统态时间(Windows上被称作privileged time)。用户态时间是CPU执行应用代码所占时间的百分比,而系统态时间则是CPU执行内核代码所占时间的百分比。系统态时间与应用相关,比如应用执行I/O操作,系统就会执行内核代码从磁盘读取文件,或者将缓冲数据发送到网络,等等。任何使用底层系统资源的操作,都会导致应用占用更多的系统态时间。
性能调优的目的是,在尽可能短的时间内让CPU使用率尽可能地高。这听起来有点不合常理,先来考虑一下,CPU使用率到底反映了什么。
首先需要注意的是,CPU使用率是一段时间内的平均数——5秒、30秒,也可能只有1秒那么短(不过永远不会比这还要短)。比如,10分钟内一个程序执行的CPU使用率为50%。如果代码调优之后,CPU使用率达到了100%,说明程序的性能翻了倍:程序只需要执行5分钟就可以了。如果性能再翻倍,CPU仍将是100%,而执行完程序只要2.5分钟。CPU使用率表示程序以多高的效率使用CPU,所以数字越大,性能越好。
如果在Linux桌面系统上运行vmstat 1,可以得到类似如下的几行信息(每隔一秒显示一行):
为了便于说明问题,这个运行示例程序只有一个活跃线程。不过即便有多个线程,也可以应用如下概念。
每秒内,CPU被占用450毫秒(42%的时间执行用户代码,3%的时间执行系统代码)。相应地,CPU空闲550毫秒。CPU空闲可能有以下原因。
- 应用被同步原语阻塞,直至锁释放才能继续执行。
- 应用在等待某些东西,例如数据库调用所返回的响应。
- 应用的确是无所事事。
前面2种情况通常都可用来识别某些问题。如果竞争降低,或优化数据库使之发送响应更快,程序运行都能变得更快,平均CPU使用率也会上升(当然,得假定没有其他继续阻塞应用的问题)。
第3点则常常使人疑惑。如果应用有事情做(而不是因为等待锁或者其他资源而无事可干),CPU就会分配一些周期执行应用代码。这是一般性原则,并不只针对Java。比如,包含无限循环的简单脚本。这段脚本执行时,将消耗100%的CPU。以下的Windows批处理任务就是这么干的:
ECHO OFF
:BEGIN
ECHO LOOPING
GOTO BEGIN
REM We never get here…
ECHO DONE
如果这段脚本没有消耗100%CPU,意味着,操作系统还有些事可做——它可以打印一行L00PING——却选择了空闲。这种情况下,空闲并没有什么好处,如果我们正在进行一些有用(耗时)的计算,那么迫使CPU周期性空闲只会使我们得到响应的时间变得更长。
如果在单CPU机器上运行上述脚本,多数时候不会注意到它的运行。不过一旦开启新程序,或者测量其他程序的运行时间,就能看到影响了。操作系统擅长为争用CPU周期的程序分配时间片,但新程序可用的CPU变少了,它也就运行得更慢。所以基于这种经验,人们有时会认为,在其他程序可能需要CPU周期时预留一些空闲周期,没准是个好主意。
但操作系统无法猜到你接下来想做什么,所以(默认情况下)它会尽可能执行一切而不是让CPU空闲。
限制程序所用的CPU
尽可能利用CPU周期运行程序可以使程序性能最大化。不过有时并不希望如此。比如,运行`SETI@home',`
它将消耗机器所有可用的CPU周期。这在不干活的时候没事,上网冲浪或者写文档的时候也没事,否则就会降低生产率。
操作系统有许多机制可用来人为限定程序所使用的CPU,如果有程序需要使用CPU,它就会退出空闲周期。进程的优
先级也可以改变,所以那些后台任务既不会与你想运行的程序争用CPU,也不会让CPU处于空闲状态。,
`SETI@home`可以配置优先级,除非允许,否则它不会真的占用机器的所有空余周期。
Java和单CPU的使用率
Java应用——CPU周期性空闲意味着什么?这依赖于应用的类型。如果应用代码是批处理类型,工作量固定,应该永远都不会看到CPU空闲,因为这意味着没事可做。提高CPU使用率,一直都是批处理任务的目的,因为任务会很快完成。如果CPU已经达到100%,仍然可以寻找优化,使得工作完成的更快(也要尽量保持100%CPU使用率)。
如果测试接收请求的服务器应用,就可能出现因无事可做而出现的空闲:例如,Web服务器已经处理完所有未完成的HTTP请求,正在等待下一个请求的时候。这就引入了平均时间。上述vmstat的示例来自一个每秒接收一个请求的应用服务器。应用服务器花450毫秒处理请求——意思是CPU被100%占用450毫秒,550毫秒没有占用。这就是所报告的CPU被占用45%。
虽然经常因为CPU占用发生的时间粒度很小而难以可视化,但运行负载型应用时CPU的行为就是这种爆发式的。如果CPU每半秒收到一个请求而平均处理时间为225毫秒,也能从宏观层面看到同样的模式。CPU被占用225毫秒,空闲275毫秒,再占用225毫秒,空闲275毫秒:平均来看,被占用45%,空闲55%。
如果应用优化之后每个请求只需要400毫秒,整体CPU使用率就会减少(到40%)。这是仅有的降低CPU使用率有意义的情况——当系统负载量固定并且应用不受外部资源限制的时候。另一方面,优化也使系统可以承担更多负载,最终提高CPU使用率。微观来看,这种情况下的优化仍然是使CPU使用率在短时间内(执行请求花费400毫秒)变为100%——只是CPU峰值持续的时间很短,事实上,大多数工具都不会将其标记为100%。
Java和多CPU的使用率
上面的例子是假定在单个CPU上运行的单线程,但概念与一般情况下多CPU多线程相同。多线程倾向于以有趣的方式平均使用CPU。但一般来说,多CPU多线程的目的仍然是通过不阻塞线程来提高CPU使用率,或者是在线程完成工作等待更多任务时降低CPU使用率。
另外在多线程多CPU下,需要重点考虑以下CPU空闲的情形:即便有事可做,CPU仍然空闲。这在程序没有更多线程可用的时候可能会出现。典型的情况是,应用以固定尺寸的线程池运行各种任务。每个线程同时只能执行一个任务,当线程被某个任务阻塞时(例如,等待数据库的响应),它就没法捡出新任务执行了。所以此时的情况就是,有任务需要执行(有事可做),却没有线程执行它们,结果就是CPU处在空闲时间。
在这个例子中,应该增加线程池的大小。然而,不要假设所有的空闲都是因为CPU可用,从而增加线程池以完成更多工作。程序得不到CPU周期,还有可能是由于前面提到的两个原因——锁或者外部资源的瓶颈。很重要的一点就是,在决定采取行动前,需要搞清楚为什么程序得不到CPU。
检查CPU使用率是弄清楚应用性能的第一步,但它的用途不仅如此:它可以查看代码的CPU使用是否与预期的一样,或者可以指出一些同步或资源问题。