大话性能测试:JMeter实战

978-7-115-57398-8
作者: 胡通
译者:
编辑: 孙喆思

图书目录:

详情

本书以业界开源性能测试工具JMeter为依托,结合真实的工作实践,用通俗易懂的语言层层深入讲解性能测试技能。全书共分为5章和6个附录,第1章讲解性能测试基础知识,包括性能测试的整体知识体系、必备基础知识和通用标准等;第2章讲解初级性能测试技能,包括JMeter九大核心组件的使用、测试脚本的编写等;第3章讲解中级性能测试技能,包括如何扩展JMeter的功能插件、搭建性能自动化和实时可视化平台等;第4章讲解高级性能测试技能,包括Dubbo的扩展测试、中间件的基准测试、JMeter源码的解析等;第5章通过实例剖析3种典型性能测试场景;附录部分扩展讲解实用性能知识,包括典型性能问题和解决方法、性能参数调优、问题定位和优化建议等。 本书适合对性能测试有入门、进阶学习需求的测试人员,也适合对性能知识有学习需求的开发人员、运维人员等相关技术人员。

图书摘要

版权信息

书名:大话性能测试:JMeter实战

ISBN:978-7-115-57398-8

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。


编  著 胡 通

责任编辑 刘雅思

人民邮电出版社出版发行  北京市丰台区成寿寺路11号

邮编 100164  电子邮件 315@ptpress.com.cn

网址 http://www.ptpress.com.cn

读者服务热线:(010)81055410

反盗版热线:(010)81055315


本书以业界开源性能测试工具JMeter为依托,结合真实的工作实践,用通俗易懂的语言层层深入讲解性能测试技能。全书共分为5章和6个附录,第1章讲解性能测试基础知识,包括性能测试的整体知识体系、必备基础知识和通用标准等;第2章讲解初级性能测试技能,包括JMeter九大核心组件的使用、测试脚本的编写等;第3章讲解中级性能测试技能,包括如何扩展JMeter的功能插件、搭建性能自动化和实时可视化平台等;第4章讲解高级性能测试技能,包括Dubbo的扩展测试、中间件的基准测试、JMeter源码的解析等;第5章通过实例剖析3种典型性能测试场景;附录部分扩展讲解实用性能知识,包括典型性能问题和解决方法、性能参数调优、问题定位和优化建议等。

本书适合对性能测试有入门、进阶学习需求的测试人员,也适合对性能知识有学习需求的开发人员、运维人员等相关技术人员。


如今互联网飞速发展,各个公司产品比拼的不仅仅是功能齐全,更多的是体验流畅。在每日成千上万用户的访问下,用户还能拥有非常好的产品使用体验,这对于进一步增加老用户黏性和吸引新用户至关重要。性能测试是提前暴露产品问题,保障线上运营稳定的重要手段。不安于现状,努力自我增值是每个互联网人应有的品质。从事业务功能测试的工程师如果仅仅专注于业务,那么其职业生涯的发展瓶颈或困惑可能马上就会到来,而性能测试是测试行业里颇具技术含量的领域,也是广大测试工程师进一步提升自己知识技能的方向。我自硕士毕业以来,一直从事一线的性能测试工作,也是从新手真刀真枪一步步地实践成长过来的。2018年9月,我开通了微信公众号“大话性能”,开始编写原创技术文章,分享工作经验和方法,其间不断有测试同行希望我推荐性能测试的书籍。另外,我一直推崇“知识的流通才能创造其价值”的理念,于是有了写书的想法,特写此书,把个人多年的性能测试实战经验毫无保留地分享出来,让更多读者能够更加全面、系统地学习性能测试。

虽然过了而立之年,我还会继续更新技术文章,让更多人受益,少走弯路,这对我自己也是一种成长。众所周知,性能测试涉及的知识面广而深,不是掌握一个工具就能成为性能测试专家的,希望本书能给各位读者在成为专家的路上提供帮助。

本书结合我自己从初入职场到成为职场老手的学习成长经历和多年的一线JMeter实战真经,内容具有以下特点。

(1)在系统性上,本书解决了网上性能测试相关知识碎片化和片面化的问题,系统地梳理了性能测试领域知识,技能树齐全。

(2)在延展性上,本书从JMeter自带组件、第三方扩展和代码定制等多个维度扩展了工具使用的深度,满足读者的个性化需求。

(3)在实操性上,本书绝非“纸上谈兵”,而是融技术讲解于项目实战,并梳理常见典型问题和解决方法,让读者学以致用。

不论是否有性能测试基础、是否了解性能测试,也不论是否为性能测试老手,本书对各层次的读者都有所帮助,包括但不限于:

读者可借鉴本书的实战内容,掌握性能测试的完整过程并应用于实际工作。本书的实战内容包括需求分析、测试脚本开发、环境部署、测试数据构造、测试执行、问题定位和优化技能等,让读者真正能够胜任性能测试工作。本书共分为5章和6个附录。

第 1 章全面、细致地讲解性能测试相关的技能知识图谱和必备知识,帮助测试新手从全局了解性能测试,重点传授使用阿里开源工具TProfiler定位代码耗时、使用淘宝的开源工具OrzDBA诊断MySQL的异常、编写自定义Shell脚本监控服务器、通过Java代码快速构建千万数据等实战真经,可借鉴性强、实用性高。

第 2 章主要讲解在实际工作中使用最频繁的九大 JMeter 核心组件,帮助测试新手快速掌握JMeter,从而独立承担起初级脚本的编写和测试工作。

第 3 章讲解JMeter分布式压测的方法,然后提炼BeanShell的10个实战应用示例,接着介绍JMeter的自定义扩展方法和WebSocket相关组件,最后提供自动化的测试方案,并结合代码和业界常用的开源工具搭建可视化的性能测试平台,可以帮助小型互联网公司快速应用。

第 4 章讲解常用分布式架构Dubbo的扩展测试、百万TCP长连接的验证测试、消息中间件ActiveMQ和缓存中间件Redis的基准测试,并解读JMeter源码,供有兴趣的读者深入学习。

第 5 章逐一分析日常项目迭代型、方案对比型、MQTT开源协议共3种典型的性能测试场景,进一步提升读者解决实际性能问题的能力,让读者能够独立承担起具有特殊需求的性能测试。最后,本章剖析实战中3例典型问题的定位分析过程。

附录部分依次梳理常见性能测试问题和分析解决方法、性能参数调优、Java代码定位和优化建议、MySQL定位和优化建议、JVM定位和优化建议、Cookie和Session的关系等实用性能专题内容,供广大读者学习。

本书的配套资源包含性能测试报告模板、常见中间件搭建必知必会、利用Maven构建私有jar包等干货内容,读者可在异步社区本书页面下载。

感谢公司、部门给予的广阔成长空间,感谢部门领导和同事的悉心指导。

感谢人民邮电出版社的大力支持,尤其要感谢张涛和孙喆思两位编辑,他们在我写作本书期间提供了不少好的建议和帮助。

最后,谨以此书献给“大话性能”微信公众号的广大粉丝和测试同仁,希望我们一起学习、成长、进步!


