性能测试的第2条原则是多角度审视应用性能。应该测量哪个指标取决于对应用最重要的因素。
测量应用性能的最简单方法是,看它完成任务花了多少时间,例如接收10000只股票25年的历史价格并计算标准差,生成某公司50000名雇员的薪酬福利报表,以及执行1000000次循环的时间等。
在非Java的世界,可以很直接地测试流逝时间:应用记下时间点从而测量执行时间。但在Java世界中,由于即时编译(JIT),这种方法就会有些问题了。 其中的重点是,虚拟机会花几分钟(或更长时间)全面优化代码并以最高性能执行。由于这个(以及其他)原因,研究Java的性能优化就要密切注意代码优化的热身期:大多数时候,应该在运行代码执行足够长时间,已经编译并优化之后再测量性能。
其他影响应用热身的因素
通常认为的应用热身,指的就是等待编译器优化运行代码,不过基于代码的运行时长还有其他一些影响性能的因素。
例如,JPA通常都会缓存从数据库读取的数据,由于这些数据可以从缓存中获取而不需要到数据库,所以通常再次使
用时,操作就会变得更快。与此类似,应用程序读文件时,操作系统就会将文件载入内存。随后再次读取相同文件就
会变得更快,这是因为数据已经驻留在计算机主内存中,并不需要从磁盘实际读取。
一般来说,应用热身过程中有许多地方会缓存数据,虽然并不都那么明显
。
另一方面,许多情况下应用从开始到结束的整体性能更为重要。报告生成器处理10000个数据元素需要花费大量时间,但对最终用户而言,处理前5000个元素是否比后5000个慢50%并不重要。即便是像应用服务器这样的系统——其性能必定会随运行时间而改善——初始的性能依然很重要。某种配置下的应用服务器需要45分钟才能达到性能峰值。对于在这段时间访问应用的用户来说,热身期的性能就很重要。基于这些理由,许多例子都是面向批处理的(即便这看起来有些不寻常)。
吞吐量测试是基于一段时间内所能完成的工作量。虽然最常见的吞吐量测试是服务器处理客户端产生的数据,但这并非绝对的:单个独立运行的应用也可以像测量流逝时间一样测量吞吐量。
在客户端-服务器的吞吐量测试中,并不考虑客户端的思考时间。客户端向服务器发送请求,当它收到响应时,立刻发送新的请求。持续这样的过程,等到测试结束时,客户端会报告它所完成的操作总量。客户端常常有多个线程在处理,所以吞吐量就是所有客户端所完成的操作总量。通常这个数字就是每秒完成的操作量,而不是测量期间的总操作量。这个指标常常被称作每秒事务数(TPS)、每秒请求数(RPS)或每秒操作次数(OPS)。
所有的客户端-服务器测试都存在风险,即客户端不能足够快地向服务器发送数据。这可能是由于客户端机器的CPU不足以支持所需数量的客户端线程,也可能是因为客户端需要花大量时间处理响应才能发送新的请求。在这些场景中,测试衡量的其实是客户端性能而不是服务器性能,这并不是原本的目的。
其中的风险依赖于每个线程所承载的工作量(客户端机器的线程数和配置)。由于客户端线程需要执行大量工作,零思考时间(面向吞吐量)测试更可能会遇到这种情形。因此,通常吞吐量测试比响应时间测试的线程数少,线程负载也小。
通常吞吐量测试也会报告请求的平均响应时间。这是重要的信息,但它的变化并不表示性能有问题,除非报告的吞吐量相同。能够承受500 OPS、响应时间0.5秒的服务器,它的性能要好过响应时间0.3秒但只有400 OPS的服务器。
吞吐量测试总是在合适的热身期之后进行,特别是因为所测量的东西并不固定。
响应时间测试
最后一个常用的测试指标是响应时间:从客户端发送请求至收到响应之间的流逝时间。
响应时间测试和吞吐量测试(假设后者是基于客户端-服务器模式)之间的差别是,响应时间测试中的客户端线程会在操作之间休眠一段时间。这被称为思考时间。响应时间测试是尽量模拟用户行为:用户在浏览器输入URL,用一些时间阅读返回的网页,然后点击页面上的链接,花一些时间阅读返回的网页,等等。
当测试中引入思考时间时,吞吐量就固定了:指定数量的客户端,在给定思考时间下总是得到相同的TPS(少许差别)。基于这点,测量请求的响应时间就变得重要了:服务器的效率取决于它响应固定负载有多快。
思考时间和吞吐量
有两种方法可以测试客户端包括思考时间时的吞吐量。最简单的方法就是客户端在请求之间休眠一段时间。
while(!done){
time = executeOperation();
Thread.currentThread().sleep(30 * 1000);
}
这种情况下,吞吐量一定程度上依赖响应时间。如果响应时间是1秒,就意味着客户端每31秒发送一个请求,产生的吞吐量就是0.032 OPS。如果响应时间是2秒,客户端就是每32秒发送一个请求,吞吐量就是0.031OPS。
另外一种方法是周期时间(Cycle Time)。周期时间设置请求之间的总时间为30秒,所以客户端休眠的时间依赖于响应时间:
while(!done){
time = executeoperation();
Thread.currentThread().sleep(30 * 1000 - time);
}
无论响应时间是多少,这种方法都会产生固定的吞吐量,每个客户端0.033 OPS(假设本例中的响应时间都少于30秒)。
测试工具中的思考时间时常有变,平均值为特定值,但会加入随机变化以更好地模拟用户行为。另外,线程调度从来不会严格实时,所以客户端请求之间的时间也会略有不同。
因此,即便工具提供周期时间而不是思考时间,测试所报告的吞吐量也相差无几。但是,如果吞吐量远超预期,说明测试中一定有什么出错了。
衡量响应时间有两种方法。响应时间可以报告为平均值:请求时间的总和除以请求数。响应时间也可以报告为百分位请求,例如第90百分位响应时间。如果90%的请求响应小于1.5秒,且10%的请求响应不小于1.5秒,则1.5秒就是第90百分位响应时间。
两种方法的一个区别在于,平均值会受离群值影响。这是因为计算平均值时包括了离群值。离群值越大,对平均响应时间的影响就会越大。
下图展示了20个请求,它们响应时间的范围比较典型。响应时间是从1到5秒。平均响应时间(平行且靠近x轴的粗线)为2.35秒,且90%的请求发生在4秒或4秒以内(平行且远离x轴的粗线)。
对于行为正常的测试来说,这是常见的场景。如果是下图所示的数据,离群值会影响分析的准确性。
这组数据中包括一个很大的离群值:有个请求花费了100秒。结果第90百分位响应时间和平均响应时间的粗线就调换了位置。平均响应时间蹿到了5.95秒,而第90百分位响应时间为1.0秒。对于这样的案例,应该考虑减少离群值带来的影响(从而降低平均响应时间)。
一般来说,像这样的离群值很少见,不过由于GC(垃圾收集)引入的停顿,Java应用更容易发生这种情况。(并不是因为GC引入了100秒的延迟,而是尤其是对于有较小的平均响应时间的测试来说,GC停顿会引入较大的离群值。) 性能测试中通常关注的是第90百分位响应时间(有时是第95百分位或第99百分位响应时间,这里说第90百分位并没有什么神奇之处)。如果只能盯住一个数字,那最好选择基于百分位数的响应时间,因为它的减少会让大多数用户受益。最好一并考虑平均响应时间和至少一种百分位响应时间,这样就不会错过有很大离群值的场景了。
快速小结
第3条原则是性能测试的结果会随时间而变。即便程序每次处理的数据集都相同,产生的结果也仍然会有差别。因为有很多因素会影响程序的运行,如机器上的后台进程,网络时不时的拥堵等。好的基准测试不会每次都处理相同的数据集,而是会在测试中制造一些随机行为以模拟真实的世界。这就会带来一个问题:运行结果之间的差别,到底是因为性能有变化,还是因为测试的随机性。
可用以下方法来解决这个问题,即多次运行测试,然后对结果求平均。当被测代码发生变化时,就再多次运行测试,对结果求平均,然后比较两个平均值。但事情并没有想象中那么简单。要想弄清楚测试间的差别是真实的性能变化还是随机变化并不容易——这就是性能调优的关键所在。
比较基准测试的结果时,不可能知道平均值的差异是真的性能有差还是随机涨落。最好的办法是先假设“平均值是一样的”,然后确定该命题为真时的几率。如果命题很大几率为假,就有信心认为平均值是真的有差别(虽然永远无法100%肯定)。
像这种因代码更改而进行的测试称为回归测试。在回归测试中,原先的代码称为基线(baseline),而新的代码称为试样(specimen)。考虑一个批处理程序的案例,基线和试样都执行3次,下表给出了所用的时间。
试样的平均值表明代码性能改善了25%。这个测试所反映出来的25%的改善,可以相信多少?试样中3个有2个的值小于基线平均值,看起来改进的步子很大——但是分析这些结果就会得出结论,试样和基线性能相同的概率有43%。观察到的这些数字说明,两组测试的基本性能在43%的时间内是相同的,因此性能不相同的时间只占57%。57%的时间内性能不相同和性能改善25%也完全不是一回事,稍后。
上述概率看起来和预期有差别,其原因是测试的结果变化很大。一般来说,结果数据差别越大,就越难判断平均值之间的差异是真实的差别还是随机变动。此处的数字43%,是学生t检验(Student’s t-test,以下称t检验)得出的结果,这是一种针对一组数据及其变化的统计分析。“学生”是首次发表该检验的科学家⁵的笔名。t
检验计算出的p
值,是指原假设(null hypothesis)成立时的概率。
回归测试中的原假设是指假设两组测试的性能一样。这个例子中的p值大约为43%,意思是相信这两组測试平均值相同的概率为43%。相反,相信平均值不同的概率为57%。57%意味着什么,两组測试的平均值不相同?严格来讲,这并不意味着我们相信性能改善25%的概率有57%——它只是意味着,相信结果不同的概率为57%。性能可能改善了25%,也可能125%,甚至试样的实际性能也许比基线还糟糕。最大的可能则是测量出来的差别就是接近于真实的差异(特别是隨着p值下降越是如此),只是永远无法肯定这点。
统计学及其语义
正确表述`t`检验结果的语句应該像这样:试样与基线有差别的可能性为57%,差别预计最大有25%。
t
检验通常与α
值一起使用,α
值是一个点,如果结果达到这个点那就是统计显著性(statistical significance)。通常α
值设置为0.1——意思是说,如果试样和基线只在10%(0.1)的时间里相同(或反过来讲,90%的时间里试样和基线有差异),那结果就被认为是统计显著。其他常用的α值还有0.05(置信度为95%)或0.01(置信度为99%)。如果測试的p值小于1-α值,则被认为是统计显著。
因此,查找代码性能变化的正确方法是先决定一个显著性水平—比如0.1—然后用t
检验判定在这个显著性水平上试样是否与基线有差别。在这个例子中,p值为0.43,在置信度为90%的情况下不能说有显著性差异,而结果表示平均值不相同。事实上,测试没有显著性差异并不意味着结果无关緊要,它仅仅表示这个測试没法形成定论。
从统计学上说,测试不能得出定论通常是因为样本数据不足。迄今为止,示例所考虑的基线和试样各是3次迭代。再加3次迭代,结果会变成这样:基线的迭代结果分别是1、1.2和0.8秒,试样的迭代结果分别是0.5、1.25和0.5秒?随着数据的增加,p值就从0.43跌落到了0.19,这意味着结果有差异的概率从57%上升到了81%。运行更多测试迭代,再加3个数据点后,概率则增加到了91%——超过了常规的统计显著性水平。
运行更多测试迭代从而达到某个统计显著性水平的方法并不总是可行。严格来说,这么做也没有必要。实际上,用以判定统计显著性的α值可以任意选择,虽然通常选的都是普遍认可的值。置信度为90%时,p值0.11不是统计显著,但置信度为89%时,它就是统计显著了。
这里得到结论:回归测试并不是非黑即白的科学。对于一组数据,不经过统计分析就没法弄清楚数字的含义,也就没法进行比较和判断。此外,由于概率的不确定性,即便用统计分析也不能给出完全可靠的结果。性能调优工程师的工作就是:考虑一堆数据(或者他们的均值)、弄清各种概率、决定往哪使力。
快速小结
t
检验来比较测试结果,实现上述目的。t
检验可以告知变动存在的概率,却无法告诉哪种变动该忽略,而哪种该追查。如何在两者之间找到平衡,是性能调优工程的艺术魅力所在。