2.2 原则2:理解批处理流逝时间、吞吐量和响应时间
性能测试的第 2 条原则是多角度审视应用性能。应该测量哪个指标取决于对应用最重要的因素。
2.2.1 批处理流逝时间
测量应用性能的最简单方法是,看它完成任务花了多少时间,例如接收 10 000 只股票 25 年的历史价格并计算标准差,生成某公司 50 000 名雇员的薪酬福利报表,以及执行 1 000 000 次循环的时间等。
在非 Java 的世界,可以很直接地测试流逝时间:应用记下时间点从而测量执行时间。但在 Java 世界中,由于即时编译(JIT),这种方法就会有些问题了。第 4 章描述了这个过程,其中的重点是,虚拟机会花几分钟(或更长时间)全面优化代码并以最高性能执行。由于这个(以及其他)原因,研究 Java 的性能优化就要密切注意代码优化的热身期:大多数时候,应该在运行代码执行足够长时间,已经编译并优化之后再测量性能。
其他影响应用热身的因素
通常认为的应用热身,指的就是等待编译器优化运行代码,不过基于代码的运行时长还有其他一些影响性能的因素。
例如,JPA 通常都会缓存从数据库读取的数据(参见第 11 章),由于这些数据可以从缓存中获取而不需要长途跋涉到数据库,所以通常再次使用时,操作就会变得更快。与此类似,应用程序读文件时,操作系统就会将文件载入内存。随后再次读取相同文件就会变得更快,这是因为数据已经驻留在计算机主内存中,并不需要从磁盘实际读取。
一般来说,应用热身过程中有许多地方会缓存数据,虽然并不都那么明显。
另一方面,许多情况下应用从开始到结束的整体性能更为重要。报告生成器处理 10 000 个数据元素需要花费大量时间,但对最终用户而言,处理前 5000 个元素是否比后 5000 个慢 50% 并不重要。即便是像应用服务器这样的系统——其性能必定会随运行时间而改善——初始的性能依然很重要。某种配置下的应用服务器需要 45 分钟才能达到性能峰值。对于在这段时间访问应用的用户来说,热身期的性能就很重要了。
基于这些理由,本书的许多例子都是面向批处理的(即便这看起来有些不寻常)。
2.2.2 吞吐量测试
吞吐量测试是基于一段时间内所能完成的工作量。虽然最常见的吞吐量测试是服务器处理客户端产生的数据,但这并非绝对的:单个独立运行的应用也可以像测量流逝时间一样测量吞吐量。
在客户端 - 服务器的吞吐量测试中,并不考虑客户端的思考时间。客户端向服务器发送请求,当它收到响应时,立刻发送新的请求。持续这样的过程,等到测试结束时,客户端会报告它所完成的操作总量。客户端常常有多个线程在处理,所以吞吐量就是所有客户端所完成的操作总量。通常这个数字就是每秒完成的操作量,而不是测量期间的总操作量。这个指标常常被称作每秒事务数(TPS)、每秒请求数(RPS)或每秒操作次数(OPS)。
所有的客户端 - 服务器测试都存在风险,即客户端不能足够快地向服务器发送数据。这可能是由于客户端机器的 CPU 不足以支持所需数量的客户端线程,也可能是因为客户端需要花大量时间处理响应才能发送新的请求。在这些场景中,测试衡量的其实是客户端性能而不是服务器性能,这并不是我们的目的。
其中的风险依赖于每个线程所承载的工作量(客户端机器的线程数和配置)。由于客户端线程需要执行大量工作,零思考时间(面向吞吐量)测试更可能会遇到这种情形。因此,通常吞吐量测试比响应时间测试的线程数少,线程负载也小。
通常吞吐量测试也会报告请求的平均响应时间。这是重要的信息,但它的变化并不表示性能有问题,除非报告的吞吐量相同。能够承受 500 OPS、响应时间 0.5 秒的服务器,它的性能要好过响应时间 0.3 秒但只有 400 OPS 的服务器。
吞吐量测试总是在合适的热身期之后进行,特别是因为所测量的东西并不固定。
2.2.3 响应时间测试
最后一个常用的测试指标是响应时间:从客户端发送请求至收到响应之间的流逝时间。
响应时间测试和吞吐量测试(假设后者是基于客户端 - 服务器模式)之间的差别是,响应时间测试中的客户端线程会在操作之间休眠一段时间。这被称为思考时间。响应时间测试是尽量模拟用户行为:用户在浏览器输入 URL,用一些时间阅读返回的网页,然后点击页面上的链接,花一些时间阅读返回的网页,等等。
当测试中引入思考时间时,吞吐量就固定了:指定数量的客户端,在给定思考时间下总是得到相同的 TPS(少许差别,参见框注)。基于这点,测量请求的响应时间就变得重要了:服务器的效率取决于它响应固定负载有多快。
思考时间和吞吐量
有两种方法可以测试客户端包括思考时间时的吞吐量。最简单的方法就是客户端在请求之间休眠一段时间。
while (!done) {time = executeOperation();Thread.currentThread().sleep(301000);}这种情况下,吞吐量一定程度上依赖响应时间。如果响应时间是 1 秒,就意味着客户端每 31 秒发送一个请求,产生的吞吐量就是 0.032 OPS。如果响应时间是 2 秒,客户端就是每 32 秒发送一个请求,吞吐量就是 0.031 OPS。
另外一种方法是周期时间(Cycle Time)。周期时间设置请求之间的总时间为 30 秒,所以客户端休眠的时间依赖于响应时间:
while (!done) {time = executeOperation();Thread.currentThread().sleep(301000 - time);}无论响应时间是多少,这种方法都会产生固定的吞吐量,每个客户端 0.033 OPS(假设本例中的响应时间都少于 30 秒)。
测试工具中的思考时间时常有变,平均值为特定值,但会加入随机变化以更好地模拟用户行为。另外,线程调度从来不会严格实时,所以客户端请求之间的时间也会略有不同。
因此,即便工具提供周期时间而不是思考时间,测试所报告的吞吐量也相差无几。但是,如果吞吐量远超预期,说明测试中一定有什么出错了。
衡量响应时间有两种方法。响应时间可以报告为平均值:请求时间的总和除以请求数。响应时间也可以报告为百分位请求,例如第 90 百分位响应时间。如果 90% 的请求响应小于 1.5 秒,且 10% 的请求响应不小于 1.5 秒,则 1.5 秒就是第 90 百分位响应时间。
两种方法的一个区别在于,平均值会受离群值影响。这是因为计算平均值时包括了离群值。离群值越大,对平均响应时间的影响就会越大。
图 2-2 展示了 20 个请求,它们响应时间的范围比较典型。响应时间是从 1 到 5 秒。平均响应时间(平行且靠近 x 轴的粗线)为 2.35 秒,且 90% 的请求发生在 4 秒或 4 秒以内(平行且远离 x 轴的粗线)。