本书由异步社区出品,社区(https://www.epubit.com/)为您提供相关资源和后续服务。

本书提供性能测试报告模板、常见中间件搭建方法等配套学习资料,请在异步社区本书页面中单击,跳转到下载界面,按提示进行操作即可。注意:为保证购书读者的权益,该操作会给出相关提示,要求输入提取码进行验证。

作者和编辑尽最大努力来确保书中内容的准确性,但难免会存在疏漏。欢迎您将发现的问题反馈给我们,帮助我们提升图书的质量。

当您发现错误时,请登录异步社区,按书名搜索,进入本书页面,单击“提交勘误”,输入勘误信息,单击“提交”按钮即可。本书的作者和编辑会对您提交的勘误进行审核,确认并接受后,您将获赠异步社区的100积分。积分可用于在异步社区兑换优惠券、样书或奖品。

本书责任编辑的联系邮箱是liuyasi@ptpress.com.cn。

如果您对本书有任何疑问或建议,请您发邮件给我们,并请在邮件标题中注明本书书名,以便我们更高效地做出反馈。

如果您有兴趣出版图书、录制教学视频或者参与技术审校等工作,可以直接发邮件给本书的责任编辑。

如果您来自学校、培训机构或企业,想批量购买本书或异步社区出版的其他图书,也可以发邮件给我们。

如果您在网上发现有针对异步社区出品图书的各种形式的盗版行为,包括对图书全部或部分内容的非授权传播,请您将怀疑有侵权行为的链接通过邮件发给我们。您的这一举动是对作者权益的保护,也是我们持续为您提供有价值的内容的动力之源。

“异步社区”是人民邮电出版社旗下IT专业图书社区,致力于出版精品IT图书和相关学习产品,为作译者提供优质出版服务。异步社区创办于2015年8月,提供大量精品IT图书和电子书,以及高品质技术文章和视频课程。更多详情请访问异步社区官网https://www.epubit.com。

“异步图书”是由异步社区编辑团队策划出版的精品IT专业图书的品牌,依托于人民邮电出版社近30年的计算机图书出版积累和专业编辑团队,相关图书在封面上印有异步图书的LOGO。异步图书的出版领域包括软件开发、大数据、AI、测试、前端和网络技术等。

异步社区

微信服务号


如今,互联网电商、短视频、网络社交和支付等应用正深深地改变着人们的生活模式,成万上亿的用户每日都会使用这些应用来满足生活需求,在节假日或者特殊活动期间,应用的访问量便会呈几何级数上涨。为了保证在如此大的访问量下,用户还能拥有非常好的产品使用体验,工程师们就需要预先进行大规模的性能测试工作,以确保提前发现和解决问题。性能测试可以说是互联网行业评估平台能够支撑的访问量级、保障产品的使用流畅度、维持用户黏性的一把利刃。本章不仅会介绍性能测试基础理论知识,而且会更多地讲述作者在5年多的性能测试工作成长中,思考提炼出的对实际测试工作具有指导意义的工具、脚本和流程等实战内容,接下来我们就正式开始性能测试学习之旅。

本节主要讲解性能测试的入门基础知识。首先,介绍一些重要的性能测试基本概念;然后,从整体勾勒出提升性能测试人员能力的专项技能知识图谱;最后,简要描述新手快速入门性能测试的实战思考,让新手在全局上直观认识性能测试。

相信不管是技术人员还是非技术人员,不知道“天猫双11购物狂欢节”的人应该极少。2019年,“天猫双11购物狂欢节”开启后96秒成交额破百亿元人民币,24小时内总成交额达2684亿元人民币,创造了交易创建峰值54.4万笔每秒的历史记录。从2014年到2019年,随着技术不断更新迭代,订单量峰值也是不断创新高,如图1-1所示。“天猫双11购物狂欢节”已经不仅是购物节,更是商业和技术的奥林匹克运动会,在不断追求着更高、更快、更强。

除了电商大促,大家司空见惯的还有新浪微博热搜、微信热点事件等典型高流量场景。在热点事件发生时应用稳定运行的背后,是很多技术工程师团队花费了无数个日夜的迭代测试。其中,性能测试发挥着不可估量的作用,因此其意义和重要性可见一斑。

图1-1 2014~2019年阿里平台“天猫双11购物狂欢节”交易创建峰值(数据摘自网易科技)

性能测试不同于功能测试,性能测试需要面面俱到,考虑到每一个需求细节,一般来说性能测试更加关注系统的性能表现。概括来说,可以将性能测试的目的划分为5类:能力验证、规划能力、性能调优、缺陷发现和基准比较。

1.能力验证

我们经常会听到这样的描述“某系统能否在什么条件下具有什么能力”,这就是一个典型的能力验证问题。例如,我们为客户进行系统上线后的验收测试,或是作为第三方对一个已部署的应用的性能进行验证,这些都属于为达到这种性能测试的目的进行的测试。

该性能测试的目的具有如下特点。

(1)要求在已确定的环境下运行。

(2)需要根据典型场景设计测试方案和用例。

(3)一般采用的测试方法包括可靠性测试、压力测试和失效恢复测试等。

也就是说,它关心的是“在给定条件下,系统是否具有预期的能力表现”。

2.规划能力

通常被描述为“系统能否支持未来一段时间内的用户增长”或者是“应该如何调整系统配置,使系统能够满足增长的用户数的需要”。其实它和业界常说的容量规划大同小异,即通过性能测试和评估得到性能基线,并将其作为容量规划的一个指标,明确系统大概在什么情况下会出现瓶颈,什么时候需要进行扩容。但是以系统实际的线上观察数据作为基础会更有效,因此容量规划通过性能测试环境来模拟是不充分的,仅可作为参考。

该性能测试的目的具有如下特点。

(1)它是一种探索性的测试。

(2)它可被用于了解系统的性能以及获得扩展性能的方法。

(3)一般采用的测试方法包括负载测试、配置测试和压力测试。

也就是说,它关心的是“应该如何使系统具有我们要求的性能”或是“在某种可能发生的条件下,系统性能如何”。

3.性能调优

调优可以在多种不同的测试阶段和场合下使用。对已经部署在实际生产环境中的应用系统来说,性能调优可能会首先关注硬件环境和系统设置,例如,对服务器的调整、对数据库参数的调整,以及对应用服务器参数的调整,此时的性能调优需要在生产环境这个确定的环境下进行。但对正在开发的应用来说,性能调优会更多地关注应用逻辑的实现方法、应用中涉及的算法和数据库访问层的设计等因素,此时并不要求性能测试环境是实际的生产环境,只要整个调优过程中有一个可用于比较的基准性能测试环境即可。

该性能测试的目的具有如下特点。

(1)确定了基准环境、基准负载和基准性能指标。

(2)调整了系统运行环境和实现方法。

通常包括如下3个不同层面的调整。

需要说明的是,不要一次调过多的参数或多次修改应用实现方法,否则很难判断具体是哪个调整对系统性能产生了较为有利的影响,即通常采用单一变量实验法来进行测试验证。

(3)记录测试结果,并进行分析。循环的出口是“达到预期的性能调优目标”。

(4)一般采用的测试方法包括配置测试、负载测试、压力测试和失效恢复测试。

4.缺陷发现

日常项目迭代中,通过性能测试来发现系统中存在的缺陷和问题。

一般采用的测试方法包括并发测试、压力测试和失效恢复测试。

5.基准比较

该目的通常应用在敏捷开发过程中,即在不设定明确的性能目标的情况下,通过比较得到每次迭代中的性能表现的变化,并根据这些变化判断迭代是否达到了预期的目标。

不同系统有不同的业务特性,同一产品在不同时期,对性能测试的需求和要达成的目标也会有所不同,性能测试的目的通常包括以上5类。

 

注意

在真实测试中,性能测试的目的总是会有一些重叠。其实也不必很纠结,读者只需了解以上各类性能测试的目的即可。

 

性能测试根据目的不同又可以细分为不同的测试类型,平时工作中根据具体情况进行对应的测试。下面是最常见的性能测试分类。

(1)并发测试。用于评估当多用户并发访问同一个应用、模块、数据时是否会产生隐藏的并发问题,常用于秒杀场景,可以发现一些超买超卖、死锁等问题。

(2)压力测试。用于评估当处于或超过预期负载时系统的运行情况。压力测试的关注点在于系统在峰值负载或超出最大载荷情况下的处理能力。

(3)负载测试。通过对系统不断增加压力,测试压力或者增加压力后的持续时间,直到系统的一项或多项性能指标达到安全临界值,从而评估系统在安全阈值范围内的处理能力情况,或配合性能调优来使用。

(4)稳定性测试。若要判断系统在上线后长时间运行会不会出现性能问题、有无内存泄漏或线程泄漏,或产生其他异常,则需要进行稳定性测试。稳定性测试考察了系统在一定压力下连续运行3×24h、7×24h的状况,以确定系统足够稳定。

(5)可靠性测试。产品上线后,在运营及推广下,用户量会持续上升。某些时候因为一些运营活动会出现用户激增,导致服务器负载过高。在这种场景下保障服务正常提供且足够可靠,计算机不会运行异常,则需要进行可靠性测试。

 

提示

有些大厂,例如网易、京东,还经常会进行故障演练的测试,目的在于保证和提升系统在压力下的稳定性。通过人工制造和注入故障,了解故障发生后系统的表现,从而设计对应的保证措施,以提前验证解决方案的可行性,同时提高系统的容错率和健壮性。

容量测试是性能测试中的一种测试方法,它的目的是测量系统的最大容量,为系统扩容、性能优化提供参考,同时节省成本投入并提高资源利用率。容量测试常用于容量规划。

 

性能测试中会涉及很多性能相关的指标和术语,本节重点剖析核心的概念。

(1)在线用户,表示某个时间段内在服务器上保持登录状态的用户。但在线用户不一定是对服务器产生压力的用户,只有正在操作的活跃用户才会对服务器产生压力,在线只是一种状态。

(2)相对并发用户,类似活跃用户,表示某个时间段内与服务器保持交互的用户,理论上这些用户有同一时刻(即绝对并发)进行操作的可能(对这种可能性的度量称为并发度)。相对并发的说法主要是为了区分绝对并发。

(3)绝对并发用户,表示同一时间点(严格地说是足够短的时间段内)与服务器进行交互的用户,一般通过测试工具提供的并发控制(如JMeter的集合点)实现。

(4)思考时间,表示用户每个操作后的暂停时间,或者叫作操作之间的间隔时间,此时间内用户是不对服务器产生压力的。如果想了解系统在极端情况下的性能表现,可以设置思考时间为0;而如果要预估系统能够承受的最大压力,就应该尽可能地模拟真实思考时间。

(5)响应时间,通常包括网络传输请求的时间、服务器处理的时间,以及网络传输响应的时间。而我们重点关心的应该是服务器处理的时间,这部分受到代码处理请求的业务逻辑的影响,从中可以真正发现缺陷并对业务逻辑进行优化,而网络传输请求和响应的时间很大程度上取决于网络质量。

响应时间也就是JMeter术语中的Elapsed time,表示接收完所有响应内容的时间点减去请求开始发送的时间点。另外,Latency time表示接收到响应的第一个字节的时间点减去请求开始发送的时间点,Connection time表示建立连接所消耗的时间。

当关注响应时间时,不应该只关注平均响应时间。通常我们会采用95%的响应时间,即所有请求的响应时间按照从小到大排列,位于 95% 的响应时间。该值更有代表性,而平均响应时间未能有效地考虑波动性。

(6)TPS,指每秒处理的事务数,是直接反映系统性能的指标。该值越大,系统性能越好。通常如果一个事务包含的请求就1个,那么这个值就是每秒处理请求数。另外还有个概念叫吞吐量,它除了用于描述网络带宽能力,也指单位时间内系统处理的请求数量,JMeter聚合报告中TPS就是用该术语显示的。假如1个用户在1s内完成1笔事务,则TPS明显就是1;如果某笔业务响应时间是1ms,则1个用户在1s内能完成1000笔事务,则TPS就是1000了;如果某笔业务响应时间是1s,则1个用户在1s内只能完成1笔事务,要想TPS达到1000,则至少需要1000个用户。因此在1s内,1个用户可以完成1000笔事务,1000个用户也可以完成1000笔事务,这取决于业务响应时间。

(7)TPS波动范围。方差和标准差都是用来描述一组数据的波动性的(表现数据集中还是分散),标准差的平方就是方差。方差越大,数据的波动越大。

众所周知,性能测试依赖于特定的硬件、软件、应用服务和网络资源等,所以在性能场景执行期间,TPS可能表现为稳定,或者波动,或者遵循一定的趋势上升或下降。由此可以根据离散系数提出一个TPS波动范围的概念,并定义为TPS标准差除以TPS平均值。如果这个比值超过了一定的范围,就认为这个性能点的TPS不够稳定,也间接证明被测系统的响应波动大,不满足性能期望。

另外,从上述的术语中不难发现,TPS、并发数与响应时间之间是有一定的关系的。假设平均响应时间为t(单位为毫秒),并发量为c,每秒处理请求数为q,则q=(1000/tc就是这个关系。所以想要升高q,就只有两条路:降低t和升高c

对于降低t,只能靠优化代码方式来实现,这取决于软件工程师的编码水平或架构设计。

对于升高c,通常c与服务器程序的请求处理模型关系比较大。如果服务器程序是“一个线程对应一个请求”的模式,那么c的最大值就受制于服务器能支撑多少个线程;如果是“一个进程对应一个请求”的模式,那么c的最大值则受制于最大进程数。另外,在升高c值的过程中,不得不注意的一点是,随着线程/进程数增多,上下文切换、线程/进程调度开销会增大,这会间接地显著增大t的值,因而不能让q的值跟着c的值等比升高。所以一味增大c值通常也不会有好结果,最合适的c值应该根据实测试验得出。

 

注意

有一种特殊情况:若业务决定了该服务器提供的服务具有“数据量小、返回时间较长”的特征,即这是一个不忙但很慢的业务类型,那么可以采用NIO模式提供服务,例如Nginx就默认采用NIO模式。

在这种模式下,c值不再与线程/进程数相关,而只是与“套接字连接数”相关。通常“套接字连接数”可以非常大,在经过特殊配置的Linux服务器上,可以同时支撑百万级别的套接字连接数,在这种情况下c值可以达到100万。

在如此高的c值之下,就算t再大,也可以支撑一个很高的q,同时真正的线程/进程数可以只设置到跟CPU核数一致,以求最大化CPU利用率。当然,这一切的前提是该业务具有“数据量小、返回时间较长”的特征。

 

经过上述分析,在评定服务器的性能时,应该结合TPS和并发用户数,以TPS为主、以并发用户数为辅来衡量系统的性能。如果必须要用并发用户数来衡量,则需要一个前提——交易在多长时间内完成。因为在系统负载不高的情况下,将思考时间(思考时间的值等于交易响应时间)加到脚本中,并发用户数基本可以增加一倍,所以用并发用户数来衡量系统的性能没太大的意义。

 

提示

▪ 高并发:并发强调多任务交替执行,并发与并行是有区别的,并行是多任务同时执行。例如,一个核的CPU处理事务就是并发;多个核的CPU就会存在事务的并行处理。这里涉及的知识点包括多线程、事务和锁,设计高并发通常采用无状态、拆分、服务化、服务隔离、消息队列、数据处理和缓存等。

▪ 高可用:用系统的无故障运行时间来度量,主要作用为保证软件故障监控、数据备份和保护、系统告警、错误隔离。业务层设计包括集群、降级、限流、容错、防重和幂等。数据库设计包括分库、分表和分片等。

 

对初学者来说,培养观察与分析的思维是很重要的。图1-2为性能测试的基础曲线模型,是一个经典的压力曲线拐点图,不过在真实测试时结果不会这么理想。

其中,X轴代表并发用户数,Y轴代表资源利用率、吞吐量和响应时间。X轴与Y轴区域从左往右分别代表轻压力区、重压力区和拐点区。

图1-2 性能测试的基础曲线模型

然后,根据前面学习的性能测试的术语与指标进行理解。随着并发用户数的增加,在轻压力区的响应时间变化不大,曲线比较平缓。进入重压力区后响应时间呈现增长的趋势。最后进入拐点区后曲线倾斜率增大,响应时间急剧增加。

接着,观察吞吐量,随着并发用户数的增加,吞吐量增加。进入重压力区后吞吐量逐步平稳。到达拐点后吞吐量急剧下降,这说明系统已经达到了处理极限,有点要扛不住的感觉。同理,随着并发用户数的增加,资源利用率逐步上升,最终达到饱和状态。

最后,把所有指标融合分析。随着并发用户数的增加,吞吐量与资源利用率增加,说明系统在积极地处理事务,所以响应时间增加得并不明显,此时系统处于比较好的状态。但随着并发用户数的持续增加,压力也在持续加大,吞吐量与资源利用率都达到饱和。随后,吞吐量急剧下降,响应时间急剧上升。轻压力区与重压力区的交界点的并发用户数值是系统的最佳并发用户数,因为各种资源都利用得比较充分,响应也很快;而重压力区与拐点区的交界点的并发用户数值就是系统的最大并发用户数,超过这个点后,系统会性能急剧下降甚至崩溃。

 

提示

性能测试在寻找拐点的时候,通常采用手动方式调整并发用户数,人为地判断峰值和临界点。其实可以设计实现一个算法,采用滑动窗口的方式,自动化地寻找性能拐点,这样做效率高,大家可以动手尝试一下。

 

性能测试是一门很深的学问,是一个知识体系庞大的工程,因此想速成基本上是不可能的,需要一步步地通过实践积累经验。图1-3是性能测试由浅入深的技能知识图谱,作者在此抛砖引玉,一方面希望让大家有全局的认知,另一方面给大家提供按知识图谱去深入学习的方向,让大家有的放矢地学习。

图1-3 性能测试的技能知识图谱

作为性能测试新手,需要优先掌握哪些知识才可以开始独立开展性能测试的工作呢?答案如图1-4所示,新手需要优先掌握这5个入门核心步骤。

图1-4 新手性能测试入门

1.产品应用熟悉

首先,毫无疑问,我们需要先熟悉产品,分析并梳理出核心功能模块、复杂业务,然后对这些内容进行数据分析,量化可以测量的性能需求指标,后面的章节会单独重点讲解需求分析。接下来,明确性能测试范围和具体的性能需求指标后,我们需要进行性能测试方案设计、性能测试用例设计等一系列工作。另外,产品的部署方式和架构也是有必要去了解的,这有助于后续的性能测试环境搭建。这些准备工作做得越精细,后续的测试准确性就越高。

2.压测脚本模拟

在性能测试中,互联网行业都喜欢用开源的工具,一方面是因为其免费,另一方面因为其可扩性比较强。所以,在这里作者建议学习测试的同学优先掌握JMeter工具,本书意在分享作者系统梳理在工作中积累的JMeter常见的实战经验和一些技巧,让大家能够拿来即用,快速应用于自己的实际工作任务中。

3.测试数据构造

在压测之前,需要在数据库中准备好一定量的铺垫存量数据,有些比较复杂的数据会涉及多张表的关联关系,需要利用代码去按一定规则批量快速创建。接下来我们也会详细讲解这部分内容,并提供Python和Java两个版本的构造数据代码。

4.性能测试环境部署

为了使性能测试环境尽可能和线上环境保持一致,我们需要掌握独立部署常用中间件,以及知道每个中间件的配置信息,尤其是一些和性能息息相关的参数。所以首先我们需要学习一些Linux的常用命令,例如解压缩、查看进程和修改文件等;其次是熟悉中间件的配置信息和调优参数;最后了解一些高可用的技术和架构。

5.性能测试监控

性能脚本准备好,测试数据构造已完成,性能测试环境也有了,在开始正式执行性能测试之前,我们要先开启性能测试的监控。我们需要掌握常用的监控方法和各个指标的含义,如服务器系统层的CPU、内存和网络情况,应用层的JVM和垃圾回收(Garbage Collection,GC)情况,数据库层的SQL语句和连接池情况等。监控启动或部署后,我们就可以正式执行脚本,并观察系统的表现,根据一些异常情况或日志定位分析问题,进行调优。

至此5步,我们便可以独立地完成一些简单项目的性能测试了,新手同学可以优先掌握上面要求的内容,再在后续性能测试工作中专题式的研究其他内容,其实掌握20%的技能就可以完成80%的日常工作。

 

注意

(1)在性能测试开始前和结束后,务必要清理性能测试环境中的脏数据和构造的数据,保持性能测试环境数据的纯净有效。

(2)在环境检查时,务必确认没有涉及短信、支付、流量、推送等会引起经济损失的业务,以及引用到线上环境的地址的配置。

(3)在大规模压测开始前,务必先做好脚本单线程调试,确认好环境地址,否则如果一口气发送几百万条消息短信或者支付请求,那可不是闹着玩儿的。

 

在本节中,首先我们给出性能测试的完整工作流程,然后展开,深入浅出地讲解性能测试的需求分析、方案设计、环境搭建、数据构造、抓包分析、脚本编写、监控部署、定位分析和报告总结共9个环节。其中,我们重点分享工作中利用代码批量构造数据、自定义Shell脚本监控系统、利用阿里开源工具诊断代码、结合淘宝的OrzDBA监测MySQL,以及典型性能问题剖析等实战真经。

性能测试在项目流程中和功能测试一样,需要进行性能需求分析评审、性能测试方案评审、性能测试报告评审等几项核心工作。因为性能测试是一项系统性的工作,所以参与的人员包含产品运营人员、运维人员、测试人员、开发人员、架构师等,大家共同承担和评估性能测试工作。图1-5较为细致地阐述了测试人员主导的各个性能测试环节的工作流程,大家可以借鉴。

图1-5 性能测试完整工作流程

 

提示

(1)关于性能测试的执行时机,大家可以制定一些规则,例如按照版本号结合重要事件活动的规则。

(2)为性能测试制定的通用标准,可以因产品的形态不同动态微调。

 

性能测试最开始的需求分析工作细致与否,与后面的性能测试结果息息相关。需求分析是一个繁杂的过程,它并非我们想象的那么简单。做需求分析除了要对系统的业务非常了解,还需要有深厚的性能测试知识,这样才能够挖掘分析出真正的性能测试需求。

很多时候,性能测试的需求是比较模糊的,需要性能测试工程师去挖掘和分析。在工作中,作者经常被问到的一个问题是:应该设置多少个JMeter线程去压测?还有不懂技术的客户提出想要做性能测试,但是没有提供指标,只说系统支持100万用户。这些都是我们日常工作中司空见惯的泛泛的需求。

此时,我们可以通过“3步法”对产品进行正确的需求分析和用户业务模型建立。

1.剖析被测系统

(1)了解系统架构。首先我们需要与开发人员沟通,了解清楚系统的部署架构采用了哪些中间件、数据库、容器、缓存等,以及它们之间的架构关系,并画出对应的网络拓扑图和系统部署架构图。

(2)了解业务模型。首先我们需要采用用户行为分析,分析用户使用产品的习惯,确定系统的典型业务及发生时间。很多大型系统的业务使用都有流量高峰。这类系统业务使用的流量高峰可能出现在一天、一月、一年中的某个时间点上或时间段内。对于新浪、网易等门户网站,在周一到周五的早上刚上班时,可能使用邮件系统的用户比较多,而在中午休息时间浏览热点新闻的用户较多;对于一般的OA系统,早上阅读公告的用户较多,在其他时间可能没有用户使用系统或者仅有少量的用户,比如秘书或领导使用系统起草和审批公文;对于电信缴费系统,在月末很可能会出现用户集中使用交费业务的情况。

然后是调研历史统计数据,通过分析数据确定热点模块。如果产品已上线,可以统计和分析线上历史数据。对于Web类产品,我们可以获取和分析独立用户数、页面访问量和最大在线用户数;对于后台类产品,则可以分析如Nginx的Access log,从而得出最大的访问量;对于数据库类产品,我们还可以分析出热点SQL等。如果产品未上线,有同类产品参考,可以参考公司内同类产品或者同行的同类产品进行热点模块预估,虽然不能完全照搬,但可以根据业务增长数据进行统计分析,输出用户访问热点轨迹图。

2.选取性能测试点

选取性能测试点是性能测试需求分析中非常重要的一个环节。面对一个功能繁杂的系统,要设计出有效的测试场景,最大程度上覆盖系统的性能问题和瓶颈,需要较多的经验积累。目前我们可以按照以下原则来进行性能测试点的选取:

(1)核心业务模块,例如支付业务、核心算法;

(2)并发量较高的业务模块;

(3)逻辑较复杂的业务模块;

(4)有复杂数据库操作或事务的模块;

(5)有较频繁的磁盘读写操作的模块。

然后根据不同类型的系统应用,选取的原则也可以进一步细分。

对于Web应用,其性能测试点的选取原则为:

(1)依据业务数据统计中几种典型业务的用户使用数比例;

(2)调用频繁、占用空间大的数据库表的交易;

(3)占用最大存储空间或其他资源的交易;

(4)对磁盘、常驻内存的数据过度访问的交易。

对于后台类应用,其性能测试点的选取原则为:

(1)读(查询)、写(增删改)、读写(增删改查)混合的业务模块;

(2)配置服务器的业务模块;

(3)功能的实现方式,如同步和异步,轮询和notify等;

(4)分布式业务模块,如单客户端和多客户端,单节点和多节点;

(5)数据规模,如数据库已存在大量记录,存储可用空间少;

(6)缓存,如对文件系统缓存和数据库缓存的利用等;

(7)负载均衡,如多节点情况下是否负载均衡。

对于分布式数据库,其性能测试点的选取原则为:

(1)数据库读写混合业务模块;

(2)数据库之间数据同步;

(3)SQL语句;

(4)数据规模。

此外,当系统应用包含长连接消息服务时,其性能测试点的选取原则为:

(1)单机能支撑的最大并发长连接数;

(2)并发一定数量的用户时的消息推送情况,包括消息到达时间、消息丢失率等。

3.量化测试目标

梳理出来性能测试场景后,就需要进一步明确各个场景的测试指标,而大部分的产品经理给出的指标都是不完整的,通常情况下可以结合上面采集的数据和二八定律进行具体量化,让性能测试指标更明确。

下面举个例子说明性能测试指标量化方法。

例如,某互联应用,预计推广群体达500万人,用户使用应用的时间是每天早上8点至晚上8点,共12h。

分析建模过程如下:

(1)注册用户转化率预估为5%,那么注册用户数为5000000×5%=250000;

(2)高峰时段(比如有活动时)每日在线用户活跃率预估为10%,那么活跃用户数为250000× 10%=25000;

(3)用户常用下单到成功触发20个请求,总请求量为25000×20=500000;

(4)利用二八定律计算,得出的吞吐量为500000×0.8/(12×3600×0.2)=46.7个每秒。

若是更新需求,如发布新产品,定时抢购优惠活动(某日10点开始抢购,12点结束)。

重新建模如下:

(1)注册用户数为25万不变;

(2)高峰时段在线用户在线率预估为20%,那么这2h的在线用户数为250000×20%=50000;

(3)用户常用下单到成功触发20个请求,总请求量为50000×20=1000000;

(4)利用二八定律计算,得出的吞吐量为1000000×0.8/(2×3600×0.2)=555.6个每秒;

由此可见,评估出来的TPS值和需求业务模型息息相关。

在性能测试的前期,通过上述“3步法”整理分析的详尽程度将直接决定后续性能测试的有效性和准确性。

 

注意

在评定服务器的性能时,应该结合TPS和并发用户数,以TPS为主、并发用户数为辅来衡量系统的性能。如果必须要用并发用户数来衡量的话,则需要一个前提,那就是交易在多长时间内完成,因此只用并发用户数来衡量系统的性能没太大的意义。

 

 

提示

▪ 响应时间:根据国外的资料,一般操作的响应时间为2秒、5秒、8秒,即2秒内为优秀、5秒内为良好、8秒内为可接受;对于其他一些特殊的操作,如上传、下载,可以依据用户体验的情况延长响应时间。

▪ 二八定律:又称帕累托效应,例如,一些系统一天中80%的访问量集中在20%的时间内。

 

通过上面的性能测试需求分析,我们已经明确了此次性能测试的目的和性能测试点,接下来就需要进行方案的设计。

有可能对测试结果产生影响的因素主要包括:活跃用户数量、用户活跃时间、用户操作频率(思考时间)、用户操作路径、系统访问量随时间分布、各页面访问量(工作量)分布等。对这些因素考虑得越周全,测试的结果才会越准确。

性能测试方案中应该重点阐述此次性能测试的业务模型如何设计,具体的测试策略是什么。

1.业务模型的设计

一个系统的业务模型是通过业务调研获得的。业务模型的正确性反映在两个方面——业务选择的正确性和业务比例的正确性。

首先是业务选择。一个系统可能支持几百个业务活动(也叫作交易),但是只有少数的业务活动非常频繁,占总业务量的 80%以上,那么在性能测试时只需关心这些占了大部分业务量的少数业务活动上。

其次是业务比例。如何精确统计业务的数量是关键问题。针对一个全新的系统可能要通过对使用系统的涉众进行调研,搞清楚他们的群体数量和操作行为周期。再通过组合这些数据确定在常规交易日各种业务占总业务的比例,同时也要考虑特殊交易日的情况。

例如,某一个商务活动或周期性的业务结算日等都是特殊交易日。在特殊交易日时某一类业务活动的业务量可能突然增加很多,那么在常规交易日的业务比例就不再合适,这点在业务模型上要进行区分。常规交易日的业务模型用来测试系统容量,特殊交易日的业务模型要单独做压力负载测试。

对已上线运营的系统做业务模型的调研相对简单,不需要再去调研那么多的涉众,只需与运营维护部门进行协调,由他们协助测试需求调研人员提取系统中的历史数据即可。但是在数据选择上要有些规则,要选取时间相对长的数据,比如选取几个月的数据。如果有条件的,可以选取一年的数据,选取一年中每月平均业务量、年度高峰月业务数据和月度高峰日业务数据。

2.测试模型的设计

业务模型是根据系统运营真实数据得来的,真实反映了系统运营的业务状况。测试模型是以业务模型为基础,根据测试需求不同对业务模型进行调整,或不调整直接纳入测试场景中使用。所以测试模型的设计其实是依托于业务模型的设计,是具体需要落地实施的方法。

性能测试环境一般情况下都是搭建在Linux服务器上的,那么就有必要掌握一些常用的Linux命令和搭建性能测试环境的原则。另外,本书还分析和总结了不同级别的项目的性能测试环境搭建解决方案,供读者借鉴,希望对读者有所启迪。本节首先讲解Linux服务器上最基础的操作命令和知识要点,然后强调环境搭建的一些原则。

1.基础常用的Linux命令

(1)压缩/解压文件。安装部署源码,下载的安装包都是压缩过的文件。Linux中的打包文件一般是以.tar结尾的,压缩文件一般是以.gz结尾的。一般情况下打包和压缩是一起进行的,打包并压缩后的文件的后缀名一般是.tar.gz,打包和压缩文件命令格式为“tar -zcvf打包压缩后的文件名 要打包压缩的文件”,其中,选项的含义如下:

例如,test目录下有3个文件分别是aaa.txt、bbb.txt和ccc.txt。如果要打包成1个文件并指定压缩后的压缩包名称为test.tar.gz,则可以使用命令tar -zcvf test.tar.gz aaa.txt bbb.txt ccc.txt或tar -zcvf test.tar.gz /test/来实现。

解压压缩包命令格式为“tar [-xvf] 压缩包”,其中,x表示解压。

例如,将test目录下的test.tar.gz解压到当前目录下,可以使用命令tar -xvf test.tar.gz,将test目录下的test.tar.gz解压到根目录usr下,可以使用命令tar -xvf test.tar.gz -C /usr(-C表示指定解压的位置)。

(2)复制移动。修改目录名称命令格式“mv目录名称 新目录名称”。

mv命令用来对文件、目录和压缩包等重新命名,或者将文件从一个目录下移到另一个目录下。

复制目录命令格式为“cp -r目录名称 目录复制的目标位置”,其中,-r表示递归复制。

cp命令不仅可以用来复制目录,还可以用来复制文件、压缩包等,复制文件和压缩包时不用写-r递归。

删除目录命令格式为“rm [-rf] 目录名称。”

rm命令不仅可以用来删除目录,也可以用来删除文件或压缩包,为了减轻大家的记忆负担,作者建议无论删除任何目录或文件,都直接使用“rm -rf目录/文件/压缩包”(注意,加了f选项会强制删除,并不会出现提醒消息)。

(3)进程和端口管理。ps -ef和ps aux这两个命令都是用来查看当前系统正在运行的进程,两者的区别是展示格式不同。如果想要查看特定的进程可以使用这样的命令ps aux | grep redis(查看包括Redis字符串的进程)。

注意,如果直接用ps命令,会显示所有进程的状态,通常结合grep命令查看指定进程的状态。

在使用Linux过程中,需要了解当前系统开放了哪些端口,并且要查看开放这些端口的具体进程和用户,我们可以通过netstat命令进行简单查询。

netstat命令各个参数选项说明如下。

通常与grep结合可查看某个具体端口或服务使用情况。示例如下:

netstat -ntlp  //查看当前所有TCP端口
netstat -ntulp | grep 80  //查看所有80端口使用情况
netstat -ntulp | grep 3306  //查看所有3306端口使用情况

假设我们想查找端口3306对应的服务是什么,一般可以这么做。

首先通过lsof -i:3306或者netstat -ntulp | grep 3306查出对应的pid:

 # lsof -i:3306
COMMAND   PID  USER    FD TYPE DEVICE SIZE/OFF NODE NAME
mysqld  16422 mysql   19u IPv6 148794      0t0  TCP *:mysql (LISTEN)
mysqld  16422 mysql   39u IPv6 643698      0t0  TCP localhost:mysql->localhost:36582 (ESTABLISHED)
mysqld  16422 mysql   45u IPv6 643699      0t0  TCP localhost:mysql->localhost:36584 (ESTABLISHED)
mysqld  16422 mysql   46u IPv6 643700      0t0  TCP localhost:mysql->localhost:36586 (ESTABLISHED)
mysqld  16422 mysql   47u IPv6 643702      0t0  TCP localhost:mysql->localhost:36588 (ESTABLISHED)
mysqld  16422 mysql   48u IPv6 643704      0t0  TCP localhost:mysql->localhost:36590 (ESTABLISHED)
java    17302  root  122u IPv4 643695      0t0  TCP localhost:36582->localhost:mysql (ESTABLISHED)
java    17302  root  123u IPv4 643701      0t0  TCP localhost:36588->localhost:mysql (ESTABLISHED)
java    17302  root  124u IPv4 643696      0t0  TCP localhost:36586->localhost:mysql (ESTABLISHED)
java    17302  root  125u IPv4 643697      0t0  TCP localhost:36584->localhost:mysql (ESTABLISHED)
java    17302  root  126u IPv4 643703      0t0  TCP localhost:36590->localhost:mysql (ESTABLISHED)

或者使用如下netstat命令:

 # netstat -ntulp | grep 3306
tcp6  0   0 ::: 3306        :::*        LISTEN  16422/mysqld

找到pid后,通过ps -aux | grep 16422即可找到对应具体的进程服务:

 # ps -aux | grep 16422
mysql 16422 0.0 47.7 1340428 485944 ? Sl Jun29 1:46 /usr/sbin/mysqld --basedir=/usr --datadir=  
/var/lib/mysql --plugin-dir=/usr/lib64/mysql/plugin--log-error=/var/lib/mysql/VM_39_230_centos.err --pid-  
file=/var/lib/mysql/VM_39_230_centos.pid
root   25713 0.0 0.0 112616  700 pts/0  R+  17:04  0:00 grep --color=auto 16422

(4)修改配置。vim编辑器是Linux中的强大组件,是vi编辑器的加强版。vim编辑器的命令和快捷方式有很多,这里仅列出一些常用命令。

在实际部署环境中,使用vim编辑器的主要作用就是修改配置文件,一般步骤是:通过vim文件命令进入文件的命令模式,按I键进入编辑模式,编辑文件后按Esc键进入底行模式,最后,输入wq或q!(输入wq代表写入内容并退出,即保存;输入q!代表强制退出且不保存。)

用vim编辑器打开一个文件刚开始进入的就是命令模式,在这个模式下我们可以控制光标的移动,字符、行的删除,移动复制某段区域。在该模式下可以进入插入模式或编辑模式,也可以进入底行模式。vi命令模式的转换方法见表1-1。

表1-1 vi命令模式的转换方法

模式

方法

插入模式/编辑模式

通过vi文件名打开文件后,按A键(或I键、O键)进入编辑状态

底行模式

在编辑状态,按Esc键进入底行模式。通常在底行模式下输入wq!表示强制保存,输入q!表示强制退出且不保存

查找模式

在底行模式下,输入/,然后再输入想查找的字符串,即可查找

(5)修改文件权限。操作系统中每个文件都拥有特定的权限、所属用户和所属组。权限是操作系统用来限制资源访问的机制。在 Linux 中权限一般分为可读(readable)、可写(writable)和可执行(excutable)共3组,分别对应文件的属主(owner)、属组(group)和其他用户(other)。通过这样的机制来限制哪些用户、哪些组可以对特定的文件进行哪些操作。通过ls -l命令我们可以查看某个目录下的文件和目录的权限。

 # ls -l
drwxr-xr-x  6  hutong staff 192   Feb 24 17:52 Movies
drwxr-xr-x  4  hutong staff 128   Nov 20 11:28 Music
drwxr-xr-x  6  hutong staff 192   Nov 20 18:47 Pictures
-rw-r--r--  1  hutong staff 3986  May 11 18:09 Test Plan.jmx
drwxr-xr-x  13 hutong staff 416   May 12 10:57 eclipse-workspace
-rw-r--r--  1  hutong staff 13839 May 14 17:01 jmeter.log

文件和目录的类型表示如下。

在Linux中权限分为以下几种。

而对于文件和目录,不同权限对应的可执行操作不同,具体区别见表1-2。

表1-2 文件和目录的权限与可执行操作

类型 权限名称 可执行操作
文件 r 可以使用cat查看文件的内容
w 可以修改文件的内容
x 可以将文件运行为二进制文件
目录 r 可以查看目录下的列表
w 可以创建和删除目录下的文件
x 可以使用cd进入目录

在Linux中的每个用户必须属于一个组,不能独立于组外。在Linux中每个文件有所有者、文件所在组和其他组的概念。

那么,如何修改文件或目录的权限?修改文件或目录的权限的命令为chmod。

下面举个例子,修改test目录下的aaa.txt的权限为属主有全部权限,属主所在的组有读写权限,其他用户只有读的权限,命令如下:

chmod u=rwx, g=rw, o=r aaa.txt

上述示例还可以使用数字表示:chmod 764 aaa.txt。

以上是必须要掌握的相关命令和知识点,其他更多的命令只能靠平时一点一滴地积累了。

2.环境搭建的原则

在掌握了上面最基础的服务器操作命令和知识点后,我们就可以开始搭建各个中间件了。性能测试环境的搭建和选取有一些原则,如何尽可能地模拟真实环境,是需要总结和思考的。有些公司的环境搭建由专门的运维人员负责,但是作者认为如果有权限或者公司允许,学会自己搭建性能测试环境还是很有必要的。通过搭建环境可以进一步熟悉系统的架构和请求流经的各个中间件,这对于问题的定位比较有帮助。总的原则是尽可能地和线上真实环境保持一致。

(1)新项目。对于第一次上线的新项目,我们可以直接在这个预发布的环境上进行测试,因为不会涉及和影响真实用户。

(2)老项目。对于已经有真实用户在使用的项目,我们需要根据系统规模的大小和公司资源的情况有针对性地、灵活地搭建性能测试环境。

以上就是作者在从事大大小小的性能测试项目后,思考提炼出的性能测试环境搭建原则。当然,如果公司经济条件允许,那么完全复制线上环境的服务器配置得到的性能测试结果是最为真实的。

 

提示

读者可以自己尝试做一些Docker的容器化镜像,方便复用,一劳永逸。

像阿里这种大团队可能会采用线上环境直接压测,并利用一些标签(每个请求有tag标识)和使用配套的支撑平台等技术手段,使测试环境更加真实,成本更低,但是技术要求也更高。

 

在性能测试过程中,准备测试数据是一项非常系统化、工作量非常庞大的工作。如何准备支持不同业务操作、不同测试类型的大量测试数据来满足压力测试的需求,是性能测试过程中经常面对的一个重要话题。关于如何准备性能测试数据,相信不少性能测试人员也踩过不少坑,例如数据量不足,导致性能表现非常好,但未能暴露和发掘潜在性能问题;数据分布不合理,导致测试结果与线上差异较大。

1.性能测试数据的准备类型

在执行性能测试前,一般需要准备3类数据:初始化数据、铺底数据(历史数据)和参数化数据。

(1)初始化数据。业务系统安装部署完成后,我们并不能马上进行相关业务的性能测试,需要对业务系统进行初始化操作。系统初始化主要对系统中的基本角色信息、机构信息、权限信息和业务流程设置等增加数据,这些数据是系统能够开展相关业务的基础。准备初始化数据是为了识别数据状态并且验证测试案例的数据,需要在业务系统搭建完成后按照业务系统实际运行要求导入,以供测试时使用。

(2)铺底数据(历史数据)。当业务系统刚刚上线的时候,数据库中数据量相对较少,系统整体响应时间很快,用户使用体验较好。但随着业务的持续开展,业务系统数据库中的数据量会成倍地增加,业务系统的相关操作响应时间会因为数据库中业务数据的快速增长等原因而越来越长,用户使用体验会越来越差。因此,在性能测试时,需要加入相当规模的铺底数据,来模拟未来几年业务增长条件下系统相关操作的性能表现。如果要测试并发查询业务,那么要求对应的数据库和表中有相当的数据量,以及数据的种类应覆盖全部业务。

(3)参数化数据。在负载压力测试过程中,为了模拟不同的虚拟用户操作的真实负载情况,同时由于业务系统中大部分业务操作的交易数据不能重复使用,因此我们需要为不少用户输入信息准备大量参数化数据。参数化数据涉及的范围很广,例如,模拟不同用户登录系统,需要准备大量用户名及密码参数化数据;模拟纳税人纳税申报,需要准备大量的纳税人识别号、纳税人内码或纳税人系统内部识别号等参数化数据。准备这类数据要求符合实际运行要求,并且保证数据表之间的关联关系。

在压力测试时,通常模拟不同的用户行为或者业务行为,从系统提供的API来看,我们需要参数化用户账号等数据,如压测下单场景时,我们要参数化用户数据,哪些用户进行下单操作;参数化商品数据,这些用户购买什么产品等。

2.性能测试数据的构造原则

我们知道数据量变化会引起性能的变化。在制作测试数据时,首先要注意数据量,需要准备足够的存量/历史业务数据;其次要注意数据的分布,例如我们计算出需要并发100个虚拟用户,我们至少需要准备100个以上的账号,并对账号赋予相应的权限(浏览、发帖、删除、查询)。最后还要注意冷热数据,一般热点数据也需要准备。

铺底数据量多少合适?这个完全根据产品的规模来预估,例如预计半年后产品的注册用户数达到100万,则需要铺底100万用户账号。如果是已上线产品,则根据线上数据库的数据量进行预估,可以根据用户规模的比例进行铺底,如线上注册用户数1000万,线下铺底注册用户数100万,则总体铺底数据规模为线上数据量的十分之一。实际情况下,这里会比较复杂,例如还要考虑线上数据库集群和测试集群的硬件差异等,需做适当的调整。

在进行参数化数据准备时,对于已经上线的产品,可以统计不同铺底数据的分布规律。其实大多数数据的分布接近二八定律。比如活跃用户数占注册用户数的比例为 20%左右,非活跃或者欠活跃的用户占比为 80%左右。针对具体业务,80%左右的业务是由 20%左右的活跃用户产生,20%左右的业务是由80%左右的非活跃用户产生。所以参数化测试账号时,对这20%左右的用户账号需要进行精心铺底。

3.性能测试数据的构造方法

(1)从线上数据库导入真实数据。从线上数据库或者备库导入数据,最大的优势是数据真实性高、数据分布合理、业务压力点及瓶颈能和线上保持一致,这样测试得到的结果与线上实际表现会比较接近,比自己模拟数据要可靠得多。但是,缺点是用户数据需要进行脱敏,而且要考虑用户的数据是否能直接拿来用,例如用户的手机号就需要脱敏,涉及短信通知等业务需要进行一定的数据处理或者模拟数据等。

(2)根据业务规则构造模拟数据。模拟数据的优点是数据会符合自己定义的规则,便于使用,例如测试账号可以自己进行编号,方便参数化使用。模拟数据的缺点也比较明显,模拟数据需要大量的梳理工作,要梳理数据库表,厘清哪些是基础表、表与表之间的关联等;要充分把握产品业务,例如对社交类产品,要统计和分析用户的好友关系、每个用户的好友个数及分布,梳理每个好友发布的微博分布等,这样测试结果才能趋于与线上一致。

下面是日常工作中通过代码快速构造数据的方法。

(1)编写代码批量生成SQL语句,具体代码可参考代码清单1-1。

代码清单1-1 SQLUtil.java

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;

public class SQLUtil {
    public static void genAN_ONU() throws IOException {
        String AN_ONU = "INSERT INTO AN_ONU VALUES 
        ('NE_MODEL_CFG_TYPE456d614260156df017fdb01bb', 'HV', 'SV1.0', '%s', 
        'DISTRICT-00001', '%s',NULL, 'DEVICE_VENDOR456d614260156deffaf1501b7', 
        NULL,NULL, '%s',sysdate,NULL,0,NULL,NULL,NULL, 'cpe', 'cpe', 'rms', 'rms',NULL,
        0,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL, 'Android',
        '',sysdate,sysdate,0,NULL,NULL,NULL,NULL,1,NULL);";
        String mac_prex = "HW-";
        String sn_prex = "SN_HW00";
        String cuid_prex = "DEVICE-cuid-HW";

        BufferedWriter bw = new BufferedWriter(new FileWriter(new File("/Users/
        hutong/Downloads/an_onu.sql")));
        //100000台设备
        for (int i = 200001; i <= 300000; i++) {
              String offset = String.format("%05d",i);
              String sql = String.format(AN_ONU,mac_prex+offset,sn_prex + offset,cuid_ 
              prex + offset);
              //System.out.println(AN_ONU);
              bw.write(sql + "\n");
        }
        bw.write("commit;");
        bw.flush();
        bw.close();
    }
    public static void main(String[] args) throws IOException {
        genAN_ONU();
   }
}

(2)通过MySQL的工具导入数据,在MySQL的命令行上执行下面的语句即可:

load data infile '/Users/hutong/Downloads/an_onu.sql' into table tb_onu fields terminated by ','  
 optionally enclosed by '"' escaped by '"' lines terminated by '\n'

其中的参数解释如下。

通过该高效的方法,作者在自己的服务器上构造千万条数据只需要几分钟。

 

注意

考虑生成的测试数据量是否达到未来预期数据量只是最基础的一步,更需要考虑的是数据的分布是否合理,这需要仔细地确认程序中使用的各种查询条件。重点列的数据分布要尽可能地模拟真实的数据分布,否则测试的结果可能是无效的。

 

在现实工作中,有比较完善的接口文档是比较幸运的,很多时候文档都是比较匮乏的,此时就需要用到抓包分析。另外,对于特殊的协议,相应的基本的抓包工具是必不可少的。

1.Fiddler和Charles

Fiddler和Charles都是常用的HTTP/HTTPS的抓包分析工具。Fiddler一般在Windows上用得比较多,Charles常用在macOS上。

在Web开发中,我们无法看到Web浏览器/客户端和服务器之间发送和接收的内容,这种情况下想要确定错误在哪里是困难且耗时的。而通过使用Fiddler/Charles,我们可以很容易地看到这些内容,从而快速诊断和解决问题。Fiddler/Charles可以让开发者监视所有连接互联网的HTTP通信,包括请求、响应和HTTP头信息等。这主要用于网页的开发、调试、分析封包协议,以及模拟慢速网络。

2.Tcpdump

当涉及特殊协议(如SOAP)或者需要在服务器上抓取数据包时,可以采用Tcpdump + Wireshark的方法。

Tcpdump命令格式如下:

tcpdump [ -adeflnNOpqStvx ] [ -c数量] [ -F文件名][ -i网络接口] [ -r文件名] [ -s抓取长度]  
[ -T类型] [ -w文件名] [表达式]

常用的参数为:

常用表达式有:

要让Wireshark能分析Tcpdump抓取的数据包,关键的地方是-s参数设置,并且要将抓取的数据包保存至-w参数设置的文件中。参看下面的例子:

./tcpdump tcp -i eth1 -t -s 0 -c 100 and dst port ! 22 and src net 192.168.1.0/24 -w ./target.cap

3.Chrome浏览器的调试模式

对于Web类项目,最常用和最简单的方法就是通过Chrome浏览器的调试模式,如图1-6所示,在Chrome浏览器中按F12键进入调试模式。在调试模式下,我们可以快速查看HTTP的相关信息,方便快捷。

图1-6 Chrome浏览器调试模式下的Network界面

性能测试脚本模拟方法常用的工具有LoadRunner、JMeter、Locust、nGrinder、XMeter、Blaze Meter、Gatling、AB、wrk、腾讯WeTest、压测宝等,其中有开源的也有商业的。另外还有一些特殊的基准工具,例如NetPerf用于测试网络带宽流量。

本书主要讲解通过JMeter编写性能测试脚本,包括单业务脚本和混合业务脚本。

(1)单业务脚本。性能测试不可能对所有功能都测试,所以需要抽象一些特定的独立业务来进行测试用例的设计。独立业务实际是指一些核心业务模块对应的业务,这些模块通常具有功能比较复杂、使用比较频繁等特点。针对这类独立业务进行的性能测试称为单业务性能测试。

(2)混合业务脚本。在真实应用系统中,通常不会存在所有用户只使用一个或者几个核心业务模块的情况,即一个应用系统的每个功能模块都可能被使用到,所以性能测试既要模拟多用户的相同操作,又要模拟多用户的不同操作。混合业务性能测试是最接近用户实际使用情况的性能测试,也是性能测试的一个必要内容。混合业务性能测试的突出特点是根据用户使用系统的情况分成不同的用户组进行并发,每组的用户人数比例要根据实际情况来匹配,通常会取各个相关模块并发用户人数最大值进行组合。也就是说,在混合业务性能测试中,通常需要按照用户实际使用模块的人数比例来模拟各个模块的组合并发情况。

以上就是对在编写JMeter性能测试脚本的时候需要覆盖的内容的概述,具体的编写方法见后续的实战章节。

 

提示

▪ 同步工具:工具发送请求后,只有在收到该请求的响应后才会继续发送下一个请求。

▪ 异步工具:工具不停地发送请求,不管有没有收到响应,一般用来测试秒杀场景。

 

在真正开始执行性能测试之前,需要全方位的、立体式的部署对各个环节的监控,这样对后续发现问题和定位问题是很有帮助的,一般至少要从服务器层、应用JVM层和数据库层这3个层面进行监控。

1.服务器层监控

常见且需要重点或者优先观察的监控指标有CPU、内存、网络和连接数。

(1)CPU。

(2)内存。

(3)网络。

(4)连接数。

系统本身的连接数是有限的,通常只有65535个。我们一方面需要注意连接消耗的总数,另一方面还需要观察连接的状态,例如ESTABLISHED、TIME_WAIT等状态,如果TIME_WAIT状态的连接数太多,那么很有可能需要优化配置或代码。

接下来我们讲一下监控方法。Linux中用于监控的命令有很多,这里只讲解作者平时用得较多的命令,简要汇总如表1-3所示。

表1-3 常用Linux监控命令

命令

简单介绍

top

查看进程活动状态以及一些系统状况,是最常用的命令。一般用来获取系统整体的CPU和内存使用情况,还有每个进程的状态和消耗

free

查看系统的内存使用情况,通常用来计算内存使用率,加上-m可以以兆字节为单位显示,更易读

vmstat

通常用来查看内存是否够用,通过有没有使用虚拟内存来判断

sar

综合工具,作者通常用该命令查看网络流量、网络状态,看是否有丢包、重传情况

netstat/ss

查看网络连接的数量和状态

dmesg

查看系统本身的日志,观察是否有错误

iostat

观察系统磁盘的使用状态,重点关注IO

表1-3中具体的各项指标含义参看下面的示例。

在命令行中执行命令top,显示如下:

top - 20:23:23 up 675 days,22:04,2 users, load average:0.19,0.16,0.09
Tasks: 145 total, 1 running, 144 sleeping,0 stopped,0 zombie
%Cpu(s): 0.8 us, 0.6sy,0.0 ni,98.6id,0.0 wa,0.0 hi,0.0 si,0.0 st
KiB Mem: 8010172total,152740 free, 5595992 used,2261440 buff/cache
KiB Swap: 0 total,0 free,0 used.699464  avail Mem 

PID   USER PR NI   VIRT    RES  SHR  S %CPU %MEM      TIME+ COMMAND
24926 root 20 0 3900520 305088  7644 S 4.3  3.8       274: 47.22 java
25307 root 20 0 5755460 408544  6264 S 1.0  5.1       6648: 34 java

在Linux中,通过top命令可以简要查看一个运行中的程序,占用了多少内存和CPU,其中,VIRT(或VSS)列表示程序占用了多少虚拟内存,RES列表示程序占用了多少物理内存。

Linux使用内存有一个原则就是能使用多少就使用多少,所以,Linux会把已经调用过的包缓存起来,放在内存里。实际上,可以使用的内存应该为free+buffers+cached。在命令行中执行命令free -m,显示如下:

$ free -m
          total    used    free   shared  buffers   cached
  Mem:    4024     2507    1516   0       237       1170
-/+ buffers/cache:    1100      2924
  Swap:         1021    0      1021

那么是否内存free越少表明内存越不够用?不是这样的,free越少只能证明Linux对内存的使用率越高。通常系统可用内存分为三部分,一部分是free,一部分是Cache,还有一部分是Buffer,Cache通常指的是读Cache。当free不够用时,系统先将Cache和Buffer使用的内存供进程使用,等这些都用完了才会考虑Swap设备。

 

提示

当系统没有足够物理内存来应付所有请求的时候就会用到Swap设备,Swap设备可以是一个文件,也可以是一个磁盘分区。不过要小心的是,使用Swap设备的代价非常大。系统没有物理内存可用,就会频繁交换,如果Swap设备和程序正要访问的数据在同一个文件系统上,就会碰到严重的IO问题,最终导致整个系统迟缓,甚至崩溃。Swap设备和内存之间的交换状况是判断Linux系统性能的重要参考指标。

 

执行vmstat命令,会输出一些系统核心指标,这些指标可以让我们更详细地了解系统状态。

 $ vmstat 1 
procs ---------memory---------- ---swap-- -----io---- -system-- ------cpu----- 
 r b swpd    free  buff cache    si  so  bi    bo    in   cs us sy id wa st 
34 0  0 200889792 73708 591828    0   0   0     5     6   10 96  1  3  0  0 
32 0  0 200889920 73708 591860    0   0   0   592 13284 4282 98  1  1  0  0 
32 0  0 200890112 73708 591860    0   0   0     0  9501 2154 99  1  0  0  0 
32 0  0 200889568 73712 591856    0   0   0    48 11900 2459 99  0  0  0  0 
32 0  0 200890208 73712 591860    0   0   0     0 15898 4840 98  1  1  0  0

命令的参数1,表示每秒输出一次统计信息。输出结果中,首行提示了每一列的含义,这里介绍一些和性能调优相关的列。

从示例中的输出可以看出,大量CPU时间消耗在用户态,也就是用户应用程序消耗了CPU时间。这不一定是性能问题,需要结合r列一起分析。

执行下面的命令会输出系统日志的最后10行。

$ dmesg | tail 
[1880957.563150] perl invoked oom-killer: gfp_mask=0x280da, order=0,oom_score_adj=0 
[...] 
[1880957.563400] Out of memory:Kill process 18694 (perl) score 246 or sacrifice child
[1880957.563408] Killed process 18694 (perl) total-vm:1972392kB,anon-rss:1953348kB,file-rss:0kB
[2320864.954447] TCP:Possible SYN flooding on port 7001. Dropping request. Check SNMP counters.

从示例中的输出可以看到一次内核的oom-killer和一次TCP丢包。这些日志可以帮助排查性能问题,千万不要忘记这个命令。

iostat命令主要用于查看计算机磁盘的IO情况。

 $ iostat -xz 1 
Linux 3.13.0-49-generic (titanclusters-xxxxx) 07/14/2015 _x86_64_ (32 CPU) 
avg-cpu:   %user    %nice %system %iowait %steal    %idle 
           73.96    0.00  3.73    0.03    0.06      22.21 
Device:  rrqm/s  wrqm/s   r/s   w/s  rkB/s  wkB/s avgrq-sz avgqu-sz  await r_await w_await svctm %util 
xvda   0.00   0.23  0.21  0.18   4.52   2.08  34.37 0.00   9.98 13.80   5.42  2.44  0.09
xvdb   0.01   0.00  1.02  8.94 127.97 598.53 145.79 0.00   0.43  1.78   0.28  0.25  0.25
xvdc   0.01   0.00  1.02  8.86 127.79 595.94 146.50 0.00   0.45  1.82   0.30  0.27  0.26
dm-0   0.00   0.00  0.69  2.32  10.47  31.69  28.01 0.01   3.23  0.71   3.98  0.13  0.04
dm-1   0.00   0.00  0.00  0.94   0.01   3.78   8.00 0.33 345.84  0.04 346.81  0.01  0.00
dm-2   0.00   0.00  0.09  0.07   1.35   0.36  22.50 0.00   2.55  0.23   5.62  1.78  0.03

执行该命令输出的列主要含义如下。

sar -n可以根据关键字以不同的角度报告实时的网络流量变化,其中DEV关键字和ETCP关键字最为常用。DEV关键字表示以设备为单位提供网络统计报告,方便快速观察各网卡性能。

 $ sar -n DEV 1 
12:16:48 AM    IFACE  rxpck/s  txpck/s   rxkB/s  txkB/s  rxcmp/s  txcmp/s  rxmcst/s  
12:16:49 AM     eth0 18763.00 5032.00 20686.42 478.30    0.00    0.00     0.00   
12:16:49 AM       lo    14.00   14.00     1.36   1.36    0.00    0.00     0.00   
12:16:49 AM  docker0     0.00    0.00     0.00   0.00    0.00    0.00     0.00   
12:16:49 AM    IFACE   rxpck/s  txpck/s   rxkB/s  txkB/s  rxcmp/s  txcmp/s  rxmcst/s  
12:16:50 AM     eth0 19763.00 5101.00 21999.10 482.56    0.00    0.00     0.00   
12:16:50 AM       lo    20.00   20.00     3.25   3.25    0.00    0.00     0.00   
12:16:50 AM  docker0     0.00    0.00     0.00   0.00    0.00    0.00     0.00

该命令输出的列主要含义如下:

sar命令在这里用于查看网络设备的吞吐率。在排查性能问题时,可以通过网络设备的吞吐量,判断网络设备是否已经饱和。在示例输出中,eth0网卡设备吞吐量大概在22 MB/s,即176 Mbit/s,没有达到1Gbit/s的硬件上限。现在我们使用的所有网卡都称为自适应网卡,即能根据网络上不同网络设备导致的不同网络速度和工作模式进行自动调整的网卡。我们可以通过ethtool工具来查看网卡的配置和工作模式,并强制网卡工作在1000baseT下,例如/sbin/ethtool -s eth0 speed 1000 duplex full autoneg off。

 

提示

(1)吞吐量,指在没有帧丢失的情况下,设备能够接受的最大数据传输速率。

(2)存储的最小单位是字节(byte)。存储单位有GB、MB和KB等,它们之间的换算关系是1GB=1024MB,1MB=1024KB,1KB=1024B。

▪ bit:“比特”,有时也称为位,用字母b表示。

▪ byte:“字节”,1字节就是8比特。一个字节是8个二进制位,用字母B表示。

(3)Mbit/s(million bit per second,兆比特/秒)表示每秒传输1000000比特。该缩写用来描述数据传输速度。例如,4Mbit/s=每秒传输4兆比特。

(4)吞吐量与带宽的区分。吞吐量和带宽是很容易被混淆的两个词,两者的单位都是 Mbit/s。我们先来看一下两者对应的英语,吞吐量是throughput,带宽是bandwidth。当我们讨论通信链路的带宽时,一般是指链路上每秒所能传送的比特数。我们可以说以太网的带宽是10Mbit/s,但是,我们需要区分链路上的可用带宽(带宽)与实际链路上每秒能传送的比特数(吞吐量)。我们倾向于用“吞吐量”来表示系统的数据传输性能。因为在实现过程中受各种低效率因素的影响,所以由一段带宽为10Mbit/s的链路连接的一对节点可能只有2Mbit/s的吞吐量。这就意味着,一个主机上的应用能够以最大2Mbit/s的速率向另外的一个主机发送数据。

 

 $ sar -n TCP,ETCP 1 
12:17:19 AM  active/s  passive/s     iseg/s      oseg/s 
12:17:20 AM     1.00       0.00  10233.00    18846.00 
12:17:19 AM  atmptf/s   estres/s  retrans/s   isegerr/s   orsts/s 
12:17:20 AM     0.00       0.00      0.00        0.00     0.00 
12:17:20 AM  active/s  passive/s     iseg/s      oseg/s 
12:17:21 AM     1.00       0.00   8359.00     6039.00 
12:17:20 AM  atmptf/s   estres/s  retrans/s   isegerr/s   orsts/s 
12:17:21 AM     0.00       0.00      0.00        0.00     0.00

sar命令在这里用于查看TCP连接状态,其中的一些输出说明如下。

TCP连接数可以用来判断性能问题是否由建立了过多的连接造成,进一步可以判断是主动发起的连接,还是被动接受的连接。TCP重传可能是由网络环境恶劣,或者服务器压力过大导致丢包造成的。

通过ETCP关键字可以查看TCP层的错误统计,包括重试、断连、重传、错误等。

另外,通过如下命令可以统计服务器上各个状态的连接数。

netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S)print a,S[a]}'

返回结果示例如下:

LAST_ACK 5 
SYN_RECV 30 
ESTABLISHED 1597 
FIN_WAIT1 51 
FIN_WAIT2 504 
TIME_WAIT 1057

其中,SYN_RECV表示正在等待处理的连接数,ESTABLISHED表示正常数据传输状态的连接数,TIME_WAIT表示处理完毕,等待超时结束的连接数。

 

注意

TIME_WAIT状态的连接数的值过高会占用大量连接,影响系统的负载能力,需要调整参数,以尽快释放TIME_WAIT状态的连接。

一般TCP相关的内核参数在/etc/sysctl.conf文件中。为了能够尽快释放TIME_WAIT状态的连接,可以做以下配置:

 

// 表示开启SYN Cookies。当出现SYN等待队列溢出时,启用Cookies来处理,可防范少量SYN攻击,默认为0,表示关闭
net.ipv4.tcp_syncookies = 1
// 表示开启重用。允许将TIME_WAIT状态的套接字重新用于新的TCP连接,默认为0,表示关闭
net.ipv4.tcp_tw_reuse = 1
// 表示开启TCP连接中TIME_WAIT状态的套接字的快速回收,默认为0,表示关闭
net.ipv4.tcp_tw_recycle = 1
// 修改系统默认的TIMEOUT时间
net.ipv4.tcp_fin_timeout = 30
// 表示当keepalive启用的时候,TCP发送keepalive消息的频率。默认是2h,改为20min
net.ipv4.tcp_keepalive_time = 1200
// 表示用于向外连接的端口范围。默认情况下很小,为32768到61000,改为10000到65000(注意,这里不要将最低值设得太低,否则可能会占用正常的端口)
net.ipv4.ip_local_port_range = 10000 65000
// 表示SYN队列的长度,默认为1024,加大队列长度为8192,可以容纳更多等待连接状态的连接
net.ipv4.tcp_max_syn_backlog = 8192
// 表示系统同时保持TIME_WAIT状态的最大连接数,如果超过这个数字,TIME_WAIT状态的连接将立刻被清除并打印警告信息。默认为180000,改为5000
net.ipv4.tcp_max_tw_buckets = 5000

上面讲解的都是日常工作中最实用的命令,一般在定位问题的时候是比较常用的。而在压测过程中,还需要收集每个时间点的状态。从最早的nmon工具,到后来的Zabbix,此处是作者自己写Shell脚本来搭建收集每个时间点的监控指标平台,供读者借鉴。

该脚本通过sar、top、free等上述常用命令,按一定的时间间隔收集数据,存入InfluxDB时序数据库中,以便后续图形化展示。该脚本是基于CentOS 6和CentOS 7编写的,如代码清单1-2所示,读者可以根据自己的需要进行微调和增加监控内容。

代码清单1-2 monitor_centos.sh

#!/bin/bash
###usage: ./*.sh countNum influxdbIp project monitorIp dbname
##system:  centos 
##update: 2019/11/20
##author: hutong
##dbname: mymonitor(存储被监控计算机)/pressure(存储压测机)
##每5s收集一次数据

if [ $# != 5 ] ; then
echo "USAGE:        sh $0 countNum influxdbIp project monitorIp influxdbname"
exit 1
fi

countNum=$1
influxdbIp=$2
project=$3
monitorIp=$4
dbname=$5

if [ ! -d "/data" ]; then
 mkdir /data
fi
file_log='/data/mymonitor.log'

#获取网卡名
line=$(expr $(/usr/sbin/ifconfig |grep "$monitorIp" -n|awk -F: '{print $1}') - 1 )
netface='/usr/sbin/ifconfig |sed -n "$line p"|awk '{print $1}'|cut -d: -f 1'
#获取系统版本号
os_version='cat /etc/redhat-release|sed -r 's/.* ([0-9]+)\..*/\1/''

#确保已安装sar
result='sar -V'
if [[ "$result" =~ "sysstat version" ]]; then
 echo 'sar installed.' 1>$file_log 2>&1
else
 yum install sysstat -y 1>$file_log 2>&1
fi

#创建一个数据库,库名为项目名称
curl -i -XPOST -u root:root "http://$influxdbIp:8086/query" --data-urlencode 'q=CREATE DATABASE '$project'' 1>>$file_log 2>&1
curl -i -XPOST -u root:root "http://$influxdbIp:8086/query" --data-urlencode 'q=create retention policy "rp_7d" on "'$project'" duration 7d replication 1 default ' 1>>$file_log 2>&1

function monitor7(){
num=0
 #echo 'date'
while:;
do
  if (( $num==$1 )); 
  then  break
  else
    #计算开始时间
    startTime_s='date +%s'

    #cpu
    #cpu='sar -u 1 1| grep Average'
    #cpu_io='echo $cpu |awk '{print $6}''
    #cpu_idle='echo $cpu |awk '{print $8}''
    cpu='top -bn 1 -i -c | awk '{if($1~/^%Cpu/) print}''
    cpu_io='echo $cpu |awk '{print $10}''
    cpu_idle='echo $cpu |awk '{print $8}''

    #mem
    mem_total='free -m | grep Mem | awk '{print $2}''
    mem_avail='free -m | grep Mem | awk '{print $7}''
    mem_used=$(($mem_total - $mem_avail))

    #net
    net='sar -n DEV 1 1 | grep Average | grep $netface'
    net_rx='echo $net | awk '{print $5}'' #receive kB/s
    net_tx='echo $net | awk '{print $6}'' #transmit kB/s
    #echo $net_rx
    #echo $net_tx
    #net_all='echo "$net_rx + $net_tx"|bc' #需要安装bc
    #shell脚本中不可直接进行小数运算
    net_all=$(echo $net_rx $net_tx | awk '{ printf "%0.2f" ,$1 + $2}')
    #echo $net_all

    #rx_before='ifconfig $netface|sed -n "6p"|awk '{print $5}''
    #tx_before='ifconfig $netface|sed -n "8p"|awk '{print $5}''
    #sleep 1
    #rx_after='ifconfig $netface|sed -n "6p"|awk '{print $5}''
    #tx_after='ifconfig $netface|sed -n "8p"|awk '{print $5}''
    #rx_result=$[(rx_after-rx_before)/1024] #接收速度,单位为KB/s
    #tx_result=$[(tx_after-tx_before)/1024] #发送速度,单位为KB/s

    #tcp state
    tcp_timewait='ss -ant|grep TIME-WAIT |wc -l'
    tcp_estab='ss -ant|grep ESTAB |wc -l'
    tcp_total='ss -ant |wc -l'

    endTime_s='date +%s'
    #计算执行上面Shell命令消耗的时间
    costTime=$[ $endTime_s - $startTime_s ]
    #echo $costTime
    #echo 'date'
    sleeptime=$[ 5 - $costTime ]
    #echo "需要延迟时间:"$sleeptime
    sleep $sleeptime
    wait
    #timestamp
    #currentTimeStamp=$[$(date +%s%N)/1000000]
    #postfix=000000

    #往InfluxDB中插入数据
    curl -i -XPOST -u root:root "http://$influxdbIp:8086/write?db=$project&precision=s" --data-binary ''$dbname',host='$2' cpu_io='$cpu_io',cpu_idle='$cpu_idle',mem_total='$mem_  
total',mem_used='$mem_used',rx_net='$net_rx',tx_net='$net_tx',all_net='$net_all',tcp_wait=  
'$tcp_timewait',tcp_estab='$tcp_estab',tcp_total='$tcp_total' ' 1>>$file_log 2>&1 
    num=$(($num+1))
  fi
done
}

function monitor6(){
num=0
 #echo 'date'
while:;
do
  if (( $num==$1 ));
  then  break
  else
    #计算开始时间
    startTime_s='date +%s'

    #cpu
    #cpu='sar -u 1 1| grep Average'
    #cpu_io='echo $cpu |awk '{print $6}''
    #cpu_idle='echo $cpu |awk '{print $8}''
    cpu='top -bn 1 -i -c | awk '{if($1~/^%Cpu/) print}''
    cpu_io='echo $cpu |awk '{print $10}''
    cpu_idle='echo $cpu |awk '{print $8}''

    #mem
    mem_total='free -m | grep Mem | awk '{print $2}''
    mem_used='free -m | grep cache: | awk '{print $3}''

    #net  
    net='sar -n DEV 1 1 | grep Average | grep $netface'
    net_rx='echo $net | awk '{print $5}'' #接收速度,单位为KB/s
    net_tx='echo $net | awk '{print $6}'' #发送速度,单位为KB/s
    net_all=$(echo $net_rx $net_tx | awk '{ printf "%0.2f" ,$1 + $2}')

    #rx_before='ifconfig $netface|sed -n "9p"|awk '{print $2}'|cut -c7-'
    #tx_before='ifconfig $netface|sed -n "9p"|awk '{print $6}'|cut -c7-'
    #sleep 1
    #rx_after='ifconfig $netface|sed -n "9p"|awk '{print $2}'|cut -c7-'
    #tx_after='ifconfig $netface|sed -n "9p"|awk '{print $6}'|cut -c7-'
    #rx_result=$[(rx_after-rx_before)/1024]
    #tx_result=$[(tx_after-tx_before)/1024] 

    #tcp state
    tcp_timewait='ss -ant|grep TIME-WAIT |wc -l'
    tcp_estab='ss -ant|grep ESTAB |wc -l'
    tcp_total='ss -ant |wc -l'

    endTime_s='date +%s'
    #计算执行上面Shell命令消耗时间
    costTime=$[ $endTime_s - $startTime_s ]
    #echo $costTime
    #echo 'date'
    sleeptime=$[ 5 - $costTime ]
    #echo "需要延迟时间:"$sleeptime
    sleep $sleeptime
    wait

    #timestamp
    #currentTimeStamp=$[$(date +%s%N)/1000000]
    #postfix=000000

    curl -i -XPOST -u root:root "http://$influxdbIp:8086/write?db=$project&precision=s" --data-binary ''$dbname',host='$2' cpu_io='$cpu_io',cpu_idle='$cpu_idle',mem_total='$mem_  
total',mem_used='$mem_used',rx_net='$net_rx',tx_net='$net_tx',all_net='$net_all',tcp_wait='  
$tcp_timewait',tcp_estab='$tcp_estab',tcp_total='$tcp_total' ' 1>>$file_log 2>&1 
    num=$(($num+1))
  fi
done
}

if (( $os_version==7 ));then
monitor7 $countNum $monitorIp
else
monitor6 $countNum $monitorIp
fi

2.应用JVM层的监控和方法

虚拟机在物理上主要划分为两个,即新生代(Young Generation)和老年代(Old Generation)。

新生代用来保存那些第一次被创建的对象,它可以被分为3个区:1个Eden区(Eden Space)和2个Survivor区(Survivor Space)。

绝大多数刚刚被创建的对象会存放在Eden区。此后,在Eden区执行了第一次GC之后,存活的对象被移动到其中一个Survivor区。当这个Survivor区达到饱和,还在存活的对象会被移动到另一个Survivor区。之后会清空已经饱和的那个Survivor区。在以上的步骤中重复几次依然存活的对象,就会被移动到老年代。

(1)监控指标。

(2)监控方法。

虚拟机常出现的问题包括内存泄漏、内存溢出和频繁GC导致性能下降等。导致这些问题的原因可以通过下面虚拟机内存的监视手段来进行分析,具体实施时可能需要灵活选择这些手段,同时借助两种甚至更多的手段来共同分析。例如,通过GC日志可以分析出哪些GC较为频繁导致性能下降、是否发生内存泄漏;jstat工具和GC日志类似,同样可以用来查看GC情况、分析是否发生内存泄漏;判断发生内存泄漏后,可以通过结合使用jmap和MAT等分析工具来查看虚拟机内存快照,分析发生内存泄漏的原因;通过查看内存溢出快照可以分析出内存溢出发生的原因等。

GC日志记录。将JVM每次发生GC的情况记录下来,通过观察GC日志可以看出来GC的频度,以及每次GC都回收了哪些区域的内存,从而可以判断是否有内存泄漏发生,并以这些信息为依据来调整JVM相关设置,减小Minor GC发生的频率以及减少FGC发生的次数。

JDK自带命令工具如下。

命令具体的使用方法如表1-4所示。

表1-4 JDK自带命令使用方法

命令名称

使用方法

jstat

实时监视虚拟机运行时的类装载情况、各部分内存占用情况、GC情况、JIT编译情况等。
常用具体参数如下。
▪ -class:统计class loader行为信息。
▪ -compile:统计编译行为信息。
▪ -gc:统计JDK GC时堆信息。
▪ -gccapacity:统计不同的generations(包括新生代、老年代)相应的堆容量情况。
▪ -gccause:统计GC的情况(同-gcutil)和引起GC的事件。
▪ -gcnew:统计GC时,新生代的情况
▪ -gcnewcapacity:统计GC时,新生代堆容量。
▪ -gcold:统计GC时,老年代的情况。
▪ -gcoldcapacity:统计GC时,老年代堆容量。
▪ -gcutil:统计GC时,堆情况。
命令使用:
jstat -class 2083 1000 10(每隔1s监控一次,共做10次,其中2083是进程号)
jstat -gc 2083 2000 20(每隔2s监控一次,共做20次)

jstack

观察JVM中当前所有线程的运行情况和线程当前状态。
值得关注的线程状态如下。
▪ Deadlock,即死锁(重点关注)。
▪ Runnable,即执行中。
▪ Waiting on condition,即等待资源(重点关注)。
▪ Waiting on monitor entry,即等待获取监视器(重点关注)。
▪ Suspended,即暂停。
▪ Object.wait()或TIMED_WAITING,即对象等待中。
▪ Blocked,即阻塞(重点关注)。
▪ Parked,即停止。
命令使用:
jstack <pid>

jmap

观察运行中的JVM物理内存的占用情况。
常用的参数如下。
▪ -heap用来打印JVM堆的情况。
▪ -histo用来打印JVM堆的直方图。其输出信息包括类名,对象数量,对象占用大小。
▪ -histo: live用来打印JVM堆的直方图,但是只打印存活对象的情况。
命令使用:
jmap -heap 2083 (可以观察到具体的新生代和老年代的内存使用情况)
jmap -dump:format=b,file=heap.bin 16113(生成dump文件,获得内存快照后,可以通过MAT工具分析)

 

提示

MAT(Memory Analyzer Tool)是Eclipse的一个插件,使用起来非常方便。尤其是在分析大内存的dump文件时,使用MAT可以非常直观地看到各个对象在堆空间中所占用的内存大小、类实例数量和对象引用关系,可以利用对象查询语言(OQL)查询,以及可以很方便地找出对象GC Roots的相关信息,最吸引人的是它能够快速为开发人员生成内存泄漏报表,方便开发人员定位和分析问题。

 

在macOS的命令行中直接输入jvisualvm命令,或在Windows下找到对应的exe文件双击即可打开Java VisualVM,如图1-7所示。另外,建议安装Visual GC和BTrace Workbench,便于定位问题。

图1-7 macOS下的Java VisualVM工具界面

这是本地的Java进程监控界面,Java VisualVM还可以进行远程的监控。在图1-7左侧导航栏的Applications视图下的Remote处用鼠标右键单击,选择Add Remote Host选项,输入主机IP地址即可添加监控主机。在IP地址上用鼠标右键单击会发现有两种连接Java进程进行监控的方式,jmx和jstatd。一般如果是Tomcat修改启动脚本catalina.sh,需添加如下参数配置:

   JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote 
-Dcom.sun.management.jmxremote.port=9004 
-Dcom.sun.management.jmxremote.authenticate=false 
-Dcom.sun.management.jmxremote.ssl=false 
-Djava.net.preferIPv4Stack=true -Djava.rmi.server.hostname=192.168.0.5"

启动Tomcat,以JMX为例,在IP地址上用鼠标右键单击并选择Add JMX Connection选项,输入IP地址和端口号即可,如图1-8所示。

图1-8 macOS下配置远程应用监控

这里重点推荐一款小巧的代码定位性能的工具——阿里开源工具TProfiler。该工具小巧,安装简便,对代码没有任何侵入性,同时对性能影响也较小,甚至可以用在生产环境进行问题排查。我们只需要在启动参数中加入-javaagent:/xx/tprofiler.jar,即可监控想要监控的方法耗时,并且可以输出报告,非常方便。

首先,访问TProfiler的GitHub主页,单击Code按钮打开下载菜单,选择该菜单下的Download ZIP选项将TProfiler-master.zip下载到本地。

然后,在本地将下载的TProfiler-master.zip解压缩,将dist目录下的profile.properties,以及dist/lib目录下的tprofiler-.jar通过FTP上传到远程服务器/opt/tprofiler目录下。

最后,按如下方式编辑服务器上/opt/tprofiler/profile.properties文件的内容:

#log file name
logFileName = tprofiler.log
methodFileName = tmethod.log
samplerFileName = tsampler.log

#basic configuration items
startProfTime = 9:00:00
endProfTime = 23:00:00
eachProfUseTime = 5
eachProfIntervalTime = 50
samplerIntervalTime = 20
port = 30000
debugMode = false
needNanoTime = false
ignoreGetSetMethod = true

#file paths
logFilePath = ${user.home}/logs/${logFileName}
methodFilePath = ${user.home}/logs/${methodFileName}
samplerFilePath = ${user.home}/logs/${samplerFileName}

#include & excludes items
excludeClassLoader = org.eclipse.osgi.internal.baseadaptor.DefaultClassLoader
includePackageStartsWith =com.caucho;com.defonds;com.fasterxml;com.sun.jersey;com.sun.jmx;
org.apache;org.codehaus;org.jdbcdslog;org.mybatis;org.quartz;org.springframework
excludePackageStartsWith = com.taobao.sketch;org.apache.velocity;com.alibaba;com.taobao. forest.domain.dataobject

(3)操作使用。

首先,输入如下命令启动TProfiler:

-javaagent:/opt/tprofiler/tprofiler-.jar  -Dprofile.properties=/opt/tprofiler/profile.properties

指令操作如下所示。

$ java -cp /opt/tprofiler/tprofiler-.jar com.taobao.profile.client.TProfilerClient 127.0.0.1 30000 status
$ java -cp /opt/tprofiler/tprofiler-.jar com.taobao.profile.client.TProfilerClient 127.0.0.1 30000 stop
$ java -cp /opt/tprofiler/tprofiler-.jar com.taobao.profile.client.TProfilerClient 127.0.0.1 30000 start
$ java -cp /opt/tprofiler/tprofiler-.jar com.taobao.profile.client.TProfilerClient 127.0.0.1 30000 flushmethod

执行此命令会将数据保存到~/logs/目录下生成“TProfiler的日志.png”。

$ java -cp /opt/tprofiler/tprofiler-.jar com.taobao.profile.analysis.SamplerLogAnalysis~/logs/tsampler.log~/logs/method.log~/logs/thread.log
$ java -cp /opt/tprofiler/tprofiler-.jar com.taobao.profile.analysis.ProfilerLogAnalysis~/logs/tprofiler.log~/logs/tmethod.log~/logs/topmethod.log~/logs/topobject.log

执行上述命令显示的topmethod.log部分结果如下:

com/defonds/core/ppts/common/support/JsonUtils:object2jsonString:123 13519 154 2083584
com/caucho/hessian/client/HessianURLConnection:sendRequest:156 15894 130 2072565
com/defonds/rest/core/client/proxy/ResourceJsonInvocationHandler:invoke:39 8123 113 921340
com/defonds/core/ppts/cache/service/impl/MerBankCfgServiceImpl:selectMerBankCfgByParams:  
72 54213 15 799322
com/defonds/core/ppts/incomes/biz/sinopay/service/impl/SinoPayBankReturnServiceImpl4Json:  
updateOrderSuccess:792 2495 176 438542

结果显示的数据分别是方法信息、执行次数、平均执行时间(单位为毫秒)和全部执行时间(单位为毫秒)。

方法执行时间的统计非常重要,它是TProfiler最重要的特性,是TProfiler能够傲视所有其他性能测试类(包括JVM性能测试类)工具的关键所在,我们将会不止一次地在关键的时候受益于TProfiler的这一非常有用的特性。

根据topmethod.log统计结果,我们拿到了热点方法前10位的被调用次数、平均执行时间、全部执行时间,这些都是性能测试过程中重点观察的指标,十分有价值和意义。

 

提示

Arthas(阿尔萨斯)是阿里开源的Java诊断性能监控分析工具,它不需要用户做任何参数配置,就可以直观地获取各种维度的性能数据。通过阅读官网的介绍可以看到,当我们遇到类似以下问题而束手无策时,Arthas可以帮助我们解决。

▪ 这个类是从哪个jar包加载的?为什么会报各种类相关的异常?

▪ 我改的代码为什么没有运行?难道是我没提及?分支搞错了?

▪ 遇到问题无法在线上调试,难道只能通过加日志再重新发布吗?

▪ 线上遇到某个用户的数据处理有问题,但线上无法调试,线下无法重现?

▪ 是否有全局视角来查看系统的运行状况?

▪ 有什么办法可以监控到JVM的实时运行状态?

Arthas支持JDK 6+,支持Linux/macOS/Windows,采用命令行交互模式,同时提供丰富的Tab自动补全功能,进一步方便开发人员进行问题的定位和诊断。

 

3.数据库层监控

本书重点讲解使用最频繁的MySQL数据库。

(1)监控指标。

(2)监控方法。

淘宝开源工具OrzDBA是淘宝DBA团队开发出来的perl监控脚本,主要功能是监控MySQL数据库。其小巧精致,而且消耗资源也非常少。

最常使用的方法如下:

./orzdba -S /tmp/mysql.sock–mysql
--------           -QPS- -TPS-     -Hit%- ------threads------ -----bytes----     
 time     | ins  upd   del  sel  iud |     lor  hit| run con cre cac|  recv  send|
16: 21: 21| 0    0     0    0    0   |     0 100.00|  0    0   0   0|     0     0|
16: 21: 22| 1556 2031  120  5993 3707|  92736 99.62| 17   18   0   3|  406k  2.2m|  
16: 21: 23| 1601 2087  121  6179 3809|  98670 99.62| 17   18   0   3|  424k  2.2m|  
16: 21: 24| 1886 2452  144  7118 4482| 115044 99.63| 17   18   0   3|  497k  2.6m| 
16: 21: 25| 1618 2131  124  6425 3873| 100295 99.64| 17   18   0   3|  435k  2.3m|
16: 21: 26| 1872 2477  153  7258 4502| 115035 99.65| 17   18   0   3|  497k  2.6m|

从上面的结果可以看到,通过这个方法可以监控MySQL的增、删、改、查的速度,缓存命中率,线程连接状态,还有带宽情况,这几个都是性能测试最核心的指标。

另外,可以通过调用tcprstat来监控MySQL的响应时间:

./orzdba -rt -C 10 -i 1 -t -d sda
-------- -------------------------io-usage----------------------- --------tcprstat(us)-------- 
    time| r/s w/s rkB/s wkB/s queue await svctm %util|count avg 95-avg 99-avg|
16:53:22| 0.0 0.0   0.1   0.0   0.0   0.4   0.4   0.0|    0   0      0      0|
16:53:24| 0.0 0.0   0.0   0.0   0.0   0.0   0.0   0.0|    0   0      0      0|
16:53:25| 0.0 0.0   0.0   0.0   0.0   0.0   0.0   0.0|    0   0      0      0|
16:53:26| 0.0 0.0   0.0   0.0   0.0   0.0   0.0   0.0|    0   0      0      0|
16:53:27| 0.0 0.0   0.0   0.0   0.0   0.0   0.0   0.0|    0   0      0      0|
16:53:28| 0.0 0.0   0.0   0.0   0.0   0.0   0.0   0.0|    0   0      0      0|
16:53:29| 0.0 0.0   0.0   0.0   0.0   0.0   0.0   0.0|    0   0      0      0|
16:53:30| 0.0 0.0   0.0   0.0   0.0   0.0   0.0   0.0|    0   0      0      0|

其中一些输出的含义如下。

自定义Shell脚本的方法原理是通过show status获取服务器状态信息,我们也可以使用mysqladmin extended-status命令获得。

show status可以根据需要显示会话级别的统计结果和全局级别的统计结果。例如,显示当前会话级的信息命令show status like "Com_%";显示全局级别的信息命令show global status。

以下几个参数对MyISAM和InnoDB存储引擎都计数:

以下几个参数是针对InnoDB存储引擎计数的,累加的算法也略有不同:

以下是用Shell脚本实现的监控模板,供读者借鉴:

mysqladmin -usystem -p*** -h127.0.0.1 -P3306 -r -i 1 extended-status \
 | grep "Questions\|Queries\|Innodb_rows\|Com_select \|Com_insert \|Com_update \|Com_delete "
| Com_delete                  | 44       |
| Com_insert                  | 39796    |
| Com_select                  | 497645   |
| Com_update                  | 34154    |
| Innodb_rows_deleted         | 22       |
| Innodb_rows_inserted        | 138254   |
| Innodb_rows_read            | 7600681  |
| Innodb_rows_updated         | 39184    |
| Queries                     | 867858   |
| Questions                   | 859168   |

 

提示

目前互联网系统架构多采用分布式和微服务,打造立体化监控体系,提供分布式调用跟踪、梳理服务依赖关系、得到用户行为路径、可视化各个阶段的耗时等服务,对于定位问题和故障给予快速响应。

目前有很多应用性能管理(Application Performance Management,APM)的解决方案(开源的和未开源的)。

• Google的Drapper,最早的APM。

• 鹰眼,阿里未开源内部系统。

• 听云,国内端到端应用性能管理解决方案提供商。

• 大众点评CAT,跨服务的跟踪功能与大众点评内部的RPC框架集成。

• hydra,京东开源的基于Dubbo的调用分布跟踪系统,与Dubbo框架集成,对于服务级别的跟踪统计,可以无缝接现有业务。

• Pinpoint,采用字节码探针技术,代码无侵入,体系完善不易修改,支持Java,技术栈支持Dubbo。

• Zipkin方便集成Spring Cloud,社区支持的插件包括Dubbo、Rabbit、MySQL、HttpClient等,代码入侵度小。

 

性能问题的分析、定位或者调优,很大程度是一种技术问题,需要测试人员具备多方面的专业知识。数据库、操作系统、网络等方面的管理和技术开发都是一个合格的性能测试人员需要拥有的技能。只有这样,才能从多角度全方位地去考虑分析问题。本节分 4 个方面进行深入讲解,分别是性能问题分析方法论、性能数据解读建议、性能常见问题和案例、性能调优建议。

1.性能问题分析方法论

性能问题的定位排查过程比较复杂,可以采用“拆分问题,隔离分析”的方法进行分析,即逐步定位、从外到内、由表及里、逐层分解、隔离排除。以下的分析顺序供读者参考:日志分析→网络瓶颈(对局域网可以不考虑这一层)→服务器操作系统瓶颈(参数配置)→中间件瓶颈(参数配置、Web服务器等)→应用瓶颈(业务逻辑、算法等)→数据库瓶颈(SQL语句、数据库设计、索引)。

其实这也是根据请求流入系统的顺序层层递进分析的,而上面每一环节的监控方法在性能测试的监控部署这一节都有涉及。接下来,我们需要解读监控收集的数据,分析异常,由此可见性能测试是环环相扣的、系统性的工作。

另外,做性能测试的时候,我们一定要确保瓶颈不发生在自己的测试脚本和测试工具上。

基于上述思想的指导,在具体执行层面,可以参考如下分析过程。

首先,当系统有问题的时候,我们不要急于去调查我们代码,这毫无意义。我们首先需要看的是操作系统的报告。看看操作系统的CPU利用率、内存使用率、操作系统的IO,还有网络的IO和网络连接数等。通过观察以下这些最直观的数据,我们就可以知道软件的性能问题基本上出在哪里。

(1)查看CPU利用率,如果CPU利用率不高,但是系统的吞吐量和响应上不去,这说明我们的程序并没有忙于计算,而是忙于别的一些事,例如网络的IO。另外,CPU的利用率还要看内核态的和用户态的,一旦内核态的上去了,整个系统的性能就下来了。而对于多核CPU,CPU 0是相当关键的,如果CPU 0的负载高,则会影响其他核的性能,因为CPU各核间是需要调度的,这靠CPU 0完成。

(2)查看IO大不大,IO与CPU利用率相反,CPU利用率高则IO不大,IO大则CPU就小。关于IO,我们要看3个指标,即磁盘文件IO、网卡的IO、内存换页率。这3个指标都会影响系统性能。

(3)查看网络带宽使用情况。在Linux下,可以使用sar、iptraf、tcpdump这些命令来查看。

(4)查看系统的连接数是不是接近65535,TIME_WAIT的连接是不是非常多。若连接数较大,可能配置是有问题的。

如果CPU利用率不高、操作系统的IO不高、内存使用率不高、网络带宽使用率不高,但是系统的性能依然上不去,这说明我们的程序有问题。例如,我们的程序被阻塞了,可能是因为等某个锁、可能是因为等某个资源,或者是在切换上下文。

通过了解操作系统的性能,我们才能知道性能的问题,例如带宽不够、内存不够、TCP缓冲区不够等。很多时候不需要调整程序,只需要调整一下硬件或操作系统的配置就可以了。具体配置项的调优,可以参考调优章节。

接下来,我们需要使用性能检测工具。可以使用性能测试的监控部署这一节中讲到的阿里开源工具TProfiler,统计top的方法耗时,结合Java自带的jstack命令,查看每个线程的运行状态,定位代码有无死循环、逻辑错误等问题。

最后,其实很多时候性能问题最终都会落到数据库上,因为磁盘的读写能力永远是跟不上CPU的计算能力的。而对于数据库,我们可以通过解读淘宝开源工具OrzDBA的监控数据,分析有无出现瓶颈,是否缺失数据索引等。

以上就是性能问题分析的一种思路,从服务器操作系统层面、中间件层、应用层、数据库层,逐层排查定位。

2.性能数据解读建议

性能问题分析过程也是一个解读数据的过程,读懂了数据我们就能知道问题出在何处。随着经验的累积,我们将会很容易判断问题的根源所在,甚至在开发阶段就及时规避可能出现的问题。

表1-5中列出一些常见的性能问题异常特征和相应的分析工具。

表1-5 常见的性能问题异常特征和分析工具

性能指标类型

性能问题异常特征

分析工具

TPS及其波动范围

1.TPS有明显的大幅波动,不稳定。例如TPS轨迹缓慢下降、缓慢上升后骤降、呈瀑布型、呈矩形、分时间段有规律的波动、无规律的波动等。这些TPS的波动轨迹反映出被测试的性能点存在异常,需要性能测试工程师与开发工程师查找异常的原因
2.TPS轨迹比较平稳,但是也存在波动现象。该类波动不明显,很难直接确定是否存在异常。我们需要根据其他指标来进行判断

JMeter

响应时间

1.关注高峰负载时,用户操作的响应时间
2.关注数据库增量对用户操作响应时间的影响

JMeter

Web/数据库服务器内存

1.很高的换页率
2.频繁使用交换区,si、so值较大
3.交换区所有磁盘的活动次数过高
4.内存不够出错(Out Of Memory Errors)

top/vmstat/free

Web/数据库服务器CPU

1.响应时间很慢
2.CPU空闲时间为零
3.用户占用CPU时间过高
4.系统占用CPU时间过高
5.长时间的运行进程队列很长

top

Web/数据库服务器磁盘IO

1.磁盘利用率过高
2.磁盘等待队列太长
3.等待磁盘IO的时间所占的百分比太高
4.物理IO速率太高
5.缓存命中率过低
6.运行进程队列太长,但CPU却空闲

iostat

MySQL数据库

1.缓存命中率小于0.90
2.前10位SQL语句耗时高

OrzDBA

3.性能常见问题和案例

当性能测试实战经验丰富后,会发现常见的性能问题可以分为3类——CPU类、内存类和配置类,这里不考虑架构设计的合理性。

CPU类。代码或MySQL都可能会导致CPU爆满。CPU利用率高不是问题,由CPU利用率高引起的负载高才是问题,负载是判断系统能力指标的依据。

为什么这么说呢?以单核CPU为例,我们日常的CPU利用率在20%~30%,这其实是浪费CPU资源的,这意味着绝大多数时候CPU并没有在做事。理论上,一个系统极限的CPU利用率可以达到100%,这意味着CPU完全被利用起来处理计算密集型任务了,例如for循环、md5加密、新建对象等。但是实际不可能出现这种情况,因为应用程序中不存在消耗CPU的IO是几乎不可能的,例如读取数据库或者读取文件,所以CPU利用率不是越高越好,通常75%是一个需要引起警戒的经验值。

例如,服务部署在阿里云上,某天突然收到告警通知,CPU利用率超过阈值75%,连续告警一段时间。

在一个Java应用中,排查CPU利用率高的思路通常比较简单,有比较固定的做法,实际排查问题的时候建议打印jstack命令结果5次(至少3次),根据多次的栈内容,再结合相关代码段进行分析,定位高CPU利用率出现的原因。高CPU利用率可能是代码段中某个bug导致的而不是栈打印出来的那几行导致的。

(1)通过top命令找出CPU资源消耗过高的进程。

top H打开线程显示开关。

(2)找出进程对应的线程:

ps -mp pid -o THREAD,tid,time

(3)其次,将需要的线程ID转换为十六进制格式:

printf "%x\n" tid

(4)打印线程栈信息:

jstack pid |grep tid -A 30

(5)据此找到对应的代码逻辑。

"http-nio2-9386-exec-25" #52 daemon prio=5 os_prio=31 tid=0x00007ff46d6800 nid=0x03 waiting on condition [0x651000]
java.lang.Thread.State:WAITING (parking)
     at sun.misc.Unsafe.park(Native Method)
     - parking to wait for <0x00000007816cdbc8> (a 
java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
     at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
     at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await  
(AbstractQueuedSynchronizer.java:2039)
     at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:442)
     at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:103)
     at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:31)
     at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1067)
     at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1127)
     at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
     at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
     at java.lang.Thread.run(Thread.java:748)

