1系统性能定义
性能测试,主要是通过自动化的测试工具模拟多种正常、峰值以及异常负载条件来对系统的各项性能指标进行测试。系统性能主要包括两个值:
吞吐量(Throughtput),即每秒钟可以处理的请求数,事务数。
系统延迟(Latency),也就是系统在处理一个请求或一个事务时的延迟。
它们两者之间的关系:
Throughput越大,Latency会越差。请求量过大,系统繁忙,响应速度自然低。
Latency越好,能支持的Throughput就会越高。因为Latency短说明处理速度快,于是单位时间内就可以处理更多的请求。
2系统性能测试
要测试系统的性能,需要我们收集系统的Throughput和Latency这两个值。
首先,需要定义Latency这个值,如对于网站首页响应时间必需是4秒以内(根据不同的业务来定义)
其次,准备性能测试工具,一个工具用来制造高强度的Throughput(通常有Loadrunner、Jmeter等),另一个工具用来测量Latency。关于如何测量Latency,你可以在代码中测量,但是这样会影响程序的执行,而且只能测试到程序内部的Latency,真正的Latency是整个系统都算上,包括操作系统和网络的延时,一般性能测试工具都有配套组件。
最后,开始性能测试。不断地提升测试环境的Throughput,然后观察系统的负载情况,如果系统顶得住,那就观察Latency的值。这样,就可以找到系统的最大负载,并且你可以知道系统的响应延时是多少。
下面主要通过三个大的步骤来说明性能测试实施过程:
前期准备
1、确定用户、业务、系统需求(目标)
- 确定实际业务需求
主要确定用户的业务请求分布等:主要业务请求、平均日交易量、年交易量、峰值交易量等等。
- 确定系统需求
主要工作是分析系统的性能需求、确定合理的性能目标。
- 确定客户的需求和期望
在需求分析文档的支持下,对软件系统上的用户业务使用情况进行分析,提出我们所关注的性能测试需求,并告知业务人员。让业务人员来判断我们的性能需求是否满足客户的真实需求。
2、确定系统类别
分清系统类别是我们掌握什么样技术的前提,掌握相应技术做性能测试才可能成功。例如:系统类别是B/S结构,需要掌握HTTP协议,java,C#,html等技术。或者是C/S结构,可能要了解操作系统,winsock,com等。
3、确定系统构成
不同的系统构成性能测试会得到不同的结果。
4、确定实际网络带宽
便于测试时对带宽做模拟,尽可能真实的反馈带宽使用情况。
5、确定测试服务器与测试机配置清单
了解性能测试硬件资源(包括所测服务器,测试机等),根据实际情况添加设备。
6、系统功能流程图
便于测试人员分析系统哪些模块易出现瓶颈,从而针对性做性能测试。
7、测试时间评估
根据测试时间,制定相应的测试执行策略。
在我的实际性能测试工作中,会把上面作为问题列表的形式打印出来,然后通过不断沟通和分析去完善它,以便帮助我后期更好的制定性能测试策略。这样的问题列表如:
- 具体哪些业务需要做性能测试?
- 从测试类型来看(列出性能测试类型、对应的解释及指标,方便调查对象参考),要对性能关键业务做以上一项或多项测试?
- 采用什么系统部署环境?系统的版本和位数是多少?
- ……
测试实施
1、根据前期准备制定编写性能测试方案
应该包括性能测试通过的标准、所测的对象(业务或场景)、测试环境说明、测试所用时间及资源,测试策略等。
2、设计性能测试用例
指导测试人员进行性能测试。
3、编写性能测试脚本
测试脚本应该具有可重复执行性,可并发执行性,能尽可能真实模拟单用户业务使用场景。
4、模拟运行场景
用性能工具模拟用户负载使用系统的场景。
5、配置运行场景
被测环境应该在测试前做好备份;配置包括被测系统日志、中间件(MySQL、Apache等)使用情况、服务器资源使用情况的数据收据;运行应避免偶然性事件,所以一个测试场景应该至少运行两次及以上。
瓶颈分析和性能优化
瓶颈分析难点在于理解收集到的数据反馈的真是含义,所以要求测试人员具备相应的技术知识储备(包括数据库的知识、计算机原理知识、网络基础、各中间件性能计量含义等),这里先仅列举下性能测试结果分析原则:
- 把事实与推测分开,总是用实际的证据来证明你的推测;
- 在没有足够证据前,不对程序进行优化;
- 优先验证简单的假设(推测瓶颈);
- 日志文件中没有错误不代表真的没有错误;
- 从系统到应用、从外到内进行层层剥离,缩小范围;
- 范围缩小后,再分割成多个单元,对每个单元进行轮番压力测试,来证明或者否定是哪个单元引起的性能问题 。
瓶颈优化应该有难到易的进行优化,下面为推荐(应根据公司实际情况调整):
服务器硬件瓶颈->网络瓶颈->服务器操作系统瓶颈->中间件瓶颈->应用瓶颈
3定位性能瓶颈
3.1查看操作系统负载
系统有问题首要需要看的是操作系统报告。查看操作系统的CPU利用率,内存使用率,操作系统的IO,还有网络的IO,网络链接数等。
- 先看CPU利用率,如果CPU利用率不高,但是系统的Throughput和Latency上不去了,这说明程序并没有忙于计算,而是忙做其它事,比如IO。(另外,CPU的利用率还要看内核态的和用户态的,内核态的上去了,整个系统的性能就下来了。而对于多核CPU来说,CPU 0 是相当关键的,如果CPU 0的负载高,那么会影响其它核的性能,因为CPU各核间是需要有调度的,这靠CPU0完成)
- 然后,我们可以查看一下IO大小,IO和CPU一般是反着来的,CPU利用率高则IO不大,IO大则CPU就小。关于IO,我们要看三个事,一个是磁盘文件IO,一个是驱动程序的IO(如:网卡),一个是内存换页率。这三个事都会影响系统性能。
- 然后,查看一下网络带宽使用情况。
- 如果CPU不高,IO不高,内存使用不高,网络带宽使用不高。但是系统的性能上不去。这说明你的程序有问题,比如,你的程序被阻塞了。可能是因为等那个锁,可能是因为等某个资源,或者是在切换上下文。
通过了解操作系统的性能,我们才知道性能的问题,比如:带宽不够,内存不够,TCP缓冲区不够,等等,很多时候,不需要调整程序的,只需要调整一下硬件或操作系统的配置就可以了。
3.2使用profiler测试程序瓶颈
使用某个Profiler来查看一下我们程序的运行性能。如:Java的JProfiler。使用Profiler工具,可以让你查看程序中各个模块函数甚至指令的很多东西,如:运行的时间 ,调用的次数,CPU的利用率,等等。
我们重点观察运行时间最多,调用次数最多的那些函数和指令。这里注意一下,对于调用次数多但是时间很短的函数,你可能只需要轻微优化一下,你的性能就上去了。
因为Profiler会让你的程序运行的性能变低对此,一般有两个方法来定位系统瓶颈:
1)在你的代码中自己做统计,使用微秒级的计时器和函数调用计算器,每隔10秒把统计log到文件中。
2)分段注释你的代码块,让一些函数空转,做Hard Code的Mock,然后再测试一下系统的Throughput和Latency是否有质的变化,如果有,那么被注释的函数就是性能瓶颈,再在这个函数体内注释代码,直到找到最耗性能的语句。
查看汇编代码经常会给你一些意想不到的东西让你知道为什么程序的性能是那样。
对于性能测试,不同的Throughput会出现不同的测试结果,不同的测试数据也会有不同的测试结果。所以,用于性能测试的数据非常重要,性能测试中,我们需要测试观察不同Throughput的结果。
4性能调优(代码技术细节层面)
一般来说,性能优化也就是下面的几个策略:
- 用空间换时间。各种cache如CPU L1/L2/RAM到硬盘,都是用空间来换时间的策略。这样策略基本上是把计算的过程一步一步的保存或缓存下来,这样就不用每次用的时候都要再计算一遍,比如数据缓冲,CDN等。这样的策略还表现为冗余数据,比如数据镜象,负载均衡什么的。
- 用时间换空间。有时候,少量的空间可能性能会更好,比如网络传输,如果有一些压缩数据的算法(如 “Huffman 编码压缩算法” 和 “rsync 的核心算法”),这样的算法其实很耗时,但是因为瓶颈在网络传输,所以用时间来换空间反而能省时间。
- 简化代码。最高效的程序就是不执行任何代码的程序,所以,代码越少性能就越高。如:减少循环的层数,减少递归,在循环中少声明变量,少做分配和释放内存的操作,尽量把循环体内的表达式抽到循环外,条件表达的中的多个条件判断的次序,尽量在程序启动时把一些东西准备好,注意函数调用的开销(栈上的开销),注意面向对象语言中临时对象的开销,小心使用异常(不要用异常来检查一些可接受可忽略并经常发生的错误),…… 等等,这些东西需要我们非常了解编程语言和常用的库。
- 并行处理。如果CPU只有一个核,你要玩多进程,多线程,对于计算密集型的软件会反而更慢(因为操作系统调度和切换开销很大),CPU的核多了才能真正体现出多进程多线程的优势。并行处理需要我们的程序有Scalability,不能水平或垂直扩展的程序无法进行并行处理。从架构上来说,这表再为——是否可以做到不改代码只是加加机器就可以完成性能提升?
总之,根据2:8原则来说,20%的代码耗了你80%的性能,找到那20%的代码,你就可以优化那80%的性能。 下面为一些最有价值的性能调优的的方法,供参考。
4.1算法调优
算法非常重要,好的算法会有更好的性能。下面为几个算法调优例子:
- 一个是过滤算法,系统需要对收到的请求做过滤,我们把可以被filter in/out的东西配置在了一个文件中,原有的过滤算法是遍历过滤配置,后来我们找到了一种方法可以对这个过滤配置进行排序,这样就可以用二分折半的方法来过滤,系统性能增加了50%。
- 一个是哈希算法。计算哈希算法的函数并不高效,一方面是计算太费时,另一方面是碰撞太高,碰撞高了就跟单向链表一个性能。我们知道,算法都是和需要处理的数据很有关系的,就算是被大家所嘲笑的“冒泡排序”在某些情况下(大多数数据是排好序的)其效率会高于所有的排序算法。哈希算法也一样,广为人知的哈希算法都是用英文字典做测试,但是我们的业务在数据有其特殊性,所以,对于还需要根据自己的数据来挑选适合的哈希算法。
- 分而治之和预处理。以前有一个程序为了生成月报表,每次都需要计算很长的时间,有时候需要花将近一整天的时间。于是我们把我们找到了一种方法可以把这个算法发成增量式的,也就是说我每天都把当天的数据计算好了后和前一天的报表合并,这样可以大大的节省计算时间,每天的数据计算量只需要20分钟,但是如果我要算整个月的,系统则需要10个小时以上(SQL语句在大数据量面前性能成级数性下降)。这种分而治之的思路在大数据面前对性能有很帮助,就像merge排序一样。SQL语句和数据库的性能优化也是这一策略,如:使用嵌套式的Select而不是笛卡尔积的Select,使用视图,等等。
4.2代码调优
代码上的调优大致有下面这几点:
- 字符串操作。这是最费系统性能的事了,无论是strcpy, strcat还是strlen,最需要注意的是字符串匹配。所以,能用整型最好用整型。
- 多线程调优。有人说thread is evil,这个对于系统性能在某些时候是个问题。因为多线程瓶颈就在于互斥和同步的锁上,以及线程上下文切换的成本,怎么样的少用锁或不用锁是根本(比如:多版本并发控制(MVCC)在分布式系统中的应用 中说的乐观锁可以解决性能问题),此外,还有读写锁也可以解决大多数是读操作的并发的性能问题。另外,线程不是越多越好,线程间的调度和上下文切换也是很夸张的事,尽可能的在一个线程里干,尽可能的不要同步线程。这会让你有很多的性能。
- 内存分配。不要小看程序的内存分配。malloc/realloc/calloc这样的系统调用非常耗时,尤其是当内存出现碎片的时候。池化技术,如线程池,连接池等。池化技术对于一些短作业来说(如http服务)相当的有效。这项技术可以减少链接建立,线程创建的开销,从而提高性能。
- 异步操作。我们知道Unix下的文件操作是有block和non-block的方式的,像有些系统调用也是block式的,如:Socket下的select,Windows下的WaitforObject之类的,如果我们的程序是同步操作,那么会非常影响性能,我们可以改成异步的,但是改成异步的方式会让你的程序变复杂。异步方式一般要通过队列,要注意队列的性能问题,另外,异步下的状态通知通常是个问题,比如消息事件通知方式,有callback方式,等,这些方式同样可能会影响你的性能。但是通常来说,异步操作会让性能的吞吐率有很大提升(Throughput),但是会牺牲系统的响应时间(latency)。这需要业务上支持。
- 语言和代码库。我们要熟悉语言以及所使用的函数库或类库的性能。比如:STL中的很多容器分配了内存后,那怕你删除元素,内存也不会回收,其会造成内存泄露的假像,并可能造成内存碎片问题。再如,STL某些容器的size()==0 和 empty()是不一样的,因为,size()是O(n)复杂度,empty()是O(1)的复杂度,这个要小心。如Java中的JVM调优需要使用的这些参数:-Xms -Xmx -Xmn -XX:SurvivorRatio -XX:MaxTenuringThreshold,还需要注意JVM的GC,GC的威力大家都知道,尤其是full GC(还整理内存碎片),他运行的时候,整个世界的时间都停止了。
4.3网络调优
下面只讲一些概念上的东西。