图 2-2:一组典型的响应时间
对于行为正常的测试来说,这是常见的场景。离群值会影响分析的准确性,就像图 2-3 显示的数据那样。

图 2-3:一组含有离群值的响应时间
这组数据中包括一个很大的离群值:有个请求花费了 100 秒。结果第 90 百分位响应时间和平均响应时间的粗线就调换了位置。平均响应时间蹿到了 5.95 秒,而第 90 百分位响应时间为 1.0 秒。对于这样的案例,应该考虑减少离群值带来的影响(从而降低平均响应时间)。
一般来说,像这样的离群值很少见,不过由于 GC(垃圾收集)引入的停顿,Java 应用更容易发生这种情况。(并不是因为 GC 引入了 100 秒的延迟,而是尤其是对于有较小的平均响应时间的测试来说,GC 停顿会引入较大的离群值。)性能测试中通常关注的是第 90 百分位响应时间(有时是第 95 百分位或第 99 百分位响应时间,这里说第 90 百分位并没有什么神奇之处)。如果你只能盯住一个数字,那最好选择基于百分位数的响应时间,因为它的减少会让大多数用户受益。不过,最好一并考虑平均响应时间和至少一种百分位响应时间,你就不会错过有很大离群值的场景了。
负载生成器
有许多开源和商业的负载生成器。本书以 Faban(http://faban.org/)为例,这是一个开源的、基于 Java 的负载生成器。Faban 带有一个简单程序(fhb),可用来测试简单 URL 的性能:
% fhb -W 1000 -r 300/300/60 -c 25 http://host:port/StockServlet?stock=SDOops/sec: 8.247% errors: 0.0avg. time: 0.022max time: 0.04590th %: 0.03095th %: 0.03599th %: 0.035这个测试例子中有 25 个客户端(
-c 25)向 StockServlet 发送请求(股票代码 SDO),每个请求的周期时间为 1 秒(-W 1000)。-r 300/300/60表示,基准测试的热身期为 5 分钟(300 秒),接下来是 5 分钟测试周期和 1 分钟减速期。测试之后,fhb报告该测试的 OPS 和各种响应时间(由于包括思考时间,响应时间就成为重要的度量,而 OPS 则在不断变化)。只要替换有限的几个参数,
fhb就可以处理POST数据,用少量脚本就可以处理多个 URL。对于更为复杂的测试来说,Faban 提供了很有用的 Java 框架来定义基准测试负载生成器。4
4fhb 的命令行,请参见 http://faban.org/1.2/docs/man/fhb.html。——译者注
快速小结
1. Java 性能测试中很少使用面向批处理的测试(或者任何没有热身期的测试),但这种测试可以产生很有价值的结果。
2. 其他可以测量吞吐量或响应时间的测试,则依赖负载是否以固定的速率加载(也就是说,基于模拟的客户端思考时间)。