根据以上常规的利用jstack定位CPU的问题套路,我们编写了如代码清单1-3所示的脚本。

代码清单1-3 jstack_check.sh

#!/bin/bash
##system:centos 
##update:
##author:hutong
pid=$1
sfile="/tmp/java.$pid.trace"
tfile="/tmp/java.$pid.trace.tmp"
rm -f $sfile $tfile
echo "pid $pid"

jstack $pid > $tfile
ps -mp $pid -o THREAD,tid,time|awk '{if ($2>0 && $8 != "-") print $8,$2}'|while read line;
do
    nid=$(echo "$line"|awk '{printf("0x%x",$1)}')
    cpu=$(echo "$line"|awk '{print $2}')
    echo "nid: $nid,cpu: $cpu %">>$sfile
    lines='grep $nid -A 100 $tfile |grep -n '^$'|head -1|awk -F':' '{print $1}''
    ((lines=$lines-1))
    if [ "$lines" = "-1" ];
    then
       grep $nid -A 100 $tfile >>$sfile
       echo '' >>$sfile
    else
       grep $nid -A $lines $tfile >>$sfile
    fi
done
rm -f $tfile
echo "read msg in $sfile"
########### end ############

执行上述脚本后的结果如下,通过分析我们可以迅速找到相关代码,然后相应地去解决问题。

