全栈性能测试详解
创始人
2024-05-01 03:33:23
0

一、全栈性能测试概述

1、全栈工程师

全栈工程师能够完成产品设计,技术选型,架构落地;可以开发前端和后台程序,并部署到生产环境。一人多用,省成本,完全没有多人配合时的工作推诿和沟通不畅等情况发生,这是创业公司找工程师时,全栈工程师是首选的原因。

大互联网公司的系统平台更复杂,需要更多的角色通力协作完成任务。那是不是全栈工程师在大企业就没有存在的必要了呢?当然有必要。大企业需要从全局考虑来做顶层设计,对于做顶层设计的人来说知识面宽尤为重要。这个人可以不是每一个细分领域的专家,但能够与某些领域专家交流畅顺,能够理会对方意思,尽可能地从全局考虑项目的优化设计,这类人是全栈工程师的典型代表。一位互联网全栈工程师需

要掌握包括但不限于如下技术:

  • 前端(简单列举):HTML、H5、CSS、JavaScript、React、Vue、Angular、NodeJS、WebSocket、HTTP等。
  • 后台:中间件(Tomcat、Jetty)、消息中间件(Kafka、RabbitMQ、RocketMQ等)、开发框架(Springboot/Cloud、Dubbo等,ORM:Hibernate、MyBatis、Spring JPA等)。
  • 数据库:关系数据库(MySQL、Oracle等)、NoSQL数据库(Redis MongoDB HBase等)。
  • 集成工具:Git、Gitlab、CVS、Jenkins、Sonar、Maven等。
  • 容器及编排工具:Docker、Kubernetes等。
  • 监控工具:Prometheus、Skywalking、Zabbix等。
  • 操作系统:Linux系列(CentOS、Fedora、Debian、Ubuntu之一或者多种)。

如果你是从事大数据方面的开发,还需要掌握的技术如下:

  • Hadoop、Spark、Storm、Flink、Tensorflow、Lucene、Solr、ElasticSearc、Hive/Impala等。当然,你还要熟悉各种算法、统计方法,数学等。

2、全栈性能测试引入

网络平台的崛起,带来巨大的流量,也催生技术的革新。高度集中和融合的系统,结构复杂,技术栈庞大,使测试变得困难。在定位、分析问题时往往会跨技术栈,测试人员靠单一技能并不能较好地完成测试工作。与全栈开发一样,全能型(跨技术栈)测试人才变得抢手,也自然催生出全栈测试概念。

以我们熟悉的支付系统来说,我们在手机上轻轻点一下就把钱支付的看似简单的操作,在其工作流程的背后可能要经过十几个系统,涉及商家、平台、支付企业、银行等多个角色。对这类系统测试时链路长,尤其是做链条测试时,每个系统都要检查,第三方系统还要沟通配合。如对接银行系统的过程中,一般在沟通上最耗时,这也是测试工程师最无力把控的部分。

例如支付企业,整个支付系统加上金融产品,大大小小共90多个子系统,测试团队部署新版本时都感到头痛,主要风险在版本管理及配置管理方面,特别是进行敏捷开发时,面对多个测试环境(开发环境、集成环境、测试环境、QA验证、模拟环境、生产环境)的情况下,负责功能测试的同事苦不堪言。测试工作除了考验测试人员的业务、技术,还考验测试人员的心态。这个时候能够从容不迫、心静如水的往往都是技术过硬的人。

技术过硬的人已经熟悉了技术栈、业务流向、测试环境,能够有条不紊地开展工作。所以这些技术过硬的人往往都是测试团队的主力。大数据分析、AI、云计算、Devops是当前系统应用的主流技术,这方面的测试对测试人员的技术要求更高。

以大数据分析为例,测试数据的制作就是一门学问。测试人员最好能够了解实现功能的算法,设计有针对性的数据。

全栈概念很广,测试人员可以选择一些点突破。如做云计算的测试,要了解虚拟化、容器、虚拟网络、服务编排工具等;做AI测试要了解AI的算法、训练模型,力争储备更多的知识。

3、全栈性能测试概要

在软件行业、互联网行业,软件性能是企业的竞争力。良好的性能才能保证优质的用户体验,谁也不想忍受“蜗牛般”的应用加载,这样的应用会丢失用户。技术在发展,用户对性能的要求在提高,测试人员面对不断更新的技术,庞大的技术栈,性能测试的技术储备也要拓宽。

下面看一下Web请求的生命之旅:

(1)用户通过浏览器发出访问请求,浏览器把请求编码,再通过网卡把请求通过互联网传给服务端。

(2)服务端的负载均衡器接收到请求,路由到应用服务器;通常负载均衡器也具备限流的作用。

(3)应用服务器由网卡收到请求,如果网卡繁忙可能会排队;网卡的数据传递给中间件,中间件(如Tomcat)进行协议解码,应用程序才能识别请求;然后由应用程序来处理请求,如果有数据访问或者存储需求则要访问持久层。

(4)持久层负责数据的访问与存储,比如结构数据(狭义地说就是能够存到关系数据库的数据)或者非结构数据(如存到HBase中的数据),如果请求量大,也可能遇到性能问题,这样应用服务就必须等待持久层的响应。

(5)等到持久层的响应后,应用服务把处理好的数据回传给用户。如果网络状态不好,还可能丢包。

(6)用户浏览器收到数据并进行渲染。我们来看这6步中涉及哪些性能相关技术。

① 浏览器作为展示部分,访问的是前端程序,如今前端程序大量依赖JavaScript,JavaScript程序的性能会影响渲染效率。目前,前端开发的主流框架有React、Vue、Angular等,不良的程序写法会大量占用用户机器的CPU、内存,也有可能“卡死”浏览器。

② 负载均衡器有很多种(Nginx、Haproxy、LVM等),功能大同小异,有些具备缓存功能和多路复用功能。这些功能有些是需要配置的,所以了解负载均衡器的性能配置就有必要了。

③ 中间件帮我们抽象出了通信功能、IO功能、请求监听功能,我们不用从Socket通信开始写程序,不用自己去写监听程序来接收用户的请求;中间件还帮我们实现了线程池;为了适应不同应用场景,中间件还提供了很多优化配置。因此,用好中间件对性能的提升还是很明显的,自然也就要了解中间件的原理,学会分析它、配置它。应用程序处理用户请求,对于Java程序的性能分析,了解HotSpot VM是有帮助的。

④ 持久层可以有很多种,如关系数据库、非关系数据库、缓存、文件系统等,对这些持久化工具的优势与劣势的掌握显然是优化性能所需要的。

⑤ 很多系统都运行在云上。对于云来说,网络是稀有资源,由于网络导致的性能问题还是很多的,要掌握网络分析技能。

⑥ 页面渲染是DOM(文档对象模型,Document Object Model)对象的构建过程,了解渲染过程有助于定位分析前端性能问题。可以看到,与全栈开发一样,性能测试要掌握的东西很广泛。要从
前端测试到后端,不仅需要了解开发(分析和定位开发问题),还要了解运维(部署、执行负载时监控性能指标,从指标中找出风险与问题),更要了解业务,设计合适的压测场景,制作合理的测试数据。移动互联网产品种类多,包含的技术丰富,因此性能测试的技术面要求很宽。全栈性能测试就是要利用多种跨领域的知识,深入分析和发现移动互联网产品的问题,解决问题,为系统保驾护航。 

4、全栈性能测试需要具备哪些技术能力

1. 性能测试要解决的问题

“我们想做一下性能测试看一下系统有没有性能问题。”

“我们想测试一下系统能承载的最大用户数。”

“我们想测试一下系统能支持多少并发用户。”

做过性能测试的朋友对上面3句话应该是耳熟能详。

抛开性能需求不谈,性能测试的确是要解决最大用户数(能支持的最大并发用户数)的问题。它抽象出了在一定场景下系统要满足的刚性需求,最大用户数反映到性能测试上就是系统的最大处理能力,只不过“一定场景”这个前提不清晰而已。

性能测试是一项综合性工作,致力于暴露性能问题,评估系统性能变化趋势。性能测试工作实质上是通过程序或者工具模拟大量用户操作来验证系统承载能力,找出潜在的性能问题,分析并解决这些问题;找出系统性能变化趋势,为后续的系统扩展提供参考。这里我们提到的“性能变化趋势”“系统承载能力”,它们怎么与最大用户数联系在一起呢?

以医院场景为例,医院的挂号窗口是相当忙碌的(尤其是三甲名院),那医院要开多少窗口才合适呢?(挂号这个业务的处理能力有多大?)此时患者是用户,不管用户有多少,医院的“挂号能力”(比如一分钟可以挂多少个号)决定了有多少人可以挂号。而医生每天能看多少病人决定了挂号量,挂号量决定挂号窗口每天只能放出多少号,所以即使挂号窗口的“挂号能力”很强,其处理能力也是受限的,挂号窗口要赶在患者看医生前挂完号即可。考虑到患者还要缴纳医药费的流量,我们
简单把这个流量与挂号的流量对等。假设当值医生有100名,上午有效工作时间4小时,每位患者平均5分钟,那么医生效率是48人/医生/上午,我们用表来统计一下。

类别速度工作量时间100位医生看诊/上午挂号总耗时需要窗口数
挂号39秒/人48人24分钟4800人40小时10
看诊50分/人48人/医生240分钟4800人400小时
缴费30秒/人48人24分钟4800人40小时10

挂号与缴费总耗时80小时,约开20个窗口即可以在上午完成4 800人的挂号及缴费工作。如果这些窗口提前半小时开始挂号(工作时间变成4.5小时,其他条件不变),则需要17个窗口来完成挂号及缴费工作。

排除网上挂号的(很多医生的号只能网上预约到)和自助机器上挂号的情况,也许10个窗口就够用了。因此可以在一楼开放10个窗口,在门诊楼其他楼层分别设置缴费窗口,方便患者,提高缴费效率。

本例中4 800人是医院的最大处理能力(系统承载能力),比这再多的用户超出了医院处理能力,可以拒之。这和线上系统超载了,请求也可以拒之是一样的道理。那如何提高看诊量呢?当然是提高医生看诊效率,利用医生的每一分钟;增加医生是最直接的解决办法,就好像在系统上增加机器一样。

那“性能变化趋势”呢?试想医院起初也不知道每年或每天的患者流量,随着医院的运营,历史的流量是能够统计出来的,这些数据反映了医院服务能力的“性能变化趋势”。基于这些数据就能够帮助决策,到底要开多少个窗口?哪个时段多开一些窗口?哪个时段关闭一些窗口?哪个季节患者流量大?要增加多少个医生?增加哪个类别的医生?医院要不要扩建?

说到“性能变化趋势”,我们看一下图所示的性能变化曲线。

上图中标记了4个点,x轴代表负载的增加,y轴代表吞吐量(TPS)及响应时间(RT)的增加。

随着负载的增加,响应时间与吞吐量线性增长,到达临界点(标记为3)时,吞吐量不再增加,继续增加负载会导致过载(比如我们在计算机上开启的软件过多导致系统卡死),吞吐量会受到拖累而减小,响应时间可能陡增(标记为4)。此时标记3代表了系统的最大处理能力。

如果标记3的处理能力不能满足要求,那被测试的系统性能就堪忧了,需要进行性能诊断和优化。
有时,我们会要求响应时间要小于一个标准,比如请求某一个页面需要响应时间控制在3秒之内,时间长了用户体验差。

假设图中标记1、2、3的响应时间都不满足要求,即使吞吐量达到要求,我们也需要想办法缩短响应时间。再次假设在标记2处响应时间与吞吐量都满足性能要求,那么说明性能良好,而且性能还有提升空间,标记3才是系统性能的极限,标记4是系统性能“强弩之末”。

因此,我们在讨论性能时,至少要从系统响应时间与吞吐量两个维度来看是否满足性能要求;另外对于主机的资源(CPU、内存、磁盘、网络等)使用率也需要关注,太低则浪费,太高则危险。正如我们不会让发动机一直运行在极速情况下一样,也不会让计算机一直处在峰值处理能力状态,以延长机器的使用寿命及减少故障的发生。

上图这样的性能曲线诠释了软件的性能变化趋势。当取得软件的性能曲线后,我们在运营系统过程中就能够做到心中有数,知道什么时候要扩展,什么时候可以减少机器资源(比如在云上做自动伸缩)。当前,系统部署到云正在取代部署到物理机,而云服务商多数都是互联网企业。

为什么会是它们呢?一个重要原因是,它们的物理主机很多,机器多是为了应付业务“暴涨”的场景,比如双11、6·18、春节抢红包等场景;而它们的业务量又不是一直都在满负荷运行,闲时的机器资源不利用就是浪费,所以把物理机器抽象成云来提供给第三方用户。

2. 如何开展性能测试

生产企业使用流水线来提高生产率,做性能测试也有流程。我们基于经验总结,参照项目管理实践来裁剪性能测试流程,下图所示是性能测试常规流程。

(1)学习业务:通过查看文档,并咨询提出测试需求的人员,手工操作系统来了解系统功能。

(2)分析需求:分析系统非功能需求,圈定性能测试的范围,了解系统性能指标。比如用户规模有多大,用户需要哪些业务功能,产生的业务量有多大,这些业务操作的时间分布如何,系统的部署结构怎样,部署资源有多大等。测试需求获取的途径主要有需求文档、业务分析人员,包括但不限于产品经理、项目经理等(在敏捷开发过程下,最直接的途径是从项目的负责人或者产品经理处获取相关需求信息)。

(3)工作评估:分解工作量,评估工作量,计划资源投入(需要多少个人,多少个工作日来完成性能测试工作)。

(4)设计模型:圈定性能测试范围后,把业务模型映射成测试模型。什么是测试模型呢?比如一个支付系统需要与银行的系统进行交互(充值或者提现),由于银行不能够提供支持,我们会开发程序来代替银行系统功能(这就是挡板程序、Mock程序),保证此功能的性能测试能够开展,这个过程就是设计测试模型。通俗点说就是把测试需求落实,业务可测,可大规模使用负载程序去模拟用户操作,具有可操作性、可验证性;并根据不同的测试目的组合成不同的测试场景。

(5)编写计划:计划测试工作,在文档中明确列出测试范围、人力投入、持续时间、工作内容、风险评估、风险应对策略等。

(6)开发脚本:录制或者编写性能测试脚本(现在很多被测系统都是无法录制脚本的,我们需要手工开发脚本),开发测试挡板程序和测试程序等。有时候如果没有第三方工具可用,甚至需要开发测试程序或者工具。

(7)准备测试环境:准备性能测试环境包括服务器与负载机两部分,服务器是被测系统的运行平台(包括硬件与软件),负载机是我们用来产生负载的机器,用来安装负载工具,运行测试脚本。

(8)准备测试数据:根据数据模型来准备被测系统的主数据与业务数据(主数据是保证业务能够运行畅通的基础,比如菜单、用户等数据;业务数据是运行业务产生的数据,比如订单;订单出库需要库存数据,库存数据也是业务数据)。我们知道数据量变会引起性能的变化,在测试的时候往往要准备一些存量/历史业务数据,这些数据需要考虑数量与分布。

(9)执行测试:执行测试是性能测试成败的关键,同样的脚本,不同执行人员得出的结果可能差异较大。这些差异主要体现在场景设计与测试执行上。

(10)缺陷管理:对性能测试过程中发现的缺陷进行管理。比如使用Jira、ALM等工具进行缺陷记录,跟踪缺陷状态,统计分析缺陷类别、原因;并及时反馈给开发团队,以此为鉴,避免或者少犯同类错误。

(11)性能分析:性能分析指对性能测试过程中暴露出来的问题进行分析,找出原因。比如通过堆分析找出内存溢出问题。

(12)性能调优:性能调优指性能测试工程师与开发工程师一起来解决性能问题。性能测试工程师监控到异常指标,分析定位到程序,开发工程师对程序进行优化。

(13)评审(准出检查):对性能报告中的内容进行评审,确认问题、评估上线风险。有些系统虽然测试结果不理想,但基于成本及时间的考虑也会在评审会议中通过从而上线。

(14)编写测试报告,汇报工作:测试报告是测试工作的重要交付件,对测试结果进行记录总结,主要包括常见的性能指标说明(TPS、RT、CPU Using……)、发现的问题等。

性能测试主要交付件有:

  • 测试计划;
  • 测试脚本;
  • 测试程序;
  • 测试报告或者阶段性测试报告;

如果性能测试执行过程比较长,换句话说性能测试过程中性能问题比较多,经过了多轮的性能调优,需要执行多次回归测试,那么在这个过程中需要提交阶段性测试报告。

3. 全栈性能测试技术栈

性能测试不仅仅是录制脚本或者编写测试程序(测试工具),对基本的性能理论、执行策略必须要了解。同样的脚本,新手与行家分别执行,测试结果也许会大相径庭。实际上我们需要对系统进行一系列复杂的需求分析,制定完善的测试计划,设计出贴近实际用户使用场景的用例,压测时更是要产生有效负载,经过反复测试实验,找到性能问题。

下图所示为性能测试常用的技术栈(包括但不限于):

1) 性能测试理论

性能测试理论用来指导我们开展性能测试,指导我们要得到什么结果,让我们了解测试过程是否可靠,测试结果是否具备可参考性。

对于性能测试理论,我们主要关注下面几点:

  1. 测试需求分析要能够准确挖掘出性能需求,圈定测试范围,并有明确的性能指标;
  2. 测试模型要能够尽量真实地反映系统的实际使用情况;
  3. 测试环境尽量对标实际(避免使用云主机,避免使用虚机);
  4. 测试数据在量与结构上尽量与实际对标;
  5. 测试场景要考虑业务关联,尽量还原实际使用情况;
  6. 测试监控尽可能少地影响系统性能;
  7. 测试执行时测试结果要趋于稳定

2)测试开发技能

系统(产品)的多样性决定了测试程序的多样性,不是所有的系统都有工具可以帮助进行测试的,有时候我们需要自己动手开发测试程序。比如RPC作为通信方式时,针对此类接口的测试就很少有工具支持(现在已经有一些专用的,如Dubbo),通常需要自己开发。另外,我们在做性能诊断分析时,不可避免地会面对代码,没有开发技能,诊断分析问题谈何容易?所以具备开发能力会更从容地解决问题。

当然不具备开发能力也不代表一定不会诊断分析问题,利用工具、遵循理论,也是可以找到问题所在的;长期的积累,也会具备调优能力。

3)负载工具

合适的性能测试工具能够帮助提高测试效率,让我们腾出时间专注于问题分析。主流的性能测试工具有LoadRunner与JMeter(其他还有很多,比如Ab、Grinder等)。当然,工具也不能解决所有问题,有时候还是需要自己编写程序来实现测试脚本。很多初学者认为这两个工具只能用来做性能测试,其实能做性能测试的工具也可以做功能自动化测试。

不是非得Selenium、WebDriver才能做自动化测试。

如何选择工具呢?

首先我们要明白负载工具是帮助我们来模拟负载的。对于性能测试来说,工具并不是核心,找出、分析、评估性能问题才是核心,这些是主观因素。工具是客观因素,自然要降低其对结果的影响,因此选择工具时,我们要考虑几个方面。

  1. 专业、稳定、高效,比如工业级性能负载工具LoadRunner。
  2. 简单、易上手,在测试脚本上不用花太多时间。
  3. 有技术支持,文档完善,不用在疑难问题上花费时间,可集中精力在性能分析上。
  4. 要考虑投入产出比,比如我们可以选择免费开源的JMeter。

当然,有时候自研或者使用开源不一定比商业工具更省钱,因为要做技术上的投资、时间上的投资。

常见的性能测试工具:

  1. HP公司的LoadRunner;
  2. Apache JMeter(开源);
  3. Grinder(开源);
  4. CompuWare公司的QALoad;
  5. Microsoft公司的WAS;
  6. RadView公司的WebLoad;
  7. IBM公司的RPT;
  8. OPENSTA;
  9. BAT(百度、阿里巴巴和腾讯)等企业的在线压力测试平台。

下面对选择自研及开源工具与商业工具做一下比较:

自研/开源 商业工具
能够开发出最适合应用的测试工具 依赖于工具本身提供的特性,较难扩展
易于学习和使用 依赖于工具的易用性和所提供的文档
工具的稳定性和可靠性不足 稳定性和可靠性有一定保证
可形成团队特有的测试工具体系 很难与其他产品集成
成本低,技术要求高 短期购买成本高,技术要求低

总之,我们要认清性能测试的核心是性能分析,重要的是思想和实现方式,不在于工具。大家要本着简单、稳定、专业、高效、省钱的原则来选择工具。

4)前端监听诊断

目前的开发形式多采用前后端分离的方式,一套后端系统处理多套前端请求;用户通过手机里的App(Hybrid应用、H5应用、Native应用)和PC中的浏览器来访问系统。JavaScript的运用让前端技术发展迅速,App的运用让前端可以存储、处理更多业务。随着功能的增多自然会带来性能问题,前端的性能问题越来越成为广泛的性能问题。幸运的是前端应用性能的监控工具也有不少。

5)服务器监听诊断

不管我们的程序如何“高大上”,也不管用什么语言开发程序,程序运行时最终还是要依赖服务器硬件。服务器硬件是性能之本,所有性能都会反映到硬件指标上,我们想要分析性能,少不了服务器知识。测试人员要对服务器“几大件”,如CPU、存储、内存、网络的性能指标及监控方法都需要熟练掌握。管理这些硬件的操作系统原理、性能配置参数也需要掌握。要掌握这部分需要学习很多运维和开发知识。

了解操作系统及其内核对于系统分析至关重要。作为性能测试工程师,我们需要对系统做分析:系统调用是如何执行的、CPU是如何调度线程的、有限大小的内存是如何影响性能的、文件系统是如何处理IO的,这些都是我们判断系统瓶颈的依据和线索。

对于操作系统,我们主要掌握Linux与Windows Server(其他如AIX、Solaris主要应用在传统的大型国企、金融企业,专业性强,由供应商提供商业服务)。

(1)Linux

Linux是开源的类UNIX操作系统,Linux继承了UNIX以网络为核心的设计思想,是一个性能稳定的多用户网络操作系统。越来越多的企业用这个系统作为服务器的操作系统,因此作为性能测试从业者来说,它是必须掌握的操作系统之一。

目前Linux/UNIX的分支很多,比较普及的有CentOS、Ubuntu、RedHat、AIX、Solaris等。

(2)Windows Server

Windows Server是Microsoft Windows Server System(WSS)的核心,是服务器操作系统。目前使用此系统比较多的是中小型公司。

Windows Server的资源监视器功能完善,图形界面友好,使用非常方便。用户只需要了解各项指标的意义,就可以方便、快捷地对运行的程序进行诊断分析。

6)中间件监听诊断

主流的中间件非Tomcat莫属了。Tomcat作为一个载体,帮助我们实现了通信及作业功能,提供了一套规范,我们只需要遵循规范,开发出实现业务逻辑的代码,就可以发布成系统。它为我们省掉了基础通信功能和多线程程序的开发,让我们可以专注业务逻辑的实现。

Tomcat在不同场景下也会遇到性能瓶颈,熟练使用Tomcat对于性能测试工程师解决性能问题也是必要的。

Tomcat有一些性能指标用来反映服务的“健康状况”,如活动线程数、JVM内存分配、垃圾回收情况、数据库连接池使用情况等。

中间件不只有Tomcat这样的Java Servlet容器,类似Tomcat支持Java程序的中间件还有Jetty、Weblogic、WebSphere、Jboss等。

现在分布式系统架构是主流,系统间的数据通信(同步)很多通过消息中间件来解决,如ActiveMQ、Kafka、Rabbitmq等,这些中间件的配置使用对性能也会产生显著影响。

7)持久化产品监听

数据的持久化有结构数据、非结构数据、块数据、对象数据,存储时对应不同类型的存储产品。比如关系型数据库(MySQL、Oracle等),非关系型数据库(Redis、HBase等),分布式存储(hdfs、ceph等),这些都属于IO操作。

IO操作一直都是性能“重灾区”,因为不管什么类型的存储,终究是把数据存到存储介质上。存储介质广泛使用的是固态硬盘(SSD)和机械硬盘。机械硬盘是物理读写(IO),其的读写速度相对固态硬盘相差巨大。我们启动计算机时,如果操作系统不在固态硬盘上,启动用时至少30多秒;而操作系统在固态硬盘上,启动时间是几秒。

下图所示是三星固态硬盘官方公布的主要读写性能指标:

下图所示是希捷机械硬盘读写性能指标:

我们可以通过监听存储介质的性能指标来诊断程序在IO上的耗时,并针对性地优化对存储的访问(比如减少请求次数来减少IO)。存储介质的性能是一定的,我们需要对依赖它的持久化产品做文章,如MySQL数据库的慢查询,Redis存储的键值长度等。所有这些都需要我们去监控,通过监控推导出问题所在。

8)代码分析能力

作为IT部门的一员,不可避免地要和代码打交道,了解编程知识既能加深对性能测试的理解,还能提高和程序员沟通效率。更重要的是,做自动化测试、单元测试、性能测试、安全测试都离不开对代码的理解。所以我们要掌握一些使用率高的编程语言和脚本语言,如Java、Python等。

代码问题通常集中在事务、多线程、通信、存储及算法方面。测试人员可以不必去写一段优秀的代码,但要能够定位问题到代码段。

9)架构

高性能的系统架构与普通系统架构也不一样。性能优化或者性能规划要依照系统的用户规模来设计,了解架构有助于快速判断系统性能风险,有针对性地进行性能压测实验,提出合适的解决方案。

10)中间件性能分析

中间件的性能指标反映了系统的运行状况,我们要能够通过这些指标推导出系统的问题所在。有些可以通过调整中间件的配置来改善系统性能,比如用户请求过多,可以适当增大线程池;当JVM内存回收,特别是Full GC过于频繁时,我们就要分析到底是哪些程序导致了大量的Heap(堆)内存申请;当CPU过于繁忙时,我们会去分析哪个线程占用了大量CPU资源,通过线程信息定位到程序。这些都是常见的分析方法,也容易掌握,掌握这些分析方法能够解决80%以上的性能定位问题。

11)操作系统

操作系统统筹管理计算机硬件资源,针对不同业务,不同场景也会有一些可以优化的参数。我们首先要知道操作系统的限制,这需要从监控的指标中推导。常见调优方法有:文件句柄数设置、网络参数优化、亲和性设置、缓存设置等。

12)数据库分析

系统中流转的数据离不开持久化,持久化需要数据库。数据在数据库中的存储结构和搜索方式直接影响性能,大多数的性能调优都集中在数据库的存储及查询上。

学好数据库的理论知识,学会分析SQL的执行计划是一种基础技能。现在很多系统都用Redis来做热点数据的存储,在测试时对于影响Redis性能的因素要了解。比如Key-Value存储时Value过长,性能就会急剧下降,因为网络传输时数据包的MTU(最大数据包大小,Maximum Transmission Unit,这也是操作系统的知识点)通常是1500字节,大的数据包需要在网络中多次传输,当然效率低下。

如何优化数据库呢?最直接的想法是减少Value长度,分析为什么Value这么长,能否减少或者压缩,之后才是从数据库的业务逻辑上去考虑优化。

13)效率工具/持续集成

性能测试是一个反反复复的过程,发布后执行压测,分析问题、找到问题、修改问题,再发布、再执行压测。使用持续集成工具有利于提高工作效率。当下通常用Git/SVN来管理代码版本,使用Jenkins来做持续集成,同时我们也可以利用Jenkins来自动化性能压测过程。

云计算已成为主流技术,我们的服务会部署在云环境,因此对于云环境的了解自然不能落下。我们在云环境用虚机或容器(如Docker)技术来发布程序,这些对于性能的影响也是我们要考虑的问题,熟悉虚机与容器是自然的事。Docker容器是当前市场占有量最大的容器产品之一,对主流产品的学习也可以帮我们扩大知识面;Kubernetes已经成为容器编排产品的集大成者。

14)性能测试相关术语

(1)并发(Concurrency)/并行(Parallelism):如果CPU是8核的,理论上同一时刻CPU可以同时处理8个任务。当有8个请求同时进来时,这些任务被CPU的8核分别处理,它们都拥有CPU资源并不相互干扰,此时CPU是在并行地处理任务。如果是单核CPU,同时有8个请求进来时,请求只能排队被CPU处理,此时8个请求是并发的,因为它们同一时刻进来,而处理是一个一个的。所以并发是针对一个对象(单核CPU)发生多个事件(请求),并行是多个对象(多个CPU核)同时处理多个事件(请求)。

同理,当请求多于8个,那么也可以在多核CPU前形成并发的情况。我们常听到的“并发用户数”并不是像本例中的8个请求,比如一个系统有200个用户,即使他们都在线,也不能代表他们都操作了业务,用户可能仅仅是上去看一下信息然后挂机,所以通常听到的“并发用户数”并不能作为真实的性能测试需求,而应该以产生的业务量(交易量/请求数)为性能需求。

(2)负载:模拟业务操作对服务器造成压力的过程,比如模拟100个用户进行订单提交。负载的产生受数据的影响,我们常说量变引起质变,比如查询100条与查询100万条数据的响应时间很可能有差异。

(3)性能测试(Performance Testing):模拟用户负载来测试在负载情况下,系统的响应时间、吞吐量等指标是否满足性能要求。

(4)负载测试(Load Testing):在一定软硬件环境下,通过不断加大负载(不同虚拟用户数)来确定在满足性能指标情况下的承载极限。简单地说,它可以帮助我们对系统进行定容定量,找出系统性能的拐点,给出生产环境规划的建议。这里的性能指标包括TPS(每秒事务数)、RT(事务平均响应时间)、CPU Using(CPU利用率)、Mem Using(内存使用率)等软硬件指标。从操作层面上来说,负载测试也是一种性能测试手段,比如配置测试就需要变换不同的负载来进行测
试。

(5)配置测试(Configuration Testing):为了合理地调配资源,提高系统运行效率,通过测试手段来获取、验证、调整配置信息的过程。通过这个过程我们可以收集到不同配置反映的不同性能,从而为设备选择、设备配置提供参考。

(6)压力/强度测试(Stress Testing):在一定软硬件环境下,通过高负载的手段使服务器资源(强调服务器资源,硬件资源)处于极限状态,测试系统在极限状态下长时间运行是否稳定,确定是否稳定的指标包括TPS、RT、CPU使用率、Mem使用率等。

(7)稳定性测试(Endurance Testing):在一定软硬件环境下,长时间运行一定负载,确定系统在满足性能指标的前提下是否运行稳定。与上面的压力/强度测试区别在于,稳定性测试负载并不强调是在极限状态下,着重的是满足性能要求的情况下系统的稳定性,比如响应时间是否稳定、TPS是否稳定、主机是否稳定。一般我们会在满足性能要求的负载情况下加大1.5~2倍的负载量进行测试。

(8)TPS:每秒完成的事务数,有时用QPS(每秒查询数)来代替,通常指每秒成功的事务数,这是性能测试中重要的综合性性能指标。一个事务是一个业务度量单位,有时一个事务会包括多个子操作,但为了方便统计,我们会把这些子操作计为一个事务。比如一笔电子支付操作,在后台系统中可能会经历会员系统、账务系统、支付系统、会计系统、银行网关等,但对于用户来说只想知道整笔支付花费了多长时间。

(9)RT/ART(Response Time/Average Response Time):响应时间/平均响应时间,指一个事务花费多长时间完成(多长时间响应客户请求),为了使这个响应时间更具代表性,会统计更多的响应时间然后取平均值,即得到了事务平均响应时间(ART),通常我们说的RT是代指平均响应时间。

(10)PV(Page View):每秒用户访问页面的次数,此参数用来分析平均每秒有多少用户访问页面。

(11)Vuser虚拟用户(Virtual User):模拟真实业务逻辑步骤的虚拟用户,虚拟用户模拟的操作步骤都被记录在虚拟用户脚本里。Vuser脚本用于描述Vuser在场景中执行的操作。

(12)场景(Scenario):性能测试过程中为了模拟真实用户的业务处理过程,在测试工具中构建的基于事务、脚本、虚拟用户、运行设置、运行计划、监控、分析等一系列动作的集合,称之为性能测试场景。

此场景中包含了待执行脚本、脚本组、并发用户数、负载生成器、测试目标、测试执行时的配置条件等。简单地说,就是把若干个业务的性能测试脚本组织成一个执行单元,对执行单元进行一揽子的配置来保证测试的有效执行。比如负载测试时,我们可以设置一种下图所示的阶梯形负载增长场景。

(13)思考时间(Think Time):模拟正式用户在实际操作时的停顿间隔时间。从业务的角度来讲,思考时间指的是用户在进行操作时,每个请求之间的间隔时间;在测试脚本中,思考时间体现为脚本中两个请求语句之间的间隔时间。

(14)标准差(Std. Deviation):该标准差根据数理统计的概念得来,标准差越小,说明波动越小,系统越稳定;反之,标准差越大,说明波动越大,系统越不稳定。常见的标准差包括响应时间标准差、TPS标准差、Running Vuser标准差、Load标准差、CPU资源利用率标准差等。 

二、性能测试理论

1、性能测试简要

在处理性能问题方面,调优是主角。如果能进行性能调优,那就可以认为有能力解决性能问题。而具备这种能力的人才,也通常被认为是解决性能问题的专家。

在进行调优的时候,“性能测试”是不可或缺的。要让调优顺利进行下去,最重要的工作就是通过测试来验证。不过,如果将调优结果直接在生产环境中验证,会有一定风险,因此通常是在验证环境中获得性能测试的结果,来验证调优的成果。在通往调优专家或性能专家的道路上,性能测试可以说是最重要的要素。

1. 项目工程中的性能测试

普通的系统开发项目工程,粗略划分的话如图所示。粗体字部分就是与性能测试相关的任务。

其中,性能测试实际上主要在系统测试阶段执行。这是因为性能测试原则上是为了确认“系统在生产环境中运行时是否会有性能上的问题”,所以它是在完成集成测试后,确认系统能在生产环境中正常运行之后的阶段执行的。

即便如此,如果认为在进入系统测试阶段之前不需要考虑性能测试,那就错了。这里将为大家介绍项目的各个工程阶段必须考虑的事情和任务,以及如何有效地进行性能测试。

2. 不同职责的性能测试相关人员

1)项目经理

在运营项目的过程中,系统的性能问题想必尤其让项目经理(PM)感到头疼。有经验的项目经理一定知道,即使使用容量充足的硬件和已经验证过的应用程序,也有可能在意想不到的地方隐藏着性能问题的陷阱。

2)基础设施设计负责人

基础设施设计和应用程序设计在不同的流程中实施,很多情况下,基础设施会先于应用程序发布。在应用程序的细节还未确定的时候,基础设施设计负责人可能就会被问到“这个基础设施的架构能充分发挥性能吗”之类的问题。另外,流量控制和容量管理等与应用程序密切相关的部分也需要基础设施设计负责人来设计,这是很辛苦的。而如何对基础设施本身进行性能评估,这一点也是令人苦恼的地方。针对这些问题,如果能积极地提出方案或建议,比如在项目整体工程中,需要别的负责人提供哪些信息、确定哪些内容等,自己的任务也会顺利地进行下去。

3)基础设施运维负责人

基础设施运维负责人的任务是,拿到验证过的系统后,把系统部署到生产环境中运行。如果在运行过程中出现故障,发现存在性能问题,那么运维负责人就要参与并协助进行费力的调查分析工作。这个是非常规工作,为了尽量避免,就需要在接收系统之前进行完备的检查。

4)应用程序设计负责人

应用程序设计负责人一般不会考虑系统整体的性能,而是满脑子考虑如何使用新的框架和中间件来实现需要的业务和功能。虽然这一点的确很重要,但是如果之后出现了性能问题,比如在几乎忘记这个系统的相关内容的时候,甚至就需要重新进行系统设计。这是完全可以预见到的风
险。为了避免这样的风险,需要基于项目整体来确保应用程序的性能,以及为了充分保证性能,而不仅仅是完成需要的功能,应用程序设计负责人应该进行哪些工作。

5)性能测试负责人

性能测试负责人中可能有人之前已经参与过一些性能测试,应该能从这些经验中了解到为了顺利进行性能测试,不光要靠测试的技能,其周边相关领域的理解与协助,以及项目工程前期阶段的准备与条件的整理也是很重要的。

6)发包方

作为系统的发包方,可以说只要能拿到一套按预期运行的稳定的系统就足够了。但是在“按预期运行”“稳定”这些基准上,系统存在难点。保证10 个人同时使用的系统与保证 1000 个人同时使用的系统的开发费用和时间是完全不同的。另外,即使实际上是 10 个人在同时使用,根据这10 个人的操作的不同,需要的服务器规格也不同。甚至有时必要的性能需求不明确,只完成了明确规定的验证工作就交接、验收,就可能在实际使用的时候才意识到有问题。

开发者说到底也只是按照合同制作符合规定的产品,因此如果之后提出“想要再这样改一下”等要求,由于涉及需求变更,就需要支付额外的费用。因此,作为发包方,最好能把这些性能目标明确体现在需求中,把握好应该验证哪些方面。

2、性能测试现场问题

1. 不能在期限内完成

在制作应用程序的过程中第一次执行性能测试时,这是种比较常见的模式。即使你期待着在系统测试阶段仪式性地做一下性能测试,然后就这样告一段落,也还是有可能发生诸多状况,比如执行完性能测试后性能完全达不到要求,或者发生了意料之外的性能方面的故障,于是被迫解析、调优,重新进行测试,甚至有些情况下需要重新设计等,致使原本已经临近的发布日期延迟。

像这样,之所以后期工程中隐藏着性能问题,原因有如下几点。

  • 只有在生产环境中才会出现
  • 问题的显现需要很多条件(环境、数据、负载生成)
  • 因为特定的操作才导致发生性能问题

基于以上原因,一般至少为系统测试中的性能测试留出1 个月的时间。即使是认真地进行了准备、规划的项目,也需要这么久的时间。虽说在项目的收尾阶段很难保证这样长的时间来做性能测试,但如果不事先确保这么久的时间,非但不能完成完整的性能测试,更有可能因为没有达到预期的性能目标而被迫修改日程等,对自己的业务产生不良影响。

2. 性能很差!解决不了性能问题

如果执行性能测试后发生了性能问题,那该怎么办呢?首先,调查导致性能变差的原因。虽说如此,但可以怀疑的地方却有很多。比如网络、负载均衡器、Web 服务器、AP 服务器、开发应用程序的方式、数据库、存储、与其他系统有数据关联的部分、数据大小、SQL 等。

为了详细调查这些因素,就需要各个模块的专业知识。此外,对横跨多个领域的性能问题进行排查的时候,如果不能综合多个领域来考虑,负责人就只会一直说“我负责的那部分没有问题”,导致问题无法解决。此外,即使汇集了有专业知识的员工及体制,若没有采用正确的验证和分析方法,非但会使效率变差,有时甚至会让调查朝着错误的方向进行,最终也就找不到答案了。于是,我们就会陷入这样一种状态:虽然知道性能很差,但却无法查明原因,问题始终得不到解决。

特别是最近几年 Web 系统中模块的层级愈加复杂,问题越来越难排查,需要的专业知识范围越来越广,于是我们就越来越容易陷入这种状态。

3. 由于没有考虑到环境差异而导致发生问题

有时进行了性能测试,也确认了系统性能能够满足生产环境的要求,结果顺利发布后,却发现在生产环境中并没有获得预期的性能。这种情况下可能就会被追究责任,被追问性能测试的相关内容,比如“真的做了性能测试吗”“为什么会出现性能问题”等。

特别是如果在生产环境中运行的时候发生问题,比如系统停止工作,或者是出现故障,那么就会对
业务产生影响,有时甚至还会导致金钱上的损失,所以必须防止这种情况的发生。

至于为什么会出现此类问题,大家在调查原因时很容易忽视一点,那就是测试环境与生产环境之间的差异。比如,硬件和使用的基础软件的类型的差异、磁盘延迟的差异、网络延迟的差异等。除了以上容易想到的形式上的差异之外,在生产环境中,由于内存量很大,因此有可能导致软件内存管理模块的开销也很大。

另外,因为 CPU 核数很多,所以有可能导致多线程管理的开销也会变大。像这样,乍一看性能应该会提升,但实际上反而拖了后腿的情况也时有发生。

在无法准备和生产环境一样的性能测试环境的情况下,或者无法使用生产环境进行性能测试的情况下,当然就会有失败的风险。可能的话,在报价阶段就把准备与生产环境一样的测试环境的费用也一起考虑进去,或者准备好在生产环境中运行后出现问题的情况下可以立即切换回老系统的功能,也能让性能故障的损失降低到最小程度。

4. 压力场景设计不完备导致发生问题

在性能测试时完全达到了期望中的性能,但在同样结构的生产环境中实际运行时,即使是同样的处理数量,性能也很差,这样的情况时有发生。在这种情况下,重要的是首先确认执行了怎样的性能测试。

常见的有以下几种情况:

  • 实际上有多个种类的页面操作,但是测试中只执行了单一的页面操作,漏掉了更复杂的处理
  • 测试时和实际运行时访问登录、查找页面等负载较大的页面的比例有差异
  • 用户的停留时间超过测试时的预估,在多个页面之间迁移,这些迁移信息累积在会话中,导致使用的内存也超出预估

5. 没有考虑到缓冲、缓存的使用而导致发生问题

性能测试时性能很好,但是在生产环境中性能却很差,这种情况下就需要注意系统的缓冲和缓存的使用状态。

如果多次访问同一个页面,那么与该页面相关的缓存就会保存在 LB、Web 服务器、AP 服务器的缓存或数据缓存,或者 DB 的缓冲缓存等中,响应速度就会变快。速度变快就证明可以按照预期的那样使用功能,性能也就没有问题。但无论如何,测试时与实际运行时的缓存使用量都会出现差别。

以下是几种比较常见的情况:

  • 测试时只访问了同种类型的页面
  • 只使用了同一用户 ID 来访问
  • 只访问了相同的查找对象(商品名称等)
  • 查找时只使用了相同的过滤条件

以上这些情况下,在性能测试的时候,利用缓存能够最快地返回响应,所以性能很好,也能以很低的资源负载来完成处理。但是,真正在实际生产环境中运行的时候,就会出现各种各样的处理和访问,缓存使用率就不像测试时那样高,也就变成了慢响应和高负载的模式。

为了防止出现这样的情况,就需要在测试的时候预估好实际运行时的缓存命中率,并动态变更请求等。

6. 没有考虑到思考时间而导致发生问题

在性能测试时获得了很好的结果,但在生产环境中,即使施加了同样的处理负载,却还是达到了系统的性能极限,这种情况下我们能想到的另外一个原因就是压力测试场景的“思考时间”(Think Time)。这一点很容易被忽视,但却很常见。

思考时间指的是使用系统的用户的思考时间的预估值。用户会连续进行很多个处理,比如,打开页面后,进行登录→选择菜单→输入搜索关键字→从列表中选择条目→填写表单等操作,各个步骤之间的跳转并不是一瞬间完成的,用户在阅读或者填写时都会花费一定的时间(从几秒钟到几分钟)。

预想到实际的使用状态,正确地预算好时间来设计压力场景,或者只是单纯地使用测试工具在前一个处理完成后就立即发出下一个请求,这两种做法对系统的负载是有差异的。

对于服务器来说,即使在相同的吞吐(访问处理数 / 秒)状态下,也会由于思考时间的有无所造成的差异,导致 HTTP 并发连接数以及应用程序的会话保持数产生很大的不同。每秒访问处理数相同但思考时间不一样的情况下,思考时间越长,在系统上同时滞留的用户数和会话数也会越多。这就导致系统的内存和会话管理的压力出现差异。因此,在考虑压力场景时,如果没有把真实用户的思考时间纳入考量,那么这个性能测试就脱离了生产环境。

7. 报告内容难以理解导致客户不能认同

向客户报告性能测试结果与普通测试结果的情况是不一样的。普通测试的情况下,只要能按照事先设计的那样来运行,就是合格。也就是说,如果使用○ × 来判断,那么只要全部项目都是○,就能得到客户的认同。

性能测试则不是通过○ × 来判定,而是通过数字来评价的。如果只是简单地拿一个数字作为指标,达到这个指标就算合格,那么向客户报告说“响应时间在 3 秒以内”“达到了每秒处理 1000 条的要求”等,有时也可以获得客户的认可。

但实际上并没有这么简单,例如:“每秒处理条数是 1000 条的时候,响应时间是 2.5 秒,CPU 使用率是60%,但是当每秒处理条数是 1200 条的时候,响应时间变为了 4 秒,CPU 使用率是 80%。不过,场景的思考时间从平均 5 秒减少到 3 秒的时候,负载就下降了 30%。另外,在登录比例比较高的场景中,每100 次登录会有 2 次出错。”

如果像这样认真地报告结果,客户反而会表示“完全听不懂”。

实际上,客户关心的焦点集中在“在实际生产环境中运行时是否会出现性能方面的问题”。因此在报告性能测试结果时,有时不得不使用“在○○范围内的话没有问题,超过这个范围的话就会达到性能极限”这样的表述。

对此,客户可能会误解,觉得没有简明扼要地做出说明,甚至有些客户也可能完全不接受这种说明。

为了能获得客户的理解而进行解释的一个例子。缺失了这其中任何一个环节,逻辑上看起来都会比较跳跃,也就会被客户要求补充说明,或者被客户抱怨难以理解。

8. 客户因为存在不信任感而不能认同

一般来说,客户很容易对性能测试结果的报告产生不信任感。花费很多时间反复调优,反复分析,却没有得出明确的结论,或者对于“在生产环境中运行时是否会出问题”这样本质性的问题只能给出附带条件的、模棱两可的回答,就会让客户误以为是在糊弄他。很多情况下,由于不能很好地共享性能测试的整体过程,或者本来关于性能需求或性能测试设计的需求等的约定就很模糊,导致结论与评价基准等也变得很模糊,引起沟通不畅。

如果得不到客户的信任,那么项目方不论做什么都需要去消除客户的疑虑,以及做一些举证等不必要的工作,导致效率变差。特别是需要客户合作的项目,这样一来就会陷入一个非常不好的状态。

9. 测试很花时间

性能测试所花费的时间要远远超出想象。功能测试的情况下,一般只需执行预计的操作,看其结果是○还是×,就能立即得出答案。而性能测试则不一样。

一般来说,下面列举的各项工作会特别花费时间:

  • 搭建与生产环境一样的结构

在性能测试的时候,必须搭建与生产环境一样的结构,而这个搭建工作可能会与普通的搭建工作花费相同的时间。

这个结构所必需的项目主要有网络、存储、OS、中间件、实例生成、应用程序部署等。

  • 生成用于产生负载的环境与路径

把产生负载的工具连接到哪个网络点、形成怎样的工具配置结构,以及如何设置防火墙与负载均衡、如何设置负载工具本身等,这些工作与普通的搭建工作一样要花费很多时间。

在设计时没有考虑到性能测试的路径和配置,临近测试时才临时抱佛脚,讨论可配置的结构、预留地址和路由、设置机器、调整设置场所等,这种情况都是常有的事。

  • 设置用于性能测试的资源统计监控

即使进行普通的运维监控,有些情况下也需要通过别的方法来进行用于性能测试的资源统计。

通常的运维监控以 5 分钟的间隔来监控就足够了,而在性能测试的时候,想要按 1 分钟的间隔来监控,从而确认详细的运行情况为了便于在性能测试的时候分析瓶颈,想要更详细地进行资源统计
在上述很多情况下,都需要重新设置监控登记的设定。

  • 负载生成场景脚本的生成

为了实际运行负载生成场景,需要将其脚本化。即使是在测试准备阶段,这个工作有时也要花费相当长的时间。首先,需要讨论场景和分配等,比如什么样的页面操作流程更接近实际的负载。

接着,结合应用程序的特性,在登录的认证模拟器、Ajax 和 Web 服务器通信等的应对处理,以及多个查找关键字和菜单选项等的多样化设置方面,考虑将发向服务器的请求的参数中哪部分以变量形式实现等,然后据此生成场景脚本。

此外,以 CSV 等形式生成像数据银行(Data Bank)这样的登录 ID 或输入值列表,并设置为每次从中读取不同的数据,这部分做起来也很花时间。

举个极端的例子,即使是经验丰富的性能测试工程师,并且使用自己熟悉的方便的测试工具(Oracle Application Testing Suite 等),估计 1 天最多也只能完成 3 个脚本。不熟练或难度高的情况下,设置 1 星期也完成不了 1 个。

  • 生成用于性能测试的模拟数据

仔细想来,模拟数据的生成其实是一个很费力的工作。并不是简单地把随机的数字插入到数据库中就可以了,而是需要保证和实际相近的文字模式和散列程度(Cardinality,数据种类的浓度、分布的偏差值)等,而且要生成 100 万条数据本身也是件很复杂的事。多个系统间合作的情
况下,还需要以能够相互联动的数据结构来准备。

此外,在用生产环境生成测试数据的时候,需要先备份生产环境的数据,然后导入测试用的数据,在测试结束后再恢复生产环境数据。光是这个备份恢复工作,有时也可能会由于数据量大而耗费一整天的时间。

  • 性能测试的实施周期

实际执行一下性能测试就会发现,实施周期也远远超出想象。

后面我们还会提到,一个恰当的性能测试会逐渐增加并发度。如果按照预期的那样来实施测试,就会发现每次测试大概需要 30 分钟到 1 小时。

另外,在测试前后还要对测试结果进行分析和评价、备份与恢复数据、进行调优等。这样一来,即使是独占系统的状态下,实际上 1 天能完成的测试次数也不到 3 次。

  • 评价结果

评价测试结果有时也很花时间。例如,即使可以作出“速报中达到了每秒 1000 条的吞吐”这样的报告,但是各个场景、各个操作步骤的响应分别达到了预定的目标吗?资源使用率在各个统计对象中都处于目标范围内吗?在日志中出现了错误信息吗?从服务器返回的内容都是正常的内容吗?如果对此逐一进行检查的话,每次测试都将花费很长时间。

  • 排查瓶颈

排查瓶颈这项工作所需的时间完全由调查负责人的技术水平决定。如果是对网络、LB、AP、DB、存储、应用程序的内部结构和 Java VM 等的实现方式等全部了如指掌的超级工程师,那么不管怎样的性能问题,都能在比较短的时间内排查出来。

但是,实际上这样的人几乎很难找到,所以一般会各自分开调查,或者咨询产品供应商,对于异常的部分一个不漏地慢慢调查,最终排查出瓶颈,找出问题。如果技术水平比较低,可能到最后都不能找到瓶颈。这样的话,这个系统也就不能发布了。

也就是说,存在非常花费时间,或者不知道到底会花多少时间的风险。

  • 生成结果报告

简单来说,只要能在结果报告中记录测试结果、结论以及支持这些结果结论的数据并进行说明的话就足够了。不过,当涉及的数据量很大时,光是汇总统计、作图就很费时间。而且,在对复杂的瓶颈和调优进行说明时,还需要在资料中记述理论证明的过程,这就几乎是写一篇论文的工作量了。

此外,即使觉得结果报告没问题了,提交给客户之后,如果客户理解不了,或者是被指出缺失了什么重要的信息,也有可能要重新制作一份。

  • 制作性能测试计划、调整工作分配

如前所述,性能测试具有耗时较长的特性,因此有必要认真地制作计划,调整工作分配,并有条理地进行。

如果涉及的人员较多,或者系统的供应商很多,那么制作计划的商讨和调整、沟通、安排等工作甚至可能会花掉数周时间。

如果不尽早安排这些计划,就会导致性能测试的开始时间延迟,这一点请注意。

3、性能测试的种类

根据目的与情况的不同,性能测试可以分为很多类,接下来就让我们来看一下。

1. 实施的周期

在通常的开发、搭建项目的各个阶段,性能测试有几个变种。

作为判断性能的基准,最重要的测试就是(狭义的)“性能测试”。该测试也决定了系统能否发布。除此之外的其他测试则是为了更有效地运营项目或者其他目的而实施的。

“Rush Test”“压力测试”等不是根据测试目的来定义的,它们表示的只是测试的执行方式,因此在什么时间执行是由测试目的决定的。压力测试根据目的的不同,可以分为“性能测试”“临界测试”“耐久测试”等类型,具体做法都是在短时间内向系统发起大量的访问,以此来测量结果。一般会再现多个同时在线的用户的使用情况。在某些情况下,批处理时大量数据的流入也属于这一类。 

2. 狭义的性能测试

这个是最为重要的测试,目的是判断是否能达到要求的性能。

  • 实施时间

在系统测试阶段实施。前提是系统测试的功能部分已经全部通过测试,确定之后不再需要系统变更,并且已经做好了进行运用管理和批处理操作的准备,包含此动作的性能测试也已经准备好。如果没有满足这些前提条件,那么性能测试完成后,系统性能也有可能会发生变化,所以请尽量在上述前提下执行性能测试。

  • 测量项目

性能测试需要确认以下 3 个性能指标是否均已达成:

  1. 吞吐(处理条数 / 秒)
  2. 响应时间(秒)
  3. 同时使用数(用户数)

此外,确认服务器日志以及压力测试工具的记录,同时确认在测试中是否出现了错误信息。如果出现了错误信息,那么这个处理就有可能在执行过程中被跳过了,也就没有完成充分的性能验证。这种情况下就需要首先消除错误,然后再次执行测试。

如果已经定义了系统运行时资源使用率的上限(例如,遵守 CPU 使用率在 50% 以下等),则还需要一起确认资源使用率。

3. 临界测试(临界性能、回退性能、故障测试)

前面提到过,性能测试用于判定系统能否发布,而除此之外还存在从其他角度进行判定的压力测试。

  • 临界测试(最低性能)

测试是否达到了性能目标的基准。临界测试作为性能测试实施之前的预实施,不用进行太严密的用户场景定义与资源统计,其目的仅仅在于对能否处理预计的处理条数进行简单的确认。另外,在实际执行性能测试的时候,可能会出现负载对象的性能不足、施加负载的一方性能不足、结构错误等情况,临界测试的另一个目的就是发现这些问题。

若无需进行大规模的准备工作就能立即执行的话,应该在系统完成集成测试之后或之前执行一下。

1)实施时间

在大规模地正式实施性能测试之前,需要与各相关部门进行协调。有时候性能测试的实施时间是有限制的,因此测试负责人或服务器管理者应该事先确认好自己责任范围内的测试是否能正常进行。

2)测量项目

不需要花费太多精力,只要进行最小限度的确认就可以了。

  • 临界测试(最大性能)

这个测试的目的在于,在负载超过性能目标的情况下,把握系统承受程度的上限,以及当时的情况和瓶颈。

如果需求定义中没有要求进行临界性能的测量,那就没必要实施这个测试了。不过,在实际向客户报告测试结果的时候,可能会被问到“超过这个负载的时候能正常运行吗”“验证过流量控制和超时功能了吗”等问题。系统发布后,在超负载的情况下,如果流量控制、超时、运维监控的阈值检测机制等不能按照预想的那样正常运行,就会出问题,有时甚至会被当成残次品。为了消除这些隐患,我们要执行临界测试。要想通过验收,性能测试是必不可少的一项工作。而临界测试则非如此,而是项目方为了维护项目成果、避免风险而自发实施的测试。

1)实施时间

在系统测试阶段顺利通过性能测试后,如果有足够的时间就进行此项测试。这个时候可以进行两种测试。第一种是偏向基础设施的测试,在施加了与生产环境相似的流量控制的状态下,确认流量控制功能能否正常运行。另一种是在不进行流量控制的状态下,确认系统所能处理的上限以及这个时候的情况和瓶颈原因。在进行了这两个测试后,如果能基于明确的记录,对系统在超负载时的情况进行说明,客户一定能认可这个报告。在某些情况下,如果很好地执行了后面提到的“基础设施性能测试”,也可以不实施这里的第一种测试。

如果系统是横向扩展结构,那么也需要验证横向扩展结构下达到临界负载时的运行情况。理想情况下,在达到最大负载时,AP 服务器和 DB服务器的 CPU 使用率会达到 100%,或者网络带宽的使用率会接近100%,像这样施加负载让资源达到上限的话,那么作为临界性能测试的测试结果就可以说足够了。如果资源使用率没有达到 100% 就已经到了性能界线,吞吐也不能继续提升,或者增加负载也只是导致响应变差,那一定是哪里的设置存在瓶颈,必须搞清楚原因。

在从长远的角度计算系统使用人数的增加量以及针对这种情况的估算指标时,除了在纸上计算之外,还可以一并参考临界测试中阶段性的负载增加以及资源使用量。特别是与公司内部系统不同,在互联网系统中,可能会出现用户突然增加的情况,因此为了建立估算战略,事先进行测量是非常重要的。

2)测量项目

实施临界测试的方式是一直施加负载直到达到最大吞吐。使用压力测试工具增加并发度来生成负载的情况下,并发度增加到什么程度也是一个基准。此外,如前所述,为了判断资源是否用尽,也要一起参考服务器的 CPU 使用率。

  • 回退性能测试

回退性能测试也属于一种故障测试。在那些为了确保可用性而使用了冗余结构的系统中,我们需要验证当其中一部分处于停止状态时,是否能获得预期的性能。如果存在回退情况下的性能需求定义,就要进行这个测试。即使没有进行需求定义,如果在生产环境中运行时发生回退,导致没有获得预期的性能,也会很棘手,所以要尽可能地把这个测试加入到项目的验证计划中。

1)实施时间

分为两种情况,一种是在系统测试阶段的性能测试结束后执行,另一种是在后面将会提到的基础设施性能测试的过程中执行。如果冗余结构以及可用性功能在系统基础设施中就完成了,并且能够与搭载的应用程序剥离开来,那么只需在基础设施性能测试中执行回退性能测试就可以了。其他情况下,由于要让应用程序在类似于生产环境的环境中运行来进行测试,因此就要在性能测试之后来执行了。

不仅要对一部分处于停止状态的结构进行性能测试,也要对运行过程中停止或者再次启动时响应时间的变化进行确认。

2)测量项目

通常的检验方法是,作为测量指标,通过吞吐来确认最大性能,以及通过响应时间和是否发生错误来确认行为的变化。

3)故障测试

故障测试实际上并不属于性能测试的种类,一般被归类到集成测试或系统测试中执行的故障测试。不过,在发生与性能相关的故障时,需要结合压力测试一起实施,且故障测试与这里介绍的其他测试手法相近。

故障测试的目的是触发高负载时会出现的故障,判断那种情况下系统的行为以及错误恢复是否与预计的一样。特别是那些会出现高负载但又追求高可用性的系统,故障测试是必需的。

4)实施时间

如果作为基础设施可以分离开来的话,可以在集成测试和系统测试的基础设施上进行故障测试。如果不能分离,则可以在性能测试和临界测试等完成后,基于已经确立的性能测试和临界测试的手法来进行测试。

需要注意的是,如果在一般的性能测试和临界测试等场景中直接执行的话,有可能不能触发目标故障点,而是在别的地方出现瓶颈,导致不能触发希望出现的性能故障。这个时候,需要重新考虑负载场景、系统结构和设置,直到可以触发希望出现的性能故障。

5)测量项目

首先着眼于服务器以及负载终端的错误,确认那个时候的吞吐以及平均响应时间,将其作为参考指标。 

4. 基础设施性能测试

在最近的系统搭建中,大多会将应用程序和基础设施分离开来,分别制定搭建计划,然后在集成测试或系统测试中才将其汇合到一起。基础设施中包含中间件(DB 或 AP 服务器)的情况也很多。此外,基础设施作为基础,有专业负责人或供应商执行别的调度计划和检查,采用和应用程序不同的流程更容易推进。综合基础设施和私有云等一开始往往不能准备好应用程序,所以有时就需要在没有应用程序的情况下进行基础设施的发布及基础设施测试。

  • 基础设施性能测试的目的与必要性

基础设施性能测试是与应用程序分离开,从基础设施的观点来进行的性能测试。基础设施性能测试的目的是防止在后面的系统测试阶段中基础设施出现性能问题导致返工或计划变更。基础设施搭建团队通过预先进行负载试验,来尽量规避风险。

只要没有作为验收条件进行规定,基础设施性能测试就不是必需的。不过,如果在系统测试后的性能测试中才发现基础设施存在性能问题,返工成本就会很大,而且也有可能会影响到计划。为了不出现这样的情况,强烈建议在基础设施方面进行与实际生产环境相似的性能测试。

  • 实施时间

基础设施搭建结束后,在基础设施的集成测试中故障测试完成之后实施。

在基础设施上进行性能测试,其最大课题就是在应用程序还没有完成的状态下如何预估出需要的性能,以及怎样使用作为样本运行的应用程序。

  • 测量项目(样本应用程序)

评价的对象不同,测量的应用程序也不同。

从网络到Web服务器的基础设施性能测试在 Web 服务器上部署静态资源,然后对其发起大量访问就可以了。

包含依赖于会话的处理在内的基础设施性能测试使用依附于应用程序的样本程序。如果是WebLogic 的话,就经常使用PetShop 或 MedRec 等作为样本项目。这些程序会进行包含登录在内的会话管理,因此使用负载均衡器或 Web 服务器来进行会话和 cookie 的处理,然后分发,这样作为性能测试来说就足够了。

使用数据库或缓存网格(Cache Grid)或KVS的情况这些服务都不是直接从外部来访问,而大多是从 AP 服务器来访问的,因此很多时候不需要经过全部的基础设施。这种情况下,建议一个一个
地单独验证。各种测试工具应该都已经准备好了。

在进行数据库的基础设施测试时,为了按照事先想好的处理流程编写脚本,或者预设好实际的运行步骤并添加负载,使用 Oracle Real Application Testing 或者 Oracle Application Testing Suite 的 Load Testing Accelerator for ORACLE Database 也很方便。

在基础设施性能测试中,特别是与存储和数据库相关的测试,需要准备好与生产环境相同的数据量来验证。另外,测量备份和恢复所需的时间以及运维批处理能否在一定时间内完成也应该包含在基础设施性能测试中。

  • 基础设施性能测试的性能目标

在基础设施性能测试中,除了样本应用程序之外,另一个课题就是应该以什么样的性能目标作为基准。在讨论目标值时,一般按照下面的顺序进行。

① 提出性能目标信息

让应用程序开发部门提出严谨的性能目标信息。具体包括负载均衡器和Web 服务器上必需的同时连接数、每秒的请求数、网络流量(bps)等。DB 基础设施的情况下,只有简单的处理数和同时访问数是不够的,如果不能在应用程序这里更进一步,让其提示与业务相同级别的SQL 的同时执行信息,就不能完成充分的基础设施测试。

如果没有很好地定义这些指标就进行应用程序设计,就会在开发时忽视性能,因此应该对应用程序开发部门明确地提出要求。

② 自己预估

如果不能获取上述信息,或者对应用程序开发部分预估的信息不够放心,就需要自己来预估,顺便进行验证。

下表中汇总了预估目标信息所需的项目与知识:

使用表中的各个项目如何计算出各个目标值呢?

方法如下所示:

算式中带圆圈的数字就是表中项目的编号。

带宽(bps)的计算方法:(每小时的处理页面数)×每个页面的大小× 没有命中缓存的概率((① × ② × ④)×(⑤ × ⑥)×(1 -⑦))÷3600s(1h)×8(bit)=带宽(bps)

吞吐(请求 / 秒)的计算方法:(每小时的处理页面数)× 没有命中缓存的概率((① × ② × ④)×(1-⑦)÷3600s(1h))= 页面请求数 / 秒

同时访问数(用户人数)的计算方法:(每小 时的 处理 页面 数 )× 平均思考时间(① × ② × ④)× ③÷3600s(1h)= 同时访问数

同时访问数(活跃连接数)的计算方法:((① × ② × ③ × ④)÷3600s(1h))×(⑧或者⑤中比较小的那个值)÷(HTTP KeepAlive 的预计时间或者③中比较大的那个值)

“同时访问数”实际上会比这里的值更小。这是因为浏览器能够使用一个连接来处理多个内容。如果要进行更精确的计算,就需要考虑每个内容的平均响应与连接的整合度。 

5. 应用程序单元性能测试

应用程序的单元性能测试是在集成测试执行之前进行的测试。在集成之后发生性能问题的时候,如果不能简单地修复,就会导致集成之后的计划延期。为了防止这种事情的发生,应该提前进行应用程序单元性能测试,以防范于未然。

虽然这个测试并不是必需的,但是为了推进项目顺利进展,防止像前面那样在项目后期才发现问题导致返工,对项目计划和成本产生影响,就需要应用程序单元的开发方在确认性能之后再移交。

  • 实施时间

在应用程序开发中进行单元测试时,建议同时进行单元性能测试。可以像单元测试一样以测试优先(Test First)的形式组合,在每天进行 build时自动测试并检测出错误的机制中加入性能测试。

Java 的话也可以通过JUnitPerf 等来实施。可以在代码中直接要求,若超过了响应时间的目标就报错。

6. 耐久测试

耐久测试可以归类到故障测试这一大类中。但是,耐久测试可以沿用性能测试的方法,比起单独执行,与性能测试一起执行效率更高。因此,耐久测试作为性能测试的关联领域,多由性能测试的负责人来实施。

此项测试的目的在于确认系统长时间运作时是否会出现故障或报错、内存泄露、计划外的日志堆积,以及日志轮转(Logrotate)和每日的批处理是否可以正常运作等。建议在追求高可用性的系统中实施此项测试,不过需要确定耐久测试所需的时间。

若需要进行 1 周的连续作业测试,当然就要占用 1 周的系统,中间若出现失误需要重新实施,或在耐久性测试的结果中发现问题,就需要进行修正并重新测试。因此,在确定耐久测试所需的时间时,至少需要预留耐久测试实施时间的 3 倍的时间。

  •  实施时间

如果在完成性能测试以及临界测试后,在项目上线前还有时间,并且有
充足的时间可以占用系统的话,建议使用这个时间来进行耐久测试。

  • 测量项目

耐久测试主要关注的是以下项目:

  • 响应时间:需要观察平均响应时间是否有变差的趋势。该指标比 CPU 等的资源使用率更容易抓住问题。若有逐渐变差的趋势,就代表某个部分有可能发生了劣化。
  • 内存使用量:需观察进程中的内存使用量是否在逐步增加,据此可以检测出是否存在内存泄露。但是近年来 OS 中有缓冲和缓存的机制、堆内存(Heap Memory)和 GC 的机制,有时会先保证大量的内存,并在其中进行处理,所以需要在理解架构之后进行确认。
  • 磁盘增加量:在设计时应该有一个指标,即当访问数量是多少时日志的增加量是多少。然后再回过头来确认实际情况是否和设计时的预想一致,以及其他无关的目录下磁盘使用量是否有增加等。
  • 其他参考指标:CPU、线程数、系统内部内存(DB 的缓存、Java VM 的堆内部的动态)等。

7. 关联领域

如上所述,性能测试的关联领域有故障测试、耐久测试等。虽然这些测试单独实施起来难度较高,但由于可以利用性能测试的手法,而且大多数情况下都可以由性能测试的负责人来帮忙或者负责。

要想成功地完成项目,项目组成员就不能持有“只要我负责的那部分没问题就 OK 了”这样的态度,而是应该所有成员一起努力协作,而项目经理则需要为大家创造更加容易一起协作的环境。

这里我们围绕着系统发布前的性能测试进行了解说,但在实际的开发现场,很多时候都是在项目发布后的打补丁以及库文件更新时进行的。为了使系统稳定运行,运维的时候也要能随时在测试环境中增加负载进行测试。

4、项目工程中考虑的性能测试

虽然一心想要使性能测试成功,但测试开始之后就会发现,有些地方是无论怎么努力也无能为力的。例如在需求定义中规定的关于性能测试需求的部分,在进入测试之前的设计、开发和搭建阶段实现的内容的变更,项目日程上性能测试所需的时间太短等,这些都是在测试开始后就不能改变的。

这样一来,不但不能充分地进行性能测试,还会在对性能抱有不安的情况下把系统发布上线,这对于项目和业务来说都是不好的。因此,项目经理需要从项目整体的角度来把握性能管理,以使系统顺利发布。

1. 需求定义

  • 需求定义中的 3 个必需要素

在需求定义中,一定要定义“吞吐”“响应时间”“用户并发度”这 3 个要素。不只是在性能测试中,在系统实际运行时,这 3 个要素也对性能有重要影响。

假设吞吐为 T,响应时间为 R,用户并发为 U,那么这 3 个要素之间的关系就可以用下面的关系式来表示。

U×R = M

这个关系式意思是,任何一个数值发生变化,其他指标也都将跟着变动。看起来像是进行了需求定义,但如果忽略了其中任何一个值,那么性能测试就能随意往好的方面解释了。因此,性能目标一定要包含吞吐、响应时间和用户并发这 3 个要素。

  • 指标的计算

根据环境和测试目的的不同,吞吐指标也有所不同。例如,即使是对同一个 Web 进行压力测试,目的不同的话,测量指标也会不同,因此请务必注意。在已经确定吞吐指标的情况下,为了与处理条数进行对比,要定义每个处理相应的响应时间

吞吐的测量基准:

  • 吞吐的计算

计算吞吐的时候,如果有现成的系统,可以从它的访问日志来确认其之前在峰值时间的处理条数,然后加上将来预计的增加量,得出的结果就可以定义为目标值,再与客户一起协商。

如果没有正在使用的系统,可以首先算出预计的使用人数、这些用户的使用时间段的分布情况、用户的访问频率、每个访问对系统发出的请求次数等,然后据此进行推导。

  • 并发的思维

并发并不是作为性能目标通过听取客户意见来推导出来的,而是通常根据吞吐和响应时间计算出一个合适的值(客户即使了解业务中同时使用的人数,但是对于系统瞬间运行的并发处理的情况,他们并不清楚)。

那么,如何推导出来呢?这个时候“用户操作的预计思考时间”就很关键了。计算出平均每个用户在处理中按照什么样的频率来访问,这个平均值可以说就是 1 个并发时的吞吐(例如,每 5 秒钟进行 1 次操作 = 0.2次 / 秒的吞吐)。然后,计算并发多少个能到达目标吞吐。

案例:

  • 目标:1000 条 / 秒
  • 1 个并发的情况下是 0.2 条 / 秒
  • →它的 5000 倍就是 1000 条 / 秒
  • →因此,“5000 个并发”就是在系统中实际发生的并发度

应用程序服务器等在登录到退出期间会缓存会话信息。对于这些信息,客户端会以 cookie 形式保存,服务器端则会作为会话内存来保存。为了使其按照预想的方式来执行,也需要准备好进行实际用户滞留期间的处理的场景。

另外,在进行 HTTP 通信的时候,需要知道并发度与 TCP 连接数量是不同的。下图中,每个用户会生成多个 TCP 连接,此外,TCP 连接的状态也会因 KeepAlive 的有无而不同。

没有 KeepAlive 的 TCP 连接:

有 KeepAlive 的 TCP 连接: 

  • 在何处进行性能需求定义

性能需求定义一般是在系统设计阶段进行要求定义(必须满足的需求,其重要程度比需求定义高)的时候,作为非功能性要求来进行定义的。

在 RFI(Request For Information )以及 RFP(Request For Proposal )中明确要求,并在其中定义上述性能目标值。然后,供应商对此提出可行的系统架构方案。虽然也有一些项目在还没有对性能目标达成一致的时候就提出方案并着手进行,但在这种情况下,在系统测试阶段或者系统
发布之后,往往就会出现性能问题,导致故障。

  • 讨论性能需求时的注意事项

除了前面提到的 3 个要素之外,严谨地考虑实际场景的话,还需要讨论下表中的项目。

2. 项目规划

会回答至少需要 1 个月左右。虽然根据项目性质的不同,有的可能会进展得更有效率,但是只预留 1 周时间的话风险还是很大的。即使匆匆忙忙地进行 1 轮测试以及相应的准备、分析工作就结束,如果性能没有达标,也就必须重新制定计划,性能测试是非常费时间的。

性能测试中涉及的人员如下所示。负责各项工作的人员,即使不是全职,也要确保能在需要时立即参与到性能测试中。否则,当碰到什么问题的时候,如果不能在短时间内解决问题,就可能会影响到整体的进度。

  • 性能需求定义负责人(提案SE)

从系统提案时的信息中提取出针对性能测试的性能目标,并传达给测试负责人。另外,当系统提案时的性能目标不明确或者有矛盾的时候,也要负责与客户协商,调整性能目标。

  • 项目经理(PM)

确定性能测试项目的工作和计划,在实际进行性能测试的过程中做出决策,对计划书和报告书进行最终确认。

  • 性能测试设计负责人(架构师)

从架构的角度来确认性能测试设计中的负载生成和测量计划是否合适。另外,在测试过程中出现性能瓶颈的时候,要起到分析和调优等的主导作用。

  • 性能测试负责人①(性能测试设计、计划、报告)

作为测试团队的领导人,制作测试设计、计划书,并在测试完成后制作报告书,进行报告说明。

  • 性能测试负责人②(测试环境准备、实施、统计

生成测试脚本、准备测试数据、设置测量项目、执行测试、分析结果等。

  • 性能测试分析负责人(中间件、DB、网络、AP等各个领域的专家)

作为中间件、DB、网络、AP 等各个领域的专家,观察性能测试,在发生性能问题的时候,从各自的专业视角进行分析和调优。基础设施搭建负责人(在测试时进行基础设施的修改)

在测试过程中需要修改 LB 设置或网络、存储、服务器等的时候,负责进行修改工作。

应用程序、数据负责人(准备测试环境的数据、实施应用程序内部的调优)

生成及更新测试用的数据库,针对测试修改应用程序内部的逻辑,当应用程序逻辑部分出现性能瓶颈的时候,进行修正、调优工作。

3. 选择系统

  • 确认是否有性能测量功能

机器能否应对预想的性能测试方法(大量负载的生成)这一点也很重要。有些产品虽然宣称性能很高,但是却不支持施加大量负载进行验证,有的产品则不允许供应商指定方法之外的负载的生成。这样一来,验证方法就会受到很大的制约。

可能的话,最好选择性能测试实例丰富、项目成员经验丰富的产品,或者是积极提供性能相关信息的供应商的产品,这样能降低设计、测试时风险。

4. 性能测试环境

基本设计时应该会定义系统结构和网络拓扑等信息,这时容易忽视性能测试的流程及测试环境的使用方法。如果这些信息在设计时被忽视,在后面的系统测试阶段就有可能需要从设计开始变更环境,那就很麻烦了。

基本设计时需要定义以下项目:

1)如何搭建验证环境

在进行性能测试时,如何搭建与生产环境一样的验证环境呢?是使用生产环境的机器呢?还是使用验证机器呢?性能测试的实施基本上会独占系统,并且在系统发布后,每次执行批处理或再次发布的时候都要对系统进行性能验证。因此,一般不使用生产环境或开发环境,而是准备与生产环境同样规格、同样软件版本的验证环境。

2)使用哪种负载生成工具

根据特性的不同,有些负载生成工具能用于基础设施测试,却不能用于应用程序测试中复杂的 HTTP 会话或页面出错的判断等。

Oracle 提供的 Oracle Application Testing Suite 的 Load Testing 功能能在Windows 和 Linux 中进行分布式安装,简单地进行从基础设施到应用程序的大量压力测试。另外,用于分析瓶颈的资源统计功能也很丰富,可以一边实时分析一边进行测试,能大幅提高性能测试的执行效率。

3)性能测试时的数据放置在哪里

在进行性能测试的时候,需要在 DB 存放大量性能测试用的数据,然后实施测试。另一方面,在用户验收测试(UAT)或实际运行时需要重新替换成真实的数据,因此数据保存和备份恢复的功能很有必要。另外,多次进行更新相关的测试的情况下,如果每次测试都把数据恢复到更新前的状态,那么就能在测试的时候使用相同的场景,这样就能避免测试之间的差异。为此,需要在短时间内获得备份并进行恢复的功能。至于是在 DB 服务器上存储备份数据还是使用外部存储,这个问题虽然只在性能测试中涉及,但也需要事先考虑好。

使用 Oracle Database 的情况下,建议使用名为 Flashback Database的功能,据此能够快速恢复数据。通常的恢复操作需要等待超过 1 个小时,但是这个 DB 可选功能可以在短时间内恢复到指定的恢复点。

另外,除了 DB 级别之外,在虚拟化环境或云计算环境中,也可以从文件系统获得快照,然后恢复到那个时间点。这个方法需要重启 DB,但是能在比较短的时间内切换回来。

此外,Oracle 的名为 ZFS 的文件系统中提供了检查点(checkpoint)和恢复的机制,并且还提供了能够让 ZFS 高速运转的名为 ZFS SA 的存储设备。

如果只是查询测试数据而不需要更新,那么有一个突破性的方法,即屏蔽(Masking)生产环境的数据,使得在测试中也查询这些数据,保持与生产环境的数据同样的条数、同样的数据稀疏度来测试,在发布的时候也不进行切换。使用 Oracle Database 的情况下,可以通过 Oracle Data
Masking Pack 这个选项来实现。

4)网络结构

在性能测试的准备工作中,网络结构是否支持性能测试这一点也很容易成为问题,因此需要在基本设计阶段提前确认好必要事项。

5)负载生成设备的放置位置

在选好负载生成工具后,需要考虑把它放在哪个位置。通常会放置在从最接近用户访问路径的系统的网络外面进行访问的位置。但是,在实际的性能测试中,如果性能没有达标,那么为了对网络瓶颈进行区分调查,最好可以连接更近的网络来再次验证。另外,还需要确认网络是否连通、可以使用的 IP 地址数量是否匹配负载生成设备的台数等信息。

6)带宽

生成负载时会大量占用带宽。在这种情况下,如果与生产环境中用于其他功能的网络叠加在了一起,请注意不要对其产生影响。另外,如果能够使用的带宽很小,那么施加的负载量也会受到限制,因此请事先确认好路径上各个网络能使用的带宽大小。

7)路由与访问路径与负载均衡

也需要确认作为网络从负载生成设备可以路由的路径、防火墙等许可的访问路径、通过 LB 进行负载均衡的网络结构。在 LB 的内部与外部进行的处理不仅是单纯的负载均衡,可能也会使用内容改写、 SSL 加速或内容缓存等功能,这种情况下也需要考虑对性能测试带来的影响。

8)测量路径

在性能测试时,为了便于瓶颈分析,最好能进行比普通的运维监控更细致的资源测量。需要从网络设备通过 SNMP 测量流量和负载信息,连接服务器代理或虚拟机,来测量资源的使用情况,还需要确认在测试时是否有获取这些信息的路径。使用 Oracle Application Testing Suite 的情况下,能通过浏览器远程访问来操作、分析压力测试。使用这种工具的时候,只要能确保对于它的远程访问路径就可以了。

9)物理设置

除了前面介绍的准备工作之外,还必须确认是否能进行物理设置。连接光缆的时候,需要确认端口的闲置数量是否足够、光缆线能否连接上、是否有放置负载生成设备的空间、是否有电源等。在大规模系统中,生产环境通常在数据中心,为了进行为期几天到几个月的性能测试,还需要事先确认数据中心是否能放置设备、是否有人待的地方等,并在必要时进行协调。 

5. 其他与性能设计相关的事项

优秀的架构师在设计应用程序的时候会把性能分析也考虑进去。这不光是为了方便进行性能测试,对实际运行时出现的故障的分析也是非常有用的。下面是几个需要考虑的要点。

可以从外部获取当前队列中滞留的请求数和线程使用数队列中发生滞留、线程急速增加或达到上限等,这些都有助于找到瓶颈发生的地方。

有可以计算重要处理所需时间的机制在发生延迟的时候,如果能明确从哪里到哪里的处理有变慢、哪里没有变慢的话,就很容易区分开来。

能获得各个应用逻辑所需时间的平均值、最大值和最小值实现高水位线(High Water Mark,HWM)等(过去最大记录值)的统计后,能比较容易地把握那些不能在平均值中体现出来的暂时性的性能问题。

外部交互时使用的数据的请求和响应,其内容及所需时间能够被记录下来以方便排错(Web服务交互和SQL执行等)在多个系统之间出现性能问题的时候,很难分辨出是哪个模块导致的,而数据交互过程的可视化就有助于提高排错效率。

在日志中能记录各个处理的会话ID、序列号及所需时间要调查涉及多个模块的性能问题,就需要彻底追踪哪个请求被发送到了哪里。如果能记录处理所花费的时间,那么在记录各个重要步骤的时间
点的同时,也能区分出各个会话。

6. 性能测试设计

性能测试设计的收尾工作就是制作性能测试计划。完成之后的性能测试计划可以说是性能测试设计的汇总。

通过详细描述项目的长期计划、中期计划、短期计划,将整个工程的任务具体化,能够更好地把握和分享工程。当然,也可以使用项目管理工具。

1)长期计划

从纵观项目的角度来制作计划。单位可以是月,或者把 1 个月分为上旬、中旬和下旬,或者以周为单位分割为 4 ~ 5 份。将项目结束为止的里程碑似的重要事项记录下来。

长期计划中有一些检查的要点:性能测试是否被安排了足够的时间(1个月以上等),作为实施性能测试的前提的集成测试、基础设施单元性能测试以及后面的用户测试等能否在时间上没有冲突地执行等。

建议大家将介绍的各个项目工程中为性能测试所做的准备工作作为任务记下来。

2)中期计划

中期计划以天为单位,整理出每天做什么事情。其内容如下所示,汇总了性能测试中里程碑似的重要事项,依此来调整整个计划。

3)短期计划

短期计划是性能测试前一天及当天的工作安排。由于短期计划中会列出各个时间段的工作,因此单位是小时或者分钟。

短期计划的目的是记录测试的各个步骤开始及结束的预计时间,把握实际的工作量能否在规定的时间内完成,以及为确认各项工作负责人之间的合作事宜而整理信息。

在生产环境中进行测试的时候,也会记录停止和切换的开始时间和结束时间、备份结束时间、向全体人员发邮件报告各个里程碑似的重要事项的时间等。 

7. 人员配备与联络体制

在测试计划中,需要整理前面提到的必要功能分别由谁来负责、他们的联络方式是什么、什么时候进行处理、负责协调各部门工作的窗口是谁、需要上级来协调时的路径是什么等信息。把这些信息总结成功能划分图与组织图,来进行协调。组织图并非只有 1 个,应尽量代入各个阶段来准备。

另外,指定了负责人后,应确认该负责人能够工作的时间范围并告知大家。还应该将是否需要处理其他业务以及休假情况考虑在内,制作 1 个计划图。需要晚上工作的情况下,由于会对第 2 天的工作计划产生影响,因此在制作计划图时也需要把这个考虑进去。

8. 基础设施集成测试

基础设施性能测试主要是由基础设施搭建负责人通过以下步骤来完成的。

  • 基础设施性能目标的定义

基础设施性能目标定义中包括吞吐和并发度等目标的定义。由于定义时应用程序还没有完成、信息不全,因此一般会定义一个比预想的值更大的值。

  • 性能评价指标的定义

基础设施性能目标有两个作用:一是提前检测运行预想的应用程序时是否有足够的性能;二是确认基础设施本身是否能达到其所宣称的性能。在这一点上,根据情况可能会使用远超过应用程序的性能目标的值来进行测试。

具体项目包括网络传输量、磁盘 I/O 的吞吐、LB、Web 服务器的处理性能等。

  • 性能测量手段的设置

作为基础设施性能测量的手段,既可以使用应用程序的压力测试工具,例如 Oracle Application Testing Suite ,也可以使用各种专门的工具。

  • 性能测量

在负载生成的时候,并不仅仅测量这个负载和吞吐的上限值,还要测量其他的相关资源来获取信息。在后面对应用程序进行性能测试的过程中,在定位瓶颈时可能会用到这些信息。如果没有事先获取以上信息,在后期就要再次进行同样的工作,这就可能会导致项目延期。

基本的测量项目:

以下几项是基础设施性能测试中调查性能不足的原因时所必需的,但基本上都不能立刻用上,可以理解为是后面的工程中需要的。

  • CPU 使用率(linux: idle、user、sys、wio、st ;Windows: user time、kernel time)
  • 中断和系统调用的发生率(int、call)
  • 磁盘繁忙度
  • 网络详细统计(例如 netstat。不管是 windows 还是 Linux,都可以使用这个命令来掌握详细的统计信息)
  • 服务器进程的内存使用量
  • 服务器自身的内部统计报告

9. 集成测试

在性能测试实施前的集成测试阶段,容易忘记检验多并发运行时的运行情况。这个虽然不是性能测试而是功能测试,但如果没有进行这个测试,往往就会在后面工程的性能测试阶段出现问题,调查原因后发现是功能上的问题,然后就需要进行修正。检查的项目如下所示。

  • 多个线程执行时是否能保持个别的处理流程(保持一致性);
  • 在进行多个处理时是否会混入其他处理的数据(数据混入);
  • 使用同一个用户 ID 同时登录时或者使用不同的用户 ID 登录时,运行是否正常;
  • 是否会因为缓存等错误操作,使得前面的用户 ID 或输入值、设置值等影响下一个用户(数据污染);
  • 同时执行数较少的时候,是否会引起 CPU、内存或磁盘的大量资源消耗(资源过度消耗);
  • 流量控制与超负载时的异常处理是否正常;
  • 超时功能是否能正常运行;

10. 压力测试、临界测试、耐久测试(系统测试)

如前所述,根据目的的不同,性能测试可以分为很多种类型,而且实施的优先顺序也不同。

11. 性能监控测试、故障测试(运维测试)

  • 性能监控测试

从性能监控的观点设置系统的阈值,或者具有生成性能测试报告的功能的情况下,可以施加一定的处理负载来对这些功能进行验证。至于如何施加这个负载,使用性能测试的负载生成方法比较有效。

有两种情况:在进行性能测试的时候一起确认或者与性能测试分开确认。一起确认的情况下,由于设置了出现高负载就发出警报的功能,因此可能会发出大量的警报。如果这样没有问题,可以一起确认。

  • 故障测试

为了确认只有在高负载时才会发生的故障,可以使用性能测试的方法来施加负载,如下所示。

  1. 在高负载时发生故障迁移(Fail Over);
  2. 在高负载时执行拔下网线等操作,确认故障时的情况;
  3. 验证高负载时停止、重启实例的情况以及所需要的时间;

12. 性能测试结果的验收报告

普通的测试结果报告书与性能测试结果报告书在内容上有很大的不同。普通的测试结果报告书是使用 ○× 来确认执行结果,将 × 的地方作为bug,并新建一个 ticket,让程序员修正。而在性能测试结果报告书中,则要记录下测试时达到的数字,并进行综合评价。

下面列举了性能评估报告书中必须记录的几点内容:

  1. 性能是否达到了能够发布的水平(结论写在最前面);
  2. 能够证明以上结论的全部性能相关的数值;
  3. 作为上面的补充信息,记录在什么样的场景下执行了什么样的测试,那个时候的吞吐、响应及并发度的变化情况;
  4. 记录那个时候系统资源的使用率;
  5. 记录错误信息以及考察出错原因的过程;
  6. 记录这个系统的性能瓶颈在哪里;
  7. 说明从测试结果得出的调优方面的关键点及其机制;
  8. 记录在运行时性能方面需要注意的地方;

13. 初期运行确认

为了合理地进行预估,需要积累在各领域来回操作的经验。 

那些在性能测试中确定其性能已达到可以发布的水平的系统,如果在实际发布后没有达到预期的负载和资源使用率,就必须重新审视测量结果,否则在运维时可能就会产生意想不到的性能故障。在运维初期,建议基于下面的观点来检查系统性能。

“在预计的数据量以及使用条数下,与测试的时候相比,平均响应时间是否相同、资源使用率是否相同。”

为了进行这个检查,也需要在性能测试与运维时使用相同的评价标准和观点进行性能测量。 

5、性能测试技巧

1. 性能预估能力

下面介绍一下预估性能时需要的能力。

  • 检查 RFI/RFP 的遗漏

近年来,在系统企划提案阶段,一般会根据 RFI 和 RFP 确定以怎样的规模和结构来搭建一个怎样的系统。虽然性能在其中被定义为非功能性需求,但是在系统企划阶段定义的需求会影响到后面的性能测试和发布判断,是非常重要的。因此,对于性能测试来说,准确的 RFI、RFP 和提案书是必不可少的。

另外,对于发包方来说,为了确保最后拿到的系统的性能,也必须定义合适的性能需求。

请特别注意如下项目:

  1. 是否定义了响应时间的需求
  2. 是否定义了吞吐的需求
  3. 是否定义了同时使用数的需求
  4. 是否定义了回退时或批处理运行时性能的服从率
  • 数据不足时如何确定性能目标

如果系统在更新之前实际运行过,那么在更新时就会预估将来的使用数会增加到现在的多少倍,然后将倍数与现在的数值相乘,来确定系统的容量和性能目标等。

如果是新发布的系统,可能就没有这些使用数的相关数据。在这种情况下,要计算将来会增加多少就很困难。不过,制作一个类似于前面介绍过的“基础设施性能测试的性能目标”那样的模型,就可以得到与实际接近的结果了。

  • 与性能相关的参数设计做到什么程度

在瀑布流的开发中,如果出现工程返工的情况,就会导致计划延迟或变更,因此要尽量避免。在这个观点下,最困难的就是性能相关参数的设计。参数设计是在详细设计阶段和基础设施设计阶段进行的,而至于这个性能参数实际上是否合适,则是在系统测试阶段的性能测试和临界测试中验证的。

在这个测试阶段往往会进行参数的修正。这可以称为工程的返工,不过也是必要的修正工作。如果把工程返工当作一个课题的话,需要从下面两类中选择其一。

① 以之后会再次修正参数为前提定义项目工程

在瀑布流的开发工程中加入以下步骤:

  • 事先进行原型性能测试
  • 基础设施初步搭建后进行基础设施临界测试
  • 在系统测试之前进行临界测试
  • 在系统测试过程中进行参数验证测试
  • 调整参数后再体现到设计工程上

 ②仅使用曾经使用过的要素进行设计

要想成功完成瀑布流的开发,需要注意的一点是不要使用在系统架构中没有使用过的要素。以下项目中,没有使用过的就不要使用。

  • 硬件
  • 软件(包含未使用过的版本和参数)
  • 数据量
  • 用户访问量以及模式
  • 拓扑

只使用之前使用过的要素,这样就可以搭建无论是性能方面还是其他方
面都不需要返工的系统。但是如果遇到了无法完全凭借以往经验进行推
进的情况,就需要修正前面的参数,进行返工。

  • 在项目企划和开始阶段设计、修改性能参数的指针

应该在项目内部达成一致,以此为前提来安排任务和计划等。

2. 高效的反复实施能力

接着介绍一下在反复实施测试的时候如何提升效率。

  • 分析瓶颈、调优、报告与审核、再次测试所需的时间

关于实施性能测试所需要的时间,我们来看一个例子。如下表所示,这里给出了使用 Oracle ATS 等工具和没有使用这些工具的情况下分别需要的时间。

上表仅仅是一个例子,实际上根据目标应用程序、环境、项目的推进方式、项目人员的能力等的不同,所需的时间也会有所差别。

在项目中,如果软件方面的经费不足但人员和时间比较充裕的话,可以不采购用于性能测试的软件产品,而使用自己制作的工具来进行性能测试。以上表为例,其盈亏平衡点就是 118-56 = 62 人日。 

3. Oracle Application Testing Suite 的使用效果

如前所述,使用性能测试专用工具可以大幅提高性能测试的效率。下面以 Oracle ATS(Oracle Application Testing Suite)为例,介绍一下其性能测试的功能。

  • Oracle ATS 的概要

Oracle ATS 具有如下特点:

  • 通过 GUI 生成简单的脚本
  • Cookie 和 HTML 内的会话数据会自动变成参数
  • 从用户视角进行错误检查
  • 无代理程序收集 OS/AP/Network 等性能数据
  • 简单明了的分析图
  • 支持 HTTP(S)/SOAP

从用途上来说,它在以下几种情况下可以派上用场:

  • 从开发的早期阶段就想要简单地进行性能测试
  • 想要排查出导致响应时间变长的服务器
  • 不想遗漏意料之外的错误画面
  • 除了 PC 外,也要测试手机或专用设备的应用程序
  • 希望不用向服务器导入模块就能测量性能
  • 想要在测试时有效地导入大量数据

Oracle ATS 的性能测试功能 Load Testing 可以提高性能测试的效率。

  • 正确测量页面响应时间

由多个框架(Frame)组合而成的页面的请求同时进行的话,在后台服务器上生成的访问强度与实际的浏览器的强度是相同的。

  • 根据测试规模进行横向扩展

能够根据负载条件灵活配置控制器与代理。

控制器具有控制代理、获取性能信息、生成报告等功能,代理具有同时向 Web 服务器进行访问的功能。另外,还能将代理访问从多个据点进行分散。

  • 方便把用户操作场景脚本化

生成脚本的时候不需要写代码。就像使用浏览器那样操作想要进行测试的页面迁移,就能够自动保存 URL、请求字符串、POST 数据、 Cookie等信息。也支持使用 Java 代码来进行脚本的扩展。

  • HTTP 会话的自动识别与重现

Web 应用程序通过会话进行管理,如果多个用户使用同一个会话 ID,或者会话失效,那么就会出错。Oracle ATS 的 Load Testing 能够自动处理 Cookie/URL/HTML 中嵌入的会话 ID,保证发送正确的请求。

Oracle ATS 的 Load Testing 也针对 Oracle WebLogic Server、Oracle Application Development Framework、Microsoft ASP.NET 等很多通用的Web 开发环境进行了优化。

  • 通过有变化的数据实现性能测试

Oracle ATS 的 Load Testing 能模拟同时访问不同数据的场景。具体来说,使用 CSV 文件或数据库中定义的数据,让各个虚拟用户使用不同的输入数据和验证数据。该功能还提供了连续、随机、打乱顺序等多种多样的重放方式。

  • 发现潜在的错误

即使页面响应时间或者服务器的性能没有问题,也有可能显示出并非用户期待的内容。当服务器的负载变大后,请求可能会被转发到“现在服务器很忙”等 Sorry Server 上。

Load Testing 不仅能处理 Web 服务器的响应代码(4××、5××),还能从用户的角度来检查内容(HTML)是否正确。能够实时确认错误信息,便于追踪问题。

  • 支持变形与高效组合

Groupware 等认证后会话保持时间较长的应用程序,能定义为只反复进行业务处理的页面迁移。如果认证本身或者登录后显示的门户页面负载很大,可能需要专门测试登录和退出。

Load Testing 中能通过组合多个脚本来生成用户自定义描述(Profile),这样即使是简单的反复登录的流程,也能很方便地验证。

  • 整体把握系统的资源状态

能够监控各种应用程序、数据库、系统、网络设备等资源信息,不需要对目标系统引入代理等。可以监控的资源如下所示。

  1. Windows OS(Perfmon);
  2. Solaris/Linux(Telnet/SSH);
  3. AP 服务器(JMX/SNMP);
  4. 网络设备(SNMP);
  5. DB(SQL);
  6. Web 页面(URL);
  7. Ping、COM +等;
  • 快速生成瓶颈分析报告

把响应时间、错误发生率、用户数以及命中数 / 秒、页面数 / 秒等内容制作成报告或图表输出。由于能把多个测试结果汇总到一个报告中,因此能方便地进行调优前后的比较。

图表可以导成 Excel、CSV、JPEG、PNG 等形式。

  • 从分析到查找瓶颈原因都能用此工具

该工具提供了对 Oracle Enterprise Manager 12c 的访问,Oracle Enterprise Manager 12c 会进行数据库和 Java 应用程序的详细的性能分析。

  • 对 DB 进行性能测试

支持对 Oracle Database 进行压力测试。能够生成直接访问 DB 层的脚本,通过脚本能完成以下操作。

  1. 执行 DDL、DML;
  2. 执行 PL/SQL;
  3. SQL 行数计数测试;
  4. 通过 Java API 进行扩展;
  5. Oracle Real Appliation Testing;

另外,还能通过 Load Testing 的数据库重放(Database Replay)把捕获到的事务日志或者自定义 SQL、PL/SQL 脚本导入进去。

  • 对 Web 服务进行性能测试

使用 WSDL 管理器,可以导入并保存 Web 服务定义文件。除OpenScript 以及 Oracle 服务器之外,也支持 Apache AXIS、.Net 服务器等各种 WSDL 服务器。

不仅支持 SOAP 1.1、1.2 协议,也支持发送 DIME、SWA、MTOM 等二进制文件。

三、性能监控与诊断

1、MySQL监控

我们可以通过官方提供的客户端来实现MySQL监控,也可以通过命令或者SQL来完成监控任务。考虑到每个测试人员对数据库的掌握程度不一样,大家可以选择商业版的监控分析工具,如Spotlight® onMySQL、SQL Diagnostic Manager for MySQL(由webyog公司提供,此公司还提供一个名为SQLyog的MySQL客户端,通过GUI的方式来管理MySQL)。

商业版本的工具让监控诊断变得简单。SQL Diagnostic Manager forMySQL可以监控线程状态、执行SQL、帮助断言慢查询、统计临时表、监控数据库主机、提供优化建议等。

很多人都在使用Navicat作为UI来管理MySQL数据库。Navicat Monitor是一家公司推出的MySQL监控工具,虽然是收费版本,但也提供试用。

如图所示,Navicat Monitor提供慢查询分析、top线程分析、死锁分析等功能。容器部署只需要运行如下命令。 

docker run -d -p 3000:3000 navicat/navicatmonitor

不管使用商业工具还是直接用SQL及其他监控命令来监控MySQL,最重要的是需要知道监控哪些指标?这些指标代表什么意思? 

MySQL常见的监控项目:

2、JVM监控

目前企业级应用系统的开发多数会使用Java语言,并且使用Oracle J2EE(收购Sun后的)架构。Java程序运行在HotSpot VM(就是我们常说的JVM,也包括OpenJDK)之上,通过对JVM的监控,我们可以度量Java程序效率,分析程序性能问题。

对于JVM的监控,并不需要我们借助第三方工具,JDK自带的监控命令就已经很强大,这些工具随JDK一起发布,随时可以用它们监控JVM,而且这些工具也比较好用,下面讲解几个常用的监控命令和一个可视化监控工具(JvisualVM)。

对于Java的应用来讲,JVM的性能反映了Java程序的性能。JVM的监控分两大类:一是堆内存,二是线程;从堆内存可以分析大对象与内存溢出等问题,从线程状态及线程信息可以分析出低效程序,解决的是CPU资源占用的问题。

1. jps

我们要知道机器上运行的JVM进程号可以由jps得到。jps命令返回当前系统中的Java进程号。

jps命令的参数说明如下:

  • l:返回Java进程全路径。
  • q:仅显示进程ID。
  • v:返回JVM参数,例如堆大小,此命令方便我们查看JVM大小,不用去找配置文件。 

jps [ip]:列出远程机器上的Java进程信息,不过这需要安全授权,在远程器%JAVA_HOME%/bin/目录下存储jstatd.all.policy文件,内容如下。 

grant codebase "file:${java.home}/../lib/tools.jar" {
permission java.security.AllPermission;
};

然后,在远程机器下启动命令(如图5-55所示)进行注册:

jstatd -J-Djava.security.policy=jstatd.all.policy

为了方便,我们可以做一个批处理文件jstatd.bat,内容如下:

%JAVA_HOME%\bin\jstatd -J-Djava.security.policy=jstatd.all.policy

然后,在本地机器就可以使用jps访问远程机器上的JVM。

开启jstatd:

jps访问远程机器:

远程机器jstatd启动后,也可以使用JVisualvm在本地机器对远程JVM进行监控,操作界面如图所示。

下图所示是JVisualvm连接上远程机器的Tomcat:

有关jps其他详细信息大家可以查阅官方文档,地址如下:

jps - Java Virtual Machine Process Status Tool

当机器上有多个JVM进程时,如何确定自己访问的服务对应哪一个Java进程?在访问服务器时,URL中会有端口号(默认为80),Linux下可以通过ps命令得知进程号与端口号。

2. jstat

JVM内存不够用、内存溢出通过监控JVM Heap信息进行分析,jstat可以用来查看JVM堆的统计信息,命令格式如下。

jstat [ generalOption | outputOptions vmid [ interval[s|ms] [ count ] ]

generalOption代表选项,常用的选项有以下几个:

  • class:用于查看类加载情况的统计。
  • compiler:用于查看HotSpot中即时编译器编译情况的统计。
  • gc:用于查看JVM中堆的垃圾收集情况的统计。
  • gccapacity:用于查看新生代(young)、老年代(old)及持久代(permanent)的存储容量情况。
  • gccause:最后一次及当前正在发生垃圾收集的原因。
  • gcnew:用于查看新生代垃圾收集的情况。
  • gcnewcapacity:用于查看新生代的存储容量情况。
  • gcold:用于查看老年代及持久代发生GC的情况。
  • gcoldcapacity:用于查看老年代的容量。
  • gcpermcapacity:用于查看持久代的容量。
  • Printcompilation HotSpot:编译方法的统计。
  • gcutil:GC统计。

outputOptions代表输出格式,参数interval和count代表查询间隔和查询次数。

如图所示,先通过jps获取到Java进程号,然后由jstat来统计JVM中加载的类的数量与Size。

  • Loaded:加载类的数目。
  • Bytes:加载类的Size,单位为Byte。
  • Unloaded:卸载类的数目。
  • Bytes:卸载类的Size,单位为Byte。
  • Time:加载与卸载类花费的时间。 
jstat -gccapacity [java进程号]。
jstat -gccapacity -h5 [java进程号] 1000
  • -h5:每5行显示一次表头。
  • 1000:每一秒显示一次,单位为毫秒。

如图所示,用gccapacity统计JVM垃圾回收信息,下面是表头信息说明。

  • NGCMN:新生代中初始化大小(单位为Byte)。
  • NGCMX:新生代的最大容量(单位为Byte)。
  • NGC:新生代中当前的容量(单位为Byte)。
  • S0C:新生代中第一个survivor(幸存区)的容量(单位为Byte)。
  • S1C:新生代中第二个survivor(幸存区)的容量(单位为Byte)。
  • EC:新生代中Eden(伊甸园)的容量(单位为Byte)。
  • OGCMN:老年代中初始化大小(单位为Byte)。
  • OGCMX:老年代的最大容量(单位为Byte)。
  • OGC:老年代当前大小(单位为Byte)。
  • PGCMN:持久代中初始化大小(单位为Byte)。
  • PGCMX:持久代的最大容量(单位为Byte)。
  • PGC:持久代当前新生成的容量(单位为Byte)。
  • S0U:新生代中第一个survivor(幸存区)目前已使用的空间(单位为Byte)。
  • S1U:新生代中第二个survivor(幸存区)目前已使用的空间(单位为Byte)。
  • EU:新生代中Eden(伊甸园)目前已使用的空间(单位为Byte)。
  • OC:老年代的容量(单位为Byte)。
  • OU:老年代目前已使用的空间(单位为Byte)。
  • PC:持久代的容量(单位为Byte)。
  • PU:持久代目前已使用的空间(单位为Byte)。
  • YGC:JVM启动到采样时新生代中gc的次数。
  • YGCT:JVM启动到采样时新生代中gc所用的时间(单位为秒)。
  • FGC:JVM启动到采样时老年代(Full gc)gc的次数。
  • FGCT:JVM启动到采样时老年代(Full gc)gc所用的时间(单位为秒)。
  • GCT:JVM启动到采样时gc用的总时间(单位为秒)。 

Full gc会暂停用户响应,也就是不处理用户请求,等待Full gc完成后响应用户请求,这个等待时间过大就会影响用户体验,所以Full gc是JVM调优的重点。

如图所示,用gcutil统计GC情况,表头信息说明如下。

  • S0:新生代中第一个survivor(幸存区)已使用的占当前容量百分比。
  • S1:新生代中第二个survivor(幸存区)已使用的占当前容量百分比。
  • E:新生代中Eden(伊甸园)已使用的占当前容量百分比。
  • O:老年代已使用的占当前容量百分比。
  • P:持久代已使用的占当前容量百分比,JDK 8之后取消了持久代,改为元数据,列名为M。
  • YGC:JVM启动到采样时新生代中gc的次数。
  • YGCT:JVM启动到采样时新生代中gc所用的时间(单位为秒)。
  • FGC:JVM启动到采样时老年代(全gc)gc的次数。
  • FGCT:JVM启动到采样时老年代(全gc)gc所用的时间(单位为秒)。
  • GCT:JVM启动到采样时gc用的总时间(单位为秒)。

从图中可以看到FGCT有6次,用时598毫秒,平均99毫秒一次,需要结合JVM运行时长来看这个Full gc是否合理。例如一天才6次,且99毫秒一次,对于平均响应时间1秒的应用来说这完全没问题;如果是1小时6次,对于响应时间几十毫秒的应用,这就有影响了。因此GC的时间消耗是否合理是相对的。

与jps一样,jstat也支持远程监控(如图5-62所示),同样也需要开启安全授权,方法参照jps。

每一秒获取一次,后面的时间是以毫秒为单位的,1000就是1秒。

长时间监控会输出很多条记录,表头会滚到上方,这样不方便查看,我们可以使用-h参数来指定打印表头的频率,格式如下。 

jstat -

例如,jstat –gcutil –h5 386 5000,意思是每5行(-h5)打印一次表头,5 000毫秒打印一次监控数据,386是Java进程id,gcutil监听内存回收。

上面我们只是简单地讲解了jstat的相关用法,有兴趣的朋友可以访问如下链接,参考官方文档。

jstat - Java Virtual Machine Statistics Monitoring Tool

JDK 8的相关参考,可以访问如下链接获得: https://docs.oracle.com/javase/8/docs/technotes/tools/unix/jstat.html

3. jstack

我们已经用到了jstack命令,jstack用于生成Java虚拟机当前时刻的线程信息(快照)。快照信息主要用来了解线程出现长时间停顿的原因,如死锁、死循环、IO等待、请求外部资源导致的长时间
等待等。从快照信息可以知道线程执行到哪个方法,甚至哪一行,帮助我们快速定位程序问题。

jstack命令格式:

jstack [-l] 

如果dump的JVM进程处于Hung的状态,可以添加-F参数。

jstack –F [-m] [-l] 

 jstack命令不仅可以dump本机的线程信息,还可以dump远程的JVM中的线程信息,格式如下。

jstack [-m] [-l] [server_id@]

远程使用dump需要配置jstatd。

4. jmap

jmap命令可以用来统计JVM内存状况,也可以为JVM生成快照(通常说的dump堆内存信息)。如图所示,为进程id是2568的JVM生成快照,快照文件D:\heap.hprof。

 典型获取方式是:

jmap -dump:format=b,file=d:\heap.hprof [pid]

heap.hprof是堆快照文件,打开此文件需要特定的工具。

为了方便,我们使用JVisualvm来打开(JDK自带不用安装),下面讲解用JVisualvm(Java VisualVM)查看堆快照。在%JAVA_HOME%/bin下找到JVisualvm并双击打开,选择“文件
(F)”→“装入(L)”,文件类型是hprof;然后选择刚才dump的文件并打开。
在“类”视图中,可以找到示例程序cn.seling.www.outMem. PermGen。

选中cn.seling.www.outMem.PermGen,然后右键选择“在实例视图中显示”,就可以看到PermGenClass在堆中的具体信息。

PermGen对象持有一个list对象,一般来说内存溢出往往是因为大对象不能回收造成的,这些大对象往往就是集合对象,例如PermGen对象中的list,如果list中的对象足够多就会造成内存溢出。 

在“实例数”视图中可以统计出实例化的对象数目,分组统计排序就能够知道哪些对象实例化得多,然后分析对象的引用就可以找到是谁在实例化此对象,从而找到产生大对象的原因,这就是寻找大对象、分析内存溢出的方法。

5. JVisualvm

JVisualvm是JDK自带的JVM可视化监控工具,它能提供强大的分析能力,可以使用JVisualvm监控堆内存变化情况、线程状态、CPU使用情况、分析线程死锁等。JVisualvm可以监控本地JVM,也可以监控远程的JVM,本地监控无需进行配置,远程监控一般以JMX的方式进行。

下面主要讲解远程监控,这也与实际运用相符合(在测试或者运营中基本都是远程监控)。

1)启动远程监控

(1)在远程中间件的启动文件中配置如下字串

-Djava.rmi.server.hostname=[自己填]
-Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.port=9008
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.ssl=false

如果你使用的中间件是Tomcat,那么要修改的是%TOMCAT_HOME%\bin\catalina.bat/sh文件,下图所示是Windows系统下Tomcat启动文件中的JMX配置示例。

上面是JMX的连接方式,另外一种就是利用jstatd来进行远程监控,jstatd配置启动参照前面内容。这种方式不能监控JVM中的CPU使用情况,一般来说,JVM的CPU使用情况也不用监控,通常是监控系统层面的用户CPU使用情况。

(2)启动远程服务,以Tomcat为例就是启动Tomcat服务,即运行catalina.bat或者是startup.bat。

(3)JVisualvm配置JMX连接,在%JAVA_HOME%\bin目录下找到 ,然后双击启动,在
下图中的“连接”框中填写远程主机IP,注意端口号是刚才在catalina.bat中配置的9008端口,然后单击“确定”按钮,即可连接成功 

2) 监控界面

(1)Summary View

在此可以查看到JVM的启动参数及系统属性,可以对JVM的运行环境有所了解,在图中可以看到JVM:JavaHotSpot(TM)Server VM(21.0-b16,mixed mode),这表明Tomcat是以Server方式在运行。

如果测试的Java应用不是以Server方式运行,就要修改,通常情况下Sever方式较Client方式性能好。

(2)Monitor View

在此显示了JVM CPU利用率,堆与非堆内存使用情况,加载了多少类,有多少线程数;可以做dump操作,查看堆内存明细。堆的回收曲线能够直观反映堆内存回收频率,是否有内存溢出等问题。

从图中可以看到堆经历了3次回收,类加载不到10 000,CPU使用率几乎为零,线程数也不到100,所以JVM还是比较空闲的。 

(3)Thread View

如下图所示,在此监控线程状态,查看哪些线程有风险,对于Blocked(监控状态)线程可以在dump后分析线程活动,确定是否有性能问题。在此视图中我们主要关心的是那些“监视”状态的线程,单击“线程dump”可以导出JVM当前的线程栈信息,通过分析这些信息定位到程序问题。

(4)堆快照视图

分析大对象的产生、分析内存溢出需要分析堆信息,一般步骤是dump堆信息,然后在堆快照视图中进行分析,分析方法请参照前面部分内容。

如图所示,可在堆快照中查看对象具体信息,并看到此对象的属性,PermGen对象持有一个list对象,这个对象并没有引用对象。 

6. JDK 8与JDK 7在监控方面的变化

虽然推出很多年了,但JDK 8的广泛使用还是在近几年。相对于前面的版本,JDK 8带来了一些新特性,例如Lambda表达式、Stream API、Date Time API、加入了新的JavaScript引擎Nashorn等(具体参考Java官网)。这些与测试工程师关系似乎并不紧密,我们最需要了解的是JDK 8在JVM上的变化。

1)元数据

JDK 8取消了持久代,取而代之的是元数据。如图所示,Permanent Generation将取消掉,Metadata取而代之,使用JVisualvm监控时可以清楚地看到不再有PermGen部分,而是用Metaspace代替。

JDK 7/JDK 8 JVM内存空间变化:

使用元数据代替持久代的好处是java.lang.OutOfMemoryError:PermGen的空间溢出问题不复存在了。

元数据直接使用系统内存,当然系统内存也有不够用的时候,也就是并没消除类和类加载器的内存泄露问题,只是把问题抛给系统内存了。当然,元数据内存的大小还可以使用指令来限制(通过-XX:MetaspaceSize和-XX:MaxMetaspaceSize来进行调整),避免对系统资源无限制地占用,当到达-XX:MetaspaceSize所指定的阈值后会触发对死亡对象和类加载器的垃圾回收来缩减空间。

元数据空间也需要监控一下,并调整到适当大小。反之当系统内存都不够元数据使用,会报java.lang.OutOfMemoryError: Metadata space错误,这是比较极端的问题了,但不用担心,这种问题是比较容易发现的。通常只需要进行长时间的压测,监控记录元数据空间的增长就可以发现问题。 

JVisualvm监控JDK 7与JDK 8的不同处:

2)类依赖分析器jdeps

jdeps可以显示Java类的包级别或类级别的依赖关系。这有什么好处呢?

我们知道,Java程序开发方便是因为其类库非常多,这些类库可能功能相似,可能功能相同但版本不同,可能名称或者包名相同但功能不同。

例如JSON的工具包,有的用fastjson,有的用gson,还有的用jackson。当依赖引用出问题时,如果语法没报错,但运行时出错了,我们需要查找并分析问题。此时jdeps就派上用场了,它可以查找到包依赖关系,接受.class文件,或者目录,或者一个jar文件作为入参,输出依赖关系。jdeps默认把结果输出到控制台。

下图中显示了JMeter(JMeter的启动类)类文件的依赖,可以看到我们加了-cp参数,指定了查找的包为ApacheJMeter_core.jar,这只是显示了依赖的类的包名。如果我们想看清楚此包下的哪些类被依赖呢?

我们可以加-verbose:class参数,如图所示(由于依赖比较多,有118行,所以只截取了部分内容)。 

 在命令后加上-h参数也可以看到帮助:

7. trace跟踪

在测试时一个业务可能会调用多个方法,那到底哪一个方法耗时最长呢?通常我们的解决办法是在程序架构上做文章,利用切面的方式统计每个方法的执行用时,再把统计数据输出到日志。但是在做性能压测时,日志数据势必很多,查找日志数据很困难。有没有工具可以解决这个问题呢?当然有,例如anatomy,这是一个开源的性能跟踪程序,利用字节码注入的方式给方法加上一个拦截,从而统计出方法的耗时。

anatomy的trace功能统计整个调用链路上的所有性能开销和追踪调用链路,帮助定位和查找某一接口响应较慢,主要损耗在哪一个环节。

安装代码如下:

curl -sLk http://ompc.oss.aliyuncs.com/greys/install.sh|sh

启动代码如下:

greys.sh 

跟踪代码如下:

trace –n <显示的条数><类> <方法>

类支持正则表达式,下面跟踪TestActionServiceImpl.start方法:

ga?>trace -n 2 *TestActionServiceImpl start
Press Ctrl+D to abort.
Affect(class-cnt:2 , method-cnt:2) cost in 158 ms.
'---+Tracing for : thread_name="http-nio-7001-exec-2"
thread_id=0x27;is_daemon=true;priority=5;
'---+
[601,601ms]com.chinatele.manage.service.impl.TestActionServiceImpl:start()
+---[0,0ms]com.chinatele.manage.form.PlanActionForm:getPlanId(@82)
+---[0,0ms]java.lang.String:length(@82)
+---[0,0ms]com.chinatele.manage.form.PlanActionForm:getPlanId(@83)
+---
[6,5ms]com.chinatele.manage.service.TestPlanService:getTestPlanById(@83)
+---[6,0ms]com.chinatele.manage.entity.TestPlanEntity:getVu(@84)
+---[6,0ms]com.chinatele.manage.entity.TestPlanEntity:getVu(@87)
+---
[11,5ms]com.chinatele.manage.service.TestPlanService:buildJmxFile(@88)
+---
[11,0ms]com.chinatele.cmeter.common.result.Result:getSuccess(@89)
+---
[11,0ms]com.chinatele.manage.entity.TestPlanEntity:getDelayTime(@93)
+---
[11,0ms]com.chinatele.manage.entity.TestPlanEntity:getPlanId(@109)
'---
[601,590ms]com.chinatele.manage.service.impl.TestActionServiceImpl:run(@10
9)
'---+Tracing for : thread_name="http-nio-7001-exec-2"
thread_id=0x27;is_daemon=true;priority=5;
'---+
[601,601ms]com.chinatele.manage.service.impl.TestActionServiceImpl:start()
+---[0,0ms]com.chinatele.manage.form.PlanActionForm:getPlanId(@82)
+---[0,0ms]java.lang.String:length(@82)
+---[0,0ms]com.chinatele.manage.form.PlanActionForm:getPlanId(@83)
+---
[6,5ms]com.chinatele.manage.service.TestPlanService:getTestPlanById(@83)
+---[6,0ms]com.chinatele.manage.entity.TestPlanEntity:getVu(@84)
+---[6,0ms]com.chinatele.manage.entity.TestPlanEntity:getVu(@87)
+---
[11,5ms]com.chinatele.manage.service.TestPlanService:buildJmxFile(@88)
+---
[11,0ms]com.chinatele.cmeter.common.result.Result:getSuccess(@89)
+---
[11,0ms]com.chinatele.manage.entity.TestPlanEntity:getDelayTime(@93)
+---
[11,0ms]com.chinatele.manage.entity.TestPlanEntity:getPlanId(@109)
'---
[601,590ms]com.chinatele.manage.service.impl.TestActionServiceImpl:run(@10
9)

[]中的部分是时间统计,单位为毫秒,其他功能请参考官方网站。

3、性能诊断小工具

要想做好程序的性能诊断,编码功底必须得补上。是不是没写过程序就无法诊断分析性能了呢?这也不尽然,利用工具、遵循规律找到大多数的性能问题还是有可能的。

下面介绍一个开源的分析工具:

Arthas(阿尔萨斯),也是效率工具,能减轻诊断分析性能工作量。

Arthas能做些什么呢?

官方网站介绍如下:

  1. 这个类从哪个jar包加载的?为什么会报各种类相关的Exception?
  2. 我改的代码为什么没有执行?难道是我没commit?分支搞错了?
  3. 遇到问题无法在线上debug,难道只能通过加日志再重新发布吗?
  4. 线上遇到某个用户的数据处理有问题,但线上同样无法debug,线下无法重现!
  5. 是否有一个全局视角来查看系统的运行状况?
  6. 有什么办法可以监控到JVM的实时运行状态?

Arthas的用户文档比较详细,在此不赘述,请参考:

arthas

arthas/README_CN.md at master · alibaba/arthas · GitHub

GitHub - alibaba/arthas: Alibaba Java Diagnostic Tool Arthas/Alibaba Java诊断利器Arthas

4、全链路监控

目前互联网流行微服务,传统的业务链路被拆分成由多个子系统来完成,由于业务跨子系统,在性能诊断分析时我们需要跨子系统进行性能分析,需要知道业务在每一个系统中的耗时。在运维监控时也需要知道系统的负载状态,方便对系统进行针对性地扩充。完成这种监控需求的工具就是我们常说的APM(Application Performance Management)工具。

多年前,我们团队需要测试由90多个子系统组成的支付系统,测试时需要知道业务在每个子系统中调用了哪些方法?耗时多少?为整个支付体系做一次全方位的性能检查。当时我们只能在每个系统中去看日志,苦不堪言。

这时我们是多么希望能够有一个全链路监控工具,尤其还是开源的。全链路监控对我们来说只能是一个奢望。我们花了很大精力才把整个系统测试一遍。

现在不用这么辛苦了,市面上出现了很多APM工具。

APM工具的推动与发展深受Google Dapper论文启发,2010年Google发表了论文Dapper,a Large-Scale Distributed Systems Tracing Infrastructure,介绍了Google生产环境中大规模分布式系统下的跟踪系统Dapper的设计和使用经验。

下面我们列举几个流行的APM开源工具:

1. Skywalking

Skywalking是国人(吴晟)主导的开源项目,现在已经是Apache的开源项目,支持Java、C#、PHP、Node.js、Go等语言的程序监听。

Skywalking采用字节码注入的方式介入程序,前期对程序员开发代码无任何侵入,使用时只需要在启动的时候加上探针。加上探针后对于原程序的性能损耗(吞吐量TPS)在5%左右(与施加的负载当量及主机性能有关),如果担心在生产环境影响性能,可以只部署少量的探针。

网上也有一些APM工具性能的对比,大家感兴趣可以搜索一下,也可以自己进行实测。

下图所示是官方给出的Skywalking架构:

探针(Tracing)采集到监控数据后传送到平台进行存储(支持多种结构的存储,例如ElasticSearch、MySQL等,我们通常是选择ElasticSearch进行存储,毕竟检索是它的强项)。用户通过Skywalking UI(采用React、Antd前端框架)来检索监控结果,其能够把子系统间的调用关系、时间消耗关联显示出来,还可以对JVM等进行监听,另外也支持插件开发,以开发适合
自己企业的监控。

具体技术方案、实现原理在此不做赘述,请参看官方的指导文档进行开发。

使用SkyWalking和Elasticsearch实现全链路监控 - 检索分析服务Elasticsearch版 - 阿里云

Skywalking官方演示地址可从GitHub - apache/skywalking: APM, Application Performance Monitoring System获取,Skywalking的版本迭代还是比较快的,当时使用的Skywalking还是5.0版本,等到一年后进行审核时,Skywalking已经到了7.x的版本(只能默默心疼自己的15页内容要删掉),文档也已经很完善,官网提供docker-compose.yml文件帮助快速部署,在kubernetes中使用helm部署更方便。

1. 快速部署Skywalking

实验环境中我们采用单机部署Skywalking和ElasticSearch,拓扑结构如图所示。

第一部分部署Skywalking UI与后端,第二部分部署存储(ElasticSearch),第三部分部署应用,并使用探针收集其性能数据。

实验时建议使用docker-compose快速部署,官方的dockercompose.yml配置了Skywalking UI、Backend(后端程序OAP)、ES的启动。

cat > docker-compose.yml <

访问方式:http://[ip]:8080

helm chart获取地址:
https://github.com/apache/skywalking/tree/master/install/kubernetes/helm

2. Java程序监控

Java程序的监控原理是采用字节码注入的方式,要为Java程序部署一个探针程序,目录结构如图所示。

skywalking-agent.jar放入程序的环境变量,随Java程序一起启动,完成字节码的注入。

Tomcat中的加入方式如下:

 Windows系统:

set "CATALINA_OPTS=-javaagent:/path/to/skywalking-agent/skywalkingagent.jar"

Linux系统:

CATALINA_OPTS="$CATALINA_OPTS -javaagent:/path/to/skywalkingagent/skywalking-agent.jar"; export CATALINA_OPTS

其他类型探针启动方式请参照官方网站:

GitHub - apache/skywalking: APM, Application Performance Monitoring System

上面只是配置了注入,还需要把收集到的数据传给Skywalking后端(由ES来存储),通过修改config/agent.conf来配置数据去向。

# 给你的服务配置一个服务名,在Skywalking UI搜索时区分服务
collector.servers =[server name]
# 监听数据投递到Skywalking后端
collector.backend_service =[ip]:11800
# 其他参数请参考:
https://github.com/apache/skywalking/blob/master/docs/en/setup/service-
# agent/java-agent/README.md

以上配置完成后启动服务即可,稍后就可以在Skywalking中搜索到监控结果,Skywalking提供服务调用拓扑图、服务追踪分析、告警等服务。

3. 监控示例

(1)准备应用

这里准备了4个Java程序,调用顺序如图5-84所示,用户在UI上访问
http://url:7001/ server01/getOrderInfo?orderId=1002&userId=1008,server01会调用server02的接口,sever02会调用server03与server04的接口。

服务追踪分析:

程序调用顺序: 

示例程序可以从GitHub - selingchen/skywalking-test获取。mvn clean package会生成4个jar包。 

生成的4个jar包:

启动方式:java –jar [包名].jar,例如java –jar server01-0.0.1-SNAPSHOT.jar。

我们把这4个jar包放在一个主机上启动(如果你想分布在多台主机,请修改项目中的application.properties文件,其中server02.url、server03.url、server04.url指定调用的应用的访问地址)。

(2)给应用部署探针

从http://skywalking.apache.org/downloads/下载Skywalking APM的二进制文件,解压后目录结构如图所示,复制agent目录到server01。 

我们有4个应用,所以需要复制4份agent来配置config/agent.config。

下面的代码我们只需要修改两个地方(加粗部分):

  • 给应用指定一个别名,server01服务用service01作为名字,同理,server02用service02命名。
  • 指定探针把数据投递到服务端的地址。
agent.service_name=demo
# Backend service addresses.
#collector.backend_service=${SW_AGENT_COLLECTOR_BACKEND_SERVICES:127.0.0.1
:11800}
collector.backend_service=127.0.0.1:11800 #本例在一台主机上进行,
UI,Backend,ES同主机
# Logging file_name
logging.file_name=${SW_LOGGING_FILE_NAME:skywalking-api.log}
# Logging level
logging.level=${SW_LOGGING_LEVEL:DEBUG}

修改完成后就可以启动应用注入探针。我们的应用是Springboot项目,可以直接运行jar包,启动命令如下:

本例server01对应server01,server02对应server02,依次类推,4个服务分别对应自己的探针配置与程序。启动完4个jar包,稍等片刻(探针要以字节码的形式注入),就可以访问应用,我们只需要访问server01的服务(如图5-86所示)即可调用其他3个应用。

稍等片刻,等Skywalking后端程序收集到监控数据后就可以通过UI来查看监控数据。

如图所示是server01/getOrderInfo的调用链,焦点移上去就会显示调用时间,哪个服务消耗时长一目了然。

下图所示为对服务的响应时间、吞吐量等进行了统计。

下图所示是对JVM的监控。 

有些朋友的性能测试环境可能比较复杂,无论是机器网络环境还是机器数量都会比较多,监控会比较麻烦。

这种情况往往出现在规模较大的企业,这种企业都会有专业的运维团队,因此可以借助运维团队的帮助进行性能测试。运维团队一般都会部署诸如Zabbix、Nagios这样的监控平台,性能测试工程师直接复用这个平台就可以省不少工作量。

微服务流行,系统调用关系复杂,性能测试时迫切需要全链路监控,如果不想对程序有侵入性,注入式的APM工具是首选。用户可以使用Skywalking这种全链路的监控工具,从而简单化业务性能分析。

性能分析要求的知识体系不但广,而且深,有时还需要团队协作,工作效率更高,尤其是系统复杂度高的生态系统(子系统繁多,一起组成一个生态链,例如支付、电商、金融等)。性能测试工程师可以不精通某一方面的知识,但一定要了解,正如我们要了解需要监控哪些性能指标一样。

四、系统整体架构性能优化思路

高可用性、高可靠性、可扩展性及运维能力是高并发系统的设计要求(当然也要顾及成本)。

可扩展性希望服务能力(或者容量)的增长与增加的硬件数量是线性关系。例如一台服务器服务能力是100QPS,增加一台同样的机器后容量应该接近200QPS,这种线性的容量伸缩方式就是常说的水平伸缩。大家最为熟悉的电商(天猫、京东等)、网络支付(支付宝、微信等)、即时通信(QQ、微信等)都具备良好的水平扩展能力。

以微信朋友圈发红包的功能为例,2016年除夕当日,微信红包的参与人数达到4.2亿人,收发总量达80.8亿个,最高峰发生在00:06:09,每秒收发40.9万个红包,系统依然稳定运行。此例就很好地演示了系统的可运维性,其线上公测收放自如,也证实了微信具备高可用性、高可靠性、可扩展性。互联网企业现在拼的不仅仅是商业模式,也在拼技术,性能已经是系统设计首要考虑的问题了。

性能分析与调优旨在帮助客户打造一个高可用、高可靠的系统。性能分析的目的是找出性能瓶颈与风险所在;性能调优就是要用更少的资源提供更好的服务,使效益最大化。

随着业务规模的扩大,传统的单机服务已经不能够满足性能要求。

单机性能总有上限(就好比一个人的能力再强,也无法完成所有事情),于是就出现了集群方案。传统的集群方案后来也不能满足互联网的高并发要求,阿里开展的去IOE(IBM服务器、Oracle数据库、EMC的专业存储设备)化正是基于成本与性能的考虑。一方面是因为IOE的成本高,另一方面是因为成本高还不能满足性能要求,所以分而治之成为必然选择。

于是,分布式集群方案开始大行其道,其水平扩展能力是传统架构无法比拟的。围绕分布式主题也诞生了不少分布式的框架与产品(例如dubbo、dubbox、jd-hydra、memcache/redis),相应的性能分析与调优也面临着调整,不仅要关注单个系统的性能,还要关注整个分布式框架体系下的各组成部分的性能。

多数人都会觉得性能调优是一个高深的话题,但其本质并不复杂。我们可以从很多的生活实例中得到启发。例如一根绳子拉不起重物时,我们可以用多根绳子,这就是集群思想;火车分班次运行,集齐一车人之后才运行一个班次,而不是来一个客人就运行一个班次,这就是批处理。

性能调优的常规手段有如下几种:

(1)空间换时间。内存缓存就是典型的空间换时间的例子。利用内存缓存从磁盘上取出数据,CPU请求数据时直接从内存中获取,从而获取比从磁盘读取数据更高的效率。

(2)时间换空间。当空间成为瓶颈时,切分数据并分批次处理,用更少的空间完成任务处理。上传大附件时经常用这种方式。

(3)分而治之。把任务分开执行,也方便并行执行来提高效率。Hadoop中的HDFS、mapreduce都是应用这个原理。

(4)异步处理。业务链路上有的任务消耗时间较长,可以拆分业务,甚至使用异步方式,减少阻塞影响,这就是我们常说的解耦。常见的异步处理机制有MQ(消息队列),目前在互联网应用中大量使用。

(5)并行。并行指用多个进程或者线程同时处理业务,缩短业务处理时间。例如,我们在银行办业务时,在排队人数较多时,银行会加开窗口。Spark对数据的分析就可以配置作业并行处理。

(6)离用户更近一点。例如CDN技术,把用户请求的静态资源放在离用户更近的地方。

(7)一切可扩展,业务模块化、服务化(无状态、幂等)、良好的水平扩展能力。

注意:

  • 高可用性:通常来描述一个系统经过专门的设计,可以减少停工时间,从而保持其服务的高度可用性。
  • 高可靠性:产品在规定的条件下、在规定的时间内完成规定的功能的能力。
  • 可扩展:易于扩大规模,尤其是具有良好的水平扩展能力,处理能力与机器数量呈线性关系。

1、单机性能调优

顾名思义,我们要在单机上对系统的性能进行调优。

不管你的应用使用的是什么框架、什么技术,性能都会显现在对系统软硬件资源的需求上。程序问题可能在前端,可能在后端,或者是存储(数据库或者文件存储,如MySQL、Redis)。通过单机性能调优,降低了问题的复杂度,更利于解决问题。下面从几个方面讲解常用的单机性能调优方式。

1. 程序优化

程序调优是治本的手段,当前的性能测试往往在集成测试完成后进行,性能问题暴露得太晚,这个时候去修改代码,风险较大。我们需要考虑关联业务的相互影响,因此我们要重新进行集成测试、性能回归测试等一长串的测试工作。这势必会加长项目周期,时间成本又是不能回避的问题,尤其是敏捷开发,系统实时性很强。诸多的不确定性导致了我们不敢、不能、不提倡去做“伤筋动骨”的程序调整,只能局限在小范围之内。这样做导致的结果往往就是随着对问题的深入研究,发现需要做许多的调整,甚至可能推翻先前的设计,以及对业务实现的改动,这就很费事了。

由此可见,性能测试往往要提前规划,先架构、后程序优化(先整体后个体)。

  • 系统框架选择

SSH(Struts/Spring MVC、Spring、Hibernate)架构是当下流行的MVC模型。SSH架构为我们提供了明晰的层次结构,各层协同完成业务实现,简化了程序设计过程,加快了程序交付进程。架构丰富的组件虽
然给我们带来了便利,但也有它的短板。

例如,对于大型的业务系统,特别是大数据量的分析计算过程,我们如果把大量的数据从数据库取出后利用应用程序(Java)来进行分析计算,势必会增加网络的传输,而且在程序中进行处理可能并不是最佳实践。如果换成在数据库中进行处理,我们可以进行连接查询、批处理等操作,不断减少网络传输,性能也会得到提升。因此我们不能为了遵循架构,为了开发方便而唯架构论,应该根据不同的应用场景选择更合适的处理方式。

  • 程序优化

低效代码优化,这里说的低效代码排除上面说到的架构问题,纯粹是程序逻辑及算法低效。例如逻辑混乱、调用继承不合理、内存泄露等。

常用的解决方法如下:

(1)表单压缩

压缩表单,减少网络的传输量,以达到提高响应速度的效果。

(2)局部刷新

页面中采取局部内容获取的方式,减少向服务器的请求,服务器由于负载小就能更快地响应客户请求,客户的体验也会更好。

(3)仅取所需

只向服务器请求必要的内容,并只向客户端发送必要的表单内容,以减少网络传输,减轻服务器负担。

(4)逻辑清晰

程序逻辑清晰,方便维护和分析问题;不做错误及多余调用。

(5)谨慎继承

开发过程中要了解系统架构,特别是一些基类、公共组件,实现合理利用,减少大对象产生的可能。

(6)程序算法优化

试着用算法来提高程序效率,例如,我们可以用二分法来做物料计划(不用扫描整个库存数据与物料需求做对比,我们只需要找到满足需要的库存数据即可停止遍历,这样做的效率至少可以提高一个数量级,当然也取决于库存数量与需求的物料种类及数量)。

(7)批处理

对于大批量的数据处理,最好能够做成批处理,这样就不会因为单次操作而影响系统的正常使用。

(8)延迟加载

对于大对象的展示,可以采用延迟加载的方式,层层递进地显示明细。例如,我们分页显示列表内容,往往只显示主表的内容,附表的内容在查看明细时才去请求。

(9)防止内存泄露

内存泄露是由于对象无法回收造成的,特别是一些长生命周期的对象风险较大。例如,用户登录成功后,系统往往会把用户的状态保存在Session中,同一用户再次登录时(前一次并没退出),我们会在Session中检查一下此用户是否已经在线,如果是就更新Session状态,不是就记录Session信息。另外,我们还会做一个过滤器,对于长时间不活动的用户进行Session过期处理。以前碰到过系统不做这样的处理,最后导致内存溢出。

(10)减少大对象引用

防止在程序中声明及实例化大对象,不能为了方便而设计出大对象。例如,有些工程师为了图方便,会把用户的功能权限、数据权限、用户信息都放在一个对象中,其占用的堆空间自然就大。而实际上系统中多数用户并不一定都要用这些信息,所以这个对象中存放这么多信息就是浪费。因此,我们可以将其拆分成多个更小的类,或者使用如Redis这样的缓存去存储而不是放在堆内存中。

(11)防止争用死锁

如果出现线程同步的场景,不同线程对同一资源的争用通常会导致等待,处理不当会导致死锁。可以适当采用监听器、观察者模式来处理这类场景,核心思想就是同步向异步转化。如果是OLTP系统,在程序优化的背后还有数据库的优化,涉及表结构、索引、存储过程及内存分配等的优化。

(12)索引

编写合理的SQL,尽量利用索引。

(13)存储过程

为了减少数据传输到应用程序层面,一般会在数据库层面利用存储过程来完成数据的逻辑运算,只需要回传少量结果给应用层。当然,现在的分布式数据库并不主张用存储过程,数据库仅仅用来做存储,并从物理设计、并发处理方面来提升性能。

(14)内存分配

合理地分配数据库内存,以Oracle为例,我们合理设置PGA与SGA的大小;当然我们在操作数据库的同时也要避免冲击内存的上限,例如,对于大数据,不提供Order by的操作,避免PGA区域被占满,即使允许排序,也要限定查询条件来减小数据集的范围。

(15)并行

使用多个进程或者线程来处理任务,例如,Oracle中的并行查询,Tomcat的线程池。当然也要避免并行时的数据争用而导致的死锁,OLTP类型系统并行及数据争用的概率比较大,尤其要注意提高程序效率,减少争用对象的等待。程序要防止互锁(甲需要资源A、B,乙需要B、A;此时甲占有A等待B,正好乙占有B等待A,此时就容易互锁)。

(16)异步

例如,用MQ(消息中间件)来解耦系统之间的依赖关系,减少阻塞。

(17)使用好的设计模式来优化程序

例如,用回调来减少阻塞,使用监听器来阻塞依赖。

(18)选择合适的IO模式

如NIO、AIO等。

(19)缓存

把经常引用的数据缓存到内存中,提高读取的响应速度。这就是常说的空间换时间的概念。

(20)分散压力

在性能优化中也可以分散数据来缓解压力。

例如我们每秒要处理200万条日志数据,分析这200万条数据中藏着的业务机会。我们首先想到的是把数据分而治之,例如,分成20个处理队列,这样每队处理10万条数据,分别进行分析。这样似乎没有问题,但仔细想想:这样性能够好吗?10万条数据按规则处理通常也得10秒左右(这已经是很快了),能够更快吗?当然可以。

可以预见不是每一条数据都有意义或者说能够产生商机,我们可以先排除无效数据,然后再进行分析,自然效率会更高。就如上面说的,把压力分散在各个环节,验证数据时去除掉一部分无效数据,要分析的样本就变少了,性能自然就上去了。

2. 配置优化

配置优化主要包括JVM、连接池、线程池、缓存机制、CDN等优化手段,这些优化提高了资源利用率,最大限度地提升了服务器性能。

JVM配置优化:合理地分配堆与非堆的内存,配置适合的内存回收算法,提高系统服务能力。

连接池:数据库连接池可以减少建立连接与关闭连接的资源消耗。

线程池:通过缓存线程的状态来减少新建线程与关闭线程的开销,一般是在中间件中进行配置,如在Tomcat的server.xml文件中进行配置。

缓存机制:通过数据的缓存来减少磁盘的读写压力,缩小存储与CPU的效率差。

数据库配置优化:例如,在使用MySQL数据库时,我们可以设置更大的缓存空间。

3. 数据库连接池优化

数据库连接池存在的意义是让连接复用,通过建立一个数据库连接池(缓冲区)以及一套连接的使用、分配、管理策略,使得该连接池中的连接可以得到高效、安全的复用,避免了数据库连接频繁建立、关闭的开销。

连接池的好处及如何建立我们不再赘述,直接进入大家最关心的问题:

  1. 配置连接池参数;
  2. 配置连接池数量;
  3. 监控连接池;

连接池原理大同小异,在此我们以C3P0为例进行讲解。

1)配置连接池参数

在实际运用中,我们常利用数据库线程池来提高连接的效率,C3P0是常见的连接池实现。代码清单6-1是典型的Spring+Hibernate+C3P0的配置,具体含义也进行了注释,大家也可以参照Mchange官网的说明进一步理解。

























org.hibernate.dialect.OracleDialecttruefalsetrueautotruetruefalsefalse

2)配置连接池数量

上面我们列出了C3P0连接池的相关配置,那么到底配置多少个连接合适呢?

配置原则:按需分配,够用就好。

配置公式:没有精确的计算公式,可以通过测试来估算。例如,以单位时间的业务量或者并发数为单位,监控使用了多少连接数,再以此为单位进行放大。一般来说,数据库连接池的数量要小于中间件线程池的连接数量。

3)监控连接池

通过对中间件的监控来监控数据库连接池。

如图所示,我们通过Probe(用来监控Tomcat,有兴趣的朋友可以从https://github.com/psi%02probe/psi-probe获取)来监控连接池,其中3个连接都是BLOCKED,说明此时连接正忙。如果长时间不释放,后续的请求就获取不到连接,直到等待超时。

监控工具有多种,如Probe、PL/SQL等。我们既可以用命令进行查询,也可以用监控工具监控其状态。下面我们直接用命令来查询MySQL的连接状态。

如图所示,如果Command列一直是Execute,就代表连接一直在执行任务。如果连接数全占满,后续请求就只有等待,要么等到释放后有空的线程,要么超时报错。

大家可到MySQL官方网站搜索有关MySQL线程状态的知识。

4. 线程优化

线程的优化本来属于配置的优化,把线程优化作为一小节是为了清楚地说明线程优化方法。

1)线程池优化

为什么要有线程池?线程池是为了减少创建新线程和销毁线程所造成的系统资源消耗。

系统性能差一般有以下两种明显的表现:

第一种是CPU使用率不高,用户感觉交易响应时间很长;

第二种是CPU使用率很高,用户感觉交易响应时间很长。

第一种情况可能是由于系统的某一小部分造成了瓶颈,导致了所有的请求都在等待。例如,线程池的数量太小,没有可用的线程使用,所有的请求都在排队等待进入线程池,导致交易响应时间很长。

第二种情况产生的原因比较复杂,可能是硬件资源不够,也可能是应用系统中产生了较多的大对象,还可能是程序算法等问题。

2)CPU处理能力

线程池配置数量与CPU处理能力相关。我们知道,单核CPU同一时刻是只能处理一个任务,那么对于一个4核CPU,理论上线程池配置数量是4(4个连接)。如果一个任务处理只需要100毫秒,单核一秒可以处理10个任务,4核可以处理40个任务,那么单核CPU配置40个连接,任务就可以处理完吗?这就矛盾了,到底配置多少呢?

我们可以这样理解,只要你发送请求够快,4个连接是可以在1秒完成40个任务的处理的(为了方便说明,忽略CPU的中断与切换时间、网络延时等);如果配置40个连接,请求就会排队,处理不完就是过载,CPU还得花时间接收这些任务,让它们排队或者直接拒绝,反而占用了CPU用来处理任务的时间。对于这个例子来说,最佳的性能就是40TPS(或者QPS),考虑到CPU的中断与切换及网络延时,根据实际测试时的连接状态可以再多加几个连接,4个只能是一个理论参考。

下面我们推导一下并得到一个公式:

有这样一个服务,用户请求到App Server,然后App Server要从数据库获取数据。在App Server中,我们配置线程池时要受制于数据库的处理能力。例如,App Server处理请求CPU耗时20ms,数据库处理耗时40ms,那么整个查询请求至少耗时20+40=60ms(这里不考虑网络传输的耗时)。也就是说在整个60ms过程中CPU可以有40ms的空闲,利用这40ms的时间就可以处理两个查询请求(也就是说CPU等待别的资源返回数据的过程中可以去处理另外的任务,统筹安排时间,不考虑CPU中断切换的耗时)。因此当前的情况下CPU可以应付3个线程(查询请求),那么线程池就可以配置为3个,这样线程池的Size就大于CPU的数量了。

由此得出公式:

服务器端最佳线程数量=[(线程等待时间+线程CPU时间)/线程CPU时间] CPU数量

线程等待时间就是上面例子中的 40ms(数据库处理请求的耗时),线程CPU时间就是20ms的App Server处理时间,现在都是多核CPU,所以还要乘以CPU数量。

计算示例:

具有4核CPU的服务器一台,服务器运算时间为20ms,DB查询产生IO的时间为40ms。

如果数据库是整个业务链路上的瓶颈,App Server要等待数据库返回结果(App Server处理能力强,但数据库处理慢,任务阻塞在数据库,间接导致App Server阻塞),按木桶原理,线程CPU时间就是40ms。

线程数量计算如下:

线程数量=4(40+20)/40 =6。原先线程CPU时间为20ms时,线程数=4(40+20)/20=12。

可以看到,数据库瓶颈的线程数量要减小一半,此时我们对性能调整首先要从数据库开始。

IO开销较多的应用其CPU线程等待时间会比较长,所以线程数量可以多一些,CPU可以腾出时间来处理别的线程任务,相反则线程数量要少一些。

CPU开销较多的应用一般只能开到CPU个数的线程数量。这种应用的TPS似乎不高,但如果计算相当快,此应用的TPS也可以很高。

下面我们来分析一下线程数与TPS的关系:

(1)在达到最佳线程数之前,TPS(见图6-3①TPS线)和线程数是互相递增的关系。到了最佳线程数之后,随着线程数的增长,TPS不再上升,甚至略有下降,同时RT(响应时间)持续上升。

线程数与TPS的关系:

(2)就同一个系统而言,支持的线程数越多(最佳线程数越多而不是配置的线程数越多),TPS越高。

3)内存容量

每一个线程的实例都会占用一定的内存(栈空间)空间,这个值是累加的,我们可以很快计算出服务器到底能支持多少线程。例如:一个线程如果需要开辟256KB内存(Heap栈的大小)来保存其运行时的状态,那么1GB内存就可以支持4 096个线程。

这里进行了一个实验,验证在Java环境下不同堆空间时能够开启的线程数,如代码下所示,我们指定线程栈的大小为1MB,在本机上可以开启1801个线程;把线程栈大小设为128KB,则可以开启13401个线程(注意有的操作系统对线程数会有限制)。 

-Xms32M -Xmx64M -Xss1M -XX:+HeapDumpOnOutOfMemoryError
counts: 1801
Exception in thread "main" java.lang.OutOfMemoryError: unable to create
new native thread
//把Xmx设大可以建立的线程数反而变小
-Xms64M -Xmx128M -Xss1M -XX:+HeapDumpOnOutOfMemoryError
counts: 1601
Exception in thread "main" java.lang.OutOfMemoryError: unable to create
new native thread
//把Xss设小线程数反而更多
-Xms16M -Xmx32M -Xss128K -XX:+HeapDumpOnOutOfMemoryError
counts: 13401
Exception in thread "main" java.lang.OutOfMemoryError: unable to create
new native thread

也就是说,内存对于线程数的影响,我们可以通过加大内存容量及调整JVM堆空间大小来调节。那为什么会与JVM相关呢?

这是因为在JVM内存空间中每开辟一个线程时,操作系统相应地也开辟一个线程与之对应,所以有下面的计算公式:

Number of threads = (MaxProcessMemory-JVMMemoryReservedOsMemory)/ThreadStackSize
  • MaxProcessMemory:系统识别的最大内存,例如32位系统识别2GB内存空间,64位系统识别的内存空间基本无上限。
  • JVMMemory:JVM内存。
  • ReservedOsMemory:保留给操作系统运行的内存大小。
  • ThreadStackSize:线程栈的大小,例如JDK 6默认的线程栈大小是1MB。

下图所示为SQL Server官方对于线程数的推荐配置,可以作为参考。

SQL Server官方推荐配置的线程数: 

4) 系统线程数限制

Linux下查看线程:

ps -efL | grep [进程名] | wc -l

Linux下查看系统线程数限制:

/proc/sys/kernel/pid_max
/proc/sys/kernel/thread-max
max_user_process(ulimit -u)
/proc/sys/vm/max_map_count

Windows下查看线程:

最简单的方式就是在任务管理器中查看,注意有一个“线程数”列。如果无法看到,大家可以到“选择进程页列”中勾选“线程数”选项。也可以用命令行的方式查询,如tasklist(需要下载安装)。

Windows下查看线程数限制:

在注册表中查找TcpNumConnections这个键(现在的Windows 7已经没有限制了)。

线程调整总结:

线程数的控制基本就是一个漏斗模型。我们要从漏斗模型的每一个关节加以分析,上层开口大,下层开口小,当下层有瓶颈而处理不了时就会造成业务阻塞。在经过了上面的各项计算与预想后,我们还要进行实际的测试,以验证线程数是否合适。 

如果觉得实验来确定线程数的设置太复杂,可以使用经验值来进行配置,后期密切监控。

例如,如果有8个处理器,它们都同时运行,并出现堵塞线程,为了提高效率就要降低线程数。为了适当地提高客户体验,可以容忍部分线程排队,也就是让acceptCount(Tomcat中线程池配置参数)数量的线程排队。 

5. DB(数据库)优化

对使用数据库通常有3个要求:性能好、数据一致性有保障、数据安全可靠。数据库优化的前提也是这3个要求。有一句玩笑话叫作“少做少犯错,不做不犯错”。DB优化的思路就是少做:减少请求次数,减少数据传输量,减少运算量(查询、排序、统计)。

以Oracle为例,大体从下面几个方向进行优化:

(1)优化物理结构。

数据库逻辑设计与物理设计要科学高效,例如分区设置,索引建立,字段类型及长短、冗余设计等。

(2)共享SQL、绑定变量、降低高水位。

共享SQL、绑定变量旨在减少SQL语句的编译分析时间;降低高水位旨在减少遍历范围,提高查询效率。

(3)优化查询器。

特殊情况下调整执行计划,指定的执行计划加快查找速度。例如连接查询时指定驱动表,减少表的扫描次数。

(4)优化单条SQL。

对单条SQL进行优化分析,例如查询条件选择索引列。

(5)并行SQL。

对数据量巨大的表的数据遍历,用多个线程分块处理任务。

(6)减少资源争用(锁、闩锁、缓存)。

可以提高IO效率,减小响应时间,从而提高吞吐量来缓解争用,例如用缓存,可以用物理拆分把热点数据分布在不同表空间。

(7)优化内存、减少物理IO访问。

SGA(缓存高频访问数据),例如我们把客户信息加载到内存中。

PGA(排序、散列)。

AMM(自动内存管理)人工干预。

(8)优化IO,进行条带化、读写分离、减少热点等。

注意:

单系统性能分析的思路是通过现象结合监控锁定性能问题(程序、配置、IO等)。

单系统性能调优的思路是减少资源占用,减少请求。

6. 空间换时间

性能优化时我们常会听到“空间换时间”的说法,这种优化手段被广泛使用。例如计算机中内存的作用就是拿空间来换时间,内存缓存CPU需要的数据,CPU需要的数据尽量从内存获取而不是直接从磁盘读取。

现在的系统中大量使用Redis,用来存储频繁访问的数据,甚至我们会使用Redis来构建一个分布式的持久化存储,例如用户信息及用户状态,一些公共配置信息,这都是拿空间来换取时间。所以我们在系统调优时,也可以尝试用这种方法。例如电商平台可以把用户信息存放在缓存中,用户“鉴权”时直接查询缓存完成,这样可以提高吞吐量。使用数据库时,我们应学会使用索引,这样查询更快,无疑这也是一种空间换时间的做法。

7. 时间换空间

利用时间来换取空间显然不是一个高效的办法,时间都增加了还谈什么性能呢?但有时候我们不妨试一下,充分利用某一资源的长处来弥补另一资源的短处。例如用CPU时间来换取内存空间,我们查询数据时经常有分页的操作,大家也可能听说过真分页与假分页,真分页是每页只取定量的记录条数,假分页是记录全取,但只是在前端显示定量的记录,真分页实际上就是一种时间换空间的做法。

我们不可能把数据库中的数据全取出来,如果有上千万条数据,网络传输就是一个大问题,解
决的办法是,我们多次去取数据,每次取少量。

8. 数据过滤

系统中通常会有一些统计和分析的功能,以前我们主要针对结构化数据(关系型数据库存储)进行分析,利用SQL语句来做处理。我们会利用过滤条件来过滤数据,这些过滤条件最好能够利用上索引,或者利用上内存临时表来做运算,这些都是优化性能的手段。

现在大数据是热点,对于从事大数据分析的从业者来说,好的算法能够提高运算效率。但算法也不是万能的,数据多到一定量级,总会遇到瓶颈。此时我们不仅要在算法上下功夫,还要在业务上下功夫。

当你在享受快乐假期时,可能会收到周围商圈的推荐信息,有没想过为什么会选中您呢?是巧合吗?您是被大数据分析过的用户,被打上标签了。这是大数据分析的商业案例之一——精准营销。那么问题来了,这和性能优化有什么关系?和数据过滤有什么关系?对于您个体来说,知道您在哪儿很简单,但对于服务商来说,商户的潜在客户是您,在商户周边多少千米范围之内的和您一样的游客是商户要推送消息的目标,过亿的移动电话用户,不断移动的位置,商户几分钟之内就能定位到具体位置。若希望用有限的资源、在有限的时间内来完成数据的分析,性能问题就变得棘手了。

我们是以商户为中心去查找用户在不在周边呢?还是以用户为中心去查看周边的商户呢?通常我们会去建立一个
用户的索引(基于经纬度,通常会选择Redis地理位置方案),这个索引周期性地更新,因为人是在移动的,然后以商户的位置为条件去查询用户索引,过滤出目标对象,过滤时的精度(商户与用户的距离)会严重影响性能,所以我们会有精度上的折中,在生成或者修改用户索引时就考虑到精度,帮助快速过滤掉非目标用户。我们同时可以把用户所在的位置信息按省份分别建立索引,以商户位置为条件检索时范围进一步缩小。

我们换另外一个场景,例如服务商帮我们搜索周边美食的场景。我们并不需要服务商主动推送信息,而是希望手机中的App根据位置信息定位我们的坐标(经纬度),然后可以主动用坐标去向服务商查询周边的商家;或者我们给商家的经纬度算出一个值(可以利用Hash算法来算出一个值),把我们的位置也算出一个值,然后来匹配这两个值的相似性,高度的相似代表距离更近。其实Redis已经有这种地理位置支持,建立地理位置索引,把用户的位置(经纬度)作为条件去查询。

9. 服务器与操作系统优化

我们常说:让专业的人做专业的事,对于服务器来说也有其擅长的方面。例如运行AI算法时,我们会选择使用具有GPU的机器,数据库服务器我们会选择磁盘效率高、CPU强劲、内存大的服务器。所以服务与业务要匹配,要有侧重点。

即使你选择对了服务器,能不能发挥好服务器性能也是一个问题。

服务器硬件资源的调动是由操作系统来控制的,操作系统为了满足复杂的资源调度需求,也会有很多的可选、可配的操作。例如我们在使用kubernetes时,会要求关闭swap内存。由于kubernetes会管理很多pod,打开的文件句柄自然会很多,调整可打开的文件数很必要;反之,服务器的硬件资源的利用率可能会很低。为了提高pod间服务的互访效率,我们理所当然地会想到在同一宿主机上的pod的互访是否可以在内核中完成通信,所以就有了ipvs的方案。

因此,服务器被不同的服务使用时,配置有侧重,操作系统的配置也有侧重。在进行性能压测时,我们定位到了限制,就去修改相应的配置,直到硬件资源利用率达到极致。

通常我们要关注的优化包含(但不限于)如下几个方面:

(1)内核能够开启的任务数(kernel.pid_max),针对性能强劲的服务器,例如64核256GB内存。

(2)系统级别的能够打开的文件句柄的数量(fs.file-max),针对性能强劲的服务器。

(3)用户级别的能够打开的文件句柄的数量(soft nofile/hardnofile)。

(4)进程可以拥有的VMA(虚拟内存区域)的数量(max_map_count),例如使用JVM运行Java应用,要支持大量连接,这个值就可以适当扩大。例如ElasticSearch的使用中就有一个需要注意的地方,报错类似max virtual memory areas vm.max_map_count [65530] is too low, increase to at least [262144]。

(5)Tcp优化,例如:

#减小保持在FIN-WAIT-2状态的时间,对于短连接多的服务器可以考虑设置
net.ipv4.tcp_fin_timeout = 30
#重用TIME_WAIT资源
net.ipv4.tcp_tw_reuse=1
net.ipv4.tcp_tw_recycle=1

(6)是否关闭Swap,例如在使用kubernetes时就建议关闭Swap。

(7)是否设置CPU的“亲和性”。

10. JVM优化

JVM的优化在测试同行中一度是一件神秘的事情,如今JVM的优化也走进了寻常项目中。JVM优化的规则是:遵循理论,分析指标。

1)JVM架构

JVM是JDK(Java Development Kit)运行的环境,现今OracleHotSpot JDK已经发展到13的版本,然而很多企业还在大量使用Oracle HotSpot JDK 8,甚至是Oracle HotSpot JDK 7,有以下几方面原因。

(1)需要与旧版本程序兼容。

(2)当前版本已知的问题,这么多年已经差不多解决了,系统运行稳定,不用冒险更换版本。

(3)SUN被Oracle收购后,官方支持延后,新版本官方支持不明朗,使用版本风险很大。

(4)新版本并没有给大多数应用带来根本上的性能提升,增加的一些语法对工程开发效率提升不显著。

由于Oracle对JDK的政策也导致了很多厂商自己开始维护旧版本,例如IBM JVM、Dalvik VM、JRockit JVM、OpenJDK。

不管JDK有多少,这些JDK的JVM架构大同小异,回收算法、垃圾收集器也都相似。

下所示是Oracle HotSpot JVM架构:

类加载器(Class Loader):用于加载类文件,程序从Java文件编译成类(Class)文件,类加载器把Class文件加载到JVM的方法区。

方法区(Method Area):用于存储类的数据,例如字段、常量池、方法数据等;方法区就如一个工厂,有大量模具,也有工具,能够生产出对象。

堆(Heap Area):堆就像仓库,也像一个“社会”,里面“活动”的是对象,例如,一笔订单数据被抽象成一个对象存在于堆中,一批订单数据(结构相同,属性可能不同)被抽象成对象后存储在集合(ArrayList、Map……)对象中。

Java虚拟机栈(Stack Area):如果把人的活动比作一个线程,大脑就是桢栈,帮我们记住今天要干什么?明天要到哪里去?虚拟机栈就是社会。我们取得的成绩,买下的房子,欠下的债,赚到的钱都是对象,会放到堆中存储。Java线程存储数据的内存单元称为线程栈,每个线程的栈都是私有的,就如我们的人生轨迹是私有的一样,高兴?伤心?快乐?忧郁?都是你的状态数据,都会存入桢栈中,就看你选择把哪些释放掉?Java栈用于存储局部变量表、动态链接、操作数、方法出口等信息,也就是线程的运行时态、过程数据都存在此。

Oracle HotSpot JVM架构:

程序计数器(PC Registers):顾名思义,用来为程序计数,通俗地说就是记录执行的指令、分支、循环、跳转等,实际是记录当前运行指令的地址以及下一条指令的地址,保证程序能够有序地执行下去。

本地方法栈(Native Method Stack):Java虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,本地方法栈则是为虚拟机使用Native方法服务,什么是Native方法呢?就是调用Windows平台或者Linux平台的函数去处理IO读写。

执行引擎(Execution Engine):包含JIT(即时)编译器和垃圾收集器编译器,以及解释器。通俗地讲JIT编译器就是把字节码转换成机器码,垃圾收集器帮助清除、回收JVM中的无用内存。

下面介绍一下Java程序的运行过程:

  1. 启动Java程序,此时类加载器帮我们把程序加载到方法区。
  2. 程序启动过程中通常有一些初始化操作,会产生一些对象,这些对象会存放在堆内存中。
  3. Java程序为了能够快速地完成某些任务,不会任务来了才去找“帮手”(线程),会事先叫来一些“工作人员”(线程)待命,这就是创建Java线程池。
  4. 执行引擎会把运行区(Runtime Data Areas)的信息转化为机器代码,由机器执行完成。
  5. Java程序接收到用户请求,线程开始处理用户请求,执行过程的数据会存储在线程栈中。如果执行过程中有对象产生,则会把对象存入堆中,程序计数器帮助记录方法的执行出入口,确保不要“跑偏”(程序异常);线程在则栈在,线程亡则栈亡(自动清理掉)。
  6. 如果有读写磁盘的操作,程序会调用Native方法去完成对磁盘IO操作。
  7. 程序运行过程中,堆中的数据如果不清理则会越存越多,最终会“爆掉”。所以当完成任务后,一些不再被需要的对象是可以被回收掉的。当堆空间占用到一定程度会触发垃圾收集器,如果堆空间不够用,回收后还不够用,此时就会堆溢出,这就是常说的内存泄露,通常报错信息如下:Exception in thread “main”: java.lang.OutOfMemoryError: Java heap space。
  8. 如果不断有类被加载到方法区,这个区终究会被占满,直到溢出。通常报错信息如下:Exception in thread “main”: java.lang.OutOfMemoryError: PermGen space。
  9. Java程序接收到大量请求,每个请求由一个线程来处理,线程的状态和数据由栈中的桢栈来存储。堆内存加栈内存占满整个物理内存,也会内存溢出。通常报错信息如下:java.lang.OutOfMemoryError: unable to create new native thread。
  10. JVM中的栈是可以设置大小的,如果栈中的数据激增,也会把栈占满,此时就是栈溢出。通常报错信息为:java.lang.StackOverflowError。

为了避免内存溢出的情况,我们主动进行内存回收管理,这就是我们常听说的垃圾回收,回收当然是讲究方法的,所以就有垃圾回收算法,把回收算法有效地组织起来的机制就是我们常听说的垃圾收集器。

2)垃圾回收算法

垃圾回收算法种类并不多,也比较好理解,主要有如下几种。

(1)标记—清除算法(Mark-Sweep)

这是最原始的垃圾回收算法。算法分两步,第一步对需要回收的对象做标记,第二步对有标记的对象进行清除。算法的弊端是在清理完成后会产生内存碎片,如果有大对象需要连续的内存空间时,还需要进行碎片整理。

(2)复制算法(Copying)

如图所示,对象从Eden到From Survivor0区,再到To Survivor1区,实际上是一个复制过程。复制算法就是把对象从一块复制到另一块,前一块复制完后就可以清理干净。复制算法把空间至少要一分为二,所以空间利用率不高。

(3)标记—整理(或叫压缩)算法(Mark-Compact)

这种算法先标记不需要进行回收的对象,然后将标记过的对象移动到一起,使得内存连续,减少了碎片。此算法清除的是标记边界以外的内存,比较适用于持久代,因为持久代的对象变动较新生代、老年代都要少,不会高频回收。

(4)分代收集算法

假设绝大部分对象的生命周期都非常短暂,就把Java堆分为新生代和老年代,根据各个年代的特点采用最适当的收集算法。例如,在新生代中,每次垃圾收集时都有大批对象没被引用,会被回收掉。也就是只有少量存活,可以选择复制算法,把存活的复制到From Surivivor区,只需要付出少量存活对象的复制成本就可以完成收集。

而在老年代中的对象存活机率高,不需要像新生代设置多个区,不需要额外空间去做复制,使用“标记—清除”或“标记—整理”算法进行回收即可。

图中所示是HotSpot JVM的内存结构(参照JDK 7)。Heap采用分代的结构,分新生代与老年代。新生代又分Eden、From Survivor0、To Survivor1区域,新对象在Eden中产生,在垃圾收集器的作用下有幸生存就会复制到From Survivor0区,From Survivor0区中有幸生存的对象就复制到To Survivor1区,From Survivor0与To Survivor1周而复始,n次后还有幸生存就复制到老年代(Old Generation)。

有的新生代的对象如果“个头够大”,可以直接跨到老年代,省去在新生代中被频繁复制的过程。这就是常说的分代垃圾回收策略。

每种回收算法都有其优点与缺点,于是人们把这些回收算法配合一起使用,这就是垃圾收集器。垃圾收集器针对特定的场景来发挥它的优势,用户通过配置垃圾收集器的参数来达到最佳效果。 

3)垃圾收集器

垃圾收集器在工作中会导致用户线程暂停(Stop The World),因此我们要做的不仅是选择合适的垃圾收集器,还要减少收集次数及收集时间(减小停顿时间)。如果没法减小到合适的值,是不是可以适当控制停顿时间呢?

我们先统一几个概念:

  • 用户线程:用来处理用户请求的线程。
  • 垃圾收集线程:用来进行垃圾回收的线程。现在带有多核CPU的计算机已经普及,为了提高垃圾收集效率,开发人员自然会想到采用多线程的方式来进行垃圾收集,这就是我们常说的并行收集。如果能够做到垃圾收集线程与用户线程一起(同时或者交替)运行,这就是并发收集。
  • 并行(Parallel):多个垃圾收集线程并行工作,全部用在收集工作上,此时用户线程处于等待状态(Stop The World)。
  • 并发(Concurrent):垃圾收集线程和用户线程同时执行,不一定是并行,也可能是交替执行,总之用户线程继续执行,垃圾收集线程并不干扰用户线程的执行。

下图所示是常见的垃圾收集器,收集器之间用连线说明它们的配合。

下面简单介绍一下几种“久负盛名”的垃圾收集器。

(1)Serial

它是串行收集器,主要作用于新生代的收集器,单线程运行,并使用复制算法,在进行垃圾回收的时候会将用户线程暂停(Stop The World),如图6-10所示。这对于现在高并发的系统来说是不可接受的,对于单CPU、新生代空间较小及对暂停时间要求不是非常高的应用还是可以的。

JDK有server与client两类模式,server模式适合我们部署服务端程序,client适合我们部署客户端程序,例如JWT实现的收银客户端。client默认的垃圾收集方式是Serial,可以通过-XX:+UseSerialGC来强制指定。现在Serial这种古老收集器已经很少用了。

(2)ParNew

它是并行收集器,也是以吞吐量优先的收集器,主要作用于新生代的收集器,可简单理解为Serial的多线程版,所以也是使用复制算法,并可以与CMS配合。因此,我们可以选择新生代ParNew、老年代CMS。

(3)Parallel Scavenge

它是吞吐量优先收集器,并行收集,目标就是达到一个可控的吞吐量。Parallel Scavenge提供了两个参数,用来精确控制吞吐量,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis参数(单位:毫秒),以及直接设置吞吐量大小的-XX:GCTimeRatio参数(0~100,不包括首尾)。

Parallel Scavenge收集器还有一个参数,用来开启垃圾收集的自适应调节策略,只需要将JVM基本内存设置好,并且制定上述两个参数中的一个来作为JVM的优化目标,JVM就可以根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大吞吐量,这个参数就是-XX:+UseAdaptiveSizePolicy。自适应调节策略也是Parallel Scavenge收集器相对于ParNew收集器的一个重要区别。

ParNew收集器需要手工指定新生代大小(-Xmn)、Eden与Survivor的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数。可用-XX:+UseParallelGC来强制指定Parallel Scavenge收集器,用-XX:ParallelGCThreads=[整数]来指定垃圾收集线程数。

(4)CMS(Concurrent Mark Sweep)

它是并发收集器,英文直译是并发标记清除,作用在老年代。我们知道Serial有停顿问题,现在高并发系统都希望减少Stop The World的情况,CMS可以帮助减轻这个问题。CMS收集器是基于“标记—清除”算法实现的老年代并发垃圾收集器。

并发垃圾收集器的整个运行过程大致分为4个步骤:

1)初始标记(CMS initial mark);

2)并发标记(CMS concurrent mark);

3)重新标记(CMS remark);

4)并发清除(CMS concurrent sweep)。

其中初始标记、重新标记这两个步骤仍然需要Stop The World。初始标记只是标记一下GC Roots(后面会解释,如图所示)能直接关联到的对象,所以速度很快,基本不让我们感觉Stop The World。并发标记阶段是进行GC Roots根搜索算法的过程,会判定对象是否存活。

重新标记阶段则是为了修正并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,意思是我在标记,你还在新增对象或者修改对象。因此要修正标记,这个阶段的停顿时间会比初始标记阶段稍长,但比并发标记阶段短,时间短到你感觉不到。

并发清除,清除时线程与用户线程是并发执行的,不会Stop The World。

在整个收集器运行过程中耗时最长的是并发标记和并发清除,在这两个阶段中收集器线程都可以与用户线程一起并发工作,所以整体来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。

CMS收集器也有如下3个缺点:

1)对CPU资源非常敏感。在并发阶段,虽然不会导致用户线程停顿,但会因为占用了一部分线程(或者说CPU资源)而导致应用程序变慢,总吞吐量会降低。CMS默认启动的回收线程数是(CPU数量+3)/4,也就是当CPU在4个以上时,并发回收时垃圾收集线程不少于25%的CPU资源,并且随着CPU数量的增加而下降。但是当CPU不足4个时(例如2个),CMS对用户程序的影响就可能变得很大。如果本来CPU负载就比较大,还要分出一半的运算能力去执行收集器线程,就可能导致用户程序的执行速度忽然降低了50%,也让人无法接受。

2)无法处理浮动垃圾(Floating Garbage),可能出现Concurrent Mode Failure而导致另一次Full GC的产生。由于CMS并发清理阶段用户线程还在运行,新的垃圾大概率还会产生。这部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好放到下一次垃圾收集清理,这部分垃圾就被称为“浮动垃圾”。由于有浮动垃圾存在,我们就不能等到老年代占满再回收,不然还没回收完,浮动垃圾都没位置了,自然内存溢出,所以还得留点空间,可以通过CMSInitiatingOccupancyFraction来设置这个阈值。

3)标记—清除算法会导致空间碎片。CMS正是基于“标记—清除”算法实现的收集器,这意味着收集结束时会有大量空间碎片产生。空间碎片过多时,将会给大对象分配带来很大麻烦,这样就会出现空间还有,但无法找到足够大的连续空间来分配大对象。

GC Roots:回收算法在做标记时显然是需要规则的,给对象加一个计数器,如果对象引用记数为零则标记,后面就可被回收。而对象的引用是有一个链的,A引用B,B引用C,就形成了链,链的根就是GC Roots。如果一个对象到GC Roots没有任何引用链相连,则说明此对象 没有引用,也就不可用,自然要回收掉,如图所示。

(5)Parallel Old

它是并行收集器,作用在老年代。Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。如果老年代选择了Parallel Old,那新生代就只能选择Parallel Scavenge收集器。 

(6)Serial Old

Serial Old自然是Serial收集器的老年代版本,也是采取“标记—整理”算法,可以作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure的时候使用。

(7)G1(Garbage-First Garbage Collector)收集器

现在互联网流量非常大,应用响应必须快。看了上面介绍的垃圾收集器,是不是觉得没有一个能适应新应用的?我们是时候弄一个低暂停垃圾收集器了。

G1收集器又叫作“垃圾优先型垃圾收集器”,作用于新生代与老年代,使用分代回收方式,制定可控暂停时间,回收过程是并发和并行都可以,目标就是替换掉CMS收集器。 

G1收集器在JDK 7以后才有,主要避免像CMS收集器容易产生内存碎片这类问题的发生,所以G1收集器采用标记整理算法;为了减小暂停时间,G1可以控制停顿时间。

G1的收集过程如下:

  • 对象在新生代创建,幸存的对象晋升到老年代;
  • 在老年代,当堆占用率超过阈值时,触发标记阶段,并发(并行)标记存活对象;
  • 并行地复制压缩存活对象,恢复空闲内存;

G1主要特点如下:

并行与并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短Stop The World停顿时间,部分其他收集器原本需要停顿Java线程执行的垃圾收集动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。

分代收集:与其他收集器一样,分代概念在G1中依然得以保留。虽然G1可以不需要其他收集器配合就能独立管理整个垃圾收集堆,但它能够采用不同方式去处理新创建的对象和已存活一段时间、“熬过”多次垃圾收集的旧对象来获取更好的收集效果。

空间整合:G1从整体来看是基于“标记—整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的。这意味着G1运行期间不会产生内存空间碎片,收集后能提供规整的可用内存。此特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次垃圾收集。

可预测的停顿:这是G1相对CMS的一大优势。降低停顿时间是G1和CMS共同的关注点,但G1除了降低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。

G1是区域化、分代式垃圾回收器:Java 对象堆(堆)被划分成大小相同的若干区域(这个与以前的收集器都不一样),不存在像复制算法那样要空间减半,回收时也是基于区域的,也不是一次就要回收完,灵活性强,适合管理大堆。所以有很多人建议当堆大于6GB时,才推荐用G1,当然JDK最好是JDK 8以上。

到此,我们简单介绍了垃圾回收算法、垃圾收集器,是不是想尝试对JVM调参了呢?别急,先总结一下垃圾回收器怎么选择?如何设置?

下图中列出了垃圾收集器的组合,我们可以知道哪些垃圾收集器是可以存储的,少做无用功。如图所示,我们把垃圾收集器组合、适用场景、如何设置列了一张表。

例如,响应时间是关键指标,我们需要减少停顿时间,可以选择CMS垃圾收集器,-XX:+UseConcMarkSweepGC(该参数隐式启用-XX:+UseParNewGC)。

同样是响应时间优先,如果您的JDK版本是JDK 8或JDK 8以上,并且JVM内存大于6GB,也可以选择G1。

例如,吞吐量是关键指标,可以选择Parallel Scavenge垃圾收集器,-XX:+UseParallelOldGC(该参数隐式启用-XX:+UseParallelGC)。

4)JDK性能参数

数据是存储在堆内存中的,虽然我们知道如何选择垃圾收集器,但堆的设置也很关键。通常来说,堆越大,存放的对象越多,性能理应更好。实际也是差不多,但也有个度,我们知道人少好管理,堆也如此;堆大到一定程度,回收效率也会下降,所以适度则好。接下来我们认识一下这些性能参数,知道用哪些参数设置堆及给堆分配合适的区,如何开启指定的垃圾收集器,帮助大家成为调参能手。

(1)大小设置类

  • -Xmn

此选项设置新生代的堆的初始大小和最大值,在字母后面加上k或K表示千字节,加上m或M表示兆字节,加上g或G表示千兆字节。Oracle建议用户将新生代的大小保持在整个堆大小的1/2到1/4之间。

下面是设置堆内存为256MB的几种表达方式:

-Xmn256m;
-Xmn262144k;
-Xmn268435456;

可以使用-XX:NewSize设置初始大小和-XX:MaxNewSize设置最大值。

  • -Xms

此选项设置堆的初始大小。此值必须是1 024的倍数且大于1 MB。

在字母后面加上k或K表示千字节,m或M表示兆字节,g或G表示千兆字节。

以下是设置堆初始大小为6 MB的几种方式:

-Xms6291456;
-Xms6144k;
-Xms6m;

如果未设置此项,则初始大小为老年代和新生代大小之和。

  • -Xmx

此选项指定堆的最大值,单位与-Xms一致,可以使用K、M、G来表示,等价于-XX:MaxHeapSize。设置-Xms与-Xmx一样大,可以有效减少对象迁移。

  • -Xss大小

此选项设置线程栈大小,可以使用K、M、G来表示,默认值取决于平台。

Linux / ARM(32位):320 KB;
Linux / i386(32位):320 KB;
Linux / x64(64位):1024 KB;
OS X(64位):1024 KB;
Oracle Solaris / i386(32位):320 KB;
Oracle Solaris / x64(64位):1024 KB;

设置方式:-Xss1m或者-Xss1024k,等效于-XX:ThreadStackSize。

当连接数多,内存紧张时,可以适当把线程栈减小一点,这样可以容纳更多的线程(连接)。

  • -XX:InitialSurvivorRatio

此选项设置吞吐量垃圾收集器(Parallel)使用的初始幸存者空间比率(由-XX: +UseParallelGC和/或- XX:+UseParallelOldGC选项启用)。

默认情况下,吞吐量垃圾收集器通过使用-XX:+UseParallelGC和-XX:+UseParallelOldGC选项来启用自适应大小调整,并根据应用程序的行为从初始值开始调整幸存者空间的大小。如果禁用了自适应大小调整(使用-XX:-UseAdaptiveSizePolicy选项),-XX:SurvivorRatio则使用该选项来设置整个应用程序执行过程中幸存者空间的大小。以下公式可用于根据新生代(Y)的大小和初始幸存者空间比率(R)计算幸存者空间的初始大小(S)。

等式中的2表示两个幸存者空间(Survivor)。指定为初始生存空间(Eden)比率的值越大,初始生存空间尺寸就越小。默认情况下,初始生存者空间比率设置为8。

以下示例是将初始幸存者空间比率设置为4:

-XX:InitialSurvivorRatio = 4
  • -XX:MaxGCPauseMillis

此选项设置最大垃圾收集暂停时间的目标(以毫秒为单位)。这是一个目标值。默认情况下,没有最大暂停时间值。下面的示例显示如何将最大目标暂停时间设置为500毫秒。

-XX:MaxGCPauseMillis = 500
  • -XX:MaxHeapFreeRatio

此选项设置垃圾收集事件后允许的最大可用堆空间百分比(0%~100%)。如果可用堆空间扩展到该值以上,则堆将缩小。默认情况下,此值设置为70%。

下面显示如何将最大可用堆比率设置为75%:

-XX:MaxHeapFreeRatio = 75
  • -XX:PermSize

此选项设置分配给持久代的空间,如果超出该空间,则会触发垃圾回收。此选项在JDK 8中已弃用,并已由-XX:MetaspaceSize选项取代。

  • -XX:MaxPermSize

此选项设置最大持久代空间大小。此选项在JDK 8中已弃用,并由-XX:MaxMetaspaceSize选项取代。

  • -XX:MaxMetaspaceSize

此选项设置可以分配给元数据的最大内存(不占JVM内存,直接占用主机内存)。默认情况下,大小不受限制。应用程序的元数据量取决于应用程序本身、其他正在运行的应用程序以及系统上可用的内存量。

下面的示例显示如何将最大元数据大小设置为256MB:

-XX:MaxMetaspaceSize = 256m

平常我们可以这样设置:

JDK7: -XX: PermSize=128m -XX:MaxPermSize=512m

持久代一般不会太多,默认64MB,现在内存一般够用,保险起见可以设置得大一点,如果实际占用太大,程序有问题的概率比较大。

JDK8: -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512m

可以做个保护设置,设置MaxMetaspaceSize可以占用的最大值,这样就不会无限制占用内存,否则程序有问题的概率比较大。

  • -XX:MaxTenuringThreshold

对象在Survivor区最多“熬过”多少次Young后晋升到老年代,并行(吞吐量)收集器的默认值为15,而CMS收集器的默认值为6。

以下示例显示如何将最大期限阈值设置为10:

-XX:MaxTenuringThreshold = 10
  • -XX:MetaspaceSize

此选项设置分配的元数据空间的大小。

  • -XX:MinHeapFreeRatio

此选项设置垃圾收集事件后允许的最小可用堆空间百分比(0%~100%)。如果可用堆空间低于此值,那么堆将被扩展。默认情况下,此值设置为40%。

下面的示例显示如何将最小可用堆比率设置为25%:

-XX:MinHeapFreeRatio = 25
  • -XX:NewRatio

此选项设置新生代与老年代内存大小比率。默认情况下,此选项设置为2。下面的示例演示如何将新生代/老年代比率设置为1。

-XX:NewRatio = 1
  • -XX:ParallelGCThreads

此选项设置新生代与老年代中用于并行垃圾回收的线程数。默认值取决于JVM可用的CPU数量。例如,要将并行垃圾收集的线程数设置为2,请指定以下选项。

-XX:ParallelGCThreads = 2

可以参照下面的公式:

ParallelGCThreads = 8+( Processor − 8 ) ( 5/8 )
ConcGCThreads = (ParallelGCThreads + 3)/4

ConcGCThreads为线程数,小于8个处理器时,ParallelGCThreads按处理器数量处理,反之则按上述公式处理。

例如24Processor、ParallelGCThreads=15、ConcGCThreads=5

  • -XX:ConcGCThreads = 线程

此选项设置用于并发垃圾收集的线程数。默认值取决于JVM可用的CPU数量。例如,要将并行GC的线程数设置为2,请指定以下选项。

-XX:ConcGCThreads = 2
  • -XX:SurvivorRatio

此选项设置Eden空间大小与Survivor空间大小之间的比率。默认情况下,此选项设置为8。以下示例显示如何将Eden/Survivor空间比率设置为4。

-XX:SurvivorRatio = 4
  • -XX:TargetSurvivorRatio

此选项设置垃圾回收后所需的剩余空间百分比(0%~100%)。默认情况下,此选项设置为50%。以下示例显示如何将Survivor to空间比率设置为30%。

-XX:TargetSurvivorRatio = 30
  • -XX:+AlwaysPreTouch

在JVM初始化期间启用对Java堆上每个页面的接触,简单地说就是把要加到内存中的数据都加进去,要分配的内存都分配到位,好比战士上战场前,该准备的东西都要准备好。默认情况下此选项是禁用的,建议打开,无非就是启动慢点,但后面访问时会更流畅。例如,页面会连续分配,或不会在新生代晋升到老年代时才去访问页面使得垃圾收集停顿时间加长。

(2)垃圾收集器配置类

  • -XX:+UseG1GC

此选项启用G1垃圾收集器,适用于具有大量RAM的多处理器计算机。设置时尽量满足垃圾收集暂停时间目标,同时保持良好的吞吐量。建议将G1收集器用于大堆(大小约为6GB或更大)且对垃圾收集暂停时间要求较高的应用程序。默认情况下,此选项是禁用的,并且将根据计算机的配置和JVM的类型自动选择收集器。

  • -XX:G1HeapRegionSize

此选项设置使用G1收集器时将Java堆细分为Region的大小。取值范围是1 MB~32 MB。默认区域大小是根据堆大小确定的。

下面的示例显示设置Region大小为16MB:

-XX:G1HeapRegionSize = 16m
  • -XX:G1ReservePercent

此选项设置堆内存的预留空间百分比,用于降低晋升失败的风险,此选项设置为10%。下面的示例显示如何将预留空间设置为20%。

-XX:G1ReservePercent = 20
  • -XX:+ UseParallelGC

此选项设置使用并行垃圾收集器(也称为吞吐量收集器),以利用多个处理器来提高应用程序的性能。默认情况下,此选项是禁用的,并且将根据计算机的配置和JVM的类型自动选择收集器。如果启用此选项,也会同时自动开启-XX:+UseParallelOldGC,除非您明确禁用它。

  • -XX:+ UseParallelOldGC

此选项设置使用并行垃圾收集器,新生代与老年代都有。默认情况下,此选项是禁用的。启用它会自动启用-XX:+UseParallelGC选项。

  • -XX:+ UseParNewGC

此选项设置在新生代中使用并行线程进行收集。默认情况下,此选项是禁用的。设置-XX:+UseConcMarkSweepGC选项后,它将自动启用。

  • -XX:+ UseSerialGC

此选项设置使用串行垃圾收集器。对于不需要垃圾回收,具有任何特殊功能的小型和简单应用程序,通常是最佳选择。默认情况下,此选项是禁用的,并且将根据计算机的配置和JVM的类型自动选择收集器。

  • UseConcMarkSweepGC

此选项设置使用CMS(并发垃圾收集器)垃圾收集器。当吞吐量(-XX:+UseParallelGC)垃圾收集器无法满足应用程序延迟要求时,Oracle建议用户使用CMS垃圾收集器。G1垃圾收集器(-
XX:+UseG1GC)是另一种选择,对大内存的效果更好(6GB或以上)。默认情况下,此选项是禁用的,并且将根据计算机的配置和JVM的类型自动选择收集器。启用此选项后,-XX:+UseParNewGC选项将自动设置,并且用户不应禁用它。JDK 8中已弃用此选项组合-
XX:+UseConcMarkSweepGC -XX:-UseParNewGC。

通常按如下方式设置:

-XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=75 -
XX:+UseCMSInitiatingOccupancyOnly

监控内存达到75%就开始垃圾回收,不要等到有浮动垃圾导致内存溢出。为了让这个设置生效,需要设置-XX:+UseCMSInitiatingOccupancyOnly,否则75%只被用来做开始的参考值。

  • -XX:CMSInitiatingOccupancyFraction

此选项触发CMS的老年代使用率,默认值−1。以下示例显示如何将使用率设置为20%。

-XX:CMSInitiatingOccupancyFraction = 20
  • CMSScavengeBeforeRemark

此选项在CMS做标记(remark)之前做一次YGC(新生代垃圾收集),减少GC Roots扫描的对象数,从而提高remark的效率。这也有可能增加垃圾收集时间,例如YGC回收效果并不理想,后面还得跟一次Full垃圾收集,这样整个垃圾收集时间就被拉长了,视情况决定是否开启。

  • -XX:+ ExplicitGCInvokesConcurrent

此选项允许程序中调用System.gc()来做Full垃圾收集,默认情况下处于禁用状态,并且只能与-XX:+UseConcMarkSweepGC选项一起启用。

5)诊断参数

诊断参数主要帮助做诊断,例如打印一下垃圾收集日志,内存溢出时把堆导出来供分析。

(1)-XX:+ HeapDumpOnOutOfMemoryError

此选项指java.lang.OutOfMemoryError引发异常时,使用堆分析器(HPROF)将Java堆转储到当前目录。您可以使用-XX:HeapDumpPath设置堆转储文件的路径和名称。默认情况下,禁用此选项。

(2)-XX:HeapDumpPath

此选项指设置-XX:+HeapDumpOnOutOfMemoryError选项时,设置堆转储文件的路径和名称,下面示例把堆转储到jforum.hprof文件。

-XX:HeapDumpPath=jforum.hprof

(3)-XX:LogFile

此选项设置写入日志数据的路径和文件名。默认情况下,该文件在当前工作目录中创建,并且名为hotspot.log。以下示例显示如何将日志文件设置为/var/log/java/hotspot.log。

-XX:LogFile = / var / log / java / hotspot.log

(4)-XX:+ PrintClassHistogram

此选项启用在Control+C事件(SIGTERM)之后打印类实例直方图的功能。默认情况下,此选项是禁用的。设置此选项等效于运行jmap -histo命令或jcmd pid GC.class_histogram命令,其中pid是当前Java进程标识符。

(5)-XX:+ PrintGC

每次垃圾收集都打印垃圾收集消息,默认情况下此选项禁用。

(6)-XX:+ PrintGCApplicationConcurrentTime

此选项打印自上次暂停(如垃圾收集暂停)以来经过的时间。默认情况下此选项禁用。

(7)-XX:+ PrintGCApplicationStoppedTime

此选项允许打印暂停(如垃圾收集暂停)持续了多长时间。默认情况下此选项禁用。

(8)-XX:+ PrintGCDateStamps

此选项设置每次垃圾收集打印日期戳。默认情况下此选项禁用。

(9)-XX:+ PrintGC详细信息

此选项设置每次垃圾收集打印详细消息。默认情况下此选项禁用。

(10)-XX:+ PrintGCTaskTimeStamps

此选项设置垃圾收集工作线程启动时的时间戳打印。默认情况下此选项禁用。

(11)-XX:+ PrintGCTimeStamps

此选项设置每次垃圾收集打印的时间戳。默认情况下此选项禁用。

(12)-XX:+ G1PrintHeapRegions

此选项设置打印有关G1收集器分配了哪些区域以及回收了哪些区域。默认情况下此选项禁用。

6)参考配置

了解完上面的参数,大家可以参照图6-9中的垃圾收集器类型组合来进行设置。每种垃圾收集器都有自己的特性,我们在使用时可以根据业务需求、内存形态来设置各种内存的大小,例如,堆要多大、新生代与老年代多大、经历多少次YGC一个新生代的对象才能到达老年代。

(1)如果设置了垃圾收集器,JDK会有默认的参数,使用下面两条命令可以查看这些参数。

java -XX:+PrintCommandLineFlags –version
java -XX:+PrintGCDetails –version
[root@75ed2bb63940 bin]# java -XX:+PrintCommandLineFlags -version
-XX:InitialHeapSize=64626688 -XX:MaxHeapSize=1034027008 -
XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -
XX:+UseCompressedOops -XX:+UseParallelGC
openjdk version "1.8.0_212"
OpenJDK Runtime Environment (Zulu 8.38.0.13-linux64)-Microsoft-Azurerestricted (build 1.8.0_212-b04)
OpenJDK 64-Bit Server VM (Zulu 8.38.0.13-linux64)-Microsoft-Azurerestricted (build 25.212-b04, mixed mode)
[root@75ed2bb63940 bin]# java -XX:+PrintGCDetails -version
openjdk version "1.8.0_212"
OpenJDK Runtime Environment (Zulu 8.38.0.13-linux64)-Microsoft-Azurerestricted (build 1.8.0_212-b04)
OpenJDK 64-Bit Server VM (Zulu 8.38.0.13-linux64)-Microsoft-Azurerestricted (build 25.212-b04, mixed mode)
Heap
PSYoungGen total 18432K, used 635K [0x00000000eb700000,
0x00000000ecb80000, 0x0000000100000000)
eden space 15872K, 4% used
[0x00000000eb700000,0x00000000eb79ed08,0x00000000ec680000]
from space 2560K, 0% used
[0x00000000ec900000,0x00000000ec900000,0x00000000ecb80000]
to space 2560K, 0% used
[0x00000000ec680000,0x00000000ec680000,0x00000000ec900000)
ParOldGen total 42496K, used 0K [0x00000000c2400000,
0x00000000c4d80000, 0x00000000eb700000)
object space 42496K, 0% used
[0x00000000c2400000,0x00000000c2400000,0x00000000c4d80000)
Metaspace used 2206K, capacity 4480K, committed 4480K, reserved
1056768K
class space used 240K, capacity 384K, committed 384K, reserved
1048576K

UseParallelGC代表新生代和老年代都使用并行垃圾收集器,即:

Parallel Scavenge(新生代)+Parallel Old(老年代)

JDK 7、JDK 8、JDK 9的默认垃圾收集器是:

  • JDK 7默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)。
  • JDK 8默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)。
  • JDK 9默认垃圾收集器G1。

(2)还可以使用java -XX:+PrintFlagsInitial来查看有哪些初始值可以设置,PrintFlagsFinal检查有哪些默认值。

java -XX:+PrintFlagsFinal -version| grep ParallelGCThreads

(3)如果已经设置了垃圾收集器,也设置了堆的大小,怎么才能验证是否设置正确。可以通过jinfo –flags [进程号]来获取启动参数用以验证。下面我们参照JMeter 5.2(JDK 1.8 64bit)在Windows上的启动参数。

-XX:+HeapDumpOnOutOfMemoryError #内存溢出时输出Heap Dump文件
-Xms1g #初始堆大小
-Xmx1g #最大堆大小,与Xms一致一步到位,减少堆扩充时的性能消耗
-XX:MaxMetaspaceSize=256m #元数据最大空间
-XX:+UseG1GC #使用G1垃圾收集器
-XX:MaxGCPauseMillis=100 # 垃圾收集最大暂停时间,到时间收集不完也放弃
-XX:G1ReservePercent=20 #预留堆大小防止对象晋升时无空间

上述是JMeter的启动文件配置的一些参数,但还有很多的默认参数会生效,使用jinfo –flag [进程号]获取的其他参数如图所示。

运行JMeter时可以这样写:

java -XX:+HeapDumpOnOutOfMemoryError -Xms1g -Xmx1g -
XX:MaxMetaspaceSize=256m -XX:+UseG1GC -XX:MaxGCPauseMillis=100 -
XX:G1ReservePercent=20 -jar ApacheJMeter.jar

以下整理中的参数,其中斜体部分参数是自动生成的,带下划线的参数是默认配置。因此有些参数根本不需要我们去配置,我们只需要配置一些影响大的参数。

-XX:CICompilerCount=4
-XX:ConcGCThreads=2
-XX:G1HeapRegionSize=1048576
-XX:G1ReservePercent=20
-XX:+HeapDumpOnOutOfMemoryError
-XX:InitialHeapSize=1073741824
-XX:MarkStackSize=4194304
-XX:MaxGCPauseMillis=100
-XX:MaxHeapSize=1073741824
-XX:MaxMetaspaceSize=268435456
-XX:MaxNewSize=643825664
-XX:MinHeapDeltaBytes=1048576
-XX:+UseCompressedClassPointers
-XX:+UseCompressedOops
-XX:+UseFastUnorderedTimeStamps
-XX:+UseG1GC
-XX:-UseLargePagesIndividualAllocation
-XX:+HeapDumpOnOutOfMemoryError
-Xms1g
-Xmx1g
-XX:MaxMetaspaceSize=256m
-XX:+UseG1GC
-XX:MaxGCPauseMillis=100
-XX:G1ReservePercent=20

(4)另外,使用Oracle JDK自带的jconsole(jvisualvm.exe)也可以查看到启动参数。

(5)同样是使用jconsole,还可以在MBean选项卡下面找到垃圾收集信息。

(6)通过JMX可以访问GarbageCollerctor对象,并可以写程序来获取垃圾收集器。

List gcList =
ManagementFactory.getGarbageCollectorMXBeans();if ( null != gcList) {for (int i = 0; i < gcList.size(); i++) {GarbageCollectorMXBean garbageCollectorMXBean = gcList.get(i);if (i == 0){System.out.println("young gc:" + garbageCollectorMXBean.getName());}else if (i == 1){System.out.println("old gc:" + garbageCollectorMXBean.getName());}}
}

(7)上面我们说过G1垃圾收集器最好适用于大内存,下面把G1垃圾收集器改为CMS垃圾收集器比较一下哪种能使系统性能提升一些,CMS参考配置如下。

-XX:+HeapDumpOnOutOfMemoryError #内存溢出时输出Heap Dump文件
-Xms1g #初始堆大小
-Xmx1g #最大堆大小,与Xms一致一步到位,减少堆扩充时的性能消耗
-XX:MaxMetaspaceSize=256m #元数据最大空间
-XX:+UseConcMarkSweepGC #使用CMS垃圾收集器
-XX:+UseParNewGC # 新生代并行回收
-XX:+CMSInitiatingOccupancyFraction=75 #老年代占用75%触发回收
-XX:+UseCMSInitiatingOccupancyOnly # 与上面配置组合
-XX:MaxGCPauseMillis=100 #垃圾收集最大暂停时间,到时间收集不完也放弃
-XX:G1ReservePercent=20 #预留堆大小防止对象(浮动垃圾)晋升时无空间

我们参考Elasticsearch的启动配置,Elasticsearch对性能提升很高。

## CMS垃圾回收器配置
8-13:-XX:+UseConcMarkSweepGC
8-13:-XX:CMSInitiatingOccupancyFraction=75
8-13:-XX:+UseCMSInitiatingOccupancyOnly
## G1和CMS垃圾回收器配置
# to use G1GC, uncomment the next two lines and update the version on the
# following three lines to your version of the JDK
# 8-13:-XX:-UseConcMarkSweepGC
# 8-13:-XX:-UseCMSInitiatingOccupancyOnly
14-:-XX:+UseG1GC
14-:-XX:G1ReservePercent=25
14-:-XX:InitiatingHeapOccupancyPercent=30 #堆占用多少开始触发标记,默认占用率
是整个Java堆的45%

上述G1的配置基本是按官方的建议来配置的:

(1)新生代大小。避免使用 -Xmn 选项或 -XX:NewRatio 等其他相关选项显式设置新生代大小。固定新生代的大小会覆盖垃圾收集暂停时间目标。

(2)垃圾收集暂停时间目标。每当对垃圾回收进行评估或调优时,都会涉及延迟与吞吐量的权衡。G1是增量垃圾收集器,暂停时间统一,同时应用程序线程的开销也更多。G1的垃圾收集暂停时间目标是90%的应用程序时间和10%的垃圾回收时间。Java HotSpot VM的垃圾收集暂停时间目标是99%的应用程序时间和1%的垃圾回收时间。

因此,当评估G1的吞吐量时,垃圾收集暂停时间目标设置不要太严苛。目标设置太过严苛表示用户愿意承受更多的垃圾回收开销,而这会直接影响到吞吐量。

(3)加上一些辅助配置,例如用G1ReservePercent、InitiatingHeapOccupancyPercent来帮助优化性能。

综合我们列举的参数配置,这些参数的设置也是有规律的。使用JDK 8时,如果堆内存小于6GB,选择CMS垃圾收集器;如果堆内存为6GB及以上,果断选择G1垃圾收集器;如果CPU不够强,例如不到4核,可以选择UseParallel垃圾收集器。至于堆到底多大合适,可以在性能压测时监控堆的使用情况,如果堆很快被占满,那么在排除程序问题时,就要考虑设置的堆太小问题,应对堆设置做适当调整,然后再验证效果。

如果用户不想自己配置参数,可以访问PerfMa官网去体验其Java虚拟机参数分析,帮用户自动生成相对靠谱的参数配置,下图所示是参数配置生成的页面。

其生成参数配置代码如下:

-Xmx10880M
-Xms10880M
-XX:MaxMetaspaceSize=512M #斜体是重要性能参数
-XX:MetaspaceSize=512M
-XX:+UseG1GC
-XX:MaxGCPauseMillis=100
-XX:+ParallelRefProcEnabled
-XX:+PrintGCDetails # 打印垃圾收集日志详情,用Print开头是为了方便性能分析
-XX:+PrintGCDateStamps
-XX:+HeapDumpOnOutOfMemoryError
-XX:+PrintClassHistogramBeforeFullGC
-XX:+PrintClassHistogramAfterFullGC
-XX:+PrintCommandLineFlags
-XX:+PrintGCApplicationConcurrentTime
-XX:+PrintGCApplicationStoppedTime
-XX:+PrintTenuringDistribution
-XX:+PrintHeapAtGC

JVM参数配置生成的页面:

另外,还有一个工具同样可以用来配置JVM参数。

通过jvmmemory.com配置JVM参数:

2、业务结构优化

1. 业务流程优化

准确地说,业务流程优化是业务架构调整。业务架构是整个系统好坏的关键,对此处做调整就是推翻先前的架构设计,风险比较大。架构调整伴随着程序修改、系统测试、性能测试,对于即将上线的项目是不能承受之重(就如同房子建好了,工人告诉我框架有问题,需要推倒重来)。如果要做调整,最好是在业务架构设计之初就考虑这些问题。

对于架构师来说,不仅要具备强大的业务分析能力,做出优秀的业务架构,还要能够结合程序实现做出一个高效的程序架构。这显然不是一件容易的事情,我们只有期望在架构设计阶段,不管是业务架构还是程序架构都能够重点考虑性能。而不要为了系统快速上线,不考虑性能,等系统的用户量上去后性能问题爆发,再调整、再重构。

2. 业务异步化

医院的医生看病时,会让病人先做一个检查,接着继续看下一个病人;等到检查结果出来后,病人再去找医生;医生暂停叫号,先看病人的检查报告,然后进行诊断。

以上就是一个时间统筹安排的过程,把这种方式引入系统开发就是异步通信。很多的中间件都在使用异步IO机制,像Tomcat、Jetty、Jboss等,受其启发,我们把异步也用到业务上,例如Netty就是一个高效的异步框架,能够有效地提高吞吐量。

我们应该或多或少用到过消息中间件,例如Kafka、RabbitMQ、JMS等,这些消息中间件能够很好地帮助我们把业务解耦,这个解耦过程抽象来看也是一个异步的过程。上游系统把请求传给下游,不用去等待响应,继续处理其他请求,这样本系统的吞吐量上去了;下游系统处理完后会返回结果,结果可以由上游系统定时来拿,也可由下游系统通知上游。通知的这种方式我们通常叫回
调。有兴趣的朋友可以了解一下Java的回调函数。

当业务链路较长、性能堪忧时,我们可以考虑拆分业务,把紧密业务解耦来提高效率。现在流行的微服务基本上运用这种思想。

3. 有效的数据冗余

我们先看一个订单的例子。我们在设计订单的数据结构时通常会采用主附表的方式,订单主表记录订单的概要信息,例如客户信息(ID、客户名、电话等),附表记录详细的商品名称、ID、订购数量、价钱等信息。

同时有这样一个功能用来显示订单列表(如图6-23所示),列表中包括客户信息、订单总额等信息;单击订单编号下拉列表显示订单详细信息。每位用户的订单额是从订单子表中计算出来的,这会导致显示订单列表时不仅要查询主表,还要对子表数据进行统计运算。

当数据量变大后,这个显示功能是有严重的性能风险的。我们理想的方式是,显示订单列表不要去查询订单子表,因此我们可以在下订单时把订单额算出来直接存到订单主表的总额字段,查询时直接从主表获取。如果订单修改,这个值重新更新。

上面我们举了一个简单的例子,在实际的系统设计过程中我们要心系性能。如果能够用简单的冗余解决的问题,就直接解决。冗余的实现方式有很多种,我们选择适合业务需求,同时能够保证一定性能的方案即可。 

3、系统架构优化

业务增长导致的性能问题推动着架构的发展,下面我们沿着系统架构的演变过程来分析系统性能与调优方式。

1. 单机结构

下图所示是常见的传统架构。Model V1模型中Web服务与App服务在一台服务器上(Web服务做页面渲染,App应用程序执行业务逻辑)。

随着业务的增长,单节点的Web服务或者App服务不堪重负,毕竟机器硬件提供的性能是有限的。在程序无法优化(基于成本)的情况下,最直接的办法就是增强机器性能,或者如Model V2模型,把Web服务与App服务拆分。

同样随着业务的快速增长会继续出现性能瓶颈,尤以DB的性能瓶颈最常见。例如DB承受的IO压力大,导致IO等待,从而影响客户体验。对于Web&App服务频繁读写文件也会导致IO瓶颈,例如日志(业务日志、访问日志等)写,实际上多数性能瓶颈最终都落到磁盘瓶颈上。

为了满足性能要求,通常我们会进行性能优化,当我们进行单系统性能调优后仍然无法满足性能要求(假设暂时没有有效的性能优化手段,或者已经优化到极致)时,我们就只有采用分而治之的办法,于是集群结构方案就产生了。 

2. 集群结构

如图所示,Model V3结构中Web&App服务都可以用多台机器来进行负载分担,DB的瓶颈也可以采用分区、分库、分表的方式来缓解(分库、分区、分表的宗旨是减小遍历范围,提高响应速度)。

另外,还可以采用读写分离的方式来减轻单台服务器的IO负担,相当于增加了机器的处理能力。读写分离比较适合以读操作为主的应用,可以减轻写服务器的压力,但是读服务器会有一定的延迟。当一些热点数据过多时,我们还可以对这些热点数据进行缓存(如上图所示的Model V4)。

对于负载均衡层,目前主要是在TCP\IP协议的四层与七层进行负载分发。

四层负载流行的有LVS(LVS集群采用IP负载均衡技术和基于内容请求分发技术,目前互联网公司大量使用此技术,如阿里、京东等)、F5(强大的商业交换机,好处是快,但就是贵)。

七层流行的有Tengine、Nginx、Haproxy、Vanish、ATS、Squid等。目前互联网企业多采用LVS+Tengine/Nginx的组合来实现负载均衡。

Model V3、Model V4的集群架构基本能够解决多数企业的性能问题,但缺点也比较明显。

多个Web服务器之间的用户请求状态(Session)需要同步(为保证高可用,如果其中一台宕机,另一台服务器能够正常处理用户请求,专业术语叫Session黏滞),这会消耗不少的CPU资源。

另外,数据库实现读写分离后,数据同步(数据一致性保证)成为一个性能问题,大量数据的同步IO会面临瓶颈。而且业务量大以后,数据的安全保障机制也受到挑战,备份问题凸显,因此也催生了分布式的发展。 

3. 分布式结构

系统分层、系统服务化(SOA架构、微服务化等)、服务分布式、DB分布式、缓存分布式及良好的水平扩展能力是当前分布式架构的典型特征。哪一个服务性能不佳,直接增加机器即可,性能与机器数量呈线性增长关系,从而解决前面架构遇到的问题。

下面先关注以下4个问题:

  1. 为什么要服务化?
  2. DB分布式的好处有哪些?
  3. 为什么要使用缓存,缓存哪些数据?
  4. 怎样具有良好的扩展性?
  • 为什么要服务化?

采用Model V4的架构基本能够解决多数企业的业务性能需求,但是如果换成BAT(百度、阿里巴巴、腾讯)这种大的互联网企业的系统,以它们的QPS(每秒请求数)来说,这样的架构支撑就比较勉强了。

首先,业务复杂度变高,导致程序实现难度增加,出错率也大大增加,不利于代码的维护与管理;大量业务融合在一个系统中导致耦合度太差,运营管理也比较麻烦;业务相互影响,其中一部分出问题时很可能导致另一部分也出问题。

要解决这些问题,我们自然会想到进行业务隔离,把系统中若干主要功能拆分成多个子项目,降低开发难度,更方便维护。使用不同的War包、不同的服务器进行发布,每个服务器完成特定的业务功能,这就是服务化。

其次,我们也会遇到一些较长的业务链路,往往性能问题是由于某一个功能的性能低下导致的,这就造成在运维及分析时都极不方便。如果把长的业务链路拆成多个子业务,在分析时就方便了。例如一个业务既有复杂计算又有简单操作,我们可以把复杂计算拆分出来部署在运算能力较强的服务器上,简单操作部署在普通服务器上。

业务拆分后将会面临着系统间的集成。如果业务链路上有一个服务比较耗时,而请求是阻塞式,那么我们要一直等待响应结果,这样的用户体验并不好,那么我们可以使用消息机制来解耦,上游系统请求发送到消息中间件,下游系统从消息中间件获取消息然后处理,解放上游系统,节省上游系统在等待时浪费的资源,从而利用这些资源来提高业务处理能力。

  • DB分布式的好处有哪些?

Modle V3/V4的结构最终都会面临庞大的数据存储及运算问题,存储在DB中的数据与类别越来越多,DB设计会变得异常复杂,管理将是一件十分头疼的事。DB主、从服务器的数据同步问题也是一个突出问题。总之,大数据会导致性能低下、维护成本高等问题;而且不要忘记业务关联复杂也会导致表的物理设计提升了难度等级,性能自然也会受到影响。

如果当表的数据够多时能够自动水平扩展(分片、分表),自动维护一个有序的索引,在查询时直接可以落到所在分片上进行遍历,而不用整张表遍历,那么效率将会提高不少。分布式DB就可以完成这个场景,数据存放时经过Hash算法存到指定片区,这个片区可能是某一台机器上的某一个库的某一个表分区,查询时直接Hash一下就可以得到数据所在机器、所在的库、所在的表分区。

现在许多公司都基于Mariadb来开发自己的分布式数据库。在Mariadb的上层建立一个数据访问层,通过Hash算法对数据进行分片,均匀打散数据的存储,查询时多任务执行来提高效率。阿里2019年“双11”期间成交2 684多亿订单,坊间及官方都流传出它们支持异地多活能力。异地多活的首要难题就是数据的同步速率及数据访问速率。

有兴趣的朋友可以了解一下阿里开源的OceanBase。OceanBase是阳振坤博士领导的一个开源的分布式数据库项目。

  • 为什么要使用缓存,缓存哪些数据?

我们知道,从磁盘读取数据相比从内存中读取数据慢很多,所以在实际业务中大量使用缓存,一般缓存的数据以读居多。以Linux为例,我们用vmstat 命令可以看到buff、cache的监控信息。

buff对块设备的读写进行缓冲来缓解CPU与块设备的速度差,因此CPU非空闲等待时间会更少。

cache给文件做缓冲,直接把内容放在内存,因此CPU访问时更快,减少CPU的IO等待。

例如客户信息、产品信息等,我们在应用系统中可以缓存到内存,不用每次都从DB中查询。用过hibernate的应该知道其支持二级缓存。对于缓存产品,目前流行的、成熟的有Redis、memcache等。一些秒杀场景直接使用Redis作为数据持久化介质。

另外,缓存也用来保存用户请求状态,Web服务器之间再也不用同步用户Session状态。

  • 怎样具有良好的扩展性?

如果一个项目能够很方便地进行部署,例如直接增加一台机器并放上War包,启动中间件即可以加入集群提供服务,服务“挂掉”后能够从集群节点上自动删除,这无疑具备了良好的可扩展性。

Dubbo就是这样一个高效的分布式服务框架,使用Dubbo框架开发的应用可以通过注册中心(Zookeeper)注册服务。

用户请求通过注册中心查找到服务,然后发送请求到目的服务器。用户不用关心是哪台服务器在处理。注册中心能够感知服务是否存活(服务器是否可以提供服务),服务“挂掉”就从注册中心抹掉。

使用Dubbo的系统要具备良好的可扩展性,系统需要服务化,业务处理需要无状态。什么叫无状态呢?例如请求被接收后,在任何一台提供相同服务的服务器上处理后的结果都是一致的,不会依赖于请求的某种状态(必须在某一台机器上处理)而产生不同的结果。例如我们在任何一台计算机上用计算器计算2+2都等于4。

下图所示为当前流行的分布式架构(简化后的,实际项目中更复杂),接下来对此架构进行简单说明。

(1)DNS&CDN静态加速

DNS:智能DNS,用户请求进入后,域名解析服务器智能判断用户请求的线路。如果是电信用户,就解析到电信IP,联通用户就解析到联通IP。

CDN:用户访问Web页面时往往会有很多静态资源(图片、样式、JS等),而这些资源都是比较耗时的。我们希望把这些资源放得离用户更近一点,响应用户就更快一点,这是CDN的职责。CDN就是多台静态资源服务器加智能DNS的结合体,CDN服务其实就是把静态页面缓存到不同地区很多台专门的缓存服务器上,然后根据用户线路所在的地区通过CND服务商的智能DNS自动选择一个最近的缓存服务器让用户访问,以此提高速度。这种方案对静态页面效果非常好,同时它也需要智能DNS的帮助才能把用户引导到离自己最近的缓存服务器上。

(2)负载均衡器

负载均衡器的作用是把用户请求按一定规则分发到不同的服务器进行处理。在使用负载均衡集群时,分发负载对性能的要求极高。流行的产品有LVS、Tengine、Nginx、Apache、F5等。

LVS:LVS集群采用IP负载均衡技术和基于内容请求分发技术,也就是能够在TCP/IP层的第四层进行请求分发。LVS调度器具有很好的吞吐率,将请求均衡地转移到不同的服务器上执行,且调度器自动屏蔽掉服务器的故障,从而将一组服务器构成一个高性能的、高可用的虚拟服务器。整个服务器集群的结构对客户透明,无须修改客户端和服务器端的程序。

为此,在设计时需要考虑系统的透明性、可伸缩性、高可用性和易维护性。关键一点是LVS开源而且效率高,相比商业负载工具F5,赢在免费,而且效率达到F5的60%。

Tengine:Tengine是一个强大的高性能反向代理服务器。Tengine是由淘宝网发起的Web服务器项目,它在Nginx的基础上针对大访问量网站的需求,添加了很多高级功能和特性。

目前很多公司采用LVS+Tengine/Nginx的负载架构来构建自己的负载均衡部分。

(3)Web服务分布式集群

Web:Web服务层,按照MVC的设计理念,Web服务层主要进行页面渲染、Session保持等工作。这些应用部署在Tomcat、Jetty、Jboss容器上。

上图所示为一个典型的分布式Web结构(已经简化)。Client请求通过前端负载均衡器(如LVS+Tengine)分发到Web层,Web层通过ZK(Zookeeper)注册中心找到提供业务处理(App层中的某一个节点)的节点。Web层请求传送到App层的路由器负载算法(用程序实现的负载路由)来实现,通常叫软路由,它能够把请求按一定规则分发到App层的各节点上。Dubbo框架中就内置了这样的软路由。

对于Web层来说,请求会话状态(用Session来代替)的保持是一个问题,Session同步是一个容易引起性能问题的地方。在分布式框架中一般会把Session信息独立出来放到缓存设备中,例如用Redis来存储Session信息。当然,以亿来计的Session信息如果保存在一台或者少量几台Redis中也会造成风险,首先是需要一个大的内存来存储数据,另外要考虑数据安全,当服务器“挂掉”后数据如何恢复?想想一个200GB的Redis数据集想恢复得花多长时间。本着风险分散原则,还是拆分成多个Redis节点保险,所以Redis分布式集群也变得很有必要。不少互联网公司会在Redis之上加上一个中间层,来构建分布式缓存服务。

注意:

Zookeeper:开放源码的分布式应用程序协调服务,是Hadoop和HBase的重要组件,为分布式应用提供一致性服务,例如配置维护、域名服务、分布式同步、组服务等。

(4)App服务分布式集群

App:应用服务层,实现主要的业务逻辑。应用服务不仅在单机上要具备更优的性能,而且在结构上要易于水平扩展,功能服务化且服务无状态。

例如我们网购,选择商品准备结算时,如果没有登录会跳到登录框,提交登录请求会调用会员系统进行身份验证,这是一个服务;会员系统调用账务系统查询余额是另一个服务。这些服务部署多个服务器,任意一台处理请求返回结果都一样(幂等性),这样就具备良好的水平扩展能力。当遇到某一类服务性能吃紧时,直接增加机器就可以了。

Dubbo就是经过实践验证的使用广泛的分布式服务框架,具备良好的水平扩展能力,每天为2 000+个服务器提供3 000 000 000+次访问量支持。实际上很多互联网企业都做到了水平自动扩展,有兴趣的朋友可以了解一下Docker,基于Docker来进行服务水平扩展是一个不错的选择。

(5)分布式缓存

Cache:缓存数据到内存,解决热点数据问题。例如Redis、Memcache等缓存产品。

在内存中存储数据时,不可忽视的问题是数据的安全性与存储量,当前解决数据安全性的方法主要是数据持久化与数据冗余(主从缓存服务器结构,为了性能会进行读写分离)。解决存储量的问题主要是分而治之,进行分布式存储,每一个存储节点称为分片,例如100GB的数据,我们分5个片区来存储,每个分片就是20GB。

下图所示为常见的分布式缓存架构,Cache 1与Cache n构成分布式缓存集群。以Redis为例(假设Cache 1由Redis担当),Cache 1是一个分片(物理节点),Cache n是第n个分片(物理节点),Redis以(Key,Value)结构存储数据(有关Redis的知识请自行查阅相关资料)。

Web/App服务先从Zookeeper中心取得缓存服务器访问地址(如Cache 1地址),然后向缓存服务器发起请求(读、写、修改)。缓存服务器由Zookeeper来提供一致性服务,这样很方便对缓存服务器数据进行冗余(读写分离),保证数据安全,提高访问效率。当缓存数据过多时,可以水平扩展来提高服务能力。

问题随之而来:

如何把数据有序存放到各分片呢?

如何访问这些分片呢?怎么知道我要的数据在这个分片上?

Hash算法将数据映射到具体的节点上,如key%n(key就是数据,n是机器节点数)。这种简单的Hash算法有个问题,如果一个机器加入或退出这个集群,则所有的数据映射都无效了。当然我们可以持久化数据,失效后重新载入,但这是要花时间的,Hash一致性算法可以用来解决这个问题。

1)Hash算法将机器映射到环中,如下图,node1、node2、node3、node4。 

Hash一致性算:

2)Hash(Key1)=k1,Key1经过Hash后值为k1;k1沿着顺时针方向找到离它最近的节点node2,k1即存入node2,同理k2存入node2,k3存入node3。

3)当node2节点“挂掉”或者删除掉,k1、k2则存入node3节点(顺时针方向找最近的节点),这时node1、node4不受影响,如图所示。 

4)当增加一个节点时(数据量太多,加节点分担),k3就迁移到node5节点,如下图,其他Key保持原有存储位置。一致性Hash算法在保持了单调性的同时还使数据的迁移更小,完成速度更快。 

5)如果node2、node3都“挂掉”时,存在node2、node3上的key都要迁移到node4,node4上存储的数据会激增,相比node1来说要多很多,这样node1、node4就不平衡了,性能也会有差异。在实际应用中,node4的风险会很大。

在此背景下就产生了虚拟节点这个方案,如下图。

Node 1-1、Node 1-2实际对应物理节点Node1,Node 2-1、Node 2-2实际对应物理节点Node2,Node 3-1、Node 3-2实际对应物理节点Node3,Node 4-1、Node 4-2实际对应物理节点Node4;

其中一个物理节点删除或者出现故障,其影响被分散,整体性能影响会减小。有的朋友可能会问,这样看来物理节点越多,虚拟节点越多,影响应该越小?是的,理论上是这样,但是物理节点的增多也增加了管理负担,所以还要综合考虑。 

以上是分布式缓存的实现原理,基于Redis、Zookeeper来开发的分布式缓存架构的服务能力在普通PC(双核,16GB内存)上轻松可达2万TPS(每秒请求数)。当然,在实际运用中不只是这么简单,相应地要开发监控管理平台,自适应功能(自动收缩),数据更新持久化策略、安全策略等功能。

分布式缓存不仅解决了热点数据问题,有些企业直接用其作为数据持久化介质,如秒杀。分布式缓存在整个分布式架构中是重要的组成部分。

(6)分布式数据库

随着数据的激增,传统集中式的数据库结构为提供良好的用户体验,成本也越来越高。对于海量数据,基本上用分区、分表、读写分离这些手段。海量数据的访问使得对CPU、内存、磁盘的要求更高,最后依然是无法突破瓶颈。我们并不能生产出更强的服务器(暂时办不到,也没必要),就像我们搬不动一堆东西时,我们可以分开搬,也可以几个人一起搬。所以我们可以分而治之,用普通的PC来做高端服务器的工作。

分布式数据库是一种趋势,用廉价的普通PC设备堆叠出具备高可用性、高扩展性的服务集群,正如去IOE化,摆脱对大型设备的依赖,减少运营成本,提高服务能力。

类似上一节中的分布式缓存,在存储数据时通过Hash算法把数据均匀分散到各数据库节点,如下图所示,数据在DB Access层经过Hash处理,找到相应的DB存储节点(DB1、DB2、DBn中的一个节点)。

DB Access层的服务又是可以水平扩展的,也就不用担心它的性能了。

DB Access层除了在持久时帮助找到存储节点,还要完成SQL的解析。例如一张客户表有5亿条记录,被持久化到10个节点,上游系统在查询时显然不能够启用10个连接,每个节点去查询(这样开发难度加大,一旦节点数增加或者减少,程序都得修改,这个设计显然低效)。

DB Access层要让上游系统像使用一个库、一张表那样方便,所以在这一层就需要实现SQL的解析功能,收到上游系统SQL语句解析并分发给多个节点进行,最后合并结果返回给上游系统。

ZK(Zookeeper)集群用来管理DB Access层的服务,与分布式缓存中的ZK作用一样,上游系统通过ZK找到可以访问的DB Access服务节点。

实际运用过程中,分布式数据库远比讲述得复杂。例如每个节点还要实现冗余功能,读写分离;热点数据缓存功能;谁也不能保证不出现问题,必须要有问题跟踪机制;运营需要运维,运维功能不可或缺。

目前使用广泛的分布式持久化工具有HDFS、HBase、Mariadb等。

HDFS取自Hadoop中的分布式文件存储;HBase也是Hadoop下的一个子项目,是一个适合于非结构化数据存储的数据库;Mariadb是MySQL的开源版本。有兴趣的朋友可以关注一下Sharding-JDBC。Sharding-JDBC是应用框架ddframe中的组成部分,从关系型数据库模块dd-rdb中分离出来的数据库水平分片框架,实现透明化数据库分库分表访问。

建议大家了解这些产品,扩展知识面,不断学习新知识是对IT从业人员的基本要求。

注意:

  • Dubbo:阿里巴巴出品的分布式服务框架,众多互联网公司在使用。
  • Memcache:支持分布式的缓存产品,实际可以当数据库用。
  • Redis:支持分布式的缓存产品,实际可以当数据库用,众多秒杀系统中经常用到。
  • Mariadb:开源数据库产品。

在分析调优过程中,我们提到了LVS、Tengine、Tomcat、Jetty、Jboss、HDFS、Mariadb、HBase等开源的项目,开源是主流趋势。

建议志在性能调优方面发展的朋友,多学习一些开源项目开拓一下思路,多数企业也是基于这些开源的项目来建立自己的分布式架构的。

五、Web中间件性能分析与调优实践

在国内互联网公司中,Web中间件用得最多的就是Apache和Nginx这两款了。很多大型电商网站,比如淘宝、京东、苏宁易购等,都在使用Nginx或者Apache作为Web中间件。

而且很多编程语言在进行Web开发时,会将Apache或者Nginx作为其绑定的固定组件,比如用PHP语言进行Web开发时,就经常和Apache联系在一起,使得Apache成为了PHP在Web开发时的一个标配。而Nginx不管是在作为Web静态资源访问管理,或者作为动态的请求代理,性能都是非常高效的。当然Nginx或者Apache有时候也会存在性能瓶颈,需要进行性能分析和调优以支持更高的并发处理能力。

1、Nginx 性能分析与调优实践

1. Nginx负载均衡策略的介绍与调优

在一般情况下,Web中间件最大的作用就是负责对请求进行分发,也就是我们常说的起到负载均衡的作用。当然负载均衡只是Nginx的作用之一,Nginx常见的负载均衡策略一般包括轮询、指定权重
(weight)、ip_hash、least_conn、fair、url_hash等六种。其中默认执行的策略为轮询,fair和url_hash属于第三方策略,这两种策略不是Nginx自带支持的策略,需要安装第三方的插件来辅助支持。

在不同的场景下,每一种策略的选择对系统的整体性能影响都非常大,一般建议根据实际场景和服务器配置来选择对应的负载均衡策略。

  • 轮询策略:Nginx的负载均衡通过配置upstream来实现请求转发。如果在upstream中没有指定其他任何的策略时,Nginx会自动执行轮询转发策略,upstream中配置每台服务器的权重都一样,会按照顺序依次转发。如下所示就是一个简单的upstream配置,由于配置了192.168.1.14和192.168.1.15两台服务器,所以请求会按照接收到的顺序,依次轮询地转发给192.168.1.14和192.168.1.15两台服务器进行执行。Nginx能自动感知需要转发到的后端服务器是否挂掉,如果挂掉,Nginx会自动将那台挂掉的服务器从upstream中剔除。
upstream applicationServer {server 192.168.1.14;server 192.168.1.15;
}

使用轮询策略时,其他非必填的辅助参数如下表所示。

  • 指定权重(weight):通过在upstream配置中给相应的服务器指定weight权重参数来实现按照权重分发请求。weight参数值的大小和请求转发比率成正比,该配置一般用于后端应用程序服务器硬件配置差异大而导致承受的访问压力不一样的情况下。

配置示例如下: 

upstream applicationServer {server 192.168.1.14 weight=8;server 192.168.1.15 weight=10;
}
  • ip_hash:每个请求按原始访问ip的hash结果来进行请求转发。由于同一个ip的hash值肯定是不变的,这样每个固定客户端就会只访问一个后端应用程序服务器。此种配置一般可以用来解决多个应用程序服务器的session复制和同步的问题,因为同一个ip的请求都转发到了同一台服务器的应用程序上了,所以也就不会有session不同步的问题了。但是这可能会导致后端应用服务器的负载不均的情况,因为在这种策略下后端应用服务器收到的请求数肯定是很难一样多。

示例配置如下:

upstream applicationServer {
ip_hash;server 192.168.1.14;server 192.168.1.15;
}
  • least_conn:通过在upstream配置中增加least_conn配置后,Nginx在接收到请求后会把请求转发给连接数较少的后端应用程序服务器。前面讲到的轮询算法是把请求平均地转发给各个后端,使它们的负载大致相同,但是有些请求占用的时间很长,会导致其所在的后端负载较高。这种情况下,least_conn这种方式就可以达到更好的负载均衡效果。

示例配置如下:

upstream applicationServer {
least_conn;server 192.168.1.14;server 192.168.1.15;
}
  • fair:fair属于第三方策略,即不是Nginx本身自带的策略,需要安装对应的第三方插件。fair是按照服务器端的响应时间来分配请求给后端应用程序服务器,响应时间短的优先分配。

示例配置如下:

upstream applicationServer {server 192.168.1.14;server 192.168.1.15;fair;
}
  • url_hash:url_hash同样属于第三方策略,也是需要安装对应的第三方插件。url_hash是按照访问的目标url的hash值来分配请求,使同一个url的请求转发到同一个后端应用程序服务器,请求的分发策略和ip_hash有点类似。在进行性能调优时,主要是适用对缓存命中进行调优,同一个资源(也就是同一个目标url地址)多次请求,可能会到达不同的后端应用程序服务器上,会导致不必要的多次下载。使用url_hash后,可以使得同一个目标url(也就是同一个资源请求)会到达同一台后端应用程序服务器,这样可以在服务端进行资源缓存,再次收到请求后,就可以直接从缓存中读取了。

示例配置如下:

upstream applicationServer {server 192.168.1.14;server 192.168.1.15;hash $request_uri;
}

2. Nginx进程数的配置调优

Nginx服务启动后会包括两个重要的进程:

  • master进程:可以控制Nginx服务的启动、停止、重启、配置文件的重新加载。
  • worker进程:处理用户请求信息,将收到的用户请求转发到后端应用服务器上。

worker进程的个数可以在配置文件nginx.conf中进行配置,如下所示:

worker_processes 1; # Nginx配置文件中worker_processes指令后面的数值代表了Nginx
启动后worker进程的个数。

worker进程的数量一般建议等于CPU的核数或者CPU核数的两倍。

通过执行lscpu命令可以获取到CPU的核数,如下图所示。

或者通过执行grep processor /proc/cpuinfo|wc -l 命令也可以直接获取到CPU的核数。 

[root@localhost conf]#grep processor /proc/cpuinfo|wc -l

在配置完worker进程的数量后,还建议将每一个worker进程绑定到不同的CPU核上,这样可以避免出现CPU的争抢。将worker进程绑定到不同的CPU核时,可以通过在nginx.conf中增加worker_cpu_affinity配置,例如将worker进程分配到4核的CPU上,可以按照如下配置进行配置。

worker_processes 4;
worker_cpu_affinity 0001 0010 0100 1000;

3. Nginx事件处理模型的分析与调优

为了性能得到最优处理,Nginx的连接处理机制在不同的操作系统中一般会采用不同的I/O事件模型。在Linux操作系统中,一般使用epoll的I/O多路复用模型;在FreeBSD操作系统中,使用kqueue的I/O多路复用模型;在Solaris操作系统中,使用/dev/pool方式的I/O多路复用模型;在Windows操作系统中,使用的icop模型。

在实际使用Nginx时,我们也是需要根据不同的操作系统来选择事件处理模型,很多事件模型都只能在对应的操作系统上得到支持。比如我们在Linux操作系统中,可以使用如下配置来使用epoll事件处理模型。

events {
worker_connections 1024;
use epoll;
}

关于I/O多路复用做个说明:在Nginx中可以配置让一个进程处理多个I/O事件和多个调用请求,这种处理方式就像Redis中的单线程处理模式一样。

Redis缓存读写处理时采用的虽然是单线程,但是性能和效率却是非常的高,这就是因为Redis采用了异步非阻塞I/O多路复用的策略,导致资源的开销很小,不需要重复去创建和释放资源,而是共用一个处理线程。

Nginx中也同样采用异步非阻塞I/O策略,每个worker进程会同时启动一个固定的线程,以利用epoll监听各种需要处理的事件,当有事件需要处理时,会将事件注册到epoll模型中进行处理。异步非阻塞I/O策略在处理时,线程可以不用因为某个I/O的处理耗时很长而一直导致线程阻塞等待,线程可以不用也不必等待响应,而可以继续处理其他的I/O事件。

当I/O事件处理完成后,操作系统内核会通知I/O事件已经处理完成,这时线程才会去获取处理好的结果。

下表列出了Nginx常用事件处理模型的详细介绍:

4. Nginx客户端连接数的调优

在高并发的请求调用中,连接数有时候很容易成为性能的一个瓶颈。

Nginx可以通过如下方式来调整Nginx的连接数:

  • 配置Nginx单个进程允许的客户端最大连接数:可以修改Nginx中的nginx.conf配置文件中的配置如下: 
events #可以设置Nginx的工作模式以及连接数上限
{worker_connections 1024;
}
  • 配置Nginx worker进程可以打开的最大文件数:可以修改Nginx中的nginx.conf配置文件中的配置如下:
worker_processes 2;
worker_rlimit_nofile 2048; # 设置worker进程可以打开的文件数

Linux内核的优化:在Linux操作系统的/etc/sysctl.conf配置文件中,可以重新配置很多Linux系统的内核参数。

5. Nginx中文件传输的性能优化

Nginx中文件传输一般需要优化的是如下表所示的几个参数。

在nginx.conf配置文件中开启sendfile参数的方式配置示例如下: 

sendfile on ;#默认情况下sendfile是off

 在nginx.conf配置文件中开启tcp_nopush参数的方式配置示例如下:

tcp_nopush on ;#默认情况下tcp_nopush是off

在nginx.conf配置文件中关闭tcp_nodelay参数的方式配置示例如下:

tcp_nodelay off;#默认情况下tcp_nodelay是on

6. Nginx中FastCGI配置的分析与调优

FastCGI是在CGI基础上的优化升级。CGI是Web服务器与CGI程序间传输数据的一种标准,运行在服务器上的CGI程序按照这个协议标准提供了传输接口,具体介绍如下。

  • CGI:CGI是英文Common Gateway Interface的简写,翻译过来就是通用网关接口,这套接口描述了Web服务器与同一台计算机上的软件的通信方式。有了CGI标准后,集成了CGI的Web服务器就可以通过CGI接口调用服务器上各种动态语言实现的程序了,这些程序只要通过CGI标准提供对应的调用接口即可。

CGI的处理的一般流程如图所示:

  • FastCGI:FastCGI是一个传输快速可伸缩的、用于HTTP服务器和动态脚本语言间通信的接口,它为所有Internet应用程序提供了高性能,而不受Web服务器API的限制。包括Apache、Nginx在内的大多数Web服务都支持FastCGI,同时FastCGI也被许多脚本语言(例如Python、PHP等)所支持。

Nginx本身并不支持对外部动态程序的直接调用或者解析,所有的外部编程语言编写的程序(比如Python、PHP)必须通过FastCGI接口才能调用。

FastCGI相关参数说明如下表所示:

7. Nginx的性能监控

Nginx自带了监控模块,但是需要在Nginx编译安装时指定安装监控模块。默认情况下是不会安装该监控模块的,需要指定的编译参数为--with-http_stub_status_module。

编译安装完成后,Nginx的配置文件nginx.conf中还是不会开启监控,需要在配置文件中增加如下配置,其中allow 192.168.1.102代表允许访问监控页面的IP地址,如图所示。

location = /nginx_status {stub_status on;access_log off;allow 192.168.1.102;deny all;
}

修改完配置文件后,通过执行nginx-s reload来重新加载配置信息,然后通过访问http://nginx服务器IP地址:端口号/nginx_status就可以进入监控页面了,如图所示。

从图中可以看到当前已经建立的连接数、服务器已经接收的请求数、请求的处理情况等监控信息。

2、Apache 性能分析与调优实践

在Web中间件中,除了Nginx外,另一个用得最多的中间件就是Apache。Apache几乎可以运行在所有的操作系统中,支持HTTP、SSL、Socket、FastCGI、SSO、负载均衡、服务器代理等众多功能模块。在性能测试分析中发现,如果Apache使用不当,那么Apache有时候也可能会成为高并发访问的瓶颈。

1. Apache的工作模式选择和进程数调优

Apache的工作模式主要是指Apache在运行时内存分配、CPU、进程以及线程的使用管理和请求任务的调度等。Apache比较稳定的工作模式有prefork模式、worker模式、event模式,这三种模式也是Apache经常使用的模式。Apache默认使用的是prefork模式,一般可以在编译安装Apache时通过参数--with-mpm来指定安装后使用的工作模式。

可以通过执行httpd -V命令来查看Apache当前使用的工作模式,如下图所示,可以看到当前的工作模式为默认的prefork模式。

1) prefork模式

prefork是Apache的默认工作模式,采用非线程型的预派生方式来处理请求。在工作时使用多进程,每个进程在同一个固定的时间只单独处理一个连接,这种方式效率高,但由于是多进程的方式,所以内存使用比较大。

如图所示,可以看到prefork模式下启动了多个进程。

prefork工作模式在收到请求后的处理过程如图所示:

从图中可以看到处理过程是单进程和单线程的方式,由于不存在线程安全问题,因此这种模式非常适合于没有线程安全库而需要避免线程安全性问题的系统。虽然它解决了线程安全问题,但是也必然会导致无法处理高并发请求的场景,prefork模式会将请求放进队列中,一直等到有可用子进程请求才会被处理,也很容易导致请求队列积压。

prefork工作模式主要的配置参数如下表所示。

StartServers 8MinSpareServers 8MaxSpareServers 10MaxRequestWorkers 512MaxConnectionsPerChild 1000

2)worker模式

worker模式使用了多进程和多线程相结合的混合模式来处理请求,如图所示,work模式下也是主进程会首先派生出一批子进程。

但和prefork模式不同的是,work模式下每个子进程会创建多个线程,每个请求会分配给一个不同的线程处理。work模式中处理请求时,由于采用了多线程的处理方式,所以高并发下处理能力会更强,但是由于是多线程处理方式,所以这种模式下需要考虑线程安全问题。

worker工作模式主要的配置参数如下表所示:

StartServers 4ServerLimit 20MinSpareThreads 65MaxSpareThreads 256ThreadsPerChild 30MaxRequestWorkers 410MaxConnectionsPerChild 1200

3)event模式

event模式和worker工作模式有点类似。在event工作模式中,会有一些专门的线程来承担管理和分配线程的工作,通过这种方法解决了HTTP请求keep-alive长连接的时候占用线程资源被浪费的问题。因为会有一些专门的线程用来管理这些keep-alive类型的工作线程,当有真实请求过来时,将请求传递给服务器端可用的工作线程进行处理,处理完毕后又允许其释放资源,如图所示。 

event工作模式主要的配置参数如下: 

StartServers 3ServerLimit 16MinSpareThreads 75MaxSpareThreads 250ThreadsPerChild 25MaxRequestWorkers 400MaxConnectionsPerChild 1000

event工作模式的配置参数几乎与worker模式是一样的,因为event模式本身就是对worker模式的一种升级改进。

2. Apache的mod选择与调优

Apache中和性能调优相关的常见模块如下表所示:

Apache缓存的设置:Apache涉及的缓存模块有mod_cache、mod_disk_cache、mod_file_cache、mod_mem_cache,如果要使用缓存,必须启用这四个缓存模块。Apache缓存分为硬盘缓存mod_disk_cache和内存缓存mod_mem_cache,这两个缓存都依赖于mod_cache。基于硬盘缓存和物理内存缓存的缓存配置如下表所示。

示例配置如下: 

3. Apache的KeepAlive调优

HTTP请求中开启KeepAlive选项相当于长连接的作用,多次HTTP请求共用一个TCP连接来完成,这样可以节约网络和系统资源。

一般开启KeepAlive适用于如下场景: 

  • 如果有较多的静态资源(例如JS、CSS、图片等)需要访问,则建议开启长连接。
  • 如果并发请求非常大,频繁地出现连接建立和连接关闭,则建议开启长连接。
  • 如果出现这种情况可以考虑关闭KeepAlive选项:服务器内存较少并且存在大量的动态请求或者文件访问,则建议关闭长连接以节省系统内存和提高Apache访问的稳定性。

在Apache中开启keepAlive选项的方式是通过vi httpd.conf来增加或者修改httpd.conf中的配置。

KeepAlive选项如下表所示:

KeepAlive On
MaxKeepAliveRequests 100
KeepAliveTimeout 15

4. Apache的ab压力测试工具

Apache中自带了性能压测工具ab,一些比较简单的压力测试请求可以直接使用Apache自带的性能压测工具ab来完成。ab使用起来非常简单方便,直接可以通过命令行执行ab命令以及在命令后面加上对应的参数即可开启性能压测。

ab支持的常用参数如下:

  • -n requests:表示总计发送多少请求。
  • -c concurrency:代表客户端请求的并发连接的数量。
  • -k:开启Http KeepAlive。
  • -s timeout:设置响应的超时时间,默认为30秒。
  • -b windowsize:设置TCP请求发送和接收的缓冲区大小,单位为字节。
  • -f protocol:指定SSL/TLS协议(支持SSL3、TLS1、TLS1.1、TLS1.2或者all)。
  • -g filename:输出收集到的压测数据到gnuplot格式的文件中,gnuplot是一个命令行的交互式绘图工具。
  • -e filename:输出收集到的压测数据到csv格式的文件中。
  • -r:表示在socket接收到错误时,ab压测不退出。
  • -X proxy:port:设置压测请求地址的代理服务器地址。

示例:ab -n 10000 -c 60 -k http://127.0.0.1:80/表示总共发送10000次压测请求,并发连接数为60,并且在压测时客户端开启KeepAlive。

[root@localhost conf]# ab -n 10000 -c 60 -k http://127.0.0.1:80/
This is ApacheBench, Version 2.3 <$Revision: 1430300 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
Benchmarking 127.0.0.1 (be patient)
Completed 1000 requests
Completed 2000 requests
Completed 3000 requests
Completed 4000 requests
Completed 5000 requests
Completed 6000 requests
Completed 7000 requests
Completed 8000 requests
Completed 9000 requests
Completed 10000 requests
Finished 10000 requests
Server Software: Apache/2.4.6
Server Hostname: 127.0.0.1
Server Port: 80
Document Path: /
Document Length: 4897 bytes
Concurrency Level: 60
Time taken for tests: 4.014 seconds
Complete requests: 10000
Failed requests: 0
Write errors: 0
Non-2xx responses: 10000
Keep-Alive requests: 9916
Total transferred: 52046232 bytes
HTML transferred: 48970000 bytes
Requests per second: 2491.21 [#/sec] (mean)
Time per request: 24.085 [ms] (mean)
Time per request: 0.401 [ms] (mean, across all concurrent requests)
Transfer rate: 12661.90 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.5 0 8
Processing: 0 11 65.9 8 2474
Waiting: 0 11 65.9 7 2474
Total: 0 11 66.0 8 2474
Percentage of the requests served within a certain time (ms)
50% 8
66% 8
75% 8
80% 9
90% 11
95% 12
98% 15
99% 34
100% 2474 (longest request)

5. Apache的性能监控

Apache自身自带了状态监控页面,但是默认是关闭的,可以通过在httpd.conf中增加如下配置来打开监控页面。

SetHandler server-statusOrder Deny,AllowAllow from all

增加上述配置后,然后就可以通过访问http://ip:port/server‐status来查看监控页面了,如图所示。

 从上图中可以看到如下数据:

(1)Total accesses:对Apache的访问量。
(2)Total Traffic:Apache的访问流量,单位为KB。
(3)CPU Usage:CPU的使用情况。
(4)每秒收到的请求的统计信息:0.0513 requests/sec - 105B/second - 2048 B/request表示平均每秒收到0.0513个请求,平均每秒收到的请求流量为105字节,每个请求的平均大小为2048字节。
(5)请求的处理情况: 1 requests currently being processed,8 idleworkers表示目前1个请求正在被处理(状态为w,表示处理发送回复状态),目前有8个线程处于空闲状态。
(6)目前运行的Apache进程的资源使用情况以及客户端的连接情况,如图所示。

六、应用中间件性能分析与调优实践

中间件除了Web中间件外最重要的就是应用中间件。Web中间件一般主要负责静态资源(也可以称作静态请求)的处理和动态请求的转发,而动态请求一般都是由应用中间件来进行处理的。平时我们最常用的应用中间件就是Tomcat和WildFly,一般中小型公司都将Tomcat作为了自己的标准应用中间件容器。

应用中间件作为操作系统和应用程序之间的桥梁,为处于其中的应用程序组件提供一种运行环境。由于应用中间件处理的是动态请求,所以其性能的好坏往往会直接影响系统的最终处理能力。 

1、Tomcat 性能分析与调优实践

1. Tomcat的组件以及工作原理

Tomcat的运行需要JDK(Java Development Kit,Java语言开发工具包)的支撑,它是运行在JVM(Java Virtual Machine,Java虚拟机)上的一个进程,是Java应用程序和JVM之间的中间容器。

Tomcat的相关核心组件如图所示:

  • Server:代表的是整个Tomcat应用服务容器中间件,里面可以包含多组子服务,Server负责管理和启动里面配置的每组子服务。Server的核心配置定义在Tomcat中conf目录下的server.xml配置文件中,这个配置文件中包含了Service、Connector、Container、Engine、Hosts等主组件的相关配置信息,如下所示。


……

在Tomcat启动时会启动一个监听在8005端口,以接收shutdown命令的Server实例(实际就是一个JVM进程)。使用telnet命令可以连接到启动后的8005端口上,直接执行SHUTDOWN命令来关闭Tomcat Server实例。

  • Service:Tomcat Server封装对外可以提供完整的基于组件的Web应用服务,包含了Connector和Container两大核心组件,Container中又包含了多个子功能组件,每个Service之间是相对独立互不干扰,但是依附于同一个Server进程,并且共享同一个Server实例的JVM资源,在server.xml中的配置如下:

  • Connector:Tomcat Server对外提供Web应用服务的连接器,可以通过监听指定端口,接收外部调用者发给Tomcat Server的请求,传递给Container,并将Container处理的响应结果返回给外部调用者,在server.xml中的配置如下。在Connector中一般会配置对应的端口以及连接器处理的协议,比如HTTP、AJP等,也可以配置连接器的超时时间等。 

Container:Tomcat Server的核心容器,其内部由多层组件组成,它用于管理部署到容器中服务器端程序的整个生命周期(这些服务器端程序一般是用Java语言编写的),调用服务端程序相关方法来完成请求的处理。Container由Engine、Host等组件共同组成,如图所示。

  • Engine:Tomcat Server服务器端程序的处理引擎,Engine需要定义一个defaultHost属性,以作为默认的Host组件来处理没有明确指定虚拟主机的请求。一个Engine中可以包含多个Host,但是必须有一个Host的名称和defaultHost属性中指定的名称一致。
  • Host:位于Engine引擎中用于接收请求并进行相应处理的主机或虚拟主机,负责Web应用的部署和Context的创建。

Host的属性如表所示:

 Container在server.xml中的配置如下所示:

其中每个Host中可以有多个Context配置,Context指的是Web应用程序的上下文,每个上下文会包括多个Wrapper,Context负责Web应用程序配置的解析和管理所有的Web应用资源,而Wrapper是对Web应用程序的封装,负责应用程序实例的创建、执行、销毁等生命周期的管理。

Context的配置一般定义在Tomcat中conf目录下的context.xml配置文件中,如下所示:

Server中每一个Java应用程序的调用处理过程以及生命周期如图所示:

每次请求访问调用容器中的应用程序,一开始会新建应用程序的实体对象,但是Tomcat Server会限制应用程序实体的实例数目,如图所示。

另外Tomcat Server中提供了JMX(Java Management Extensions)组件,JMX是Java SE中定义的一种技术规范,是一个为Java应用程序、设备、系统等植入管理功能的框架,常用于Java应用程序的监控。

JMX可以跨越一系列异构操作系统平台(即不限定固定的操作系统,比如Windows、Linux等操作系统都可支持)、系统体系结构和网络传输协议来无缝集成系统、网络以及服务管理应用。在性能测试中,可以通过JMX来远程监控Tomcat的运行状态。 

2. Tomcat容器Connector性能参数调优

Tomcat Server中Connector性能相关的常见参数如下表所示:

3. Tomcat容器的I/O分析与调优

随着计算机编程的发展,I/O的常见处理方式一般包括同步阻塞I/O、同步非阻塞I/O、异步非阻塞I/O、I/O多路复用等。

(1)同步阻塞I/O:这是一种最传统的I/O模型,也是最早和使用的最频繁和最简单的I/O模型。当在用户态调用I/O读取操作时,如果此时操作系统内核(Kernel)还没有准备好待读取的数据或者数据还没处理完毕,用户态会一直阻塞等待,直到有数据返回,这时候就是在同步阻塞的情况下进行I/O等待。当操作系统内核准备好数据后,用户态需要继续等待操作系统内核把数据从内核态拷贝到用户态之后才可以使用。

总共会发生两次同步阻塞的I/O等待:用户态等待操作系统内核准备好可读的数据,以及等待操作系统内核把数据拷贝到用户态,如图所示。

(2)同步非阻塞I/O:对比同步阻塞I/O,同步非阻塞I/O是不需要操作系统内核把数据准备完毕才会有返回,而是不管数据是否处理好了或者数据是否准备好了都会立即返回。如果操作系统内核返回数据还未处理完毕,用户态线程会轮询不断地发起I/O请求,直到操作系统内核把数据处理完成或者准备完毕。

然后用户态开始等待操作系统内核把数据拷贝到用户态,这一步和同步阻塞I/O是一样的,如图所示。

(3)异步非阻塞I/O:对比同步非阻塞I/O,异步非阻塞I/O处理模式在发出读取数据请求后,就不需要做任何等待了,此时用户线程就可以继续去执行别的操作。在操作系统内核把数据处理完毕,并且把数据从内核拷贝到用户态后,会主动通知用户线程数据已经拷贝完成,在收到此通知后,用户线程再去读取数据即可。

如图所示,从中我们可以看到中间处理过程不会存在任何的阻塞等待,处理完成后,操作系统内核会以异步的形式进行通知。所以这种处理方式相比前面的两种I/O处理方式会更加的高效。

(4)I/O多路复用:在传统的I/O处理方式中,每来一个I/O请求就需要开启一个进程或者线程来处理,如果是在高并发的调用方式中,就需要开启大量的进程和线程来进行I/O处理,而大量的进程或者线程必然会需要大量的服务器硬件资源来支撑。I/O多路复用的处理方式很好地解决了这个问题,只需要开启单个进程或者线程,通过记录I/O流的状态来同时管理多个I/O,就可以有效地节省服务器资源,以及减少线程过多时带来的服务器CPU的中断和切换,从而提高资源的有效利用,
也可以提高服务器的吞吐能力。

在Tomcat Server中,Connector的实现模式可以是I/O阻塞的方式,也可以是I/O非阻塞的方式,可以通过在server.xml中指定Connector的I/O处理方式。在默认情况下,Tomcat Server的Connector指定的protocol为"HTTP/1.1",这种方式就是I/O阻塞的处理方式。

我们可以把它修改为protocol="org.apache.coyote.http11.Http11NioProtocol",Http11NioProtocol是一种非阻塞的I/O处理模式,如下所示:

Tomcat Server Connector中已经支持的常用I/O处理模式,如下表所示。

在Tomcat Server中,NIO一般适用于并发连接数非常大且连接时间又很短的系统架构中。而NIO2一般适用于并发连接数非常多且连接时间又很长的系统架构中。

2、WildFly 性能分析与调优实践

除了Tomcat Server外,用得比较多的另一款应用中间件就是WildFly了。WildFly也是一个开源的基于JavaEE的轻量级应用服务器。

与Tomcat相比,WildFly的管理能力更强,更加适用于大型的应用集群服务器中。

1. WildFly Standalone模式介绍

WildFly常用的部署模式一般包括Standalone(独立运行)模式和Domain(分布式运行)模式两种,一般Standalone模式用得会更多一些。从WildFly的目录中也可以看到这两种部署的启动模式,如图所示。

WildFly最大的特点是采用了模块化设计,启动时可以按照模块进行按需加载。WildFly目录下的modules文件夹存放着各个模块,每个模块以一个jar包的形式存在,并且会有一个对应的module.xml进行定义。

如下图所示,netty就是被定义成了其中的一个模块。

每个模块的module.xml配置里面都会包含如下类似的配置,在xml配置文件内部会定义该模块的名称以及对应的属性配置,如果依赖了其他模块,会在dependencies标签中定义依赖的其他模块的名称。由于依赖了其他模块,在WildFly启动加载该模块时也会一并加载其依赖的其他模块。

WildFly的Standalone模式的相关配置都在standalone目录下的configuration文件夹中,如图所示。

其中configuration文件夹下的standalone.xml文件中定义的是Standalone模式的核心配置,如下所示,这个配置中也包含了很多和性能调优相关的参数。 

2. WildFly Standalone模式管理控制台性能参数调优

WildFly的管理控制台配置也定义在standalone.xml文件中,如下所示,默认启动的端口是19990。



我们可以通过WildFly的server启动日志看到管理控制台的端口为配置文件中定义的19990,如图所示。

和性能相关的参数基本都集中在configuration菜单下,如图所示。

参数修改保存完成后,都会集中保存到前面说的configuration目录下的standalone.xml配置文件中。

1)datasource中相关参数的介绍和优化

datasource中定义的是数据库的JNDI连接配置。如果应用程序不是以JNDI的方式来连接数据库,那么就不需要对此进行配置。通过依次单击菜单选项“configuration→Subsystems→Datasources”,即可进入datasource配置,如图所示。

WildFly的datasource分为Non-XA和XA两种,这两种datasource的主要区别在于XA情况下的datasource会参与多个数据库的连接管理以及对应的事务管理,也就是说,如果一个应用程序同时使用了多个不同的数据库,那么可以使用XA datasource来管理应用程序使用的多个数据库以及多个数据库的事务处理。

进入到datasource界面后,可以对datasource中的配置参数进行调整,如图所示。

datasource中常见的参数如表所示:

2. WildFly中I/O相关参数的介绍和优化

WildFly中提供了I/O相关的配置管理,可以通过依次单击菜单选项“configuration→Subsystems→IO”进入IO配置,如图所示。

进入到IO配置界面后,可以对I/O中的相关配置参数进行调整,如图所示。

IO配置界面分为WORKER和BUFFER POOL两块,WORKER中配置的为XNIO的具体workers配置参数。XNIO是NIO(非阻塞I/O处理模式)的一种变种实现。BUFFER POOL中配置的参数是缓冲池的相关参数,一个BUFFER POOL中可以包含很多个缓冲区,这些缓冲区就构成了一个缓冲池。

WORKER中常见的参数如下表所示:

 BUFFER POOL中常见的参数如下表所示。

3. WildFly中Web/HTTP - Undertow相关参数的介绍和优化

Undertow是一个基于NIO的高性能Web应用Server,等同于我们前面说的Tomcat容器。可以通过依次单击菜单选项“configuration→Subsystems→Web/HTTP→Undertow”进Web/HTTP-Undertow配置,下图中共包含Servlet/JSP、HTTP、Filters三个部分的配置。

(1)Servlet/JSP配置:主要是对应用程序中的运行容器参数进行配置,如图所示。

常见的参数如表所示:

(2)HTTP配置:主要对HTTP监听参数进行配置,如图所示。 

常见的参数如下表所示: 

(3)Filters配置:主要配置对发送给WildFly容器的请求进行过滤和修改,如图所示。 

常见的参数如下表所示:

3. WildFly Standalone模式性能监控

WildFly server自身提供了运行性能监控,通过依次单击菜单选项“Runtime→Standalone Server”即可进入,如图所示。 

1)JVM运行监控

JVM(Java虚拟机)可显示出Java虚拟机的运行性能,它是Java程序的运行环境,单击JVM即可直接进入,如图所示。  

从图中可以获取如下运行信息,如下表所示。

2)Environment

运行环境参数的显示,如图所示。

3)Log Files

应用程序运行日志文件的显示,如图所示,日志文件可以供下载或者直接在页面中查看。有时候需要从日志中来分析问题时,即可从此处实时获取日志。

4)Subsystems

Subsystems提供Datasources、JNDI View、Transactions、Web/HTTP - Undertow、Web Services、Batch、JPA、Transaction Logs运行性能的监控,如图所示。 

 (1)Datasources:提供数据库的JNDI模式下的数据库连接和连接池监控,如图所示。

从图中可以看到如下表所示的数据库连接监控信息:

如下图所示,可以看到数据库连接池监控信息,各个监控信息说明如下表所示。 

(2)JNDI View:提供JNDI的相关运行信息展示视图,如图所示。 

(3)Transactions:提供事务处理的相关运行信息视图,如图所示。

从图中可以获取如下运行监控信息,如下表所示。 

(4)Web/HTTP - Undertow:提供Web/HTTP的Connectors运行监控视图,如图所示。 

从图中可以获取如下运行监控信息,如下表所示。

(5)Web Services:提供Web Services服务的运行监控视图,如图所示。

从图中可以看到请求的总数量以及请求被响应的总数和请求发生故障的总数。

(6)Batch:提供WildFly批处理作业中的线程池监控运行视图,如图所示。

 从图中可以获取运行监控信息,如下表所示。

 (7)Transaction Logs:提供事务恢复日志(发生故障时,恢复事务而存储的持久性信息)的监控运行视图,如图所示。

七、前端性能测试

1、前端性能风险

前端是面向用户的程序,例如我们在浏览器中看到的网站页面,我们在手机中看到的App界面、小程序等。在20多年前我们并没有前端这个说法,当时很多系统(产品或者服务)是采用C/S架构的,C(Client)为客户端,S(Server)为服务端。

例如我们在Windows系统中安装的QQ程序,聊天界面是C端,系统消息来自S端。C端负责接收用户请求,展示服务端的响应;对用户来说唯一不方便的地方就是要安装C端程序,所以这是一个痛点。

我们可以通过浏览器来访问门户网站,服务商只要按照协议提供信息,就可以发布在万维网上供用户浏览,用户并不需要去安装特定的C端程序。万维网解决了用户的痛点,为之后的互联网崛起扫清了道路。实际上是我们用浏览器代替了这个C端程序,让它变得通用,它不仅提供文字、图片内容,还可以提供视频内容。

如今,运行在浏览器上的程序我们称之为前端程序。当然,前端程序不是只有运行在浏览器中的,我们把处理用户请求,渲染响应数据的程序都叫前端。例如手机中的App、微信及微信中的小程序。用户的信息需求种类增多,信息量增多,都给前端带来了压力。现在我们在开发前端程序时重度使用JS(JavaScript),JS是解释型的即时编译语言,运行在浏览器中。是程序就可能有问题,自然性能问题也是逃不掉的。

以Web为例,我们在浏览器中访问页面时也许没少见下面几种情况:

  • 时常会遇到白屏,半天没有响应;
  • 好不容易有响应,屏幕出现卡顿;
  • 页面出来了,数据没填充完整;
  • 图片显示不完整;
  • 动画不动,视频不流畅。

这些情况可能是前端的性能问题,也可能是后端的问题,例如,后端响应数据更快,那前端数据显示就更快;网络更快,前端等待数据时间更短。当后端优化到一定程度时,前端如果有优化空间自然要优化。优化时从系统整体出发,前后端配合起来进行优化,按轻重缓急、难易及成本统一考虑。

现在的前端性能问题主要体现在数据(图片、文字、音频、影像、动画等)的展示需求与承载资源的矛盾上。承载资源指的是浏览器及网络、CPU、内存等硬件资源。浏览器在获取到后端响应数据后,对页面渲染的速度有多快?渲染由浏览器的内核来完成,怎么优化前端程序让浏览器更高效地做渲染?页面的渲染是一个构造文档对象模型(Document Object Model,DOM)的过程,如何高效地构建DOM就成了前端性能的课题。

移动互联中的前端性能问题也是我们要关注的重点,手机App分为Native应用、Hybrid应用或者Web(H5)应用。Native应用是原生程序,一般运行在机器操作系统上,例如,iOS或者Android,有很强的交互性,静态资源都存在手机上;Hybrid应用是半原生程序,伪造一个浏览器访问Web页面,还是运行在机器的操作系统上,交互性较弱,资源一般在本地或者网络(如果是从网络请求资源,用户体验受网络影响大)上;Web应用是利用浏览器(手机中的浏览器)进行访问,运行在浏览器上,不再是直接运行在操作系统上,资源一般在网络上(浏览器可以缓存),Web应用的性能除了受到后台响应的影响,也受页面渲染影响。

2、前端性能分析原理

Web应用向着富客户端(功能更多,内容种类多,内容更多)发展,页面的渲染是我们要重点
考虑的性能点。后面我们将针对Web应用在浏览器中的渲染来讲解前端性能。

通常通过浏览器完成一次用户请求有5个步骤:

(1)DNS查询:找到服务端。
(2)TCP连接:建立连接。
(3)HTTP请求及响应:发送请求,B端与S端通信完成请求交互。
(4)服务器响应:服务器后端程序处理请求返回响应数据。
(5)浏览器渲染:B端接收数据包并在浏览器中展示。

抛开网络及服务端问题,我们只谈论浏览器渲染,把浏览器渲染也分成5个步骤。

(1)处理HTML标签并构建DOM树。
(2)处理CSS标记并构建CSSOM树。
(3)将DOM与CSSOM合并成一个渲染树。
(4)根据渲染树来布局,以计算每个节点的几何信息。
(5)将各个节点绘制到屏幕上。

上面出现了几个名词,可能部分不太了解,我们选择几个简单解释一下。

DNS(Domain Name System):将域名和IP地址映射的分布式数据库,访问网站时通过域名找到IP,方便上网。

TCP连接:TCP/IP是一组网络连接协议,搭起计算机之间通信的桥梁,TCP连接是使用TCP/IP协议进行网络连接,我们访问网站会建立TCP连接。

HTML标签(标记):把构思的内容通过浏览器展示给用户,这些内容需要浏览器“认识”才能展示。HTML标签是一个规范,浏览器能够“认识”并处理这些标签,设计者能够把内容通过HTML规范来转换。

下图是网页的部分HTML内容,其中以“<”开头和“>”结尾的都是HTML标签,标签不区分大小写。

浏览器把整个页面当作一个DOM,页面中的内容(称为对象更专业一点)被组织在一个树形结构中,然后把这个DOM渲染在浏览器中就是我们看到的页面了。

下面细化一下渲染过程的5个步骤:

(1)浏览器接收到服务端的响应数据(以HTML标签、CSS、JS(JavaScript)、图片、音频、视频等组成的HTML文档)信息,开始解析HTML文档生成DOM节点树。

(2)对CSS进行解析。CSS(Cascading Style Sheets)中文翻译为“层叠样式表”,简称样式表,它可以设计文字的大小与颜色。如果解析过程中遇到引用外部CSS文件就去下载,然后构建CSSOM(CSS Object Model)树。解析HTML过程中,如果有JS文件,则执行JS;如果有引用外部的JS文件,则去下载,然后执行;如果解析HTML过程中发现标签内引用了图片,则去获取这张图片(包括自己服务端的或引用外部的)。有时我们访问Web时会遇到图片慢慢加载完整的情况,而其他的文字内容并不用等待图片下载完成才显示,所以图片的渲染并不是阻塞的。

(3)DOM树和CSSOM树生成渲染树(Render Tree)。渲染树是按顺序展示在屏幕上的一系列矩形,这些矩形带有字体、颜色等视觉属性。

(4)开始布局(Layout)。根据渲染树将节点树的每一个节点布局在屏幕上的正确位置。

(5)完成绘图(Painting)。遍历渲染树绘制所有节点,然后我们就能在浏览器中看到整个页面。这5个步骤并不一定一次性顺序完成,如果DOM或CSSOM被修改(被JS修改,例如异步获取到数据后填到DOM中),以上过程需要重复执行,这样才能计算出哪些像素需要在屏幕上进行重新渲染。实际页面中,CSS与JS往往会多次修改DOM和CSSOM。CSS与JS阻塞资源,在它们完成操作前会延迟DOM构建,CSS阻塞又先于JS执行,所以我们在写页面程序时,CSS引入先于JS,JS应尽量少影响DOM的构建。

到此,我们对页面渲染有了一些认识,由此看来页面的优化关键就在DOM构造上(抛开服务器响应的数据大小,网络延迟等因素)。

3、前端性能分析工具

了解了前端渲染过程,不难发现,想要写好前端程序其实也不是一件容易的事,我们不仅要考虑功能,还要考虑性能,更烦琐的是要考虑对不同浏览器的支持。因为不同浏览器的渲染机制有可能不一样,所以对JS的支持(JS执行引擎)也可能不一样。

以Chrome为例,渲染引擎为Blink,JS执行引擎为V8,Safari用的Webkit渲染引擎(Blink也来源于Webkit)。在此我们不打算深入到浏览器内核(渲染引擎、JS执行引擎),这里只是简单提及。

我们仅就如何做前端性能分析,有没有工具可以利用,如何使用这些工具。

1. Yslow

雅虎出品的Yslow是最早、最负盛名的前端性能分析工具之一,以浏览器插件方式进行安装,能够对访问的页面自动进行性能分析,并给出打分及优化建议。说到优化建议,就不得不提雅虎的23条前端最佳实践和规则,参照这些规则您就可以优化页面。

2. PageSpeed Insights

谷歌推出的PageSpeed Insights提供在线的页面性能诊断分析功能),并给出优化建议,被测试的页面需要允许从公网访问(从developers.google.com访问到被测试的页面)。

3. WebPageTest

WebPageTest 是在线前端性能分析网站,除了可以分析前端性能,也可以模拟移动设备上的性能,还可以做云测试(利用分布在全球的资源来访问用户的页面,帮助用户监听页面在当地的性能表现,以决定是否要在当地增加服务器来提高性能)。

下图所示是WebPageTest对网页的测试报告,可以看到报告中内容丰富,包括网络方面、渲染页面、缓存方面等的信息。

下图是Details部分的内容,其把请求在各环节的耗时进行了分组统计,性能和风险一目了然。 

4. PageSpeed Insights (with PNaCl)

上面提到PageSpeed Insights要系统在线才可以测试,如果系统没有公网访问地址怎么办呢?当然也有办法,例如用PageSpeed Insights (withPNaCl)可以分析本地页面,下载地址: chrome.google官网的webstore/detail/pagespeed-insights-with-p/
lanlbpjbalfkflkhegagflkgcfklnbnh,它是以浏览器插件方式进行安装。 

5. DevTools Performance

如果对JS比较熟悉,那么也可以直接使用Chrome开发者工具来分析前端性能,此工具套件中有一个Performance功能,旧版本叫Timeline。单击开始按钮开始录制页面(建议无痕模式打开),单击stop按钮开始分析采集到的数据。

下图所示为录制的Google官网示例,下面介绍一下分析结果。

(1)帧数(FPS,frames per second,图中区域①表示帧数)。红色(真实环境中有此色彩)代表帧数低;绿色(真实环境中有此色彩)则代表帧数高,用户感知到的图像就流畅。

(2)CPU(图中区域②表示CPU)。CPU图形中有不同颜色,本例中黄色代表JS代码消耗,绿色代表绘图(Painting)消耗,紫色代表了布局消耗。

(3)显示录制的帧(图中区域③表示录制的帧),点选后可以在Summary中看到FPS及CPU耗时。FPS越大,帧间距越小,用户体验越好。DevTools还提供了一个FPS meter的工具,如下图,可以用来看FPS。使用热键Control+ Shift+ P(Windows、Linux)打开命令菜单,输入Show Rendering,然后勾选FPS meter。 

(4)主线程上的活动火焰图(上图中区域④所示),显示各种活动的耗时,条状越长表示时间越长,条状上倒着的三角是警告,光标移到倒三角上时可以显示警告信息,如下图。 

(5)显示了渲染(Rendering,上图中区域⑤所示)占了绝大部分时间。查看Call Tree,如下图,按时间排序,可看到时间大多花在app.update上,链接到代码是app.js的第71行,这样用户就可以试着去优化这一行代码。 

6. Audits

利用DevTools Performance可以找到前端性能问题,那么如何优化呢?Chrome还提供了Audits工具。它可以模拟移动设备与计算机桌面(计算机利用浏览器访问),分析当前浏览器中的页面性能并给出建议。

Audits从5个方面给用户提供建议:

(1)Performance

页面对象消耗的性能分析,如渲染、绘画。

(2)Progressive Web App(PWA,渐进式Web App)

是否符合渐进式Web App标准。我们在使用移动设备访问网络服务时,原生(Native)的应用通常比H5或者Hybrid应用体验要好。PWA就是让Web网页服务具备类似原生App的使用体验,这样就不用在移动设备上装App了。当然这需要浏览器的支持,以Blink(Chrome、Oprea、Samsung Internet等)和Gecko(Firefox)为内核的浏览器已经支持。市面上也有一些PWA应用,例如微博(m.weibo.cn/beta),打开就是全屏显示,体验类似原生App,其实它是运行在浏览器内核之上。

(3)Best practices

最佳实践。

(4)Accessibility

可用性。例如是否对色弱人士友好,是否有一些方便用户使用的快捷键等,此项可以不选择。

(5)SEO

搜索引擎优化。其旨在让用户的网站容易被搜索到,排在同类网站前面,因此要在网页中有一些合适的关键字。单击Run audits,开始分析当前页面,下图所示是分析完成的页面。单击View Trace会跳转到Performance部分(具体使用参照前面所述DevTools Performance部分)。 

从图中可以看到性能良好。对色弱人士的亲和性只有58(不便于他们使用);按最佳实践来打分也有93分(绿色代表成绩不错,实际环境中有色彩);SEO为89分,是黄色,可以单击它转到其说明部分;PWA部分是灰色,成绩可以忽略了,表示完全没这方面的设计。

7. NativeApp性能监听工具

NativeApp是原生应用,运行在移动操作系统(iOS、Android等)上。对于此类应用的性能分析有点类似于对桌面操作系统(Windows、Linux)上的程序的性能分析。分析最多的是CPU、内存、网络等基础性能。

详细一点的有APM(Application Performance Management)分析,可以监控到某一个方法的调用耗时。在全链路监控中一般使用Skywalking,它主要是利用字节码注入的方式来注入探针采集性能数据,NativeApp的APM工具原理与Skywalking大同小异。另外,也可以采用AOP(Aspect Oriented Programming,面向切面编程,与Spring中的AOP原理类似)的方式注入切面来采集性能数据。

下面我们介绍几种分析工具:

1)腾讯QAPM

QAPM提供若干核心指标,能对性能分析起到关键的作用。

(1)IO

无论在什么操作系统中(Android/iOS/Windows等),IO对App的性能都起着至关重要的作用,甚至比CPU更重要。例如,在iOS中频繁IO可能导致App闪退。其中,磁盘中随机IO影响最大,固态硬盘或Flash芯片中写入放大效应影响最大。QAPM提供了分析工具,可以协助用户降低IO字节数和 IO 次数,避免主线程IO或SQLite全表扫描等。

(2)内存用量

App内存占用太多,轻则导致卡顿,重则导致App崩溃或闪退。特别是 Android和iOS系统,由于没有虚拟内存的设计,内存是非常稀缺的资源。

(3)CPU周期

CPU周期,适合于衡量CPU的计算量。操作系统的个别版本对CPU占用率的输出存在bug,故推荐改用CPU周期,以获得更准确的度量结果。

2)360 Argus APM移动性能监控平台

(1)Argus APM目前支持如下性能指标

  • 交互分析:分析Activity生命周期耗时,帮助提升页面打开速度,优化UI体验。
  • 网络请求分析:监控流量使用情况,发现并定位各种网络问题。
  • 内存分析:全面监控内存使用情况,降低内存占用。
  • 进程监控:针对多进程应用,统计进程启动情况,发现启动异常(耗电、存活率等)。
  • 文件监控:监控App私有文件大小和变化,避免私有文件过大导致卡顿、存储空间占用过大等问题的发生。
  • 卡顿分析:监控并发现卡顿原因,代码堆栈精准定位问题,解决明显的卡顿体验。
  • ANR分析:捕获ANR异常,解决App的“未响应”问题。

(2)Argus APM特性

  • 非侵入式:无须修改原有工程结构,无侵入接入,接入成本低。
  • 低性能损耗:Argus APM针对各个性能采集模块,优化了采集时机,在不影响原有性能的基础上进行性能的采集和分析。
  • 监控全面:目前支持UI、网络、内存、进程、文件、ANR等各个维度的性能数据分析,后续还会继续增加新的性能维度。
  • Debug模式:提供Debug模式,支持开发和测试阶段,实时采集性能数据,具有实时本地分析的能力,帮助开发和测试人员在系统上线前解决性能问题。

3)阿里巴巴的码力App监控(简称码力App)

码力App监控主要由3部分组成:码力SDK、码力云端、码力控制台。您需要将码力SDK嵌入到移动应用工程,它将负责采集和上报影响用户体验的各种性能指标数据给码力云端;再由码力云端进行数据处理;最终,您可以通过码力控制台进行性能数据查看、分析、问题定位和管理等操作。码力SDK支持iOS和Android两个平台,跟踪和反馈用户使用过程中出现的应用崩溃、加载错误以及加载缓慢等各种对用户体验造成负面影响的故障或性能问题。

4)TraceView

TraceView是Android SDK中内置的一个工具,它可以展示trace文件,用图形的形式展示代码的执行时间、次数及调用栈,便于我们分析问题。利用TraceView更多的是进行性能诊断,开发团队使用得多一些,要求具备代码能力,熟悉App的开发(至少能够进行简单开发工作)。

trace文件是我们利用工具导出的与性能相关的跟踪日志,可以使用代码、Android Studio或者DDMS(Dalvik Debug Monitor Server调试监控工具)生成。

  • 代码生成类似于桌面开发中的日志输出,如Debug.startMethodTracing。
  • Android Studio内置的Android Monitor可以生成trace文件。
  • DDMS是Android调试监控工具,能够提供截图,查看log,查看视图层级,查看内存使用等功能。

前端技术发展迅速,前端对性能的影响也越来越大,作为性能测试人员有必要在这方面储备知识。有兴趣的朋友可以访问开源社区
:https://github.com/thedaviddias/Front-End-Performance-Checklist,其中列出了各种前端优化方案、分析工具。

八、安卓APP性能测试

1、adb

adb的全称为Android Debug Bridge,是安卓SDK(可以通过网站:Android SDK - Download下载安卓SDK)提供的重要工具之一,一般在安卓SDK的platform-tools目录下,如图所示。

可以通过检测计算机的USB端口感知安卓手机连接到计算机和从计算机拔除,从而起到调试桥的作用。adb是一个功能非常全的命令行工具,可用于执行APP安装、APP调试等各种安卓手机的操作,并且提供了对底层安卓操作系统的命令行shell的访问权限。

adb工具在操作的过程中主要涉及如下3个组件:

  • Client:用于通过adb工具发送命令行操作。客户端一般就是计算机等PC设备上的命令行。
  • 守护进程服务(adbd):运行在安卓手机、安卓模拟器等设备上的后端服务,负责在设备上执行Client发出的命令。
  • Server服务:运行在计算机等PC设备上的后台进程服务,负责客户端和安卓手机、安卓模拟器等设备上守护进程服务之间的通信。Server服务在启动后,就开始自动扫描连接到PC设备上的安卓手机、模拟器,扫描时通过扫描5555~5585之间的奇数号端口来搜索安卓手机或者模拟器,一旦发现adbd守护进程服务,就通过此端口进行连接。需要说明的是,每一部安卓手机或者模拟器都会使用一对有序的端口,偶数号端口用于控制台连接,奇数号端口用于adb连接。

adb的整个通信流程如图所示:

当在某台PC计算机上启动adb客户端时,adb客户端会先检查该PC计算机上是否有adb Server服务进程正在运行,如果没有就自动启动TCP端口为5037的Server服务,并且监听adb客户端发出的命令,在此之后,adb客户端的所有命令均通过5037端口与安卓手机、安卓模拟器等设备上守护进程服务进行通信。

在adb Server服务中维护的安卓手机或者模拟器的状态如下表所示:

可以通过执行adb kill-server命令停止Server服务进程,如图所示。 

adb命令支持的其他常用参数如下表所示:

2、DDMS

DDMS是Dalvik Debug Monitor Service的简称,同样是安卓SDK提供的重要工具之一,一般在安卓SDK的tools目录下。早期的SDK中该工具的名称为ddms.bat,如图所示。

在新的SDK中ddms.bat已经被谷歌移除了,但是还是可以通过运行monitor.bat打开,如图所示,工具主要用于对安卓设备的Dalvik虚拟机进行调试监控。

 DDMS包含的功能如下:

(1)生成HPROF dump文件:有点类似JDK工具中jmap命令的作用,如图8-2-4和图所示,选中安卓APP进程,单击按钮即可生成HPROF dump文件。

生成的HPROF dump文件使用前面介绍的MAT工具是无法打开的,因为安卓APP虽然也是运行在虚拟机上,但是和JVM却是不一样类型的虚拟机,安卓中APP运行的是Dalvik虚拟机,JVM和Dalvik的底层实现完全不一样。安卓SDK的platform-tools目录下提供了hprof-conv.exe工具用于进行格式转换,如图所示。

我们可以在命令行中执行hprof-conv.exe,对刚刚生成的dump文件进行转换,如图所示。

转换完成后,就可以使用MAT打开转换后的dump文件了,如图所示。

然后就可以通过MAT工具来分析内存使用情况,特别是可以通过MAT工具来分析APP是否存在内存泄漏的情况。

(2)Method Profiling:用于采集和分析安卓APP方法运行轨迹,单击 按钮开始Method Profiling,在采集结束后,单击 按钮停止Method Profiling,即可生成如图所示的方法运行轨迹分析。MethodProfiling对我们分析很多APP的性能问题帮助非常大。

 (3)Threads:显示当前APP进程的线程运行信息,包括线程id、线程运行状态、线程运行时间、线程名称等信息,如图所示。

(4)Heap:显示当前APP进程的Heap信息,如图所示。

(5)Allocation Tracker:安卓内存分配的跟踪工具,单击Start Tracking按钮可以开始跟踪,之后可以单击Stop Tracking按钮结束跟踪。

单击Get Allocations可以获取所有的内存分配记录,如图所示。 

(6)Network Statistics:网络流量分析工具,如图所示,图中的TX代表发送,RX代表接收。 

(7)File Explorer:安卓设备文件浏览工具,如图所示。 

(8)System Information:安卓设备系统信息查看工具,包含CPU load使用、内存使用、Frame Render时间分析,如图所示。 

(9)Capture system wide trace using Android systrace:使用安卓系统轨迹来进行系统跟踪,如图所示。

单击按钮,即可弹出如下图所示的安卓系统轨迹跟踪对话框。 

上图中弹出框选项说明如表所示:

从上图所示界面中选中需要跟踪的选项后,单击OK按钮等待跟踪完成后,即可生成跟踪报告。使用浏览器打开跟踪报告,如下图所示,报告中显示了非常多的图形数据来辅助进行性能分析定位,通过下拉右侧的滚动条,还可以获取到更多的图形数据。 

单击图中所示界面中每个需要关注的区域,都可获取该区域的图形数据的详细描述信息,如下图所示。

3、Android Studio profiler

在新的Android Studio开发工具中移除了对DDMS工具的支持(可以通过网站:https://developer.android.google.cn/studio下载Android Studio这个工具),而引入了profiler分析器,如图所示。

打开Android Studio后,依次单击菜单选项“Run→Profile”,即可对某个指定的APP进行profiler分析,如下图所示。

选中需要分析的APP,单击profiler按钮后,即可进入分析器界面,如图所示。

之后在安卓设备上对该APP执行的任何操作,都可以同步看到profiler上显示的CPU、MEMORY、NETWORK、ENERGY等数据分析结果。

(1)CPU:单击profiler上的CPU区域,即可切换到CPU的详情分析结果界面,如图所示。

在图中除了可以看到当前APP的CPU使用占比之外,还可以看到对应的线程的活动信息情况。对于CPU使用的采样,分析器提供了Sample Java Methods、Trace Java Methods、Sample C/C++ Functions和Trace System Calls共四种采样录制模式,如图所示。

分析器提供的四种数据采集模式说明如下表所示:

(2)MEMORY:单击profiler上的MEMORY区域,即可切换到内存使用的详情分析结果界面,如图所示。

图中可以看到每个时间点的内存数据消耗详情,所示界面中选中某个时间点,再单击鼠标,即可查看到当前时间点下的内存实例对象详情列表,在列表中列出了实例对象的Allocations(对象实例分配的内存个数)、Deallocations(对象实例回收的内存个数)、Total Count(对象实例的数量)、Shallow Size(所有对象实例持久的内存大小)等分析数据。而且还可以通过界面中所示的下拉框切换到其他选项查看分析数据,如下图所示。

在内存区域中,单击鼠标右键,选择Dump Java Heap,即可生成Java虚拟机内存的dump文件,如图所示。 

(3)NETWORK:单击profiler上的NETWORK区域,即可切换到网络流量使用的详情分析结果界面,如图所示。图中可以看到每个时间点的网络流量使用详情。

在界面中,通过单击鼠标选中某个时间点或者某个时间范围,即可查看到对应时间点或者时间范围内的网络连接调用列表和线程调用耗时列表,如图所示。

在上图所示的网络连接列表中,选中一条调用记录,即可从界面的右侧看到HTTP请求的详细请求报文和响应报文,如图所示。 

(4)ENERGY:单击profiler上的ENERGY区域,即可切换到APP的能耗评估分析界面,如图所示。图中可以看到每个时间点的能耗评估分析详情。 

4、systrace

systrace是最新版本的安卓SDK中提供的一个系统轨迹分析工具,位于SDK的platform-tools\systrace目录下,如图所示。

这是SDK使用Python语言实现的一个小工具,因此如果需要使用systrace,需要在本地PC计算机上安装Python语言运行包,可以通过网站
Download Python | Python.org进行下载和安装。

systrace的使用方式说明如下。在命令行中切换到SDK目录下执行systrace分析命令,如图所示。

 systrace支持的参数如表所示:

备注:表中部分内容参考自Android 开发者  |  Android Developers网站中关于systrace的参数介绍。通过systrace获取到的跟踪报告与通过DDMS中的Capture systemwide trace using Android systrace功能获取到的跟踪报告基本是一致的,这两种方式都可以生成系统轨迹跟踪报告。

九、全栈性能测试实战

性能测试是一项综合性的工作,致力于发现性能问题,评估系统性能趋势。性能测试工作实质上是利用工具模拟大量用户操作来验证系统能够承受的负载情况,找出潜在的性能问题,对问题进行分析并解决;找出系统性能变化趋势,为后续的生产营运提供参考。显然,性能测试不是录制脚本那么简单的事情(而且现在很多系统无法录制脚本)。

我们以JForum论坛作为被测系统来讲解性能测试过程。JForum是著名的开源论坛,支持数十种语言,包括简体中文。JForum功能强大,界面美观,代码结构清晰,并采用BSD授权。JForum是用Java语言开发的,采用当下流行的MVC框架(非SSH框架,是自己定义的框架)。

我们先来回忆一下性能测试开展过程:

(1)学习业务:通过查看文档,咨询产品设计人员,手工操作系统来了解系统功能。

(2)分析需求:分析系统非功能需求,圈定性能测试的范围,了解系统性能指标。比如用户规模,用户要操作哪些业务,产生的业务量有多大,业务操作的分布时间,系统的部署结构、部署资源等。测试需求获取的途径主要是需求文档、业务分析人员,包括但不限于产品经理、项目经理等(敏捷过程下,最直接的是从项目的负责人或者产品经理处获取相关需求信息)。

(3)工作评估:工作量分解,评估工作量,计划资源投入(需要多少人力,多少工作日来完成性能测试工作)。

(4)设计模型:圈定性能测试范围后,把业务模型映射成测试模型。什么是测试模型呢?比如一个支付系统需要与银行的系统进行交互(充值或者提现),由于银行不能够提供支持,我们会开发程序去代替银行系统功能(这就是挡板程序,Mock程序),保证此功能的性能测试能够开展,这个过程就是设计测试模型。通俗地说就是把测试需求落实,业务可测,可大规模使用负载程序模拟用户操作,具有可操作性、可验证性;根据不同的测试目的组合成不同的测试场景。

(5)编写计划:计划测试工作,在文档中明确列出测试范围、人力投入、持续时间、工作内容、风险评估、风险应对策略等。

(6)开发脚本:录制或者编写性能测试脚本(现在很多被测系统都是无法录制脚本的,我们需要手工开发脚本),开发测试挡板程序,开发测试程序等。如果没有第三方工具可用,甚至需要开发测试工具。

(7)准备测试环境:性能测试环境准备包括服务器与负载机两部分,服务器是被测系统的运行平台(包括硬件与软件、中间件等);负载机是我们用来产生负载的机器,用来安装负载工具,运行测试脚本。

(8)准备测试数据:根据数据模型来准备被测系统的主数据与业务数据(主数据是保证业务能够运行畅通的基础,比如菜单、用户等数据;业务数据是运行业务产生的数据,比如订单;订单出库需要库存数据,库存数据也是业务数据。我们知道数据量变化会引起性能的变化,在测试的时候往往要准备一些存量/历史业务数据,这些数据需要考虑数量与分布)。

(9)执行测试:测试执行是性能测试成败的关键,同样的脚本,不同的执行人员得出的结果可能差异较大。这些差异主要体现在场景设计与测试执行上。

(10)缺陷管理:对性能测试过程中发现的缺陷进行管理。比如使用Jira、ALM等工具进行缺陷记录,跟踪缺陷状态,统计分析缺陷类别、原因;并及时反馈给开发团队,以此为鉴,避免或者少犯同类错误。

(11)性能分析:对性能测试过程中暴露出来的问题进行分析,找出原因。比如通过堆分析找出内存溢出问题,通过查询计划找出慢查询原因。

(12)性能调优:性能测试工程师与开发工程师一起解决性能问题。性能测试工程师找到性能问题,监控到异常指标,分析定位到程序;开发工程师对程序进行优化。

(13)测试报告:测试工作的重要交付件,对测试结果进行记录总结,主要包括常见的性能指标说明(TPS、RT、CPU Using……),发现的问题等。

性能测试主要交付件有:

  • 测试计划;
  • 测试脚本;
  • 测试程序;
  • 测试报告或者阶段性测试报告;

如果性能测试执行过程比较长,换句话说性能测试过程中性能问题比较多,经过了多轮的性能调优,需要执行多次回归测试,那么在这个过程中需要提交阶段性测试报告。

(14)评审(准出检查):对性能报告中的内容进行评审,确认问题、评估上线风险。有些系统虽然测试结果不理想,但基于成本及时间的考虑也会在评审会议中通过从而上线。

下面我们根据流程来讲解性能测试过程。

1、需求采集与分析

性能测试需求收集与分析要完成下面两项工作:

(1)采集性能测试需求:采集对象包括业务交易、业务量、业务量趋势、用户信息、系统架构、业务指标、系统硬件指标等。

(2)分析性能测试需求:确定性能测试范围,分析哪些业务纳入性能测试范围及性能指标是什么,另外要分析用户使用行为、业务分布、业务量;估算TPS与并发用户数等性能测试执行依据。

我们将性能测试指标分为两类:

  1. 业务指标(TPS、RT、事务成功率等)。
  2. 硬件性能指标(CPU使用率、内存利用率、磁盘繁忙度等)。

性能测试需求从哪里获取呢?

一般的获取途径是需求文档,在需求文档中有一部分内容描述的是非功能性需求(一部完整的需求文档一般会包含行为需求、数据需求与非功能需求),但是实际现状是只有极少数的需求文档中能够把这些非功能性需求描述完整,多数需求文档对于性能需求的说明都比较笼统、抽象,通常需要性能测试工程师主动向BA(业务分析师)了解性能需求。

性能需求的主要采集内容有哪些呢?

(1)系统架构(物理架构与逻辑架构,包括中间件产品与配置、数据库配置),我们在测试环境建立时需要参考。

(2)采集业务并量化业务。我们在计算TPS及并发用户数时要用到。

(3)了解业务发展趋势,例如业务年增长率是多少?未来业务量是多少?例如系统的需求中说到要满足未来3年的业务增长需求(即3年后此系统的性能还要能够满足性能要求),我们在测试时就可能需要生成3年的存量业务数据。对于关系型数据库来说,数据量大小对性能的影响还是比较明显的。

(4)了解系统是否会有归档机制?大家知道,数据库中数据量大对性能是有影响的,如果有归档机制,可以把一些无用或者过时的信息移到归档库,这样就减少了当前库中的数据,有利于提高系统性能。

(5)采集业务发生时段,例如一天产生20 000订单,而高峰时1小时就能完成10 000单。此数据主要在估算TPS与并发用户数时用到。

(6)采集在线用户数、活动用户数、业务分布。有些系统用户量特别大,会对系统造成性能瓶颈,这时可以通过分析活动用户数和业务分布来分析负载情况。

(7)系统是否与第三方系统有关联关系?这决定在测试时我们是否要做挡板程序(Mock程序,用程序来代替第三方系统功能,不依赖于第三方系统)。

(8)采集业务性能指标,例如响应时间、吞吐量(每秒支持多少业务)等。

(9)采集系统硬件指标,例如CPU利用率、内存利用率或者可用内存等。

下表是其中的一个业务需求采集示例表格。其中我们对业务名称、业务量、未来业务量、响应时间、事务成功率都进行了采集。

如何采集性能测试需求呢?

我们把被测试的系统分为3类:

(1)新应用类(NP,全新立项系统,没有原型系统)。

(2)升级改造类(CIP,旧系统重构)。

(3)需求变动类(RFC,基于现有业务系统需求变更)。

其中RFC有可能存在于新应用中,也可能存在于CIP中,所以实际上我们只讨论新应用类与CIP两类。针对不同类别的系统,我们的分析方法会有不同,对于新业务(NP类)系统,我们从需求文档中采集性能需求;对于不完善的内容,我们进行补充。对于升级类系统,我们分析原型系统业务数据即可,最直接的办法就是分析原型系统的数据、统计业务量、业务分布等信息。

1. 需求采集

下面我们以JForum论坛为例进行需求采集,首先要了解系统物理架构与逻辑架构。物理架构指导我们进行测试环境的建立,一般我们会尽量让测试环境与生产环境的架构趋于一致;这样测试结果可比较性会较强,而且比较时相对容易且可靠。

逻辑架构让我们对系统的逻辑组成有所了解,进行测试时能够清楚地划分问题出现的区域。

1)系统架构

生产环境物理架构如图所示:

Web代理:通常我们会在服务前面加上一个代理,例如用Nginx来做反向代理,既可以做负载均衡,也可以增强服务的安全性,还可以把静态资源发布到Nginx。

应用服务:用Tomcat 7发布的应用服务,负责业务处理,我们部署JForum服务。在线上环境我们为了高可用,通常会部署多个实例。

数据库存储:安装MySQL 5.6,双机热备结构,用来存储数据。物理架构规定了组成软件系统的物理元素(各种硬件设备)、这些物理元素之间的关系,以及它们部署到硬件上的策略。在建立测试环境时,需要参考物理架构进行配置部署。为了更准确地模拟生产环境负载,在物理架构上建议尽量保持与生产同步。而实际上往往测试环境不能够完全与生产环境匹配,一方面是成本问题(生产环境机器众多),另一方面是区域位置问题(例如生产环境面向全国或者全球)。我们的建议是尽量架构同步,在机器配置及数量上,我们可以缩小比例。这就衍生出另外一个课题,即如何由测试环境来推算生产环境的性能。由于侧重点不同,篇幅所限在此不展开分析,可以参考TPC-E的标准进行测评。

逻辑架构如图所示:

逻辑架构展现的是软件系统中元件之间的关系,例如用户界面、数据库、外部系统接口等。上图所示是App(应用服务)的逻辑架构,列出了系统服务组件、邮件服务、权限管理、业务服务(对于JForum就是发帖、回帖、浏览帖子)等。Web层是通过JSP与Velocity Freemark来展现的。

通过逻辑架构,我们能迅速地了解到系统的主要功能与服务,并且知道其逻辑关系,有助于我们设计测试场景。

2)业务流程

确定了系统的主要业务流程,我们就可以方便地进行写性能测试用例。我们可以从需求文档(SRS)中获取,下图所示为JForum的主要业务流程。

(1)游客

游客可以直接浏览各类帖子,如果要回复或者发帖,会定向到登录/注册页面;登录或注册完毕再回到回复或者发帖页面。

(2)注册用户

已经注册过的用户可以直接浏览各类帖子,如果要回复或者发帖,会定向到登录页面;登录完毕再回到回复或者发帖页面。

(3)管理员

管理员可以浏览审核帖子,对于内容不健康的帖子可以删除。

3)业务相关性能需求

业务需求一般来自于需求文档,在需求不明确的情况下,我们一般会向需求提供方(BA团队、产品团队等)去征询。

假设以下是需求文档中有关非功能性需求的说明:

(1)此论坛为一个技术讨论性质的论坛,注册用户规模预计是10万,每日活跃用户数预计为5%,即5 000(每天至少访问JForum一次)。

(2)用户在论坛中的活动以浏览、发帖及回帖为主,日PV预计为2万。其中浏览、发帖、回帖的比例大约为7∶1∶2。

PV即Page View,用户每访问一个页面统计为一个PV。下图所示为从Alex获取的CSDN论坛的PV统计信息。在2015年6月24日前后近一周时间内平均PV是4 486 680;有效IP平均每天是1 452 000,即每个IP平均访问约3个页面。CSDN官方网站公布的注册会员为3 000万,按Alex的统计数据估算每天的活跃用户数大约是1 452 000/3 000万≈4.84%。

(3)系统业务增长率为30%,系统在3年内不打算进行分库分表处理,需要系统在性能上能够支撑住,也就是隐性要求在3年内不进行数据归档,在测试时需要3年的存量数据。

(4)要求系统能够提供良好的系统体验,例如浏览帖子、发帖、回帖应该控制在3秒内。

(5)为了系统稳定,要求在日常营运时,CPU使用率<70%,磁盘Disk time<70%且无网络瓶颈。

为了方便阅读,我们以表的形式列出。

怎么解读表中的内容呢?根据第2条,日PV是2万,浏览∶发帖∶回帖=7∶1∶2,可以算出它们的业务量分别约为1.4万PV/天、0.2万PV/天、0.4万PV/天。由于发帖与回帖必须要登录,简单累加一下登录的PV约为0.6万PV/天,我们没有考虑有些用户登录后既发新帖也回复帖子,所以实际登录的PV可能会更小。为了留有余地,我们采用保守策略,可以接受把负载计算得稍微大一点。 

4)系统硬件指标

系统硬件指标对象是硬件资源,例如CPU、内存、磁盘、网络带宽等。下表列出了主要的硬件指标及阈值。这些指标比较抽象,在监控分析时应该进一步细化,例如CPU的性能指标在Linux中分为用户利用率、系统利用率及平均负载等重要指标。可以根据实际工作中的性能指标可能不同,这里仅作示例不代表统一标准,这些指标来源于非功能需求、组织要求(公司运维总结出来的可行性指标)或者行业建议标准。

2. 需求分析

需求分析的目的是确定性能测试范围,分析哪些业务纳入性能测试范围及性能指标是什么?另外要分析用户使用行为、业务分布、分析业务量;估算出TPS与并发用户数。

1)圈定测试范围

如何圈定测试范围呢?

(1)确定高频次的业务。

(2)确定对系统性能影响大的业务。

(3)确定此功能的可验证性。

例如我们使用支付宝来支付商品费用,如果余额不足,会让我们选择使用银行卡(借记卡、信用卡)来支付。这样支付宝会调用银行(银行网关系统)的接口来完成银行账户的扣减。如果银行的接口不提供支持,支付宝性能测试工程师就得想办法模拟银行网关这个过程(一般采用挡板程序来模拟,这个挡板程序又叫Mock程序),这就是一个可验证性分析及解决方案,最终采用Mock程序来配合测试。

回到上面的实例项目JForum,从需求采集的数据来看,业务量集中在登录、浏览帖子、发新帖、回复帖子4个业务上,我们圈定这些业务参与性能测试。

2)明确性能指标

(1)吞吐量(PV、TPS):每天的PV是2万,3年内增长30%,PV(每天)=2 (1+30%)^2≈3.38万。

(2)响应时间:要求3秒以内。

(3)成功率:99%以上。

(4)系统稳定性波动正常范围。

(5)其他硬件等性能指标3。

3)分析业务量

我们知道,测试数据的多少对测试结果会有影响,特别是系统处理成千万或上亿条数据时,对性能的影响更明显。我们可以想象一下,在10个人中找出1个人很容易,但如果让你在10 000个人中找到一个人就没那么容易了。

在性能测试时,我们除了需要准备一定数量的历史数据,还得关注业务量的增长。我们的实例系统JForum需求中说到年业务增长率是30%,可以理解成年PV也会增加30%。所以在测试时,我们要以第三年的业务量为标准来测试,把问题提前暴露出来。

4)计算TPS

TPS的意思是每秒平均事务数,例如,在我们的实例系统中发一个新帖就是一个事务,回一个帖子也是一个事务。为了方便衡量系统的吞吐量,我们在性能测试时常用TPS来表示吞吐量。

上面分析业务量的数据是以PV来统计的,现在我们要计算TPS,需要把PV转化成TPS。实际上一个PV就是对服务器的一次请求,我们把一个请求放在一个事务中来统计服务器的响应耗时,响应完成就是一次事务完成,即一个PV就是一个事务(PV并不能直接等同于TPS,PV代表了一次客户请求,一次请求可能请求了很多信息,例如图片、样式、JS信息等,发新帖时我们通常只关心发帖的动作耗时,并不关心页面刷新时JS、样式的耗时,此时我们就把PV等同于TPS),例如一个功能页面(浏览帖子)1秒会有10个PV,那么此功能的TPS即为10。

既然讲到PV了,下面“插播”一段关于PV的知识。大家知道,访问一个页面会有许多资源,例如图片、样式、JS信息、文字等;而我们的浏览器多数都拥有资源缓存功能,会帮我们把访问的图片、样式、JS信息等静态资源存储下来,下次访问同样资源时将不会再从远程服务器上下载,这大大加快了响应速度,也提升了用户体验。另外,如果我们在使用代理服务器访问外网资源时,多数代理服务器也会缓存这些静态内容。也就是说每次浏览器与服务器之间实际的交互数据是动态的(不同请求返回不同的响应数据)。而往往动态数据的Size(大小)会小于静态数据,所以浏览器是否缓存了静态数据对性能测试影响明显。

我们在做性能测试时,需要模拟大量的用户请求,其中就有许多的用户可能是新用户,在他们的浏览器上还没有缓存这些静态数据,为了更准确地模拟用户请求,我们有必要不缓存这些静态内容;所以性能测试中是否缓存访问的静态资源要根据业务情况而定。

我们一般要取系统业务高峰期的TPS值,虽然系统不是总处在高峰期,但高峰TPS才能代表系统的实际处理能力。例如一只木桶,它的最大容积就是装满水时水的体积,虽然平时我们不一定都装满水。要是木桶有一块短板,那么这只木桶最多就只能装到短板处,整个装水体积就受限于短板,这就是我们常打比喻的“木桶理论”,软件的性能也遵循这个理论。

要得到高峰期的TPS,我们需要分析业务发生时间,下图所示为百度推广官方网站的一天访问量统计信息,可以看到上午9~10点、14~15点是业务高峰期。

下图所示为近一月的流量统计,可以看到基本是在上午10点与15点附近业务量处于最高峰。

UV是指一天之内网站的独立访客数(以Cookie为依据),一天内同一访客多次访问网站只计算1个访客(小于等于PV)。回到实例项目JForum,我们要找出日高峰。

下表是日高峰JForum论坛的PV数据统计(业务量单位为PV)。 

 日高峰统计(单位为PV):

如果用折线图表示,可以看到业务量最高的时间段在上午10点与15点,20~21点还有一个小高峰。根据访问习惯来推理一下访问情况 

早上8:30~9:00上班,访客先看新闻,所以不会访问技术性质的论坛。

10~11点正是工作高峰时间,遇到不会的技术问题,访客会到论坛上找答案,所以此时论坛访问处在高峰。

12点后,午休时间当然是吃饭、看新闻、刷微信,技术性质的论坛访问者当然比较少。

14~15点是工作时间,访客有问题上技术论坛找答案,访问量升高。

5点后准备下班,自然访问量较低。

20~21点回到家吃完饭,一些技术牛人就上论坛回答问题了,又会有个小高峰。

总结一下,上午10点是访问高峰,PV约5 208(登录、浏览、回帖、发帖),这个时段TPS=5 208/3 600≈1.45。那么我们这样取平均值

就一定合适吗?

答案是否定的,1小时间隔还是太长,采集的业务数据并没有说明在这一个小时中吞吐量是平均的,我们还需要细分。如果我们能够细分到每分钟,那TPS的估算就更准确。另外,我们也可以采取别的办法来估算。80/20原则是广为流传的统计理论,经济学家认为20%的人拥有了社会80%的财富。在性能测试中可以这样理解,20%的时间做了80%的事情。按80/20原则计算TPS=5 208 80%/(3 600 20%)≈5.8,具体见下表。

TPS估算:

TPS不但帮我们量化了系统性能指标,还可以帮我们计算并发用户数。 

5)分析系统协议

分析完了上述内容,我们开始规划性能测试脚本的实现方式。一般性能测试脚本可以采取录制或者手动开发的方式来完成。录制方式对协议的依赖性相当强,因为录制的方式多数是针对协议来的。

我们一般是先分析被测系统协议,再评估用什么辅助工具来完成脚本录制。例如HTTP协议,我们可用JMeter进行录制,也可以手动开发,当然很多人也会选择LoadRunner(简单、易用但费用高)。我们可以运用JMeter JavaRequest元件与JUnit元件测试Java接口。

那么如何分析呢?

(1)我们可以向开发团队咨询,了解程序的架构与协议。

(2)我们可以进行截包分析,常用的截包工具有HttpWatch、Wireshark、Omnipeek等,这些工具都比较易用。HttpWatch主要对HTTP协议进行分析。Wireshark是目前应用最广泛的网络封包分析软件之一,支持市面上大多数的协议(HTTP、HTTPS、TCP、UDP等)。

Omnipeek能够侦听多种网络通信协议,执行管理、监控、分析、除错及最佳化的工作。

3. 并发数计算

首先要明白为什么要计算并发用户数。在性能测试时,我们通常会遇到这样的性能测试需求:

系统要满足多少(如1万)人在线;

系统要满足多少(如1万)人并发。

我们知道,用户在线时是否操作业务,或操作的业务不同,对系统产生的压力是不同的,所以单纯地用一个在线用户数或者并发用户数评判性能是不准确的。

计算并发用户数需要参照业务模型来计算每个业务场景需要多少用户、业务量的匹配关系等。并发数的计算说到底还是一个估算,不可能准确地模拟实际用户操作,在性能测试执行时需要根据实际情况调整。

衡量性能指标还是要参照TPS实际达到了多少,响应时间是多少,系统硬件(CPU、内存等)指标是否在限定范围内等参数。

业内常用的并发数估算方法有以下3种:

  1. 由TPS来估算并发数;
  2. 由在线活动用户数来估算并发数;
  3. 根据经验来估算并发数。

我们在此用第一种方法示范。

TPS反映了系统在每秒可以执行成功的事务数,是由事务数除时间得来,事务由用户完成,所以可以总结出如下公式:

Vu(业务名称)=TPS(业务名称)(RunTime+ThinkTime)

注:Vu(业务名称)表示此业务的虚拟用户数。

RunTime是测试程序/脚本运行(迭代)一次所消耗的时间,包括事务时间+非事务时间。

下面是一个脚本的伪代码(T1、TT、AT代表时间,单位为秒)。

在测试计划中,Login事务时间=T2,我们用T(login)表示。

Sender topic发新帖事务时间=T6,我们用T(Sender topic)表示。

其中断言检查事务是否成功消耗的时间分别为AT1、AT2,这个时间是包含在事务时间中的,一般都是非常小的,可以忽略。

ThinkTime是运行过程中的思考时间(模拟用户思考或者填写表单消耗的时间),这里ThinkTime=TT1+TT2。

Runtime是迭代一次运行脚本消耗的时间,Runtime=T1+…+T7;

ThinkTime是脚本消耗外的时间,所以不包含在内;迭代一次的总时间就为Runtime+ThinkTime。每迭代一次,事务只发生一次,而迭代一次的时间会大于事务的响应时间;所以在估算时我们会把这个非事务消耗的时间加入进去。

我们计算一下Vu,设TPS为5.8(前面内容中计算出的TPS)。

(1)不包括非事务时间(ThinkTime与程序消耗时间)情况下计算Vu。

 (2)包括非事务时间情况下计算Vu。

可以看到两者之间的Vu数量相差巨大。如果我们不把Runtime与ThinkTime加进去,计算出来的12个并发用户在测试执行时很有可能无法达到TPS=5.8的目标。 

业内一般把ThinkTime设为3秒,3秒刚好符合用户在页面的停留平均时间。那么我们把上面的ThinkTime时间换成3秒。

测试需求中要求响应时间小于3秒,我们以3秒为阈值。 

下表把76个并发用户按比例分配到各业务中:

并发数估算:

注意:

实际分配时,由于有小数点向上取整,所以76个并发用户分配后是77个。

此时估算出来的Vu仅仅是一个估算值,在进行基准测试时,我们会得到相对准确的Runtime时间,同时响应时间可能会小于指标中指定的最大响应时间3秒,也可能大于3秒,此时再计算(调整)Vu数值会更合理。 

另外,根据TPS估算并发用户数还有另外一个公式:

实际上是把RunTime+ThinkTime全部归为响应时间,虽然公式写法不一样,但表达的意思没有变。

下面请大家思考一个问题,如果我们估算出来的TPS比较小,导致计算出来的Vu数值为1(有小数点时向上取整,所以不会小于1),那该怎么办? 

当计算出来的Vu数量为1时,大家知道性能测试要模拟并发的场景,如果Vu只有一个,显然不能模拟并发的场景,不能满足并发要求。

此时我们可以加大Think Time,运行更多的Vu,同时设置集合点来满足并发的要求,这样在一段时间内,事务数平均后TPS还是可以回到需求水平。例如迭代一次需要1秒(事务响应时间0.5秒+Think Time 0.5秒),那么1TPS只需要1个用户;如果Think Time变为2.5秒,1TPS就需要3个(TPS 一次迭代时间=1(2.5+0.5)=3)并发用户。

2、测试模型

先了解以下3个名词:

业务模型:业务流程、业务系统在某个时间段内运行的业务种类及其业务占比,即哪些业务在哪个时间段在运行,业务量是多少。

测试模型:从业务模型中分析和整理出来的进行测试的业务,通常是高频业务、高资源占用业务,这些业务需要具有可测性、可验证性。测试模型作为性能测试场景的依据,通常会继承业务模型的大多数业务功能,有时也会因为特殊原因无法测试(例如第三方非开源加密程序,测试程序无法模拟),测试模型中将会去掉这部分业务,或者设计替代等价方案,例如第三方系统可以用挡板程序实现。

性能测试场景:参照用户使用习惯设计负载场景,例如哪些业务的测试脚本一起运行,哪些业务有先后顺序,运行多少并发用户等。

实践项目JForum的测试模型如图所示,与业务模型无异。

3、测试计划

测试计划用来规划测试工作,那么测试计划要考虑哪些内容呢?

(1)系统概述:简述系统使命、系统功能,计划往往由一些非专业人士来审核,因此我们需要讲清楚系统是做什么的。

(2)测试环境:系统生产环境、系统测试环境、测试执行环境(就是测试负载机,我们这里是运行JMeter的机器)。测试环境对测试结果有直接影响,我们需要告诉非专业人士,这个测试结果是基于什么环境来进行测试的。例如,同样是户外跑100米,晴天当然比雨天要跑得快,晴天不用担心摩擦力不够而滑倒,“性能”得以全力释放;雨天就得“收着”跑,小心滑倒。

(3)需求分析:采集系统性能需求,确认性能测试需求范围。

(4)测试策略:表明此次性能测试将用什么样的手段来进行,例如,我们可以采用JMeter来模拟用户大并发操作,对于第三方系统(系统集成第三方系统,如上面提到的要与银行系统对接,得不到支持时,我们可以开发挡板程序来代替)我们采用挡板程序。实际上是做测试可实施性分析。

(5)测试场景:如何组合业务场景进行性能测试,例如,交通部门测试街道的流量,十字路口作为一个场景进行测试时,我们需要把行人流量与车辆流量都纳入场景,通过调整红绿灯时长间隔来提高通过率。如果有高架桥,我们就不用考虑行人流量了,因为人车已经分流了。

(6)测试准备:测试前的环境准备、数据准备。

(7)时间计划:上面进行了需求分析、测试策略制订,就可以相对合理地估算测试资源及耗时,从而合理地安排测试工作。

(8)测试组织架构:包括测试相关干系人,明确不同干系人的工作职责。例如,性能测试工程师通常是做执行工作,文档记录员帮助整理交付的文档。

(9)交付物清单:包括性能测试计划、性能测试报告、性能测试脚本等交付物。

(10)系统风险:对测试过程中可能涉及的风险加以评估,确定风险应对策略。例如,人力风险可以通过加强人力储备来完成,测试人员相互备份(相互熟知对方工作范围内的工作,一人负主要责任,另一人负次要责任,不会因为其中一人离职导致工作无人能够顶替)。

4、环境搭建

我们在调研需求时了解到的部署结构是Nginx反向代理多个JForum实例,这样JForum的处理能力会更强大一些。但是在测试时,我们可以只部署JForum单实例,这样压测时需要的负载更小,更方便测试,所以示例环境中我们使用JForum单实例、db单实例。

我们总在强调测试环境尽量要与生产环境一致,这样做矛盾吗?

我们可以这样来理解,我们让每个实例的运行环境与生产环境尽量一致,不管是硬件还是软件环境。Nginx的代理做负载均衡能够帮助横向扩展服务性能,但单个实例的性能还是一定的,所以我们还不如拿掉Nginx直接压测JForum,而且是单实例,这样针对性的压测,不管是负载量,还是问题分析,都会变得更容易。

以前我们使用物理机部署服务,后来发展到虚机部署服务,现在又开始流行容器化部署。对于性能测试来说,物理环境最易于排除资源干扰(不管是网络、存储还是CPU资源),虚机与容器在测试时受干扰概率大。通常虚机与容器所在的宿主机上都会部署多个虚机或者多个容器实例,虚机甚至还可以“超售”(分配的CPU、内存等硬件资源超出宿主机,因为是逻辑分配,可以允许“超售”),实例中的程序对于硬件资源的占用可能会干扰别的实例,所以我们在测试时可能有测试结果不稳定的现象,导致压测与分析变得困难。

那测试环境我们到底如何选呢?还是那句话——测试环境尽量要与生产环境一致。虚机也好,云主机也好,容器也好,我们尽量让测试环境与生产环境一致。例如宿主(物理机)配置相当,宿主机上的虚机数或者容器数相当。

另外,我们可以把环境处理分为两步。在性能测试初期,性能压测时尽量使用单一环境,这样便于发现、分析性能问题。待性能达到要求,或者说性能优化得已经不错了,我们可以把服务再部署到虚机或者容器中去,测试出来的结果就接近实际性能,也更接近生产环境。

容器部署已经是大势所趋,所以本次实例我们使用Docker来部署服务,这样也方便建立环境。这里是在自己的Windows机器上做了一个CentOS7的虚机,虚机具有四核和4GB内存,然后在虚机上运行Docker,下表所示是JForum测试环境List。

使用Docker容器来部署服务,先要安装Docker。为了方便大家建立环境,我们使用Docker-Compose来启停服务,并提供容器镜像与Docker-Compose配置文件。

1. Docker安装

我们使用yum来进行安装,安装命令如下:

$ sudo yum install -y yum-utils device-mapper-persistent-data lvm2
# 添加yum源
$ sudo yum-config-manager --add-repo \
https://download.docker.com/linux/centos/docker-ce.repo
# 可以设置阿里源
$ sudo yum-config-manager --add-repo http://mirrors.aliyun.com/dockerce/linux/centos/ docker-ce.repo
#更新源
sudo yum clean all && sudo yum makecache fast
# 安装docker-ce(Docker开源版本现在命名为docker-ce)
$ sudo yum install docker-ce docker-ce-cli containerd.io
#默认安装docker-ce的最新版本,可以使用下面的命令查询可安装的版本
sudo yum list docker-ce --showduplicates | sort –r
#然后使用下面命令指定版本安装
$ sudo yum install docker-ce- docker-ce-cli-
 containerd.io
# 设置操作系统自启并启动Docker
$ sudo systemctl enable docker && sudo systemctl start docker
#查看安装是否成功
# docker version

国内一些网站提供了一键安装的脚本:

# DaoCloud
curl -sSL https://get.daocloud.io/docker | sh
# 阿里源
curl –sSLhttp://acs-public-mirror.oss-cn-hangzhou.aliyuncs.com/dockerengine/internet | sh -
# Docker官方的一键安装脚本:
curl -sSL https://get.docker.com/ | sh

2. Docker-Compose安装

Docker-Compose是二进制运行,直接下载Docker-Compose到/usr/local/bin目录,命令如下。

#使用curl下载,也可以使用wget下载,没有上述命令请安装(yum install –y curl
wget)
curl https://docs.rancher.cn/download/compose/v1.24.1-docker-composeLinux-x86_64 -o /usr/local/bin/docker-compose
docker-compose version

3. 部署JForum

建立docker-compose.yaml文件,命令如下。

version: '3'
services:jforumweb:image: seling/jforum:2.1.9-oracle-jdk8-64stdin_open: truetty: trueports:- 8089:8080/tcplabels:service: jforumweblinks:- jforumdb: jforumdbjforumdb:image: seling/jforumdb:lateststdin_open: truetty: trueenvironment:MYSQL_ROOT_PASSWORD: 3.1415926volumes:- /var/lib/jforumdb:/var/lib/mysqlports:- 3308:3306/tcplabels:service: jforumdb

启动服务的命令如下:

# 启动服务
docker-compose -f docker-compose.yaml up –d
# 停止服务
docker-compose -f docker-compose.yaml stop
# 删除容器
docker-compose -f docker-compose.yaml down

启动过程:

  • 先到hub.docker.com上下载镜像。
seling/jforum:2.1.9-jdk7-64 # JForum镜像
seling/jforumdb:latest # MySQL镜像,这里提供的镜像是有存量数据的
  • 启动容器。

完成后使用http://[ip]:8089/jforum-2.1.9来访问JForum,把IP替换成自己机器的IP。

进去之后建议去后台管理(在页面的底端有一个“进入后台管理”链接)修改“系统设置”中的“论坛网址”,这样“返回首页”的链接与表情图片才正确。

5、脚本开发

前面我们已经规划了测试工作,现在来开发模拟脚本。我们将按照计划中规划好的测试模型来编写测试脚本。

下面我们手工开发浏览帖子、回复帖子的脚本。

1. 浏览帖子

浏览帖子的流程如下。

登录→随机选择论坛板块→选择帖子→浏览帖子。

本例使用Chrome的开发者工具(按F12调出)协助脚本开发,登录表单如图所示。 

(1)登录是一个post操作,登录成功后重定向到/forums/list.page,命令如下。 

returnPath: http://10.1.1.80:8089/jforum-2.1.9/forums/list.page

(2)登录页Cookie中JSESSIONID在/forums/list.page页会被保留,用户的登录状态由JSESSIONID保持,所以要添加一个HTTP Cookie Manager,此元件会记下请求过程中的Cookie信息。

登录配置如图所示: 

登录是否成功需要做断言,登录成功后会重定向到版块列表(/forums/list.page)页面,失败则会停留在登录页面。所以可以断言list.page中的内容或者断言失败页面的内容。在list.page页面会有类似“jforum-2.1.9/forums/show/page”的内容,其中代表的就是版块ID。

所以我们可以做如图所示的断言:

当登录失败后,页面显示“请输入您的用户名及密码”,所以也可以如下图所示来断言,注意要勾选Not选项,找不到“请输入您的用户名及密码”才是登录成功。 

登录成功后会重定向到版块的列表页面,我们要看帖子,先需要进入版块。因为是用脚本模拟,我们自然先要取得版块的ID,所以需要对版块ID做关联。

上图所示是登录成功后返回版块的列表页面,我们需要从类似“href="/jforum-2.1.9/forums/show/1.page”的链接中匹配到数字1。使用正则表达式提取器(Regular Expression Extractor)做关联。

取到版块ID后,就可以模拟进入版块的操作了,如果获取版块ID失败,此后的操作就无意义了,所以我们在进入版块之前先要验证一下版块ID是否正确。我们使用条件控制器(If控制器)用来判断逻辑,如果没有取到moduleId(论坛版块ID)则不用进入论坛版块(停止后续操作,进行下一次迭代)。当${moduleId_g1}取不到值时会返回字符串"${moduleId_g1}",${moduleId_g1} != "\${moduleId_g1}"用来排除无法取到值,“\”是转义字符。

下面进入帖子列表页面,如图所示是我们用${moduleId_g1}来参数化版块ID。 

进入版块后,我们要选择一个帖子查看,先需要获取到帖子ID,需要继续做关联。如下图所示,我们要匹配,从中获取.page前的数字,这个数字是帖子的ID。例如Support JForum –Please read的ID是2,这些ID就是我们要关联的数字。 

继续使用正则表达式提取器来提取,如图所示。 

检查列表获取是否成功,顺便用If控制器来控制是否浏览帖子。然后从topicId_g1中获取一条帖子进行浏览,我们用Chrome浏览器来查看一个帖子,从下图中可以看到请求链接地址是:
http://10.1.1.80:8089/jforum-2.1.9/posts/list/65361.page,其中65361是帖子保存在数据库中的ID值。我们需要关联此ID,ID来自于上一步获取的topicId_g1值。 

我们用${topicId_g1}来参数化HTTP请求,如图所示。

最后检查浏览是否成功,选择了响应页面中的链接来断言(图中正则表达式“([1-9]\d*)”匹配数字)。 

2. 回复帖子

回复帖子的流程如下:

登录→随机选择论坛板块→选择帖子→查看帖子→进入回帖页面→回帖。

查看帖子脚本的部分内容,我们只需要手工编写“进入回帖页面”与“回帖”两个请求即可。

1)进入回帖页面

我们同样用Chrome浏览器来查看回帖请求的链接地址及传输内容,JForum回复帖子表单如图所示。

可以看到链接地址是http://10.1.1.80:8089/jforum-2.1.9/posts/reply/0/65361.page,而且是GET请求,需要参数化65361这个topicId_g1。

2)回帖

如图所示,下面继续用Chrome浏览器来分析表单,可以看到是POST请求,name=“action”,图中“insertSave”意思就是action=insertSave,即调用保存回帖的方法。

之后重定向到http://10.1.1.80:8089/jforum-2.1.9/posts/list/0/65361.page页面,这其实就是显示回帖内容的链接。 

参照Chrome浏览器中回帖的表单内容,我们可以参数化回帖内容,如图所示。 

3)检查回帖是否成功

当提交回帖表单后,成功则转到帖子明细页面,可以看到刚才回复的内容,响应断言用正则表达式来匹配内容,此内容匹配的是一个链接,此链接在127.0.0.1:8089/ jforum-2.1.9/
posts/list/0/120769.page#121078的响应数据中。 

6、数据准备

上一节我们在开发脚本过程中用到了用户信息,登录系统我们看到了论坛版块列表;这些是主数据,保证我们能够正常运行系统。我们进入某一个版块,然后进行发帖,产生的是业务数据。

下面我们来讨论一下数据准备要注意哪些事项。

1. 主数据准备

主数据主要包括系统正常运行时需要的配置参数及基础设置等数据,例如功能菜单、账号、论坛版块设置等。

基础数据要支持性能测试运行,就需要满足性能的需要。例如我们在需求分析中计算出来需要并发100个虚拟用户(JMeter中开启100个线程),我们至少需要准备100个以上账号,并且对账号赋予相应的权限(浏览、发帖、删除、查询),那么问题来了:

(1)为什么要准备这么多账号

我们设想一下,如果你仅用一个账号,第1个线程登录进去,还没开始发帖;第2个线程使用相同账号登录进来,正好把第1个线程的Session(客户浏览器与服务器信任的标记,有的叫sessionId,有的叫jsessionid,有的叫tokenId。

我们的实例脚本中有HTTP Cookie Manager元件,就是为了存储这个Session,正好我们的实例程序的jsessionid存储在Cookie中,我们用HTTP Cookie Manager元件来管理)冲掉,于是第1个线程就无法发送帖子了,因为它发送的jsessionid已经在服务器上不受信任,服务器上存储的是第2个线程登录时产生的jsessionid。

此时如果第3个线程以相同的账号登录进来,第2个线程的jsessionid也会被冲掉。如此周而复始,也许一个帖子也发送不成功,因此我们就得准备足够多的账号。

另外,如果我们要按账号来统计发帖量并排序,一个用户与多个用户之间还是有比较大的性能差距的。例如100人发帖,在统计时要对100个用户的发帖进行分组排序统计;而1个用户发帖则不用进行分组及排序,直接统计就可以了。其消耗的时间与资源会少很多,用户直观感觉就是响应更快。因此我们也得准备足够多的账号。

下面是准备账号的SQL参考代码,直接在MySQL的查询分析器上保存为存储过程然后运行,即可直接生成400个账号(大家可以修改账号)。

准备账号的SQL参考代码:

BEGINDECLARE userName VARCHAR(20);DECLARE userMail VARCHAR(20);DECLARE i INT DEFAULT 1;WHILE i<= 400 DOset userName = CONCAT('test',LPAD(i,3,'0'));set userMail = CONCAT(userName,'@test.com');INSERT INTO jforum_users VALUES (i, '1', userName,'823da4223e46ec671a10ea13d7823534', '0', '0',null, '2015-05-06 09:33:18', null, '0', '', null, '', '%d/%M/%Y %H:%i','0', '0', null, null, '0', '1','0', '1', '1', '1', '1', '1', '1', '0', '0', '1', '1', '0', null, '0',userMail,null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, '1', null, null, null);INSERT INTO jforum_user_groups VALUES(3,i);SET i = i+1;END WHILE;
END

jforum_users为用户表,记录账号信息。

jforum_users_groups为权限组表,记录账号对应的权限组;权限组中包括权限(功能菜单及论坛版块),拥有相应权限才可以查看相应的论坛版块。

(2)准备多少数据够用

下图所示为一个RT、TPS与线程数的变化趋势图,性能测试需求往往会要求我们对系统性能进行定容定量,所以我们在测试中会经历如图所示的测试过程;我们需要跨过③这个点直到④这个点,也就是至少要准备④这个点对应数量的用户账号。

另外,为了更真实地模拟用户使用情况,可以使用尽可能多的模拟用户,通过ThinkTime来调节这些模拟用户产生的负载(控制请求数量,从而调整TPS大小)大小。 

2. 数据制作方法

测试数据准备过程中可以使用工具,也可以使用SQL或者存储过程来完成数据生成。个人建议初学者运用SQL或者存储过程来生成数据,一方面加强SQL的技术积累,另一方面通过直接操作数据库来熟悉数据库的物理设计(索引、字段、范式、反范式等)和ER关系。

先来看一下表结构,下图所示为JForum主要数据表的结构。 

  • jforum_forums:记录论坛版块。
  • jforum_topics:记录发帖标题(帖子ID,多少人浏览,第一次回帖ID,最后一次回帖ID等)。
  • jforum_posts:记录发帖及回帖状态。
  • jforum_posts_text:记录发帖及回帖内容。

下面是增加版面的SQL语句。其中的数字5是categories_id(版块ID,就是大的论坛类别ID),可以人工指定也可以由MySQL自动分配,建议人工指定(比较省事的做法,方便在其他地方引用),一般版块ID也不会太多;数字10是版面forum_id,同样可以人工指定或由MySQL自动分配,建议人工指定。

增加版面:

-- 版块添加(需要授权并重启JForum系统)
INSERT INTO jforum_categories VALUES (5,'持续集成',5,0);
-- 增加版面(需要授权并重启JForum系统)
INSERT INTO jforum_forums VALUES(10,5,'Test Forum','This is a test forum',10,1,1,0);

下面是新增帖子的SQL语句。

新增帖子:

BEGINDECLARE topicId INT;DECLARE postId INT;DECLARE topicCount INT;DECLARE userId INT;DECLARE forumId INT;DECLARE createDate datetime;DECLARE i INT DEFAULT 1000053; -- 定义开始生成的帖子topicIdWHILE i<=1000055 DO -- 定义帖子结束的topicId/*--插入用户ID,我们生成的ID是1~400,下面是13~373的ID*/set userId = 13+ceil(rand()*360);set createDate = DATE_ADD(now(),INTERVAL -3 DAY); -- 构造一个时间set createDate = DATE_ADD(now(),INTERVAL -3 HOUR);set forumId =1+ceil(rand()*9);SET topicId=i;SET postId=i;
-- 增加帖子
-- forum_id是帖子版块ID,版块数量可以直接从基础表中获取,测试库中是1~10
INSERT INTO jforum_topics VALUES (topicId, forumId,CONCAT(topicId,'Welcome to
JForum'),userId,createDate,1,0,0,0,0,i,i,0, 0);
-- 帖子信息
INSERT INTO jforum_posts VALUES (postId,topicId,forumId,userId,createDate, '127.0.0.1',1,0,1,1,null,0,1,0,0);
-- 帖子内容
INSERT INTO jforum_posts_text VALUES (postId,'[b][color=blue][size=18]Congratulations:
!: [/size][/color][/b]\nYou have completed the installation, and JForum is up and running.
\n\nTo start administering the board, login as [i]Admin / [/i] and access the [b][url=/admBase/login.page]Admin Control Panel[/url][/b]
using the link that shows up in the bottom of the page. There you will be able to create
Categories, Forums and much more :D \n\nFor more information and support, please refer to the following pages:\n\n:arrow: Community forum:
http://www.jforum.net/community.jsp\n:arrow: Documentation: http://www.jforum.net/doc\n\nThank you for
choosing JForum.\n\n
[url=http: //www.jforum.net/doc/Team]The JForum Team[/url]\n\n','Welcome to JForum');SET i = i+1;END WHILE;
END

在新增帖子的程序中,我们编写了一个简单的存储过程来生成测试数据,可以看到我们定义了多个变量,它们用来动态生成数据。

i用来控制循环生成多少帖子:

DECLARE topicId INT

topicId是帖子ID,实际是用i的值填充的,为了让大家看得清楚,我们特别定义了这个变量,Set topicId=i用来对它赋值。

DECLARE postId INTpostId是回帖ID,实际是用i的值填充。

DECLARE topicCount INT;

WHILE i<=1000055 DO用来控制循环次数,1000055减i的初始值来控制循环的次数。

DECLARE userId INT;

userId是发帖人ID,在用户表中查询到ID是1~400,所以随机获取这些ID。即userId = 13+ceil(rand()*360),我们取13~373的ID。

DECLARE createDate datetime;

createDate是提交时间。createDate = DATE_ADD(now(),INTERVAL -3 DAY)用来设置天,
createDate = DATE_ADD(now(),INTERVAL -3 HOUR)用来设置小时。

postSubject中的内容比较杂乱,实际上是下图所示的发帖内容。

这样,我们就可以随机生成帖子信息了。重启一下JForum论坛,然后进入版块,此时会出现一个非常奇怪的问题,页面上新增加的帖子数量只有20个,手动增加第21个,看不到翻页链接。

为什么呢?

因为jforum_forums中有forum_topics与forum_last_post_id两个字段,前者记录帖子总量,后者记录最大的帖子ID,翻页的计算与之有关。

肯会问是如何知道的?

当然是分析出来的,由于看不到翻页,逆向分析数据库表就得到了;当然还可以通过分析程序源代码来知道,实际项目中就不用分析了,找相关开发人员了解即可。这个时候我们就只有修改forum_topics与forum_last_post_id这两个字段了。 

下图中的SQL语句用于统计每个论坛版块的帖子数量及最大的topic_id,回写到表中,然后重启JForum就可以正常看到帖子并且翻页。 

这样就满足数据制作要求了吗?

没有,我们只生成了发帖的数据,似乎忘记了回帖的数据。从前面的需求可知,平均回帖数量是发帖数量的2倍。为了简单一些,假设每个新帖2个回帖,修改一下下面所示的回帖脚本。

回帖脚本:

BEGINDECLARE topicId INT;DECLARE postId INT;DECLARE topicCount INT;DECLARE userId INT;DECLARE forumId INT;DECLARE createDate datetime;DECLARE i INT DEFAULT 1000053; -- 定义开始生成的帖子WHILE i<=1000055 DO/*--插入用户ID,我们生成的ID是1~400,下面是13~373的ID*/set userId = 13+ceil(rand()*360);set createDate = DATE_ADD(now(),INTERVAL -3 DAY); -- 构造一个时间set createDate = DATE_ADD(now(),INTERVAL -3 HOUR);set forumId = 1+ceil(rand()*9);SET topicId=i;SET postId=i;
-- 增加帖子
-- forum_id是帖子版块ID,版块数量可以直接从基础表中获取,测试库中是1~10
INSERT INTO jforum_topics VALUES (topicId, forumId,CONCAT(topicId,'Welcome to JForum'),userId,createDate,1,0,0,0,0,i,i+2,0, 0);
-- 帖子信息
INSERT INTO jforum_posts VALUES (postId,topicId, forumId,userId,createDate,'127.0.0.1',1,0,1,1,null,0,1,0,0);
-- 第一个回帖
INSERT INTO jforum_posts VALUES (i+1,topicId, forumId,userId,createDate,'127.0.0.1',1,0,1,1,null,0,1,0,0);
-- 第二个回帖
INSERT INTO jforum_posts VALUES (i+2,topicId, forumId,userId,createDate,'127.0.0.1',1,0,1,1,null,0,1,0,0);
-- 帖子内容
INSERT INTO jforum_posts_text VALUES (postId,'[b][color=blue][size=18]Congratulations :!: [/size][/color][/b]\nYou have completed the installation, and JForum is up and running. \n\nTo start administering the board, login as [i]Admin / 
[/i] and access the [b][url=/admBase/login.page]Admin Control Panel[/url]
[/b] using the link that shows up in the bottom of the page. There you will be able to create Categories, Forums and much more :D \n\nFor more information and support, please refer to the following pages:\n\n:arrow: Community forum: http://www.jforum.net/community.jsp\n:arrow: Documentation: http://www.jforum.net/doc\n\nThank you for choosing JForum.\n\n[url=http://www.jforum.net/doc/Team]The JForum Team[/url]\n\n','Welcome to JForum');
-- 第一个回帖内容
INSERT INTO jforum_posts_text VALUES (i+1,'[b][color=blue][size=18]Congratulations :!: [/size][/color][/b]\nYou have completed the installation, and JForum is up and running. \n\nTo start administering the board, login as [i]Admin / [/i] and access the [b][url=/admBase/login.page]Admin Control Panel[/url]
[/b] using the link that shows up in the bottom of the page. There you will be able to create Categories, Forums and much more :D \n\nFor more information and support, please refer to the following pages:\n\n:arrow: Community forum: http://www.jforum.net/community.jsp\n:arrow: Documentation: http://www.jforum.net/doc\n\nThank you for choosing JForum.\n\n[url=http://www.jforum.net/doc/Team]The JForum Team[/url]\n\n','Welcome to JForum');
-- 第二个回帖内容
INSERT INTO jforum_posts_text VALUES (i+2,'[b][color=blue][size=18]Congratulations :!: [/size][/color][/b]\nYou have completed the installation, and JForum is up and running. \n\nTo start administering the board, login as [i]Admin / 
[/i] and access the [b][url=/admBase/login.page]Admin Control Panel[/url]
[/b] using the link that shows up in the bottom of the page. There you will be able to create Categories, Forums and much more :D \n\nFor more information and support, please refer to the following pages:\n\n:arrow: Community forum: http://www.jforum.net/community.jsp\n:arrow: Documentation: http://www.jforum.net/doc\n\nThank you for choosing JForum.\n\n[url=http://www.jforum.net/doc/Team]The JForum Team[/url]\n\n','Welcome to JForum');SET i = i+3;END WHILE;
END

可以看到上述程序中“第一个回帖”“第二个回帖”“第一个回帖内容”“第二个回帖内容”部分。

多了一个i+1与i+2,分别是第一个回帖与第二个回帖的post_id,在jforum_topics表中要记录回帖的ID,分别是topic_first_post_id、topic_last_post_id。如果没有回帖,这两个字段中填写post_id;如果有回帖,topic_last_post_id字段填最后一次回帖的post_id(如图所示,topic_first_post_id,topic_last_post_id被记录)。

是不是很简单?制作数据并不是一件难事,大家可以自行编写简单、高效的SQL语句完成。 

7、场景设计与实现

1. 场景设计

在建立测试模型时已经确定了测试的业务种类,场景设计是组织虚拟用户、组合业务种类到一个测试单元,根据测试模型与测试目标,整理出下表所示的测试场景。

Sec_101基准测试:主要用来验证测试环境、验证脚本正确性、得到系统的性能基准,为后续的测试执行提供参考。基准测试采用单业务场景、单用户的方式来执行脚本;执行时长视响应时间调整,测试结果采样样本尽量大(例如响应时间1秒,1 000个事务就需要运行1 000秒以上;响应时间200毫秒,运行600秒就可以完成3 000个事务的采样)。

Sec_102配置测试:帮助分析系统相关性能配置,确保系统配置适合于当前性能需求,一般场景为混合场景(多个业务同时执行)。测试过程是一个实验过程,先找出不合理配置,然后进行修改,最后进行验证;如此周而复始直到配置满足要求。

Sec_103负载测试:负载测试的目的是帮助我们找出性能问题与风险,对系统进行定容定量,分析系统性能变化趋势;为系统优化、性能调整提供数据支撑。负载测试在执行时又分为单场景与混合场景。单场景有利于分析性能问题,因为排除了其他业务的干扰;混合场景更贴近于用户实际使用习惯,是一个综合的性能评估。

建议先做单场景的性能执行工作,后做混合场景的执行工作。

如下图所示,曲线是常见的性能变化趋势图。①这个点,通常就是我们估算的满足性能需求的点;②这个点达到系统最大吞吐量,通常是系统拐点(之后性能变差);③这个点是系统已经过载,吞吐量已经开始减小。负载测试原则上需要找出这3个点。

在负载测试执行时找出这3个点比较麻烦,常常会因为一些配置、程序问题而受到干扰。通常找出这3个点需要很多次的测试执行,所以测试执行也是一个耗时的工作。 

Sec_104稳定性测试:稳定性测试的目的是验证在当前软硬件环境下,长时间运行一定负载,确定系统在满足性能指标的前提下是否运行稳定,执行时采用混合场景。按惯例要求执行时间不低于8小时,在此我们计划运行12小时。

稳定性测试原则上是时间越长越好,有些隐藏较深的诸如内存溢出的问题是需要长时间运行才能反映出来的。

注:实例JForum系统场景比较简单,直接把多个业务组织一起即可;实际工作中会遇到一些场景复杂的业务。例如WMS(仓库管理系统)中都有盘点功能,此功能就不应该与日常功能混合在一起,因为盘点通常都是一月一次,所以组织场景时尽量要与实际业务情况一致。 

2. 场景实现

已经规划好了测试场景,下面我们尝试在JMeter中来设置。以配置测试为例,这里提供如下方式来实现上面的场景。

(1)只运行一个线程组实现基准测试场景

通过计算可以得到登录/浏览帖子/发新帖/回复帖子的比例为20∶40∶7∶10,约为3∶6∶1∶1.5,差不多每3次登录会浏览6个帖子,发送一次新帖,回复1.5次帖子。这个1.5次就不好控制了,我们需
要用点小技巧来完成。如图7-43所示,用If控制器配合线程迭代记数来控制每迭代3次发送一个新帖,即${_counter(true,)}%3==0,_counter 是迭代计数器,支持多线程(大家可以理解成多用户),这个计数器可以分开记录线程迭代的次数。我们就是用它来记录迭代次数,然后与3取余,如果是3的整数倍则运行一次发帖,也就是3次迭代发一次帖。

JMeter 5中的“如果(if)控制器”使用表达时建议使用jexl脚本或者groovy脚本,我们在这里使用groovy脚本,“如果(if)控制器”中可以引用JMeter内置的函数,我们使用_counter计数器。

同样,是否回帖也用此方法来控制,如图7-44所示,每迭代3次产生1.5个回帖操作(就是2∶1的关系)。这种情况比较简单,那大家思考一下,如果是每迭代3次会产生2次回帖,如何控制? 

每迭代3次有2次回帖,我们可以用${counter(true,)}%2==1||${counter(true,)}%3==0来控制,一个条件无法满足,我们用组合的方式来完成,刚好是每3次迭代产生2次回帖。可以看到这完全是个数学问题,这里的处理也只是一个近似值,权当抛砖引玉。

另外还有一个麻烦问题。

登录业务在迭代时每次要取不同的账号,这个容易实现,但是JForum系统对用户Session有控制,每个账号只能有一个Session在线,所以要控制线程与账号一一对应。当并发线程增多时就可能存在两个线程取到同一个账号,后登录的账号会把先登录的账号“踢出去”,导致发帖、回帖失败。而JMeter不能保证每个线程取到的参数(账号)唯一(LoadRunner可以做到)。

曾经扩展了CSV Data Set Config元件,给CSV组件添加Unique选项,此选项取值True时用来保证每个线程取不同的参数,且能够循环取值。 

例如,把登录功能与回帖、发帖功能并列,用一些账号专门来测试登录,其他账号只登录一次,然后循环测试回帖、发帖功能。这种变通就有点与实际业务模型偏离了,会导致少数账号生成的数据多且不均,一些统计功能可能会有性能偏差(本例没什么统计功能)。

例如,可以让用户账号有规则,每个线程绑定一定范围的账号,0号线程绑定test0~test9共10个账号,99号线程绑定test990~test999共10个账号,使用Beanshell前置处理器来拼装,每次修改step来控制线程要绑定的账号数量。userAcct是账号变量,可以直接在HttpSampler中${loginAcct}调用,至于密码我们可以固定一个字串。 

采用一个线程组来控制场景的优势是:参数化容易,例如,账号参数化时我们只需要一次配置,要调整线程数时,也只需要调整一个地方。

采用一个线程组来控制场景的劣势是:脚本为顺序执行,相互之间有影响,最慢的业务决定整个性能水平,脚本的复杂度高。

(2)运行多个线程组实现配置测试场景

下图中设计了多个线程组,分别是查看帖子、回复帖子、发送新帖操作。通过并发数计算我们知道它们并发数不等,所以要分开设计场景。

在实现场景之前,先搞清楚业务关联关系(登录/浏览帖子/发新帖/回复帖子的比例为20∶40∶7∶10),发帖与回帖时需要登录,回帖之前会浏览帖子,浏览帖子是可以不用登录的,发帖与回帖的并发数小于登录,所以部分用户是登录后只浏览帖子。按20∶40∶7∶10的比例来
算,回帖用户10个,发新帖用户7个,回帖与发帖都需要登录,这样登录已经有17个用户,还需要3个用户,可以安排3个用户登录后浏览帖子,最后还需要27个浏览帖子的用户。27个用户的来源是40个浏览用户减10个回帖用户(回帖前会登录及浏览帖子),再减3个登录后浏览的用户。

下图是回帖线程组设置,负载分3个阶段加载,分别是并发10个、20个、30个线程。 

下图是发帖线程组设置,负载分3个阶段加载,分别是并发7个、14个、21个线程。

下图是浏览帖子线程组设置,负载分 3 个阶段加载,分别是并发27个、54个、81个线程。

此方式的优势是:

3个线程组互不干扰,独立设置(3个线程组的并发用户之和刚好与只运行一个线程组的场景相等),简单明了,易于维护 

此方式的劣势是:

由于3个线程组分开设置,相当于3个不同的脚本,所以参数化都需要分开,而且登录账号同样不能有冲突,因此可以把用户的参数文件分成3份,每个线程组一份。虽然JMeter也支持多个线程组共用一份参数文件,但是不能保证每个线程取到参数的唯一性,同线程组中的参数也不保证唯一性。(要保证唯一性可以参考上面的Beanshell,也可以扩展一下JMeter的CSV组件。)

如果要减少业务之间的影响(最慢的业务决定整个业务的吞吐量),可以选择多线程组的方案。

(3)负载场景设计

以只运行一个线程组为例来设置负载场景。

下图是一个典型的负载场景,分3个阶段运行负载。

第一阶段只运行77个并发用户,运行10分钟。

第二阶段再加上77个并发用户,共计154个用户,运行10分钟。

第三阶段再加上77个并发用户,共计231个用户,运行10分钟。

这种场景帮助我们来定容定量的测试,最终测试结果整理成下图所示的性能变化趋势曲线图。

当然,在测试执行过程中没有这么巧合,不是测试3个点就可以得到结果曲线,常常需不断地试验,切换不同的并发数才可以得到这个曲线。测试过程中结果不稳定是常有的事,这就需要运行更长的时间,尽量取到一个稳定的结果。由此可见,测试执行是一个反反复复的过程,考验耐心。 

上面我们把测试执行时间定为10分钟?这样合理吗?

测试执行时间长度当然是越长越好,完成的事务(业务或者交易)越多,样本数据就越多,结果才会更准确。但是时间是有成本的,我们执行测试的原则是在成本范围内尽量执行时间够长,结果趋于稳定(TPS及响应时间趋于稳定)。

例如响应时间为100毫秒,并发100个用户,我们执行10分钟大概可以采样60万个样本(完成60万个事务),如果TPS与响应时间趋于稳定(值的偏差不大),这个执行时长是可接受的;例如响应时间是10秒,并发100个用户,10分钟大概是6 000个样本,相比较而言,这个采样数据就不够多,执行时间也许还应该加长一些。

如果脱离了量谈结果,偶发性风险就会增强,结果很可能不可靠。

8、测试监控

经过漫长的准备工作,即将进入测试执行阶段,执行过程中我们通过监控结果来分析系统性能。首先我们要搞清楚监控哪些指标?用什么工具来监控?如何进行分析?

内容涉及Linux操作系统、Windows操作系统、Tomcat、MySQL、JVM等。

JForum由容器部署,宿主操作系统是CentOS7,容器基础镜像是CentOS7/JDK 8 64位/ Tomcat 7,数据库MySQL也运行在容器中,数据挂载到宿主机目录。

需要关注的性能项如表所示:

我们使用Navicat Monitor进行MySQL的监控,主机监控使用CentOS下的top、vmstat、ps、netstat等命令,JVM使用jstat、jstack、jps等命令进行监控。

在测试环境下,MySQL监控有警告信息,如图所示,主要是索引问题引起的。 

以“使用索引的行”这条警告为例,Navicat Monitor给出了说明。 

我们顺着这些警告信息可以对MySQL先进行一轮优化配置,例如,加大tmp_table_size来减少磁盘临时表;加大key_buffer_size来扩大MyISAM的数据缓存,增加内存命中率;扩大Innodb_buffer_pool_pages_total来提高InnoDB缓冲池的命中率等。

如果对MySQL优化无经验,可以通过网络搜索相关指标,试验性地进行修改。 

9、测试执行

前面我们都是在做测试执行的准备工作,现在我们要开始测试执行。一般第三方性能测试会有一个测试准入条件的验证(一般做成CheckList,检查各项准备工作是否准备好)。如果是,项目组内的性能测试执行就没有这么严格了,但基本内容不会变。

基本内容包括但不限于下面这些项目:

(1)检查网络环境

确保环境独立,不会对生产系统、外部系统等的使用造成影响。

确保环境可靠,不会因为生产系统、外部系统等而影响测试结果。

为了方便分析问题,排除网络IO的影响,测试会在局域网中进行。

(2)检查测试数据

确保基础数据完整,能够支持性能测试脚本对业务功能的覆盖。

确保基础数据量,能够支持性能测试脚本的参数化要求。

确保存量数据量,能够尽量真实反映系统数据环境。

确保存量数据分布,能够对结果施加有意义的影响。

(3)检查监控设备

监控工具是否已经准备完毕并可用。

(4)脚本检查

确认脚本能够模拟业务场景。

确认脚本无性能风险,不影响测试结果。

下面我们根据测试场景来进行性能测试执行:

1. 基准测试

1)测试场景

下表所示的基准测试场景,我们采用的是单业务场景、单用户的方式来执行脚本,基准测试目的在于验证测试环境、验证脚本,得到一个性能基准。

对于思考时间的设置,之前业内有一个不成文的规定,在不可定情况下就设置成3秒,从现实体验看设置3秒不妥,设置1秒也许更合适。

为什么建议最好要给一个思考时间呢?

一方面,要想达到高负载,通常来说并发数要更多,这样与服务器的连接就会多,有助于触碰到服务器或者负载机的连接数限制,考验网络连接能力。

另一方面,思考时间对于服务器来说不是为了减轻压力。大量负载情况下,对于服务器来说量聚而引起的压力根本就没有什么思考时间可谈。反倒是思考时间给了负载机释缓性能的时间。当服务响应时间很小(例如几毫秒)时,高负载情况下可能有丢包的风险,加上响应时间可以过渡一下,让当前迭代过程中的包接收完整。

因此,给大家两个建议:

  • 思考时间还是要有;
  • 尽量用更多的并发去压测,例如思考时间放大一点,并发就多一点。单JMeter实例线程数建议200个左右,视用户的脚本复杂度而定,脚本就一个sampler,自然可以把线程适当放多一点。脚本庞大到几兆字节,也许100个线程时,堆(JMeter 5默认堆大小1GB)就差不多满了。当然堆大小可设置,我的做法是宁可开启多个JMeter实例也不去改变堆的大小(参考微服务的处理方式),尽量让JMeter实例的运行保持高效;

这里把思考时间设为10毫秒是考虑在有限的时间内、有限的可用内存情况下尽量完成更多的业务,大家可以根据实际情况调整思考时间大小;另外,测试执行时间尽可能长,这里示例10分钟,您可以自己调整,采样更多,结果更稳定准确。

2)测试执行&测试结果

一般来说,单个线程执行性能不会差,如果正好遇到基准测试时响应时间较长的业务就要进行分析。基准测试尽量执行多次,取相对稳定的结果。通常建议执行3次以上,下面是基准测试的结果。

(1)聚合报告

(2)响应时间(RT)

响应时间如图所示,可以看到各业务的响应时间都比较小,90%都能控制在32毫秒以下;“回帖”操作的响应时间有异常点,这些异常呈周期性分布。通常我们要分析这些异常点,如果呈周期性或者偶然出现且不影响TPS,是可以接受的;如果异常点比较多,直接影响到TPS就需要进行分析了。为什么如此不稳定?

周期性出现的异常通常都是一些作业任务造成的。如果你对程序不熟悉,就请开发团队告诉你系统有哪些作业任务,这些作业任务的频度是否适中。基准测试中,JForum响应时间出现异常的原因是JForum要构建全文搜索索引,此时会进行大量的文件IO。

下图所示是3年前实验环境中运行同样脚本时的响应时间图,此次主机是i7四核8线程,3年前是i5双核4线程,但本例中磁盘变为了虚拟盘,性能会差一些。可以看到响应时间差还是比较
大的,但测试结果的周期性现象还是一样的,也就是趋势一致。

从整个基准测试结果来看,单用户响应时间较快,事务成功率100%,测试环境检查通过,脚本检查通过。建议大家并发两个或两个以上用户运行一下,有些脚本的问题在并发时才显现出来。 

2. 配置测试

配置测试时,通过制造的负载来检测配置的合理性,先明确配置测试目标方向,我们压测的JForum是一个Java应用,通常这样的服务要考虑下面的配置。

(1)JVM配置:优化JVM配置。

(2)Tomcat线程池配置:确定一个较合理的Tomcat配置。

(3)数据库连接池配置:确定一个合理的连接池配置。

(4)MySQL数据库的一些配置,数据库设计问题(表结构改进、索引等)。

(5)操作系统有关性能配置(可开文件数、网络连接等)。

可以看到要考虑的方面还是不少,所以在测试前我们要设计针对性的场景。因为依赖负载,所以配置测试的场景与负载测试中的负载场景会有重合的,都是模拟大量的负载来“挤兑”被测试系统。建议使用混合场景来进行压测,这样覆盖的业务广、请求的资源多,面临要调整的配置暴露得多。

对于有经验的测试工程师,个人甚至建议把配置测试与混合场景的负载测试合二为一。

可以先根据经验设置一个相对合适的配置,在此基础上进行负载测试,一些性能问题的分析自然会命中这些配置,此时再做调整与验证,这样更节省工作量。

1)测试场景

继承在场景设计中的配置测试场景,采用混合场景。在此把思考时间设置为10毫秒,由于估算并发数是按思考时间3秒来算的,所以估计我们压测时实际的负载会高很多,本例先以77个并发开始压测,后面视情况变化来进行增减并发数。

2)测试执行、分析及调优

配置测试是不断尝试的过程,且为了减少结果的偶然性,每个场景会运行多次,取相对平稳的一次结果为准(也可以综合多次结果进行算术平均)。针对场景要求,配置场景又可以分为多个子场景。

(1)JVM配置测试,这里是以JDK 1.8版本为例(目前广泛使用的版本)。

(2)Tomcat线程池配置测试。

(3)数据库连接池配置测试。

(4)MySQL数据库优化配置测试。

操作系统的有关性能配置不必单独拿出来,负载上去后我们做好主机性能指标的监控,操作系统配置导致的问题如果暴露时再去调整。

当然也可以基于经验先调优,例如,在CentOS7中我们经常会调整下列配置。 

# /etc/sysctl.conf
net.bridge.bridge-nf-call-ip6tables=1
net.bridge.bridge-nf-call-iptables=1
net.ipv4.ip_forward=1
net.ipv4.conf.all.forwarding=1
net.ipv4.neigh.default.gc_thresh1=4096
net.ipv4.neigh.default.gc_thresh2=6144
net.ipv4.neigh.default.gc_thresh3=8192
net.ipv4.neigh.default.gc_interval=60
net.ipv4.neigh.default.gc_stale_time=120
net.ipv6.conf.all.disable_ipv6=1
net.ipv6.conf.default.disable_ipv6=1
net.ipv6.conf.lo.disable_ipv6=1
vm.swappiness=0
kernel.pid_max=100000
fs.file-max=2097152
vm.max_map_count=262144# /etc/security/limits.conf
* soft nofile 65536
* hard nofile 65536
* soft nproc 65536
* hard nproc 65536
* soft memlock unlimited
* hard memlock unlimited

这些配置都是针对响应时间小、系统服务水平高、主机性能强时,为了让主机的性能发挥更好而进行配置的。

(1)JVM配置测试

我们在做配置测试前可以根据经验,先做一版优化配置,然后再在这个基础上进行调整。本示例的JVM配置,我们预设几种场景(其他配置先保持默认)。

通常来说,JVM中的Heap内存越大越好,很久才做Full GC(通常Full GC时是不响应用户请求的),但做一次时间长,如果能接受倒也没什么问题,关键是浪费内存。现在流行微服务,我们可以把大的需求拆分,某一个需求的增大,我们对应扩大实例数即可,灵活组装,就像搭积木一样。所以JVM的内存设置也不建议太大。

当然也有例外,例如,我们容器部署ElasticSearch时,还是会把JVM调到很大,16GB是常
规操作。这是因为ElasticSearch是一个搜索程序,大量数据会缓存在内存中,所以场景不同,应对的办法自然不一样。通常我们在Kubernetes中部署Java服务时会限制内存大小在2 048MB以内,当负载很大时会对服务进行自动伸缩(自动增加实例数),所以也不强求一个实例要达到多么高的性能水平。

本例计划对JVM的配置测试两种场景,Heap内存分别为1 024MB、2 048MB,演示JVM对性能的影响。垃圾回收器分3种,分别是UseParallel、CMS、G1。

最后场景组合如下表所示:

通常我们设置-Xmn为Heap内存的3/8左右,初始设置元数据为128MB/256MB(测试时视占用情况调整),使用3类垃圾回收器,比较哪个更适合。

本次实验的JForum是由Tomcat发布的,JVM的设置通过修改TOMCAT_HOME%/bin/ catalina.bat文件来实现。

示例修改如下配置:

set JAVA_OPTS=%JAVA_OPTS% %LOGGING_CONFIG%
set JAVA_OPTS=%JAVA_OPTS% -Xmx1024M -Xms1024M -Xmn384M
-XX:MaxMetaspaceSize=128M -XX:MetaspaceSize=128M
-XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps -XX:+PrintGCDetails
-XX:+PrintCommandLineFlags -Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote. local.only=false
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management. jmxremote.port=9080
-Dcom.sun.management.jmxremote.rmi.port=9080
-Djava.rmi.server. hostname=10.1.1.80
-Dcom.sun.management.jmxremote.ssl=false
-Dcom.sun.management.jmxremote -
Dcom.sun.management.jmxremote.local.only=false
-Dcom. sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.port=9080
-Dcom.sun.management.jmxremote.rmi.port=9080 -
Djava.rmi.server.hostname=10.1.1.80
-Dcom. sun.management.jmxremote.ssl=false

上述程序中前面5行语句用来开放JMX访问,10.1.1.80是本例容器所在的宿主机IP,建议加上,否则有可能在远程连接不上容器中的JVM进程。

使用JvisualVM监控可以看到配置的JVM参数:

由于我们用容器发布,可以在Docker-Compose文件中完成修改JVM。

version: '3.7'
services:jforumweb:image: seling/jforum:2.1.9-oracle-jdk8-64stdin_open: truetty: truecap_add:- SYS_PTRACEenvironment:JAVA_OPTS: -server -Xmx1024M -Xms1024M -Xmn384M -XX:MaxMetaspaceSize=128M -XX:MetaspaceSize=128M -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.port=9080 -Dcom.sun.management.jmxremote.rmi.port=9080 -Djava.rmi.server.hostname=10.1.1.80 -Dcom.sun.management.jmxremote.ssl=false #10.1.1.80是本例容器所在的宿主
机IPports:- 8089:8080/tcp- 9080:9080/tcp #jmx监听端口labels:service: jforumwebdepends_on:- jforumdbjforumdb:image: seling/jforumdb:lateststdin_open: truetty: trueenvironment:MYSQL_ROOT_PASSWORD: 3.1415926
# volumes:
# - /var/lib/jforumdb:/var/lib/mysqlports:- 3306:3306/tcplabels:service: jforumdb

上面的配置文件中,我们去掉volumes的部分,JForum的数据不用映射到宿主机目录,这个数据我们不保留。我们让5个场景都在同一水平线上开始,每个场景的初始数据都是相同的(镜像中已经包含初始化数据)。 

我们用环境变量(environment)来传递JVM的启动配置,cap_add是允许docker使用ptrace功能,这样我们就可以用jinfo –flags pid来查看JVM启动配置,从而验证配置是否生效。

如果您是使用docker run的方式启动,可以参照下面的命令:

docker run --rm -e JAVA_OPTS='-server -Xmx1024M -Xms1024M 'seling/jforum:2.1.9- oracle-jdk8-64

进入容器使用:docker exec –it [容器id] /bin/bash。在容器中我们可以看到配置已经生效,内存单位换算成了KB。

我们编写了docker-compose.yml文件,只需要docker-compose –f docker-compose.yml up –d就可以启动JForum服务。一共两个服务,分别是数据库与JForum服务。

[root@k8sm01 jvmtest-compose]# docker-compose -f docker-composeUseParallelGC.yml up -d
Creating network "jvmtest-compose_default" with the default driver
Creating jvmtest-compose_jforumdb_1 ... done
Creating jvmtest-compose_jforumweb_1 ... done

正式开始前先运行少量的负载来让系统预热一下,别忘记测试目标。现在我们关心的是哪个JVM配置下性能更好?

以TPS来衡量,我们分别执行5个场景,并发线程为77个(视用户机器环境而定,尽量让系统或者主机中某一个资源出现瓶颈,没有压力的对比是不准确的),每个场景执行10分钟以上(如果用户有时间,执行长一点结果更准确)。

下表所示是不同JVM配置不同垃圾回收器下的测试结果:

可以看到在堆为1GB时,Parallel回收器产生了3次Full GC,而且是连接3次才把内存回收下去,耗时2.27秒,自然TPS就差了一点。

Parallel时产生的Full GC:

CMS与G1回收器虽然都没有Full GC(如图所示G1回收器),但在TPS上还是有差别的。

1GB/G1时无Full GC:

 堆为2GB时,CMS与G1的差别很小,多测试几次会更准确,在这种差别下几乎是性能相当。另外比较有趣的是1GB/G1与2GB/CMS测试结果居然一样,这种小概率问题都发生了(迭代次数不一
样,响应时间也不一样),因此建议大家在实际测试中要多测试几次,取平均值。

2GB/G1时无Full GC:

从结果来看,个人偏向于使用G1垃圾回收器,前面我们总在说G1对大堆效果更好,但我们更应该相信实际测试的结果。使用G1垃圾回收器时,1GB与2GB堆大小情况下TPS差别很小,自然选择配置1GB堆内存,节省了1个GB。

从图中可以看到,以当前的77个线程,堆根本就不用2GB,平均使用不到750MB,所以目前的负载(77个线程)情况下JVM暂时没有明显瓶颈,你还可以加更大的负载来进行压测。

从图可以看到响应时间还是很小的,所以现在负载还是不够的,大胆地加负载。

对于Parallel的场景来说,现在就可以淘汰了。 我们只需要留下G1垃圾回收器的场景,然后去测试在堆内存是1GB和2GB时的性能拐点(极限),我们可以把这个放到负载测试环节去测,一次执
行带着多个目标更节省时间。本例在此直接把线程数增加到140,测试结果如上图所示,可以看到TPS反而降下来了,因为响应时间增加了不少。

我们看堆监控,内存回收没有问题,那问题出在哪儿呢?为什么性能升不上去了?

分析之前我们回顾一下诊断套路,并分析一下:

1)TPS目前已经完全能够满足性能需求,但响应时间并不高,77个并发时回帖与发帖响应时间1秒多一点,说明还有潜力可挖,降负载与升负载都可以试一下。降负载的思路是减小排队,前提是有任务在排队,所以此时可以用vmstat命令看有没有排队,或者用top命令看负载。以便降负载使系统向最优处理能力靠近。升负载出于两方面考虑:一方面,负载不够,性能还可以提升;另一方面,性能到拐点了,出现瓶颈,要开始诊断分析问题。

2)我们把并发加大到140,这时TPS降下去了,已经是在拐点以下。因此可以再试着测一下110个并发。如果110个并发也是下降,可以减到90个并发左右。这个过程可以帮助我们找到最大TPS,这也是一个不断尝试的过程。其实这个过程放到负载测试中会更好一点,配置测试如果这么详细,工作量巨大。

3)分析并发140时的结果,发现TPS下降,要找下降原因。先看监控,如下图所示是主机监控,可以看到MySQL与Java进程占了主机主要的资源。由于示例环境限制,这里把MySQL与JForum(Java进程就是JForum服务)启动在同一个主机上。

此时MySQL使用的CPU占用最大,它是性能风险最大的地方,分析它的性能是必然的。另外也可以看到CPU负载比较大,达到了32.09,所以排队严重。排队时间又可以分开两块:一块是请求还没进到JVM,还在操作系统层面排队;另一块是请求进了JVM,Java程序处理时因为慢而排队。我们可以排除操作系统资源导致的排队,因为只有140个线程并发,本例的实验机器可支持65535个连接(调整过),所以我们主要去分析JVM。减少响应时间才能减少排队,需要分析“时间都去哪儿了”。

如图所示,使用vmstat命令来看一下IO情况。r列显示任务排队情况严重,所以我们的TPS就小了;bo列的值大表示从内存往外写数据,其实就是往磁盘写数据,是IO过于频繁的现象;wa列是非空闲等待,其实就是IO等待。所以我们要分析一下Java程序的IO。

经分析IO发生在JForum使用Lucene来做检索服务,在生成新帖时更新Lucene索引导致的频繁写磁盘。

IO会影响响应时间,同时也会加大CPU负担。这也解释了我们在图7-67中看到JVM中的CPU使用率不高,而从主机监控到的Java占的CPU很高,因为有排队,有IO,甚至有IO等待,这都是CPU使用负载高的原因。对于IO的诊断,可以使用iostat、lsof、du等命令帮助定位。iostat监控查看IO吞吐量,有无IO性能风险;du帮助找到大文件;lsof帮助找到哪个程序在写文件,然后从线程找到程序。

4)上一步分析了任务排队,接着还要深入分析导致排队的原因。IO导致的非空闲等待耗时,MySQL占CPU高,自然也会耗时,这会导致排队。那是不是JVM就没有问题呢?因为垃圾回收是影响响应时间的,必须要分析一下JVM。我们看一下堆的监控,如图所示,我们可以看到堆的回收没有问题,并没有Full GC;YGC比较多,这也正常,访问的线程多,业务忙,产生的新生代对象多,垃圾回收多是正常的。要不要降低YGC呢?也不是不行,可以调整一下分代的内存空间,新生代升级老年代次数,甚至新生代的大小,这是一个繁重的实验,有兴趣的朋友可以自己验证。

其实JVM官方给出的建议,以及网上网友提供的一些实践经验很不错,要想通过某一两个配置来提高性能有点撞运气。除非你的业务程序特殊,配置要专门优化,否则对于普通的应用,就不要在配置上一味追求极致,投入收入比不高。我们上面设计的场景中的配置已经是流行搭配,压测后监控到异常再去调配才是正确方法。

5)基于本例,因为业务逻辑很简单,就是新增记录而已。慢的原因很大程度在MySQL上,我们都已经看到MySQL占用CPU到了200%(占用了2个核)。

(2)Tomcat线程数配置测试

在JVM配置测试时,我们没有去调整Tomcat的线程数之类的参数,如下图所示是使用JvisualVM监控到的线程状态,可以看到线程是bio模式的(http-bio-8080-exec-x格式)。目前Tomcat的IO模式有3种(bio、nio、apr):bio是默认模式,是常说的阻塞模式,简单说就是一个接一个任务完成,前一个任务做完做下一个任务;nio是非阻塞模型,简单说就是不会“傻”等,等待的时候可以接新的任务,效率高一些,这就是异步通信。

Tomcat的连接在TOMCAT_HOME%/conf/server.xml文件中进行配置,Tomcat连接数配置如下,在这个配置中我们开放了线程池。 


Executor配置的具体参数解释如下:

  • namePrefix:给线程名加一个前缀,例如上面默认是http-bio-8080-exec-。
  • maxThreads:最大活动线程,默认是200。
  • minSpareThreads:无事可做时保持的空闲线程数,默认25。
  • maxIdleTime:无事可做就释放掉线程。
  • prestartminSpareThreads:启动线程池时,是否启动minSpareThreads个线程。
  • maxQueueSize:线程排除队列长度。
  • Connector配置的具体参数解释如下。
  • protocol:配置连接器的实现,上面我们配置的是aio的实现。
  • acceptCount:接收的请求的长度。
  • compression:是否开启压缩,这样网络传输减小。
  • compressableMimeType:哪些类的资源可以被压缩。

下面我们开始压测,场景3(堆1GB,G1回收器),线程77个。

可以看到图中所示的线程名称就是我们设置的前缀格式,实时线程数为119个,我们的线程池最大设置为500,所以这个指标没问题。

下图所示是堆的监控,它与bio模式时图形类似,TPS也基本相当。

就目前来看开放线程池并没能提高性能。通常来说,aio效果要比bio好,这是对于大负载、响应时间又慢的情况,所以我们保留aio的配置,待负载测试时看能否经受住高负载的验证。

(3)数据库连接池配置测试

在测试前我们可以先调整一下连接池的大小,测试过程中我们监控连接池大小,如图所示连接池开了6个连接,而我们设置的数据库连接池最大可以到100,所以当前负载情况下连接池没有问题。在平时的测试过程中,我们可以把连接池开大一些,测试时监控,得到一个实际数字,把这个数字提供给运维团队,在生产时可以通过限制连接池大小为系统稳定做贡献。

(4)MySQL数据库优化配置测试

MySQL的运行状况通常受下面几个因素影响:

1)优化配置:好的配置能够提高MySQL的运行效率,例如缓存、临时表空间等。

2)业务设计:良好的业务处理过程能够提高系统的效率,使MySQL的负担更轻。

3)物理设计:良好的业务数据库物理实现能够减少IO,提高响应效率,例如索引、大表的水平切分。

4)物理架构:MySQL的部署方式对MySQL的效率提升也会有影响,例如主从结构、读写分离;另外从备份、主备份也用来保障数据的安全。

测试时,首先使用默认配置压测,在压测过程中我们监控到的MySQL的主要指标如图所示,可以看到CPU、内存、连接数(目前为112个)都报警了。

 

 

注:使用Navicat monitor监控MySQL,容器方式运行:

docker run -d -p 3000:3000 navicat/navicatmonitor
  • Full table scan per second:每秒全表扫描次数,大于阈值50次/秒报警,尽量减少此类查询。
  • CPU usage:CPU使用过高
  • Memory usage:内存也使用过高,默认阈值76%,报警就是超过了阈值。
  • InnoDB buffer pool in use:查询在缓存中的命中率,小于80%就会报警,越大越好,说明直接查缓存就可以了,通过公式“1 -Innodb_buffer_pool_pages_free/Innodb_buffer_pool_pages_total”来计算,配置给innoDB_buffer_pool_size越大越好,甚至建议是主机内存的80%,当然还有别的部分需要内存。
  • MyISAM cache in use:MyISAM主要用于读多写少的应用来存储数据,查询时最佳的性能体现为MyISAM的数据全部缓存在内存中。计算公式为“1 - Key_blocks_unused * key_cache_block_size /key_buffer_size”。因此,缓解方案是增大key_buffer_size,只要内存空间多余,最好把整个索引都缓存进内存,默认阈值为80%,至少比80%要大。
  • Temp tables on disk:物理临时表,大于阈值10%就报警。计算公式为“Created_tmp_disk_tables/Created_tmp_tables”。缓解方案是增加tmp_table_size或max_heap_table_size,另外就是减少临时表产生,少做多表连接查询,少做计算。
  • Rows through full table scan:全表扫描的行,大于阈值20%就报警,减少读全表(不仅是索引),缓解方法是索引优化,查询优化,不去查全表,不去查整列数据。
  • Query cache hit ratio:查询缓存命中率,小于阈值80%报警,计算公式为“Qcache_hits/ (Qcache_hits + Com_select)”。缓解方式是加大缓存。

从监控中我们看到这么多的报警,是不是觉得性能很差呢?

接着我们去看一下得到的查询语句的分析,下图所示是查询语句的费时监控排序,是不是很意外?查询平均值并不大,没有超过两位数(毫秒)。

全表扫描的语句主要是小表,如下图,可以看到查询时间(TIME AVG MS列)都不大。时间最大的129.13毫秒是性能监控工具Navicat Monitor自己的查询,与JForum无关。这些全表查询导致的告警可以暂时不用理会。 

现在去查询与报警相关的MySQL的配置,有以下两种方式。

1)查看my.cnf配置文件。 

cat /etc/my.cnf
[mysqld]
#
# Remove leading # and set to the amount of RAM for the most important
data
# cache in MySQL. Start at 70% of total RAM for dedicated server, else
10%.
# innodb_buffer_pool_size = 128M
#
# Remove leading # to turn on a very important data integrity option:
logging
# changes to the binary log between backups.
# log_bin
#
# Remove leading # to set options mainly useful for reporting servers.
# The server defaults are faster for transactions and fast SELECTs.
# Adjust sizes as needed, experiment to find the optimal values.
# join_buffer_size = 128M
# sort_buffer_size = 2M
# read_rnd_buffer_size = 2M
skip-host-cache
skip-name-resolve
datadir=/var/lib/mysql
socket=/var/lib/mysql/mysql.sock
secure-file-priv=/var/lib/mysql-files
user=mysql
# Disabling symbolic-links is recommended to prevent assorted security
risks
symbolic-links=0
log-error=/var/log/mysqld.log
pid-file=/var/run/mysqld/mysqld.pid

my.cnf中几乎没有设置与性能相关的配置,但是在MySQL中也会有一些默认的配置,可以使用show variables、show status查看。

2)使用show variables查询。

如下图所示,我们查询了几个性能参数,innodb_buffer_pool_size约为128MB(134 217 728Byte换算过来),通过换算key_buffer_size约为8MB、max_heap_table_size和tmp_table_size约为16MB,最大连接数是151个。这些配置都不符合要求,我们都要修改一下。

MySQL优化配置如下所示(具体请参考MySQL官方网站):

[mysqld]
sort_buffer_size=32M
read_buffer_size=1M
read_rnd_buffer_size=4M
thread_cache_size=64
query_cache_size=32M
innodb_buffer_pool_size=1024M
innodb_flush_log_at_trx_commit=1
innodb_log_buffer_size=8M
innodb_log_file_size=150M
innodb_thread_concurrency=4
innodb_support_xa=0
innodb_max_dirty_pages_pct=60
max_prepared_stmt_count=32768
key_buffer_size=512M
tmp_table_size=128M
max_heap_table_size=128M
max_connections=500
datadir=/var/lib/mysql
socket=/var/lib/mysql/mysql.sock
secure-file-priv=/var/lib/mysql-files
user=mysql
symbolic-links=0
log-error=/var/log/mysqld.log
pid-file=/var/run/mysqld/mysqld.pid

重新压测,与上述场景一样有77个并发,测试结果如图所示。

总的TPS上升到了533,比前面的大约多了29个TPS,性能小幅上涨,说明优化还是有效果的,只是不太明显。CPU的利用率并没有降低,还是100%。警告消灭了,留下来的都是安全性警告。

MySQL使用CPU为100%已经是个危险信号,MySQL监控面板中显示CPU为100%,如下图,相当于我们已经压测到极限了。本来SQL的查询时间也不长,所以配置到此暂时对系统也没有什么好调整的了。

 

下面总结一下配置测试结果。从上面多个角度的实验来看,JForum程序的性能算是比较优秀的,响应时间比较快;JVM使用CMS或者G1垃圾回收器时基本无Full GC的情况出现(稳定性有待后面验证);Tomcat的配置默认情况下性能已经不错,不会造成性能短板;数据库虽然有部分的全表扫描,但都是小表的扫描,对性能无明显影响,后面加大缓存即可。

其他的性能调优建议如下:

1)Heap内存设置。如果是容器发布,建议设置1GB的堆(JVM设置如下),采用G1垃圾回收器,设置垃圾停顿阈值保证少停顿。

JVM设置:

-server –Xmx1024M -Xms1024M -XX:MaxMetaspaceSize=128M -XX:MetaspaceSize=128M -XX:+UseG1GC -XX:MaxGCPauseMillis=100 -XX:+ParallelRefProcEnabled -XX:+PrintGCDetails -XX:+PrintGCDateStamps

2)Tomcat IO模式由bio换到aio,并且启动线程池支持,本例线程池达到200足够,我们压测到极限时任务线程也不过126个。

3)当前测试场景下数据库连接池保持默认设置,无须扩大。

4)MySQL需要配置更大的缓存(Innodb_buffer_pool_size与key_buffer_szie)。

3. 负载测试

(1)测试场

负载测试的目的是帮助我们找出性能问题与风险,对系统进行定容定量,为系统优化、性能调整提供数据支撑。负载测试在执行时又分为单场景与混合场景:单场景有利于分析性能问题,因为排除了其他业务的干扰;混合场景更贴近于用户实际使用习惯,是一个综合的性能评估。

建议先做单场景的性能执行工作,后做混合场景的执行工作。

上面的配置测试已经帮我们找出了相对合适的配置,在负载测试开始之前我们把配置调整到建议值,在测试执行时密切关注与这些配置相关的监控数据,可以针对影响性能的配置再次调整。

负载测试时,我们需要变化负载量来对JForum服务器进行压测,下表列出了几种不同的负载当量。

与配置测试一样,这里把思考时间设为10毫秒,实际运行的线程数会有调整,一切视实际性能表现而定,我们的目标是要得到下图所示的性能变化趋势图,要知道系统性能的拐点(图中的标记①)。

(2)单场景测试执行、分析及调优

执行时我们要得到一个类似上图所示的性能曲线,这需要不断尝试不同的负载,经过多次执行,我们得到了如图所示的测试结果。

吞吐量测试结果:

可以看到每个业务的性能曲线不一样,性能拐点各异。图上看到只有几个点,实际在压测时需要不断尝试,可能与我们的计划场景中的并发数大相径庭,所谓实践检验真理。 

并发154个线程时,JMeter报出异常错误信息(见下面连接异常信息),同时TPS也不稳定,监控到大量的TIME_WAIT状态的TCP/IP连接(netstat -an|findstr“TIME_WAIT”)、失败事务增多、JForum无法访问等现象,等待一段时间后又可以访问。

这是因为JMeter运行在Windows机器上(而且我们的测试环境也是在Windows上面建立的CentOS虚机),TCP连接释放慢导致连接不够用,请求被迫排队,此时有部分连接失败。解决办法是减小TIME_WAIT连接释放时间,让TCP连接释放更快,具体操作是修改注册表。

启动注册表找到HKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/Services/Tcpip/Parameters,在Parameters目录下建立两个REG_DWORD值,如图所示。

建立TcpTimedWaitDelay,十进制值30,表示等待30秒释放;MaxUserPort十进制值65 534,表示可用的Socket连接放大到最大65 534,如果想防止攻击可以限制TCP连接数,可以把MaxUserPort设置一个合适的值。

连接异常信息:

ERROR o.a.j.p.h.s.HTTPJavaImpl: Cause: java.net.BindException: Address
already in use: connect

在执行回帖(发帖)操作时,响应时间较长,监控线程发现较多的BLOCKED状态,Dump线程栈(线程监控)显示下图所示的信息,意思是回帖(新增帖子)后进行全文搜索索引的重建,此时会进行全表扫描,所以效率会受影响。如果想要提高效率,可以把索引建立作为周期性工作,使用一个作业线程来完成,且15分钟一次。

在执行过程中监控到MySQL服务中的CPU负载持续满载,load average达到20以上,由此可见MySQL在此处是有等待的,MySQL的CPU瓶颈明显。

 

IO风险相对CPU要小,磁盘速率能够提高会更好。

上图也是对MySQL的监控(使用Navicat Monitor),可以看到优化配置(配置测试部分)后缓存命中率上去了,InnoDB Cache Hit是100%,MyISAM Key Cache Hit是95.07%,这已经是比较高的了,线程缓存由于有数据库线程池,而且连接不多,不用担心。

我们设置Total InnoDB Buffer Pool为1.07GB,可以看到其基本是空置的,可以适当改小一些。总体来说内存够用,主要是CPU能力不足,MySQL的行锁(InnoDB Row Lock Avg)开销平均为432毫秒,这是一个影响性能的点。因此需要提高CPU能力,加快处理减小锁定时间。

从下图所示的费时统计来看除了第一条SQL,其他的响应时间还可以接受。

这条SQL语句是一个对3张表的连接查询(见SQL语句中第一条)操作,这条SQL也是可以用上索引的,不修改业务的情况下暂时没有好的优化方式,后面两条SQL语句在容器环境中已经建好索引。 

SQL语句:

SELECT p.post_id, topic_id, forum_id, p.user_id, post_time, poster_ip, enable_bbcode,p.attach, enable_html, enable_smilies, enable_sig, post_edit_time, post_edit_count, status, pt.post_subject, pt.post_text, username, p.need_moderate FROM jforum_posts p, jforum_posts_text pt, jforum_users u WHERE p.post_id = pt.post_id AND topic_id = 464448 AND p.user_id = u.user_id AND p.need_moderate = ? ORDER BY post_time ASC LIMIT ?, 20;
SELECT t.*, p.user_id AS last_user_id, p.post_time, p.attach AS attach FROM jforum_topics t, jforum_posts p WHERE p.post_id = t.topic_last_post_id AND p.need_moderate = 0 ORDER BY topic_last_post_id DESC LIMIT 50;
SELECT t.*, p.user_id AS last_user_id, p.post_time, p.attach AS attach FROM jforum_topics t, jforum_posts p WHERE (t.forum_id = ? OR t.topic_moved_id = ?) AND p.post_id = t.topic_last_post_id AND p.need_moderate = 0 ORDER BY t.topic_type DESC, t.topic_last_post_id DESC LIMIT 0, 20;

(3)混合场景测试执行、分析及调优

混合场景将按照测试需求采集的业务比例来模拟,考查的重点是在不同负载情况下系统的服务能力,下表是几种不同负载下的测试结果,选择了有代表性的几个不同当量的负载测试结果,四舍五入后凑整。

思考时间为10毫秒,并发数比计划时要少,经过多次的负载调整,压测了很多次,最后留下几组有代表性的数据来反映系统拐点。当然,这也只能是一个近似数据,也没必要每次只加几个并发线程,一直找到最高点,这个工作不值得做,而且压测的时候随便有干扰,TPS数值上下有异动也很正常。

利用Excel将表中的结果做一个性能变化趋势分析,如图所示。

从图中可以看到随着并发线程数的增多,TPS会增加,过载后TPS会下降,执行过程满足我们测试需求的要求,找到了系统的性能变化趋势,在最大处理能力时系统响应时间都小于1秒,性能良好。TPS为100时为系统性能拐点,之后TPS会降低,响应时间会增大。

在执行过程中对JForum服务器进行了监控,基本上每个场景都可以让MySQL使用的CPU满负荷工作,主要是负载高、任务排队多。

下图所示的MySQL服务器监控显示负载满了:

下图所示是JForum服务器监控显示负载满了:

下图所示监控的JVM Heap回收是没有Full GC的,说明JForum的程序还是很优秀的,运行几小时都无Full GC,而且还是在高负荷的压测过程中。

下图所示是并发100个线程情况下的聚合报告。下面分析一下TPS升不上去的原因,如果TPS升不上去,响应时间就长,响应时间花在哪里了呢? 

本例的响应时间组成如下(只考虑对响应时间影响大的部分):

  • 响应时间=JMeter到JForum的网络时间+JForum处理时间+JForum到MySQL的网络时间+MySQL处理时间。
  • JForum处理时间=程序处理时间+线程排队时间。
  • MySQL处理时间=MySQL程序处理时间+线程排队时间+IO时间。

JForum与MySQL在同一主机上,网络时间可以忽略;对于JForum服务,我们监控到CPU负载很高,平均都是27以上,这个时间算到线程排队时间里;我们监控JVM的回收情况,基本上没有Full GC,JForum的程序在堆的使用上是无风险的,也没有在Java程序中做复杂运算,唯一耗时的是构建Lucene索引会造成一些性能影响,这可以取得更好的搜索效果,也可以做周期性的Lucene索引更新来减轻负担,总的来说这样可以提升系统性能。

JForum不涉及调用第三方系统(或者服务间的调用),Skywalking中的追踪功能也就用不上。

若想对整个服务的响应时间、吞吐量、JVM进行监控,可以使用Skywalking去监控。这里也提供了镜像及docker-compose.yml文件(代码段),其中包含了Skywalking相关的配置,_DSW_SERVER_NAME用来指定当前服务名,_DSW_AGENT_COLLECTOR_BACKEND_SERVICES用来指定监听数据上传的位置。

version: '3.7'
services:jforumweb:image: seling/jforum-tomcat7-skywalking:2.1.9-oracle-jdk8-64stdin_open: truetty: truecap_add:- SYS_PTRACEenvironment:CATALINA_OPTS: $CATALINA_OPTS -javaagent:/usr/local/tomcat7/agent/skywalking-agent.jarJAVA_OPTS: -server -Xmx1024M -Xms1024M -XX:MaxMetaspaceSize=128M -XX:MetaspaceSize =128M -XX:+UseG1GC -XX:MaxGCPauseMillis=100 -XX:+ParallelRefProcEnabled -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.port=9080 -Dcom.sun.management.jmxremote.rmi.port=9080 -Djava.rmi.server.hostname=10.1.1.80 -Dcom.sun.management.jmxremote.ssl=false -DSW_SERVER_NAME=jforum -DSW_AGENT_COLLECTOR_BACKEND_SERVICES=10.1.1.160:11800ports:- 8089:8080/tcp- 9080:9080/tcplabels:service: jforumwebdepends_on:- jforumdbjforumdb:image: seling/jforumdb:1.0stdin_open: truetty: trueenvironment:MYSQL_ROOT_PASSWORD: 3.1415926ports:- 3306:3306/tcplabels:service: jforumdb

下图是服务监控示例:

MySQL程序处理时间由Navicat Monitor来监控,下面看一下多个维度的监控情况。

图中给涉及的SQL查询做了耗时排序,可以看到实际的SQL处理速度还是很快的,每个查询也都可以落到索引上。 

下图中为SQL的查询等待排序,可以看到等待时间还大于查询处理的时间:

下图所示为按资源对象分类统计等待时间,可以看到CPU的等待占到99%,CPU瓶颈影响到整个性能。

下图所示为按表来统计等待时间,可以看到最“热”的表是jforum_topics,经查这张表是有索引的。 

下图中显示读的等待时间占比最大,平均等待时间也最长,磁盘读的速度也有较大的提升空间。 

下图是网络的统计,网络传速为1.51MB/s,实验环境在一台主机上,网络影响可以忽略。 

综上所述,MySQL的处理时间多数花在了CPU的等待上,IO等待时间按几十万的查询量平均后并不多,当然系统若有更快的读写速度会更好。

JForum的表结构简单,查询语句也比较简单,在不改变业务结构的前提下,目前性能已经是很不错的,如果提升CPU处理能力,性能会明显改善。

下表中把单场景与混合场景的结果进行了对比,可以看到TPS相差还是比较大的。混合场景是以一个完整的业务链路来模拟的,业务操作在测试脚本中是有先后顺序的,某个操作耗时较多都会对整个链路造成影响,自然TPS与单场景相差较大。 

登录与浏览只查询数据库,所以TPS比较大;回帖与发帖都有写数据库操作,而且都会触发生成全文搜索索引,也会查询数据,同时要把数据写到缓存中,所以TPS相对要小一些。另外也有一些有趣的数据,例如浏览操作在混合场景的RT会小一些,这时我们就要多压测一段时间,确认这不是异常,而是一直是这样。我们不能单看RT,重点要看TPS,单场景的TPS明显比混合场景的大。遇到这种情况没有其他办法,只能反复多次压测,减少偶发事件。

要清楚单场景与混合场景的运行目的。单场景方便我们分析性能问题,在单元测试阶段就可以介入,把性能测试提前,问题早发现、早面对;混合场景模拟用户实际操作,测试结果更具可靠性,当然这要建立在测试模型可靠、数据量足够、执行充分的基础上。

负载测试执行完成前后对数据库文件大小进行了监控,每增加10万条发帖、回帖记录,数据文件增长约115MB(排除附件),500万条占用空间约为5 750MB(约5.6GB),故数据库对磁盘空间并无特殊要求,市场上销售的主流磁盘都可满足。

下面简单总结一下负载测试结果:

  1. JForum在测试环境下能够满足目前的性能需求。
  2. 通过不同当量的负载测试反映了JForum在不同测试环境的性能变化趋势,在总吞吐量(TPS)约为100后出现拐点。
  3. 当前配置中,CPU是JForum系统首要风险,随着业务量的增加,CPU将首先面临瓶颈。

4. 稳定性测试

1)测试场景

稳定性测试的目的是验证在当前软硬件环境下表,长时间运行一定负载时,确定系统在满足性能指标的前提下是否运行稳定,执行时依然是用混合场景。

这里有个问题需要明确一下,稳定性测试负载量一般设为多大?一般要求是在正常性能阈值下尽量加大负载量,何为阈值呢?

例如响应时间要求3秒以下,3秒就是阈值;例如CPU利用率70%以下,70%就是阈值。设满足性能要求的负载是B,稳定性测试时负载一般是1.5B~2B(1.5倍到2倍之间),这也只是一个经验值,实际情况需酌情增减。本例的稳定性测试选择并发154个(估算出来的并发数77的2倍)线程,思考时间调到1秒,示例运行2小时。

2)测试执行与分析

下表是稳定性测试结果与测试指标的对比,由于第一阶段的性能不平稳,响应时间一直在上扬,如下图,说明性能受某些因素的影响比较大。

这种情况的稳定性测试有必要继续执行下去,找到系统性能恶化的原因。

所以继续进行第二个阶段的压测,在第二阶段中TPS趋于稳定,可惜是稳定的超时,发帖操作都到了3.8秒的平均时间,TPS总体下降23。再看一下95%的响应时间,第一阶段性能比较好,第二阶段时间都到了5、6秒了。

所以基于实验环境,压测的负载量来说,稳定性堪忧,当然也有扩大负载造成的影响,让问题显现得更早,这也是性能测试的目的。

下面我们分析一下原因:

我们把分析分为二层,即JForum应用与MySQL数据服务。先从MySQL数据服务开始。

(1)从DB的监控数据来看,回帖数据从10万增加到80万,发帖由10万增加到30万。查询的响应时间依然比较快,下图显示这些查询都是毫秒级的。

的确,在数据结构简单、索引加持、数据量几十万的情况下,MySQL承载完全没有问题,主要是由于实验环境的CPU资源有限,所以CPU基本是满负荷状态,如下图。

MySQL的配置也已经进行了适当调整,主要性能指标良好,如下图,所以MySQL数据服务相对JForum应用来说对整体性能的影响要小很多。MySQL的风险主要在CPU,后续业务增大、数据增多后,需要优先加强CPU处理能力。

(2)响应时间与TPS反映整体性能表现,下面分析JForum中程序的性能。如果您使用Skywalking来监控,可以得到每个请求的耗时,如上图。由于没有复杂的调用,监控结果相对简单,Skywalking适合
于不同服务之间的调用,且能组成一个调用链,然后可以分析这个链中哪个服务慢。

在此我们使用anatomy来帮助分析。

查看CPU使用率与负载情况,此时CPU利用率并不高,IO等待时间长,于是开始分析线程,找程序问题;先找出CPU资源占用高的线程,获取栈信息。

net.jforum.view.forum.PostAction.insertSave(PostAction.java:1142)
at net.jforum.search.LuceneIndexer.create(LuceneIndexer.java:182)

下图所示是一个CPU线程栈信息,另外我们也找到了回帖与发帖的栈信息,它们的性能与PostAction.insertSave关联。

下面我们监控这两个方法的调用时间:

我们比较少量线程与154个线程两种负载,下图是少量负载情况下监控到的两个方法的耗时,我们用表格进行比较,见下表,每种负载随机选择两次监听结果进行比较。

个位数负载与154个线程并发的性能差异明显可以理解,而154个线程并发时两个阶段的性能差异大就需要找原因了。

下图所示是对LuceneIndexer.create方法的一次分析:

这部分在JForum服务器上进行Lucene索引的更新,基本是IO操作,导致一定的IO等待,数据越来越多,系统就会越来越慢。另外监听到Lucene索引达到一定大小后会整理,整理后速度会短暂上升,之后又变慢。当Lucene索引的更新时间与整个事务的时间在一个量级后,整个事务的响应时间就相对平稳了,因此我们看到的第二阶段TPS平稳但响应时间很大。

当然,这个平衡的过程也可以打破,而且这个量变引起质变的过程会长一些。 

如下图所示,wa(非空闲等待)比系统CPU占用还高,bo的指标也比较高。

也可以直接分析Java线程对IO的占用,下图所示是线程级的IO监听。

那JVM有没有问题呢?看一下JVM的堆回收情况,下图的垃圾回收监控显示没有Full GC,说明程序在这方面控制得较好,我们选择的垃圾回收器还算合适。

上面一路分析下来,我们漏掉的还有网络连接,因为我们的连接数并不多,连接池开得大,所以在此基本忽略影响。大家在做性能测试时可以适当放大连接数限制,其占用内存不大。

到此,我们总结一下性能稳定性测试。性能稳定性是一个量变引起质变的过程,因此负载尽量多(可以把思考时间适当放大一些,多加一些线程);执行时间尽可能长,这样才能验证数据量积聚后性能是否有保障;测试结果的曲线尽量要平缓,响应时间可以是渐近式的上升,对于突变要认真分析原因。

本例中,我们实际产生的负载量是需求的10倍以上,2小时的数据量大约相当于25天的需求量,此时稳定性较差,当前的硬件配置下性能较差。

所以稳定性测试时加的负载量很考验测试工程师,如果加的负载量太大,稳定性测试结果很差的概率大,有助于暴露问题。但对于程序来说就“不公平”了,如果系统根本没这么大的负载,压测这么大显得多余。

如果我们把负载降下来,压测结果就完全不一样了,在下面图中可以看到TPS与响应时间都比较稳定。

TPS: 

响应时间: 

所以压测时也需要实际一点,负载测试能够“压榨”出最佳性能,性能稳定性测试时的负载主要参照性能需求适当放大。 

10、结果分析

前面我们经过了不同目的的测试执行工作,对系统的性能有了相对全面的了解。

(1)系统在单机(参照测试环境、软硬件配置信息)上已经能够满足性能要求。

(2)对系统的性能变化趋势做了评估,如下表,在当前环境下最高可提供15倍于当前需求的吞吐量(以TPS来计),随着负载的增加,CPU将首先遇到性能瓶颈,其次是IO。

(3)进行了配置测试,建议JVM堆空间暂时设置为1GB(JDK8),垃圾回收器推荐使用G1,Tomcat连接池设为200,数据库连接池最大连接数保持默认值为100,建议在运营过程中监控到Tomcat活动线程数、数据库连接池活动连接数达到总数的80%以上时可以适当增加堆空间。

(4)进行了稳定性测试,实验环境执行了2小时,采用保守策略,执行时的负载是需求的1.5倍,完成的业务量相当于未来10天的业务量。从结果来看,不管是响应时间还是TPS都比较稳定,事务成功率近100%,没有明显影响性能的现象。当负载进一步增大时,例如15倍于性能需求时,响应时间会激增,主要原因是Lucene构建全文索引太繁忙,这只是未来业务增大时可能面临的性能问题。

(5)为了保证高可用性,建议部署多实例,MySQL做主备,保证数据安全性。

(6)500万条数据占用空间约为5 750MB,存储需求容易满足,需要控制的是用户上传附件的大小与文件数量,此为风险,可以单独挂载磁盘来进行存储,改造为云存储是个不错的方案。

注:以上只是演示了性能测试的过程,实践中如何进行性能分析,现实中的生产系统会更复杂。但是只要按照规则来,再复杂的系统也可以拆分成若干简单的事务,总会找到解决办法。 

11、测试报告

测试报告实际上是对整个测试过程的报告。对于决策层(报告相关干系人)来说,关心的是结果;对于报告人来说,报告的是工作。

为什么这么说呢?

决策层迫切要知道的是系统能不能上线?如果不能上线,有什么问题?怎么能够尽快解决?

这两方面的需求决定了测试报告要说明测试原因、测试环境、测试需求(包括测试指标)、测试开发过程、测试执行过程、测试缺陷、测试结果、系统风险等内容,测试报告提纲如下。

(1)性能测试背景:结合系统简述一下性能测试开展的必要性。

(2)性能测试目标:此次性能测试的目标,我们要做哪方面的测试?

(3)性能测试范围:列出测试范围,参考测试计划中的测试范围。

(4)名词术语:报告中涉及的专业名词解释,参考测试计划。

(5)测试环境:报告测试结果基于的环境,不同环境中测试结果可能是大相径庭的,参考测试计划中列出的环境。

(6)测试数据:报告测试数据量,参考测试计划中估算的数量。

(7)测试进度:报告测试过程,什么时候做什么工作,例如哪一天执行了哪些测试脚本。

(8)测试结果:全面而多方位地报告测试结果,如TPS、ART、事务成功率、硬件设备资源利用率(CPU、内存、网络、IO等)。

(9)测试结论:分析给出测试结论,系统能否满足性能要求?存在什么问题?有哪些缺陷?解决了哪些问题?还有哪些问题没有解决?

(10)系统风险:报告系统可能存在的风险,帮助决策层应对风险。

总结:

我们用实例演示了常规的性能测试流程,对于关键点做了说明。

在需求分析时,我们要关注业务量、业务分布、用户规模、性能指标等信息。通过需求分析,我们可以建立起测试模型,定义测试数据(主数据及业务数据),在制作测试数据时一要注意量,二要注意数据的分布。

在脚本编写时,要注意做断言,验证事务是否成功,可以看到脚本运行成功只是测试计划的一部分,在实现场景时我们还需要做调整。我们用实例演示教大家如何设置不同业务的比例,这些都是一些小技巧,只要对JMeter各元件功能熟悉,就可以利用它们来达到你想要的效果。

测试监控是性能测试重要的一环,性能的好坏会通过硬件性能反映出来,我们也是通过这些硬件指标来分析、推测问题所在。测试执行时,我们针对不同的目的做了基准测试、配置测试、负载测试、稳定性测试,在执行过程中抛砖引玉地演示了部分常见性能问题。

例如中间件线程池大小、数据库连接池大小、JVM大小等。其实还有一些常见的问题,如日志过多导致的IO瓶颈,缺失关键索引或者没利用上索引导致的性能问题(数据库IO过大、CPU利用率过高),也是我们要关注的。

测试计划与测试报告要能够让非专业人士也能看懂,做好指标对比,用图表表达性能变化趋势。要提醒的是,计划与报告可以不详细,但性能测试的过程不能省。性能测试是严谨的工作,不要因为时间紧而偷懒,忽视重要工作项目。

相关内容

热门资讯

监控摄像头接入GB28181平... 流程简介将监控摄像头的视频在网站和APP中直播,要解决的几个问题是:1&...
Windows10添加群晖磁盘... 在使用群晖NAS时,我们需要通过本地映射的方式把NAS映射成本地的一块磁盘使用。 通过...
protocol buffer... 目录 目录 什么是protocol buffer 1.protobuf 1.1安装  1.2使用...
在Word、WPS中插入AxM... 引言 我最近需要写一些文章,在排版时发现AxMath插入的公式竟然会导致行间距异常&#...
【PdgCntEditor】解... 一、问题背景 大部分的图书对应的PDF,目录中的页码并非PDF中直接索引的页码...
Fluent中创建监测点 1 概述某些仿真问题,需要创建监测点,用于获取空间定点的数据࿰...
educoder数据结构与算法...                                                   ...
MySQL下载和安装(Wind... 前言:刚换了一台电脑,里面所有东西都需要重新配置,习惯了所...
修复 爱普生 EPSON L4... L4151 L4153 L4156 L4158 L4163 L4165 L4166 L4168 L4...
MFC文件操作  MFC提供了一个文件操作的基类CFile,这个类提供了一个没有缓存的二进制格式的磁盘...