作者:吴世超
什么是性能测试
性能测试是一种软件测试形式,重点关注运行系统的系统在特定负载下的性能。这与查找软件错误或缺陷无关。不同的性能测试类型根据基准和标准进行测量。性能测试为开发人员提供消除瓶颈所需的诊断信息。
性能测试的类型
主要的两种性能测试方法:负载测试和压力测试。但是,还有其他类型的测试方法可用于确定性能。一些例子如下:
- 负载测试:通常进行负载测试是为了了解系统在特定预期负载下的行为表现。此负载可以是应用程序上的预期并发用户数,该应用程序在设定的持续时间内执行特定数量的事务。该测试将给出所有重要业务关键事务的响应时间。测试期间还会监控数据库、应用服务器等,这将有助于识别应用软件和安装软件的硬件中的瓶颈
- 压力测试:通常用于了解系统内的容量上限。进行这种测试是为了确定系统在极端负载方面的稳健性,并帮助应用程序管理员确定如果当前负载远高于预期的最大值,系统是否能够充分运行。
- 浸泡测试:也称为耐久性测试,通常用于确定系统是否能够承受持续的预期负载。在浸泡测试期间,会监控内存利用率以检测潜在的泄漏。同样重要但经常被忽视的是性能下降,即确保在长时间持续活动之后的吞吐量和响应时间与测试开始时一样好或更好。它本质上涉及在很长一段时间内向系统施加大量负载。目标是发现系统在持续使用下的行为方式。
- 尖峰测试:通过突然增加或减少大量用户产生的负载,并观察系统的行为来完成的。目标是确定性能是否会受到影响,系统会失败,还是能够处理负载的剧烈变化。
- 断点测试:断点测试类似于压力测试。随着时间的推移施加增量负载,同时监控系统的预定故障条件。断点测试有时被称为容量测试,因为可以说它确定了系统将按照其要求的规范或服务水平协议执行的最大容量。应用于固定环境的断点分析结果可用于根据所需的硬件或应触发云环境中的横向扩展事件的条件来确定最佳扩展策略。
- 配置测试:不是从负载角度测试性能,而是创建测试以确定系统组件的配置更改对系统性能和行为的影响。一个常见的例子是尝试不同的负载均衡方法。
- 隔离测试:隔离测试并不是性能测试所独有的,而是涉及重复执行导致系统问题的测试。这样的测试通常可以隔离和确认故障域。
而我们本次的测试实践采用的是负载测试。
性能测试基本流程
测试环境是设置软件、硬件和网络以执行性能测试的地方。要使用测试环境进行性能测试,开发人员可以使用以下七个步骤:
确定测试环境:通过识别可用的硬件、软件、网络配置和工具,测试团队可以设计测试并尽早识别性能测试挑战。性能测试环境选项包括:
a. 生产系统的子集,具有较少的低规格服务器
b. 具有相同规格的较少服务器的生产系统子集
c. 生产系统副本
d. 实际生产系统- 确定性能指标:除了确定响应时间、吞吐量和约束等指标外,还要确定性能测试的成功标准是什么,具体根据项目内团队规定。
- 计划和设计性能测试:考虑到用户可变性、测试数据和目标度量的性能测试场景,可以创建一个或两个模型。
- 配置测试环境:准备监控资源所需的测试环境机器和工具。
- 开发测试脚本
- 执行测试计划:运行性能测试,监控和捕获生成的数据。
- 分析、报告、重新测试:分析数据并分享得到的性能结论。使用相同的参数和不同的参数再次运行性能测试。
我们在后续的实践中基本会遵循这个基本流程来开发。
性能测试工具
IT 团队可以使用各种性能测试工具,具体取决于其需求和偏好。性能测试工具的一些示例包括:
- JMeter:是一个 Apache 性能测试工具,可以对 Web 和应用程序服务进行负载测试。 JMeter 插件在负载测试中提供了灵活性,并涵盖了图形、线程组、计时器、函数和逻辑控制器等领域。 JMeter 支持集成开发环境 (IDE),用于对浏览器或 Web 应用程序进行测试记录,以及用于负载测试基于 Java 的操作系统的命令行模式。
- LoadRunner:由 Micro Focus 开发,用于测试和测量应用程序在负载下的性能。 LoadRunner 可以模拟成千上万的最终用户,并记录和分析负载测试。作为模拟的一部分,该软件会在应用程序组件和最终用户操作之间生成消息,类似于按键点击或鼠标移动。 LoadRunner 还包括面向云使用的版本。
- NeoLoad:由 Neotys 开发,为 Web 和移动应用程序提供负载和压力测试,专门用于在发布前测试应用程序以实现 DevOps 和持续交付 IT 团队可以使用该程序来监控 Web、数据库和应用程序服务器。 NeoLoad 可以模拟数百万用户,并在内部或通过云执行测试。
本次 PingCode 负载测试用到的测试工具就是 Jmeter。
性能测试指标
许多性能指标或关键性能指标可以帮助IT团段评估当前系统的性能状况。 性能指标通常包括:
- 吞吐量:系统在指定时间内处理多少个信息单元。
- 响应时间:从用户输入的请求到系统开始响应该请求之间经过的时间量。
- 带宽:每秒可以在工作负载之间移动的数据量,通常是指通过网络。
- 每秒 CPU 中断次数:取的是平均值。指处理器每秒接收和处理的硬件中断数。
- 内存使用:计算机上进程可用的物理内存量。
- 磁盘使用:磁盘忙于执行读取或写入请求的时间量
这些指标以及其他指标可以帮助IT团队执行多种类型的性能测试。
本次负载测试实践中我们主要关注的是:吞吐量、响应时间、内存使用以及磁盘使用情况。
一次非系统性的性能测试实践
测试类型
负载测试:测试某个具体API在最大并发用户数,持续服务的5Min,得出在基准环境下的TPS和RT值。
测试范围
由于我们的API很多,在短时间无法全部测试完毕,我们采取优先测试 「访问度」高的API。
最终决定将API服务近6个月访问数量倒序排列取10个:
- 涉及的子产品: Project , Testhub , Wiki , Insight , Goals , Flow 6个子产品。
- 基础服务:Typhon 服务
测试目的
作为Pingcode项目首次性能测试,本轮测试的目的定义如下:
- 保障系统多用户情况下系统的可靠性:通过实施不同的性能测试场景,评估并验证系统在高负载下核心功能的可靠性。
- 提供一轮系统性能测试情况,为下一轮生产环境的性能测试提供基线数据。
- 评估系统部署资源的合理性:不断搜集和分析性能测试过程数据,对接下来的系统部署所需的系统资源进行评估和建议。
- 优化和解决系统性能问题:在测试过程中,优化系统各项参数和解决发现的系统性能问题。
测试指标标准
- 根据运维提供的在线用户数,来计算生产环境的用户 TPS,通过基准环境的测试,来验证真实的 TPS 是否满足生产环境的 TPS 需求,以及我们的运维架构和资源使用是否合理。
- 保证在 CPU 利用率小于80%,内存小于80%,并且没有错误的 Http 请求。
测试实践
测试工具
- 采用 Jmeter + Grafana + Influxdb 的方式来完成性能测试。
- Jmeter GUI 图形界面工具:为了更方便开发和调试,使用图形界面编写所需要测试计划。
- Jmeter NO-GUI模式:支持大型负载测试场景、提高脚本执行效率以及方便后续将脚本配置到 Jenkins 上实现持续集成,做成自动化测试。
环境搭建
- 安装 Java 的 Jdk,版本跟 Jmeter 版本相对应。
- 安装 Jmeter 工具
Jmeter 中线程数取值依据
在实际的 Jmeter 测试实践中,我们会通过线程数去模拟多用户的操作方式,但是这个线程数取多大的值才合适呢?
我们可以通过下面两点估算出线程数:
- 首先要明白用户数、线程数、TPS的关系
由于实际测试中响应时间肯定不会一直都是 100ms。所以通常情况下,上面的这个比例都不会一成不变的,而是随着并发线程数的增加,会出现趋势上的关系。
通过上图,我们可以根据在线用户10000,通过业内基准并发度5%,计算出理想TPS为500tps;或者根据产品直接给出的500tps作为理想TPS。而这个理想的TPS,会作为计算Jmeter线程数的基准。 - 通过TPS基准计算出压力机线程数
计算公式如下:
实际上,对于压力工具来说,只要不报错,我们就关心 TPS 和响应时间就可以了,因为 TPS 反应出来的是和服务器对应的处理能力,至少压力线程数是多少,并不需要关心。
我们会在后面的实际操作中,根据算出来的线程数去不停的调节从而得到 API 请求的实际 TPS 和 RT,和实际的RT情况与理想中TPS和RT做对比,来确定当前API性能情况。
Jmeter 项目目录结构
目前我们测试过程中需要关注的是 bin
目录下的 templates
文件夹和 jmeter.properties
文件即可。
templates 文件夹
其中 templates.xml
文件是用来规定所要提供的测试计划模版列表,对应 Jmeter GUI 界面如下:
jmeter.properties 文件
其中 template.files 配置项是用来配置 Jmeter 所要读取的模版路径,但必须保证在 Jmeter 的安装根目录下,在后面的开发中为了方便存储会去修改此配置项。
项目准备以及配置文件修改
为了方便多人协作开发,我们使用代码版本管理工具 Github 来管理需要的模版和测试计划文件。为了方便下面使用,统一叫做 pc-perf
项目
克隆 Git 仓库
进入 Jmeter 的 bin 目录下
cd jmeter/bin
git clone [git项目地址]
perf-test 目录
- common文件夹:用来存放本次测试计划模版以及模版配置文件 templates.xml。
- 子应用目录:与 common 同级目录是各子应用目录,如目标 Goals、项目 Project 等。该目录下用来存放个子应用对应的 .jmx 测试计划文件。
修改 Jmeter 的配置文件 jmeter.properties
template.files = /bin/pc-perf/common/templates/templates.xml
JMeter 模版开发
在 setUp 线层组下添加用户参数处理器
该用户参数处理器用来存放一些用户级别的公用数据。例如:登录名、密码之类东西。
在 setUp 线程组下添加 BeanShell 预处理程序
该 BeanShell 预处理程序用来将公用属性全局化,全局的变量可以在多个线程组中调用。例如:请求协议、请求端口、IP、Host 以及 Influxdb 的链接等。
补充Http请求取样器的内容
api 的信息,如请求头、IP、Port、Host、URL 请求体这些信息可以从 api 文档中获取。除此之外还可以通过浏览器自带的 copy fetch 的方式获取具体的接口信息。本次我们采用第二种方式获取 api 信息。
我们通过 Copy as fetch,可以得到如下代码:
fetch("http://at.pingcode.fun/**/signin",
{
"credentials":"include",
"headers":{
"accept":"application/json, text/plain, */*",
"accept-language":"zh-CN,zh;q=0.9",
"content-type":"application/json"
},
"referrer":"http://at.pingcode.fun/signin",
"referrerPolicy":"no-referrer-when-downgrade",
"body":"{\"signin_name\":\"**\",\"password\":\"**\",\"captcha_code\":\"\"}",
"method":"POST",
"mode":"cors"
});
在 Http 请求元件内添加 JSON 提取器
此提取器用来获取登录返回的信息。
从响应报文中提取数据有多种方式,详见: Jmeter中响应报文的提取方式
在 Http 请求元件内添加调试后置处理程序
此元件可以在调试阶段查看 JSON 提取器获取的信息内容。
在 Http 请求元件内添加 JSR223 后置处理器
此元件用来格式化响应结果。比如获取相应用户、团队、token等信息并放入到 Jmeter 的局部变量中。
// 此处loginResponse_1是从JSON提取器的调试结果可得到
var loginResponseString = vars.get("loginResponse_1")
var loginResponse = JSON.parse(loginResponseString)
log.info("loginResponseString:"+ loginResponseString)
// 设置该线程组内的变量
vars.put("user", JSON.stringify(loginResponse.user))
vars.put("team", JSON.stringify(loginResponse.team))
vars.put("access_token", loginResponse.access_token)
在 Http 请求元件内添加 BeanShell 后置处理器
此元件用来将token等信息设置为全局变量供跨线程请求使用。
// 将用户、团队、token信息声明成全局变量,用于后面线程组使用
${__setProperty(user, ${user})}
${__setProperty(team, ${team})}
${__setProperty(access_token, ${access_token})}
在 Http 请求元件内添加察看结果树监听器
此元件用来查看具体的 Http 请求详情。
运行测试计划
此时,基本一个完整的请求就完成了,然后我们启动该测试计划,查看运行结果。
还可以查看 JSON 提取器的取值情况:
JSON 提取器会将响应结果当作数组处理,由于 data.value
是单个对象,所以处理后的值为 loginResponse_1
优化参数管理
局部变量调用方式:
元件中使用:${key}
脚本中使用:vars.get(key)
全局变量的调用方式:
在元件中使用:${__P(key)}
在脚本中使用:props.get(key)
创建线程组A
目前规定一个测试计划为1个子产品,一个线程组包含一个具体的子产品 API 请求。为了更好的统计信息,可以将 API 名字作为线程组名和请求名。
其中线程数我们会利用上面所讲的计算公式计算出一个合适的线程数,例如线程数为240。
那Ramp-up时间应该取多少呢?我们可以参考ramp-up 定义以及取值来确定。比如:以240为线程数, ramp-up就为240。
在Http请求元件内添加请求头管理器
请求头中的 host 以及 token 都可以从全局变量中动态引入。
在 Http 请求元件内添加察看结果树元件
红框处设置结果写入的文件路径,目前是将结果写入到jtl格式的文件中,用于后期生成html测试报告。
在 Http 请求元件中添加后端监听器
此元件用于向 Influxdb 传输数据
在 Http 请求元件内添加聚合报告监听器
此元件可用于在Jmeter查看聚合结果。
此处跟传输到influxdb,最终呈现在grafana中数据差异不大。
Grafana信息
application 为子应用名称,transaction 为 单个 API 请求。
将当前测试计划保存为模版
将模版保存到之前的Git仓库的common的templates目录下
通过 VsCode 修改 templates.xm 文件,将刚保存的测试计划配置到模版列表中:
name:模版名称,用于模版列表中展示
fileName: 要配置成模版的测试计划的路径
description:模版描述
parameters:模版参数,可通过模版UI页面对测试计划.jmx文件动态传参。
修改 pingcode-perf-template.jmx 文件,接收模版传过来的参数通过[=key]的形式接收模版的参数
开发子应用的 API 测试计划
通过修改导入的 JMeter 模版,来完成其他 API 测试。
点击模版工具栏,选择要使用的模版
编写 API 测试
由于目前规定同一个子应用的不同api在同一个测试计划下进行,所以确保测试计划的线程组执行方式为串行:
根据实际的 API 信息修改线程组A信息,依次创建不同的线程组来完成子应用的其他 API 测试。
通过 Non-GUI 方式执行所有测试计划
命令行用法:
jmeter -n -t -l
示例:jmeter -n -t test.jmx -l test.jtl
讲解:以命令行的方式运行test.jmx脚本并生成test.jtl的日志报告。
-h 打印使用信息并退出
-n 非GUI模式 -> 在非GUI模式下运行JMeter
-t 测试文件<参数> ->要运行的jmeter测试(.jmx)文件。
-l 日志文件<参数> ->生成的日志文件
-H 设置要使用的JMeter代理服务器
-P 设置要使用的JMeter代理服务器端口
执行结果如下:
执行完所有测试计划后,通过 Grafana 查看 API 请求情况,根据 Influxdb 数据,统计所有 API的实际 TPS 和 RT 数据。
到这里,我们整个负载测试就完成了。接下来只需要将每个产品的 API 的实际 TPS 和 RT 情况统计下来,然后对这些数据进行分析,我们就可以得到这些 API 中哪些是需要优化的,从而达到我们本次负载测试的目的。
资料
- 维基:< https://en.wikipedia.org/wiki...;
- Jmeter:< https://jmeter.apache.org/>;
- LoadRunner:< https://en.wikipedia.org/wiki...;
- NeoLoad: https://www.tricentis.com/pro...