nid:0x1fe1,cpu:0.1 %
"main" #1 prio=5 os_prio=0 tid=0x00007fdc008800 nid=0x1fe1 waiting on condition [0x00007fdc23829000]
  java.lang.Thread.State:TIMED_WAITING (sleeping)
  at java.lang.Thread.sleep(Native Method)
  at com.lihongkun.example.StackExample.main(StackExample.java:8)

nid:0x1feb,cpu:0.1 %
"VM Periodic Task Thread" os_prio=0 tid=0x00007fdc144800 nid=0x1feb waiting on condition 

nid:0x,cpu:1.0 %
"Attach Listener" #8 daemon prio=9 os_prio=0 tid=0x00007fdbe8001000 nid=0x200f waiting on condition [0x0000000000000000]
  java.lang.Thread.State:RUNNABLE

 

注意

不同的系统用途也不同,要找到性能瓶颈需要知道系统运行的是什么应用、有什么特点,例如Web服务器对系统的要求肯定和文件服务器不一样,所以分清不同系统的应用类型很重要,通常应用可以分为两种类型。

一种是IO相关的应用,通常用来处理大量数据,需要大量内存和存储,频繁IO操作读写数据,而对CPU的要求则较少,大部分时候CPU都在等待硬盘,例如数据库服务器、文件服务器等。

