绝大数的开发人员在平常工做过程当中都会或多或少的碰见过性能问题,本文旨在阐述性能测试的理论,从而为性能分析和开发人员作指导。本文对于那些刚刚接触性能调优和正在解决问题的开发人员也能提供一些启发性的思路。java
10 评论 算法
计算机软件做为人类智慧的结晶,帮助咱们在这个突飞猛进的社会中完成了大量工做。我 们的平常生活中已经离不开软件,玲琅满目的软件已经渗透到了咱们生活的各个角落,令咱们应接不暇。咱们都但愿软件变得更好,运行处理的速度更快,在当今硬 件性能日新月异的变革中,软件性能的提高也是一个永不落伍的话题。软件性能测试的实质,是从哲学的角度看问题,找出其内在联系,因果关系,形式内容关系, 重叠关系等等。假如这些关系咱们在分析过程当中理清了,那么性能测试问题就会变得迎刃而解。架构
在软件开发过程当中,性能测试每每在开发前期容易被 忽略。直到有一天问题暴露后,开发人员被迫的直面这个问题,大多数状况下,这是令开发人员感受到很是痛苦事情。因此在软件开发前期以及开发过程当中性能测试 的考量是必要的,那么具有相应理论知识和实践方法也是一个优秀工程师所应当具有的素养,这里咱们归纳有四项原则,这些原则能够帮助开发人员丰富、充实测试 理论,系统的开展性能测试工做,从而得到更有价值的结果。dom
第一个原则就是性能测试只有在实际项目中实施才是有意义的,这样才使得测试工做具备针对性,并且目标会更加明确。这个原则中有三个类别的基准能够指导开发人员度量性能测试的结果,可是每一种方法都有它的优势和劣势,咱们将结合实际例子,来总结阐述。
微 观基准,能够理解为在某一个方法或某一个组件中进行的单元性能测试。好比检测一个线程同步和一个非线程同步的方法运行时所须要的时间。或者对比建立一个单 独线程和使用一个线程池的性能开销。或者对比执行一个算法中的某一个迭代过程所须要的时间。当咱们遇到这些状况时,咱们经常会选择作一个方法层面的性能测 试。这些状况的性能测试,均可以尝试使用微观基准的方法进行性能测试。微观基准看似编写起来简单快捷,可是编写可以准确反映性能问题的代码并不是一件易事。 接下来经过例子让咱们从代码中发现一些问题。这是一个单线程的程序片断,经过计算 50 次循环迭代来检测执行方法所耗费的时间体现性能差别:
public void doTest() { double l; long then = System.currentTimeMillis(); int nLoops = 50; for (int i = 0; i < nLoops; i++) { l = compute(50); } long now = System.currentTimeMillis(); System.out.println("Elapsed time:" + (now - then)); } private double compute(int n){ if (n < 0) throw new IllegalArgumentException("Must be > 0"); if (n == 0) return 0d; if (n == 1) return 1d; double d = compute(n - 2) + compute(n - 1); if (Double.isInfinite(d)) throw new ArithmeticException("Overflow"); return d; }
执行这段代码咱们会发现一个问题,那就是执行时间只有短短的几秒。难道果然是程序性能很高?答案并不是如此,其实在整个执行过程当中 compute 计算方法并无调用而是被编译器自动忽略了。那么解决这个问题的办法是将 double 类型的“l”换成 volatile 实例变量。这样可以确保每个计算后所获得的结果是能够被记录下来,用 volatile 修饰的变量,线程在每次使用变量的时候,都会读取变量修改后的最后的值。
要 特别值得注意的是,当考虑为多线程写一个微基准性能测试用例时,假如几个线程同时执行一小段业务逻辑代码,这可能会引起潜在的线程同步所带来的性能开销和 瓶颈。此时微观微基准测试的结果每每引导开发人员为了保持同步进行不断的优化,这样会浪费不少时间,对于解决更紧迫的性能问题,这样作就显得得不偿失。
咱们再试想这样一个例子,微基准测试两个线程调用同步方法的状况,由于基准代码很小,那么测试用例大部分时间将消耗在同步过程当中。即便微基准测试在总体的同步过程当中只占 50%,那么两个线程尝试执行同步方法的概率也是至关高的。基准运行将会很是缓慢,添加额外的线程会形成更大的性能问题。
基 于微观基准的测试过程当中,是不能含有额外的对性能产生影响的操做,咱们知道执行 compute(1000) 和 compute(1) 在性能上是有很大差别的,假如咱们的目标是对比两个不一样实现方法之间的性能差别,那么就应当考虑一系列的输入测试值做为前提,传递给测试目标,参数就须要 多样化。这里以咱们的经验解决的办法就是使用随机值:
for (int i = 0; i < nLoops; i++) { l = compute(random.nextInt()); }
如今,产生随机数的时间也包含在了整个循环执行过程当中,所以测试结果中包含了随机数生成所须要的时间,这并不能客观的体现 compute 方法真实的性能。因此在构建微观基准时,输入的测试值必须是预先准备好的,且不会对性能测试产生额外的影响。正确的作法以下:
public void doTest() { double l; int nLoops = 10; Random random = new Random(); int[] input = new int[nLoops]; for (int i = 0; i < nLoops; i++) { input[i] = random.nextInt(); } long then = System.currentTimeMillis(); for (int i = 0; i < nLoops; i++) { try { l = compute(input[i]); } catch (IllegalArgumentException iae) { } } long now = System.currentTimeMillis(); System.out.println("Elapsed time:" + (now - then)); }
微观基准中输入的测试值必须是符合业务逻辑的。全部的输入的值并不必定会被代码用到,实际的业务可能对输入的数据有特定 的要求,不合理的输入值可能致使代码在执行过程当中就抛出异常而中断,从而使得咱们难以判断代码执行的效率。因此在准备测试数据的时候应当考虑到输入数据的 有效性,保证代码执行的完整性。好比下面的例子输入的参数若是是大于 1476 ,执行会当即中断,从而影响了真实性能结果的产生。
public double ImplSlow(int n) { if (n < 0) throw new IllegalArgumentException("Must be > 0"); if (n > 1476) throw new ArithmeticException("Must be < 1476"); return verySlowImpl(n); }
一般状况下,对参与到实际业务计算的值提早检测对提高性能是有帮助的,可是假如用户大多数输入的值是合理的,那么提早检查数据的有效性就显得冗余了。因此编写核心逻辑代码的时候,咱们建议只针对通常状况作处理,保证执行的效率的高效性。假设访问一个 collection 对象时,每一次可以节省几毫秒的话,那么在屡次的访问状况下就会对性能的提高产生重大的意义。
public class Test1 { private volatile double l; private int nLoops; private int[] input; private Test1(int n) { nLoops = n; input = new int[nLoops]; Random random = new Random(); for (int i = 0; i < nLoops; i++) { input[i] = random.nextInt(50); } } public void doTest(boolean isWarmup) { long then = System.currentTimeMillis(); for (int i = 0; i < nLoops; i++) { try { l = compute(input[i]); } catch (IllegalArgumentException iae) { } if (!isWarmup) { long now = System.currentTimeMillis(); System.out.println("Elapsed time:" + (now - then)); } } } private double compute(int n) { if (n < 0) throw new IllegalArgumentException("Must be > 0"); if (n == 0) return 0d; if (n == 1) return 1d; double d = compute(n - 2) + compute(n - 1); if (Double.isInfinite(d)) throw new ArithmeticException("Overflow"); return d; } public static void main(String[] args) { // TODO Auto-generated method stub Test1 test1 = new Test1(Integer.parseInt("10");)); test1.doTest(true); test1.doTest(false); } }
总得说来,微观基准做用是有限的,在频繁调用的方法中使用微观基准的度量方法会帮助咱们检测代码的性能,若是用在不会被频繁调用的方法中是不合适的,应当考虑其它方法。
宏观基准,当咱们测量应用程序性能时,应当纵览整个系统,影响应用程序性能的缘由多是多方面的,不能片面的认为性能瓶颈只会在程序自己上。经过下面这个例子咱们将探讨离开宏观基准的性能测试是不可能找到影响应用程序性能真正的瓶颈。
上 图数据来自客户实体,触发应用程序的核心业务计算方法,该方法从数据库加载数据,并传导给核心业务中的计算方法,获得结果保存到数据库,最终响应客户的请 求。每一个图形中的数字分别表明了这个模块所能处理客户请求的数量。核心业务模块的优化多数状况是受限于业务的要求。假设咱们优化这些核心模块,使其能够处 理 200 RPS 时,咱们发现加载数据的模块依然只能处理 100 RPS,也就是说整个系统的吞吐能力其实仍然为 100 RPS,最终对应用程序总体的性能提高是没有任何帮助的。从这个例子咱们得知,咱们花费再多的精力在核心业务上的优化意义并不大,咱们应当从总体运行状况 来看,发现真正影响性能的瓶颈来解决问题,这就是宏观基准原则的意义。
折 衷基准,相比微观基准和宏观基准,一个单独功能模块的性能测试,或者一系列特定操做的性能测试被称为折衷基准。它是介于微观基准和宏观基准之间的折衷方 案。基于微观基准测试的正确性是较难把握的,性能瓶颈的判断毫不能仅仅依赖于此。若是咱们要使用微观基准做为性能的测量方法,那么不妨在此以前先尝试基于 宏观基准的测试。它能够帮助咱们了解系统以及代码是如何工做的,从而造成一个系统总体逻辑结构图。接下来能够考虑基于折衷基准的测试,来真正发现潜在的性 能瓶颈。须要明确的是折衷基准的测试方法并非完整应用程序测试的替代方法,更多状况下咱们认为它更适用于一个功能模块的自动测试。
性能测试中的第二个重要的原则是引入多样的测量方法来分析程序的性能。
批 量执行所用时间的测量方法(耗时法),这是种简单而快速有效的方法,经过测量完成特定任务所消耗的时间来测量总体性能。可是须要特别注意,假如所测试的应 用程序中使用缓存数据技术来为了得到更好的性能表现时,屡次循环使用该方法可能没法彻底反应性能问题。那么能够尝试在初始状态开始时应用耗时法作一次性能 的评估,而后当缓存创建后,再次尝试此方法。
吞吐量的测量方法,在一段时间内考察完成任务的数量的能力,被称为吞吐量测 量方法。在测试客户服务器的应用程序时,吞吐量的测量意味着客户端发送请求到服务器是没有任何延迟的,当客户端接收到响应后,应当当即发出新的请求,直到 最终结束,统计客户端完成任务的总数。这种相对理想的测试方法一般称之为“Zero-think-time”。但是一般状况下,客户端可能会有多个线程作 同一件事情,吞吐量则意味着每秒钟内全部客户端的操做数,而不是测量的某一个时段内的全部操做总数。这种测量常常称为每秒事务/(TPS),每秒请求 (RPS),或每秒操做数 (OPS)。
测 试全部基于客户端和服务器端应用程序都存在一种风险,客户端不能以足够快的速度发送数据到服务器端,这种状况的发生多是因为客户端此时没有足够的 CPU 资源去运行须要数量的线程,或者客户端必须耗用更长的时间来处理当前的请求。这种状况下,实际上测量的是客户端的性能,而非服务器的性能,与吞吐量测量方 法是背道而驰的。其实这种风险是由每一个客户端线程处理任务的数量和硬件配置决定的。“Zero-think-time”在吞吐量测试中可能常常会碰见以上 的状况,因为每一个客户端线程都须要处理大量的任务,所以吞吐量测试一般被应用于较少的客户端线程程序。吞吐量测量方法也一样适应用于带有缓存技术的应用程 序,尤为是当测试的数据是一个并不固定的状况下。
响应时间的测量方 法,响应时间的测量方法是指客户端发出一个请求后直到接收到服务器的响应返回后的时间消耗。响应时间测量方法不一样于吞吐量测量方法,在响应时间测试过程 中,客户端线程可能会在操做的过程当中某一时刻休眠,这就引出“think- time”这个关键词,当“think- time”被引入到测试过程当中,也就是意味着待处理任务量是固定的,测量的是服务器响应请求的速率是怎样的。大多数状况下,响应时间的测量方法用来模拟用 户真实操做,从而测量应用程序的性能。
性能测试的第三个原则是理解测试结果如何随时间改变,即便每一次测试使用一样的数据,可能得到的结果也是不一样的。一些客观因素,好比后台运行的进程,网络的负载状况,这些均可能带来测试结果的不一样,因此在测试过程当中存在着一些随机性的因素。这就产生了一个问题: 当比较两次运行获得的测试结果时,它们之间的差别是由回归测试产生的,仍是是随机变化而致使的呢?
咱们不能简单的经过测量屡次运行回归测试的平均结果来评判性能的差别。这时咱们可使用统计分析的方法,假设两种状况的平均值是同样的,而后经过几率来判断这样的假设是成立的。若是假设不成立,那么就说明有很高的几率证实平均数存在差别。
在回归测试中原始代码被视为基线,新增长的代码称为样本。三次运行基线和样本,产生时间如表 1:
次数 | 基准 | 样本 |
---|---|---|
1 |
1.0 |
0.5 |
2 |
0.8 |
1.25 |
3 |
1.2 |
0.5 |
平均 |
1 |
0.75 |
看起来样本的平均值显示有 25%的提高,可事实证实样本和基线有相同性能的几率是 43%。也就是说 57%的几率存在性能上的不一样。43%是基于 T 检验所获得的结果,T 检验主要用于样本含量较小(例如 n<30),整体标准差σ未知的正态分布资料。t 检验是用 t 分布理论来推论差别发生的几率,从而比较两个平均数的差别是否显著。它与 z 检验、卡方检验并列。如今的 T 检验结果告诉咱们这样一个信息::57%几率显示样本和基线存在性能差别,差别最大值是 25%。也能够理解为性能差有 57%的置信度向理想发现发展,结果有 25%的改善。
在考量回归测试的结果时,离开了统计分析的方法,而只关注平均值来作出判断,含糊的理解这些数字的含义是不可取的。性能工程师的工做是看数据,理解这些几率,基于全部可用的数据肯定在何处花时间。
第 四个原则就是工程师应该视性能测试是整个开发过程必要的部分,尽早进行性能测试,常常进行性能的测试,是一个好的工程师应该作到的。在代码提交到代码库之 前,就应当作性能测试,由于性能问题也会致使回归测试失败。因此提前发现问题会提升整个项目的质量,减少交付的风险性。
在一个典型的项目开发周期过程当中,项目计划经常是创建一个功能提交的时间表,全部功能的开发必需要在某一个时间点所有提交到代码库中,在项目发布以前,全部的精力都致力于解决功能上的 Bug,那么颇有可能在这个过程当中发现性能问题,这会致使两个问题产生:
开发人员在时间的约束下不得不提交代码以知足时间表,一旦发现出严重的性能问题他们会很是畏惧,因此开发人员在测试开始的早期解决性能问题可以产生 1%的回归测试代价,而若是开发人员一直在等待晚上的冻结功能开发的时候才开始检查代码将会致使 20%的回归测试的代价。
任何为解决性能作出的修改都有可能带来巨大的成本,有时不只仅是代码的修改,更有多是软件架构的修改。因此最好在软件设计之时就充分的考虑到将来可能带来的性能问题。
尽早测试性能有如下四点可做为指导:
提前准备测试用户以及测试环境的设计和建立;
性能测试应该考虑尽可能用脚原本完成;
经过性能监控工具尽可能收集有可能获得的运行信息,为未来分析提供便利;
必定要在一个能真实模拟多数用户的机器环境下进行性能测试。
最后,基于咱们讲过的方法做为基础,构建一个自动化的测试系统来收集测试过程当中产生的各类信息,可以很好的帮助咱们分析发现性能瓶颈。