另一种是CPU相关的应用,需要使用大量CPU,例如高并发的Web/邮件服务器、图像/视频处理、科学计算等都可被视为CPU相关的应用。

 

内存类。内存异常、内存泄漏是最常见的问题。

例如,使用JMeter对保险公司投保接口进行压力测试,增大压力开始测试后,返回很多错误请求。观察后台接口日志,具体错误如下:

     2019-03-30 16:59:46.012 [http-nio-7112-exec-6] ERROR o.a.c.c.C.[.[localhost].[/].  [dispatcherServlet] - Servlet.service() for servlet [dispatcherServlet] in context with path []  
threw exception [Handler dispatch failed; nested exception is java.lang.OutOfMemoryError:  
unable to create new native thread] with root cause
     java.lang.OutOfMemoryError:unable to create new native thread
     at java.lang.Thread.start0(Native Method)
     at java.lang.Thread.start(Thread.java:717)
     at java.util.concurrent.ThreadPoolExecutor.addWorker(ThreadPoolExecutor.java:950)
     at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1357)
     at java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:112)
     at com.netease.baoxian.service.InsuranceService.insured(InsuranceService.java:110)
     at com.netease.baoxian.service.ServiceOrderService.addServiceOrder(ServiceOrderService.java:126)
     at com.netease.baoxian.controller.SceneController.insure(SceneController.java:180)
     at sun.reflect.GeneratedMethodAccessor154.invoke(Unknown Source)
     at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
     at java.lang.reflect.Method.invoke(Method.java:498)
     at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke  (InvocableHandlerMethod.java:205)
     at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest  (InvocableHandlerMethod.java:133)

原因分析如下。

JVM向操作系统申请创建新的原生线程(native thread)时,有可能会碰到“java.lang.OutOfMemory Error:Unable to create new native thread”错误。如果底层操作系统创建新的原生线程失败,JVM就会抛出相应的Out Of Memory Error。总体来说,导致“java.lang.OutOfMemoryError:Unable to create new native thread”错误的场景大多经历以下这些阶段。

(1)Java程序向JVM请求创建一个新的Java线程,JVM本地代码(native code)代理该请求,尝试创建一个操作系统级别的原生线程。

(2)操作系统尝试创建一个新的原生线程,同时需要分配一些内存给该线程。

(3)如果操作系统的虚拟内存已耗尽,或者是受到32位进程的地址空间限制(2 GB~4 GB),操作系统就会拒绝本地内存分配。

(4)JVM抛出“java.lang.OutOfMemoryError:Unable to create new native thread”错误。

根据栈信息,我们定位到问题代码出现在下面这个方法里,此方法实现的功能是异步调用保险公司投保接口:

public void insured(ServiceOrder serviceOrder,boolean isSync){
    if(isSync){
      insured(serviceOrder);
    }else{
      ExecutorService es = Executors.newFixedThreadPool(1);
      es.submit(()->insured(serviceOrder));
      es.shutdown();
    }
  }

分析这段代码,开发人员在处理异步任务时,使用了线程池。使用线程池处理多线程的好处有:重用了存在的线程,减少了对象创建、消亡的开销,性能佳;可有效控制最大并发线程数,提高了系统资源的使用率,同时避免了过多资源竞争,避免了堵塞;提供了定时执行、定期执行、单线程和并发数控制等功能。

问题就出现在“ExecutorService es = Executors.newFixedThreadPool(1);”这一行代码上。new FixedThreadPool方法的功能是创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。开发人员在此处创建线程池时使用了局部变量,每个请求进入这个方法时,都会新建一个固定线程个数为1的线程池。就这样大量并发请求进入后,新建了大量的线程,导致系统虚拟内存被耗尽,JVM抛出“java.lang.OutOfMemoryError:Unable to create new native thread”错误。

配置类。很多时候各个中间件本身的参数是不能满足业务的性能需求的,需要根据实际用户量灵活地调整。例如,数据库连接池不够用导致响应时间久。

问题现象:在测试一个场景时,我们发现响应时间很长,但日志无报错现象。根据调用链接逐级定位,我们发现80%的时间都是消耗在数据访问对象(Data Access Object,DAO)层的方法上,这时首先考虑的是SQL会不会有问题?于是找数据库管理员(Database Administrator,DBA)帮忙抓取SQL看一下,但DBA反映SQL执行很快,执行计划也没有问题,那问题出现在哪里呢?找不到原因就看一下线程栈,我们看看系统在执行DAO层的方法后做了什么。jstack线程栈如下:

    "DubboServerHandler-10.165.184.51:20881-thread-200" daemon prio=10 tid=0x2fd6208800  
nid=0x504b waiting on condition [0x2fc0280000]
    java.lang.Thread.State:TIMED_WAITING (parking)
    at sun.misc.Unsafe.park(Native Method)
    - parking to wait for <0x0> (a java.util.concurrent.locks.  
AbstractQueuedSynchronizer$ConditionObject)
    at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:226)
    atjava.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos  
(AbstractQueuedSynchronizer.java:2082)
    at com.alibaba.druid.pool.DruidDataSource.pollLast(DruidDataSource.java:1487)
    at com.alibaba.druid.pool.DruidDataSource.getConnectionInternal(DruidDataSource.java:1086)
    at com.alibaba.druid.pool.DruidDataSource.getConnectionDirect(DruidDataSource.java:953)
    at com.alibaba.druid.filter.FilterChainImpl.dataSource_connect(FilterChainImpl.java:4544)
    at com.alibaba.druid.filter.logging.LogFilter.dataSource_getConnection(LogFilter.java:827)
    at com.alibaba.druid.filter.FilterChainImpl.dataSource_connect(FilterChainImpl.java:4540)
    at com.alibaba.druid.pool.DruidDataSource.getConnection(DruidDataSource.java:931)
    at com.alibaba.druid.pool.DruidDataSource.getConnection(DruidDataSource.java:923)
    at com.alibaba.druid.pool.DruidDataSource.getConnection(DruidDataSource.java:100)
    at org.springframework.jdbc.datasource.DataSourceUtils.doGetConnection(DataSourceUtils.java:111)
    at org.springframework.jdbc.datasource.DataSourceUtils.getConnection(DataSourceUtils.java:77)
    at org.mybatis.spring.transaction.SpringManagedTransaction.openConnection  
(SpringManagedTransaction.java:81)
    at org.mybatis.spring.transaction.SpringManagedTransaction.getConnection  
(SpringManagedTransaction.java:67)
    at org.apache.ibatis.executor.BaseExecutor.getConnection(BaseExecutor.java:279)
    at org.apache.ibatis.executor.SimpleExecutor.prepareStatement(SimpleExecutor.java:72)
    at org.apache.ibatis.executor.SimpleExecutor.doQuery(SimpleExecutor.java:59)
    ...

问题分析过程如下。

我们先关注线程状态,发现栈信息里大量的Dubbo线程处于TIMED_WAITING状态。从waiting on condition可以看出系统在等待一个条件发生,这时的线程处于休眠(sleep)状态。一般系统会有超时时间唤醒,这里出现TIMED_WAITING状态很正常,一些等待IO都会出现这种状态,但是出现大量的TIMED_WAITING状态就要找原因了。接下来,我们观察线程栈,发现处于TIMED_WAITING状态的线程都在等待Druid获取连接池的连接,这种现象很像连接池不够用了。于是增加数据库连接池的连接数,然后TPS直接提升了3倍。

最后再做一下简要总结分析CPU占用和内存性能的方法和手段。

(1)分析CPU占用的方法和手段如下。

(2)分析内存性能的方法和手段如下。

另外,典型的影响性能的问题有以下几个。

更多日常性能测试遇到的典型问题如表1-6所示。

表1-6 日常性能测试遇到的典型问题

现象类别

典型问题现象描述

问题类别

问题定位过程描述

解决方法或优化建议

内存

压测一定时间后,RabbitMQ内存占用过高。
涉及接口:充值接口

代码

定位问题:代码问题。
Rabbit MQ只有生产者队列,没有消费者队列,这导致大量数据堆积,占用内存

增加消费者,对生产者和消费者进行平衡,避免数据堆积

CPU

压测一定时间后,Tomcat启动CPU超负载,最后提示内存溢出。
涉及接口:充值接口

代码

定位问题:代码问题。
Tomcat启动时默认读取Redis中的所有数据,Redis中有大量压测数据导致Tomcat启动失败

修改代码,Tomcat启动时不加载全部Redis中的数据

慢查询

数据库慢查询日志中频繁出现相同的几句SQL语句。
涉及接口:充值接口

数据库

定位问题:数据库问题。
做数据插入操作时会同时进行3张表的查询,导致较多慢查询出现

将数据缓存到Redis中,增加了相关表的索引

磁盘

交换区利用率过高。
涉及接口:充值接口

配置

定位问题:系统配置问题。系统参数swapness为默认,当内存用到一定程度会自动利用交换区

修改了系统参数swapness=10

其他

10个并发情况下,响应时间过长。
涉及接口:留存用户趋势情况查询、回访用户趋势情况查询(查询接口)

数据库

定位问题:数据库SQL语句问题。
添加数据库慢查询后,发现SQL语句耗时并不是很久,但接口总体耗时久,打印日志发现SQL语句个数较多

合并SQL语句,把多次查询获取数据改成通过一个SQL语句来获得数据,减少数据库查询次数

CPU

数据库的CPU,接近满负荷运行

数据库

查看慢查询结果,发现SQL语句花费时间较长,且无索引

数据库添加及优化索引

CPU

MySQL服务器的多核CPU的iowait一直大于20%,并且都集中在CPU 0这一个核上,发布不均匀

数据库

经过DBA的检查,怀疑是数据库版本问题,在另一台数据库服务器上做测试实验,正常

经过DBA的检查,后来重新安装了MySQL版本,从原来的tar包安装版本改为二进制安装的5.6.30版本

其他

性能业务指标的TPS超级小,基本上只有0.8

配置

应用服务器内存使用到交换区,CPU满了,FGC频繁

一开始默认启动该jar包时,没加参数。默认的永久代区为98MB
解决方法:java-XX:PermSize= -XX:MaxPermSize=- Xms-Xmx-Xmn -jar../release/otacm-service -impl--SNAPSHOT.jar

CPU

MySQL服务器的CPU接近满负荷

配置

大部分语句在SELECT count(*)FROM cardprofile

开启MySQL的缓存功能

其他

出现一定量的服务器异常的请求

配置

发现MySQL很多连接处理WAITING状态,导致Dubbo的请求超时

增大MySQL的连接数

CPU

测试获取 token 业务时,并发线程数200,TPS低于10,CPU消耗超过95%

代码

查看发现TPS过低,业务处理时间久,主要消耗在登录加密过程中

解决方法:
▪ 将写操作改为异步;
▪ 减小加密算法的强度,TPS可达到300;
▪ 添加用户信息缓存;
▪ 减少存放Redis的键值数量

其他

测试获取我的列表业务时,TPS 小于100,CPU 存在波动,Redis内存不断消耗完

代码

查看Redis内存不断消耗后,定位到Redis中操作过多,键值不断增加

减少Redis操作,增加缓存,TPS可达到500以上,接着将未读数量、附件数量等放入统一的Redis键中,避免循环操作Redis

慢查询

测试邀请用户业务时,数据库日志中查看到慢查询语句,查询时间超过1s

数据库

查看慢查询结果,发现SQL语句花费时间较长

解决方法:
▪ 优化了邀请单人加入的逻辑;
▪ 异步处理发短信、推送通讯录变更、同步系统群逻辑

其他

压测并发线程数200注册业务时,用户信息同步Openfire,错误显示为主键冲突

代码

在批量同步用户信息至Open fire时才发现此现象,单个用户导入并没有问题,进一步查看代码中错误输出,得知为计算机编码问题引起

解决方法:问题主要是由于部署多台服务器时,生成主键的方法需做分布式同步,通过增加计算机编码来解决

其他

压测并发线程数200创建某业务,压测1min后请求无法发送,服务器返回500内部错误

配置

性能测试环境中网关配置为默认配置,默认单个模块的最大并发数量为10

根据单台性能测试需求修改为500,修改参数:eureka.yq-mod- corp.semaphore.maxSemap hores= 500

慢查询

压测获取某列表业务时,TPS低,数据库存在慢查

数据库

数据库慢查询日志中输出慢查询语句,查询时间超过2s

解决方法:
▪ 增加查询缓存;
▪ 优化查询语句,去除统计字段的计算,改为入库时统计

其他

Redis服务器上Ping操作频繁

配置

压测时Redis服务器内存消耗不断升高,使用命令in fo commandstats查看cmdstat _ping:calls=263354524,usec= 484208487,usec_per_call=1.84中ping操作频繁

修改应用服务器中Redis配置项redis.pro perties,将redis. pool.testOnBor row= true和redis. pool.testOnReturn=true中的true改为false

CPU

数据上报接口稳定性测试过程中,CPU利用率会持续升高

配置

定位问题:Storm Spout没有开启限速机制,导致CPU利用率持续升高

开启Storm Spout限速机制

其他

证书申请流程,达到一定并发后,3%左右的p10上行处理异常,平台日志打印空指针

代码

并发时,上行的4条p10缓存中可能还没放进去就GET,导致GET不到

当GET不到时,添加等待时间

其他

签名认证流程,签名认证响应上行推送失败,报出异常

代码

模拟网关上行响应较快,上行之后缓存会清理掉,而此时平台侧还没处理完流程,依赖缓存数据,所以报错

不主动删除缓存,让缓存10min后自动生效

其他

签名认证流程,响应时间平均在1s以上,响应时间性能不达标

代码

签名短信发送完成之后等待了1s,导致响应时间平均在1s以上

去掉等待时间

其他

签名认证流程,压测脚本执行几分钟后,错误率100%,返回500错误,应用服务器执行任何命令都不能分配内存,但是服务器内存使用率并不高

代码

签名请求时,每个请求都启动了一个TimerTask,根据设置的超时时间进行等待响应,使并发请求时间长了之后,进程过多导致分配失败

将TimerTask去掉,让业务方去判断超时

内存

堆溢出

配置

java.lang.OutOfMemoryError: Java heap space

优化建议:通过-Xmn(最小值)、–Xms(初始值)、-Xmx(最大值)参数手动设置堆的大小

内存

PermGen Space溢出(永久代区溢出、运行时常量池溢出)

配置

java.lang.OutOfMemoryError: PermGen space

优化建议:通过MaxPermSize参数设置PermGen space大小

内存

栈溢出(虚拟机栈溢出、本地方法栈溢出)

配置

java.lang.StackOverflowError

优化建议:通过Xss参数调整

4.性能调优建议

性能调优是一个非常大的议题,更多的是由开发人员来进行。测试人员可以了解一些通用的调优方法,并根据性能分析过程中发现的问题,给出一些建议。当然,随着性能测试经验的积累,测试人员也会知道很多开发人员不知道的调优方法。

(1)设计优化。

设计优化处于所有调优手段的上层,它往往需要在软件开发之前进行。在软件开发之初,架构师就应该评估软件可能存在的各种潜在问题,并给出合理的设计方案。由于软件设计和架构对软件整体质量有决定性的影响,因此设计优化对软件性能的影响也是最大的。

从某种程度上说,设计优化直接决定了软件的整体品质。如果在设计层考虑不周,留下太多隐患,那么这些“质”的问题,也许无法通过代码层的优化进行弥补。因此,开发人员必须在软件设计之初,认真仔细考虑软件系统的性能问题。

进行设计优化时,设计人员必须熟悉常用的软件设计方法、设计模式、基本性能组件和常用优化思想,并将这些有机地集成在软件系统中。

注意,一个良好的软件设计可以规避很多潜在的性能问题。因此,尽可能多花些时间在系统设计上是创建高性能程序的关键。

(2)算法优化。

算法非常重要,好的算法会使系统有更好的性能。例如,分而治之和预处理的思路。某程序为了生成月报表,每次都需要计算很长的时间,有时候需要花费将近一整天的时间。于是我们找到了一种方法将这个算法改成增量式的,也就是说我们每天都把当天的数据计算好之后和前一天的报表数据合并,这样就大大节省了计算时间。调整之后,每天的数据计算时间需要大约20min,但是如果我一次性计算整个月的数据,系统需要10h以上(SQL语句在大数据量面前性能成级数下降)。这种分而治之的思路对大数据应用的性能有很大的帮助。

(3)代码优化。

代码优化是在软件开发过程中或者在软件开发完成后的软件维护过程中进行的,对程序代码的改进和优化。代码优化涉及诸多编码技巧,需要开发人员熟悉相关语言的API,并在合适的场景中正确使用相关API或类库。同时,对算法、数据结构的灵活使用,也是实现代码优化的必备技能。

虽然代码优化是从微观上对性能进行调整,但是一个“好”的实现和一个“坏”的实现对系统影响的差异也是非常大的。例如,同样作为List的实现,LinkedList和ArrayList在随机访问上的性能可以相差几个数量级;同样是文件读写的实现,使用Stream方式与Java NIO方式,其性能可能又会相差一个数量级。

因此,虽然与设计优化相比,这里将代码优化称为在微观层面上的优化,但是它却是对系统性能产生最直接影响的调优方法。

(4)JVM优化。

由于Java程序总是运行在JVM之上,因此对JVM进行优化也能在一定程度上提升Java程序的性能。JVM优化通常可以在软件开发后期进行,如在软件开发完成时或者在软件开发的某一里程碑阶段。

作为Java程序的运行平台,JVM的各项参数将会直接影响Java程序的性能。例如,JVM的堆大小和GC策略等。

要进行JVM层面的优化,需要开发人员对JVM的运行原理和基本内存结构有一定的了解,如堆的结构和GC的种类等。进而,依据应用程序的特点,设置合理的JVM启动参数。

(5)参数优化。

(6)数据库优化。

对绝大部分应用系统而言,数据库是必不可少的一部分。Java程序可以使用JDBC的方式连接数据库。对数据库的优化可以分为3个部分:在应用层对SQL语句进行优化、对数据库进行优化和对数据库软件进行优化。

数据库优化是一个很大的话题,下面是作者总结的一些经验。

数据库引擎调优。数据库的锁的方式非常的重要,因为在并发情况下,锁是非常影响性能的。它包括各种隔离级别,行锁、表锁、页锁、读写锁、事务锁,以及各种写优先和读优先机制。要想达到最高的性能最好是不要锁,所以,分库分表、冗余数据、减少一致性事务处理,可以有效地提高性能。NoSQL就是牺牲了一致性事务处理,并冗余数据,从而达到了分布式和高性能。

数据库的存储机制。不但要搞清楚各种类型字段是怎么存储的,更重要的是了解数据库的数据存储方式,即它是怎么分区、怎么管理的。了解清楚这个机制可以减轻很多的IO负载。例如,在MySQL下使用show engines,可以看到各种存储引擎的支持。不同的存储引擎有不同的侧重点,针对不同的业务或数据库设计会有不同的性能。

数据库的分布式策略。最简单的就是复制或镜像,需要了解分布式的一致性算法,或是主主同步、主从同步。通过了解这种技术的机理可以做到数据库级别的水平扩展。

SQL语句优化。关于SQL语句的优化,首先是要使用工具,例如MySQL的SQL Query Analyzer可以用于查看应用中的SQL的性能问题,还可以使用explain来查看SQL语句最终的Execution Plan是什么样的。

还有一点很重要,数据库的各种操作需要大量的内存,所以服务器的内存要够,尤其应对那些多表查询的SQL语句,是相当耗内存的。

其他一些建议。

以上是性能问题的定位分析的所有内容,知识点比较多,大家需要好好消化吸收,并在实战中多应用实践。另外在此引入一个分析问题的方法论,套用 5W2H 方法,可以提出性能分析的几个问题。

Who:发生了什么现象?

What:现象是什么样的?

When:什么时候发生?

Why:为什么会发生?

Where:哪个地方发生的问题?

How much:耗费了多少资源?

How to do:怎么解决问题?

 

提示

面对流量洪峰的策略如下。

(1)服务降级。服务降级是指在特定情况下,例如“双十一”“双十二”期间,当流量超过系统服务能力时,跳过特定的处理流程。比如在一个买家下单后,我们可能需要进行风险评估、数据校验等一系列流程,当发生服务降级时,就跳过了数据校验逻辑,以避免用户长时间等待,降低对下游链路的冲击,保证服务的稳定性。服务降级是面对流量洪峰保证用户体验和预防系统崩溃的有效手段。

(2)服务限流。服务限流是指根据服务的处理能力提前预估一个阈值,当流量大于该阈值时放弃处理直接返回错误。服务限流是应对流量达到峰值时,系统进行自我保护的重要措施。例如“双十一”零点下单峰值、余额宝九点抢购峰值以及活动结束商品信息编辑峰值,都需要进行相应的限流来保护系统。

(3)故障容灾。单机系统的容灾能力几乎为零,一旦服务崩溃就马上变成不可用。分布式系统通过异地多活,可以不间断地提供服务。同时,借助于Nginx、Apache进行负载均衡可以进一步提高可用性。

实际上,即便进行了负载均衡和服务分布式部署,系统仍然面临容灾问题。现在的大型服务,例如淘宝、天猫、微信、京东都进行了异地多活的部署。异地多活部署的主要目的是,通过多机房提供服务来降低单机房故障带来的影响,提高容灾能力。

 

经过多轮重复测试和优化,在满足性能需求后,需要编写性能测试报告,及时做好知识沉淀和总结,吸取经验,避免以后二次踩坑。

性能测试报告中至少要包括系统的测试范围、测试目标、测试方法、场景设计说明、测试数据准备、测试计划、测试结果,以及优化过程和建议等。

性能测试报告中,我们需要重点描述多轮测试和优化所做的改动和调整,并把相关的变动及时同步给运维人员和开发人员,避免上线的时候没做调整,出现线上故障,这也就是之前说的闭环的重要性。

性能测试的终点不是发布上线,上线之后我们应该继续跟踪性能情况,并将收集的数据结果用于下一次性能测试的需求分析,将线下与线上真正关联起来,形成系统的闭环,如图1-9所示。

图1-9 性能测试闭环流

通过建立性能测试闭环流,可以解决如下问题。

(1)线下性能测试调优所做的配置项信息修改,要及时反馈给产品对应的运维人员,指导其做升级发布时的修改。

(2)如果项目组上线后的产品做了后台运营或者用户行为分析,可以收集信息,以进一步指导优化性能测试脚本,使其覆盖更多实际场景。

(3)动态调整量化性能质量,分析对比测试结果和生产运行表现的差异,通过换算系数实现性能预测。

(4)全程性能跟踪,在生产环境中也加入性能监控,更快解决线上性能问题。

另外,根据项目规模的大小,作者摸索了线上和线下联动的性能测试和基线测试的解决方案,具体如图1-10和图1-11所示,供读者借鉴和扩展。

图1-10 小规模项目的性能测试和基线测试的方案

图1-11 大规模项目的性能测试和基线测试的方案

当今的互联网产品市场需要争分夺秒,机会转瞬即逝,产品的版本迭代速度非常快,而性能测试不同于功能测试,性能测试的耗时较久。另外,考虑性能测试的特殊性,没有必要每个版本迭代都做性能测试。所以作者根据版本号结合一些原则制定了性能测试的执行时机,具体如下。

(1)新产品或主版本必须执行性能测试的负载测试和稳定性测试,原则上需覆盖所有业务和接口,必要时,经项目组和测试组综合评估,部分业务和接口可不覆盖。

(2)在两个主版本之间或新产品和主版本之间,必要时,由项目组和测试组综合评估,选择合适的版本(含前一个主版本)完成压力测试、基准测试、可靠性测试和并发测试等各种性能测试,以此来检验项目的综合性能情况。性能测试的通过标准为无致命、严重问题(无内存泄漏、无表死锁、无线程死锁、无日志错误)。

(3)在两个主版本之间或新产品和主版本之间,对于一些关键节点版本、重要运营推广活动节点版本等需要开展负载测试和稳定性测试,原则上需覆盖所有业务和接口,必要时,经项目组和测试组综合评估,部分业务和接口可不覆盖。在两个主版本之间或新产品和主版本之间,至少有一个版本执行上述过程。

性能测试没有一个绝对的标准,不同的业务形态、不同的用户数量、不同的系统架构等都会有不一样的性能要求。但是在项目日常迭代中,性能测试执行结果还是需要制定一些标准,以便在相同的环境下,更加直观地判断性能测试异常现象。作者从服务器资源指标、业务指标、数据库指标和JVM指标这4个维度定义了性能测试中需要重点观察的指标项和标准,供读者参考,如表1-7所示。其中,业务指标大家可以根据业务形态进一步细分,例如支付类的业务响应时间要求是多少,查询商品类的业务响应时间是多少等。另外,业务的TPS处理的波动性其实也是很有必要的,可以考虑新增指标,若波动很大,则明显是有性能问题的。

表1-7 性能测试中需要重点观察的指标项和标准

指标维度 指标项 指标通过标准
服务器资源指标 CPU利用率 小于80%
内存使用率 小于80%
swap in、swap out 长期为0
wa% 小于30%
磁盘消耗大小 小于90%
网络吞吐量 小于70%带宽
业务指标 吞吐量 满足业务需求
95%的请求的响应时间 不大于2s
成功率 大于99.99%
数据库指标 SQL语句 最大响应时间小于1s
JVM指标
(可选)
FGC频率 时间间隔大于0.5h
FGC时间 每次FGC时间小于2s
YGC频率 时间间隔大于3s
YGC时间 每次YGC时间小于100ms

通过对本章的学习,相信很多读者不仅从宏观上了解了性能测试的全局技能知识图谱,也从微观上掌握了性能测试的相关基础知识和实操工具。我们重点传授的如使用阿里开源工具TProfiler定位代码耗时、使用淘宝的开源工具OrzDBA诊断MySQL的异常、编写自定义Shell脚本监控服务器、通过Java代码快速构建千万数据等实战真经,可借鉴性强,实用性高。新手掌握了本章的内容,就基本上可以独立完成一个简单的性能测试项目了。另外,在本章中作者结合日常实际的性能测试工作,沉淀提炼了典型的性能问题和案例分析,供读者学习,希望能给读者带来一些启迪。


相关图书

现代软件测试技术之美
现代软件测试技术之美
渗透测试技术
渗透测试技术
JUnit实战(第3版)
JUnit实战(第3版)
深入理解软件性能——一种动态视角
深入理解软件性能——一种动态视角
云原生测试实战
云原生测试实战
Android自动化测试实战:Python+Appium +unittest
Android自动化测试实战:Python+Appium +unittest

相关文章

相关课程