Apache JMeter (Performance Testing)

Table of Contents

1. JMeter 简介

The Apache JMeter™ application is open source software, a 100% pure Java application designed to load test functional behavior and measure performance.

JMeter is not a browser, it works at protocol level. JMeter does not execute the Javascript found in HTML pages.

通过使用 JMeter 可以测试下面这些应用或协议:

  • Web - HTTP, HTTPS (Java, NodeJS, PHP, ASP.NET, …)
  • SOAP / REST Webservices
  • FTP
  • Database via JDBC
  • LDAP
  • Message-oriented middleware (MOM) via JMS
  • Mail - SMTP(S), POP3(S) and IMAP(S)
  • Native commands or shell scripts
  • TCP
  • Java Objects

要启动 JMeter 图形界面,下载程序并解压后执行 ./apache-jmeter-4.0/bin/jmeter 即可,如图 1 所示。

jmeter_gui.gif

Figure 1: JMeter

参考:
JMeter User's Manual
JMeter Best Practices

2. 简单使用(以测试 REST API 为例)

下面以测试 REST API 为例,演示 JMeter 4.0 的基本使用。

2.1. 测试场景

假设服务器地址为“http://localhost:8080” ,提供了表 1 所示的两个 REST API,分别用于创建订单和查询订单。

Table 1: 待测试 REST API
REST API Method 参数例子 返回 JSON 数据例子
/webapi/order POST POST 数据实例{"prodName":"apple", "amount":5} {"orderId":"183763108372685"}
/webapi/order/status?orderId=YourOrderId GET YourOrderId 可以从上一个 API 的返回报文中得到 {"status":"ok"}或 {"status":"not_found"}

下面是用 curl 命令进行测试的实例:

$ curl -X POST -H "Content-Type: application/json" -d '{"prodName":"apple", "amount":5}' http://localhost:8080/webapi/order
{"orderId":"401496541951953"}
$ curl 'http://localhost:8080/webapi/order/status?orderId=401496541951953' # 上一步返回的订单号
{"status":"ok"}
$ curl 'http://localhost:8080/webapi/order/status?orderId=123893984123841' # 这是个不合法(乱写的)订单号
{"status":"not_found"}

下面介绍用 JMeter 进行测试的基本步骤。

2.2. 创建线程组

首先,我们需要创建一个线程组,右击“Test Plan”,再依次点击“Add”->“Threads (Users)”->“Thread Group”,默认线程组的配置如图 2 所示,保持默认配置不变。

jmeter_thread_group.gif

Figure 2: 线程组默认配置

线程组用来模拟“用户”,一个线程组可以设置多个线程,每个线程代表一个用户。 在创建“Test Plan”时,我们最好先设置一个线程,以方便调试,当“Test Plan”稳定后,我们再加大线程数量即可模拟高并发的情况。

2.3. 测试第一个 REST API

然后,我们需要添加 REST API 请求(即 HTTP 请求)模块。右击上一步创建的“Thread Group”,如图 3 所示,依次点击“Add”->“Sampler”->“HTTP Request”。

jmeter_add_http_request.gif

Figure 3: 添加 HTTP 请求模块

我们可以在新建的“HTTP Request”面板中,配置一些基本参数(如图 4 所示):
1、设置“Name”为“HTTP Request (create order)”,它可以随便填;
1、设置“Server Name of IP”为“localhost”;
2、设置“Port Number”为“8080”;
3、设置“Method”为“POST”;
4、设置“Path”为“/webapi/order”;
5、设置“Body Data”为“{"prodName":"apple", "amount":5}”,这个数据为 JSON 格式,我们需要设置报文头“Content-Type: application/json”,后面将介绍如何定制 HTTP 报文头。

jmeter_set_http_request.gif

Figure 4: 设置 HTTP Request 的参数

2.3.1. 定制 HTTP Header

前面提到,我们需要设置报文头“Content-Type: application/json”,这可以通过 HTTP Header Manager 来实现。其设置面板可以在右击“HTTP Request”后,选择“Add”->“Config Element”->“HTTP Header Manager”来找到,按照面板说明设置即可,这里不详细介绍。

Tips:如果你在一个“Thread Group”中需要测试多个 HTTP 请求,则很可能每个多个 HTTP 请求需要定制相同的 Header。这时,我们可以把“HTTP Header Manager”移到“Thread Group”下面,这样不用为每个 HTTP 请求都增加相同的“HTTP Header Manager”了。

2.3.2. 增加断言

怎么判断这个测试是否成功了,我们可以设置相应的断言。比如,可以设置如果响应报文体中包含关键字“orderId”,则认为这个测试成功。

其设置面板可以在右击“HTTP Request”后,选择“Add”->“Assertions”->“Response Assertion”来找到,按照面板说明设置即可,这里不详细介绍。

2.3.3. 增加“View Results Tree”

经前面的设置,现在已经可以运行测试了,不过没有地方查看结果。增加“View Results Tree”后,可以查看当前测试的结果以及请求和响应报文等具体信息,这有助于调试。

可以在“HTTP Request”的右键菜单中增加“View Results Tree”,也可以在“Thread Group”的右键菜单中增加“View Results Tree”。这两者的区别在于,前者只会显示当前“HTTP Request”的测试结果,而后面会显示当前“Thread Group”下所有的测试结果。推荐使用后者,这样如果一个“Thread Group”下有多个“HTTP Request”,则只用增加一次“View Results Tree”即可查看所有结果。

其面板可以在右击“Thread Group”,选择“Add”->“Listener”->“View Results Tree”来找到。

2.3.4. 测试和验证第一个请求

现在点击“Run”按键开始测试,会提示保存为 jmx 文件(它是 xml 格式的配置文件)。测试结束后,可以在“View Results Tree”面板中看到测试结果,还有请求和响应报文等信息,如图 5 所示。

jmeter_check_result.gif

Figure 5: 在“View Results Tree”面板查看测试结果及请求响应报文

2.4. 测试第二个 REST API

第二个 REST API 是查询订单信息,它需要我们输入订单号(orderId),而订单号在第一个 REST API 返回的 JSON 数据中。这需要我们为第一个 REST API 请求设置一个 JSON 提取器,把订单号保留到一个变量中,后面再引用它。

2.4.1. 设置第一个请求的 JSON 提取器

JSON 提取器面板可以在右击第一个“HTTP Request”,选择“Add”->“Post Processors”->“JSON Extractor”来找到。

在 JSON 提取器面板中,我们进行如图 6 所示的设置。
1、设置“Names of created variables”为 myOrderId (它是自定义的一个名字,后面可以通过 ${myOrderId} 来引用提取到的数据);
2、设置“JSON Path expressions”为 $.orderId (它可以匹配 JSON 数据 {"orderId":"183763108372685"} 中的 orderId 字段,关于 JSON Path 的语法可以参考http://goessner.net/articles/JsonPath/)。

这样,我们就可以把第一个请求返回的 JSON 数据(如 {"orderId":"183763108372685"} )中的 orderId 字段内容保存到变量 myOrderId 中了。

jmeter_json_extractor.gif

Figure 6: JSON Extractor 设置

2.4.1.1. 引用提取器中创建的变量

关于第二个 REST API 测试的具体配置这里不详细介绍,和测试第一个 REST API 的类似。这里仅介绍如何引用 JSON 提取器中的变量,如图 7 所示,输入 ${myOrderId} 给 “http://localhost:8080/webapi/order/status” 的查询参数 orderId 即可。

jmeter_use_created_var.gif

Figure 7: 使用提取器中创建的变量

2.4.2. 设置第一个请求失败时忽略第二个请求

如果第一个请求(创建订单)失败,默认 JMeter 会进行第二个请求(查询订单),但这可能没有意义(如订单号都得不到,无从查询其状态)。

在“Thread Group”面板中(如图 2 所示),把“Action to be taken after a Sampler error”从“Continue”改为“Start Next Thread Loop”,这样一旦有请求失败,则会跳过后面的请求。

3. 压力测试

3.1. 配置参数

进行压力测试的基本步骤是设置多个线程(每个线程代表一个用户),以及测试时间或次数进行一轮测试后得到一些性能指标;再修改线程数(即增加测试压力)进行其它轮的测试,观察系统在不同压力下的性能指标(比如响应时间、吞吐率等)。

比如,我们想要模拟 20 个用户,测试 10 分钟,则可以进行如下设置(如图 8 所示,在“Thread Group”面板中):
1、设置“Number of Threads (users)”为 20;
2、设置“Ramp-Up Period (in seconds)”为 4(也可为其它值,后面有这个参数的说明);
3、在“Loop Count”中,勾选“Forever”;
4、勾选“Scheduler”,并设置“Duration (seconds)”为 600。

jmeter_load_test.gif

Figure 8: 压力测试实例(20 个用户,测试 10 分钟)

说明:参数“Ramp-Up Period (in seconds)”表示多长时间内启动所有的线程。如果线程数为 100,而“Ramp-Up Period (in seconds)”设置为 2,则 2 秒内会有 100 个线程被启动并向服务器发送请求。显然,当设置线程数较多时,如果“Ramp-Up Period”很短,会造成服务器的瞬间高并发。

3.1.1. 错误时记录请求和响应数据(Simple Data Writer)

在 GUI 模式中,我们可以通过“View Results Tree”面板中看到测试结果,还有请求和响应报文等信息。

在 NON GUI 模式中,可能通过“Simple Data Writer”把请求和响应报文等信息记录到文件中。其设置面板可以通过右击“Thread Group”,选择“Add”->“Listener”->“Simple Data Writer”来找到。详细设置在面板的“Configure”按键中,如图 9 所示。

jmeter_simple_data_writer.gif

Figure 9: “Simple Data Writer”可保存请求和响应报文到文件

除此外,使用“Listener”中的“Save Responses to a file”面板可以记录响应报文到单独的文件中。

3.1.2. 不同测试线程使用不同数据(CSV Data Set Config)

我们启动多个线程时,如何实现每个线程测试不一样的数据呢?这可以通过“CSV Data Set Config”实现,其面板可以通过右击“Thread Group”,选择“Add”->“Config Element”->“CSV Data Set Config”来找到。

比如,启动 3 个线程测试前面介绍的创建订单 API“http://localhost:8080/webapi/order” ,想每个线程提交不同的 POST 数据:

{"prodName":"apple", "amount":5}
{"prodName":"pear", "amount":7}
{"prodName":"orange", "amount":8}

把上面数据保存为文件“postdata.csv”,在“CSV Data Set Config”面板中指定这个文件名,指定创建的变量名(比如 myPostData ),这样 JMeter 会把文件的每行内容读取到这个变量名中,设置 HTTP 请求的“Body Data”时引用这个变量(即 ${myPostData} )即可。

参考:http://jmeter.apache.org/usermanual/component_reference.html#CSV_Data_Set_Config

3.2. 从命令行启动测试

不要在 GUI 模式下运行压力测试!应该用 -n 参数启动 NON GUI 模式进行压力测试。如:

$ ./bin/jmeter -n -t test.jmx -l result.log -e -o report/
Created the tree successfully using HTTP_Request.jmx
Starting the test @ Thu Mar 15 14:48:58 CST 2018 (1521096538613)
Waiting for possible Shutdown/StopTestNow/Heapdump message on port 4445
......
summary =   1695 in 00:13:35 =    2.1/s Avg:  2385 Min:     0 Max: 15325 Err:    16 (0.94%)
summary +     59 in 00:00:27 =    2.2/s Avg:  2451 Min:   697 Max:  5726 Err:     0 (0.00%) Active: 3 Started: 3 Finished: 0
summary =   1754 in 00:14:02 =    2.1/s Avg:  2388 Min:     0 Max: 15325 Err:    16 (0.91%)
summary +     50 in 00:00:29 =    1.7/s Avg:  3022 Min:     1 Max:  8508 Err:     1 (2.00%) Active: 3 Started: 3 Finished: 0
summary =   1804 in 00:14:31 =    2.1/s Avg:  2405 Min:     0 Max: 15325 Err:    17 (0.94%)

注 1: -t 用于指定 JMeter 测试文件(在 GUI 模式下生成), -e 用于指定在测试结束后生成报告。使用 ./bin/jmeter -? 可以查看它支持的所有参数及说明。
注 2:上面输出中:“summary +”表示新增刚完成的测试数据;“Avg/Min/Max”表示响应时间(单位为毫秒);“Err”表示出现错误的个数;“Active/Started/Finished”表示线程个数。

3.2.1. 停止测试

用在命令中启动测试后,会有如下提示(从中可以找到管理端口号):

Starting the test @ Wed Mar 14 18:04:44 CST 2018 (1521021884229)
Waiting for possible Shutdown/StopTestNow/Heapdump message on port 4445

想在测试还未正常结束时停止测试,可以运行:

$ ./bin/stoptest.sh 4445     # 端口号4445是启动测试时提示的端口号

3.3. 测试报告

测试结束后,打开报告目录(通过 -o 参数指定的目录)中的 index.html,可以看到相关的测试报告。

这里重点关注“Statistics”表格,如图 10 是设置 3 个线程,测试 60 秒钟得到的“Statistics”表格。

jmeter_statistics_table.gif

Figure 10: 设置 3 个线程,测试 60 秒钟得到的 Statistics 数据

相关说明如下:
1、“#Samples”表示测试实例的个数。本例中一共发送了 135 个“create orderer”请求,132 个“query orderer”请求;
2、“KO”和“Error %”表示测试失败的个数和百分比。本例中没有失败的测试。
3、“Average/Min/Max”表示“平均/最小/最大”响应时间(单位为毫秒)。
4、“90th pct/95th pct/99th pct”分别表示测试实例按响应时间从小到大排序后,第 90/95/99 个测试实例的响应时间。
5、“Throughput”表示吞吐率,即 1 秒内系统为多少个请求提供了服务。本例中吞吐率 2.24 和 2.27 分别由“135/60”和“132/60”计算而来(有浮点小误差)。

4. Tips

4.1. 使用“User Defined Variables”增加灵活性

如果测试中要发送多个 HTTP 请求到同一个服务器,我们可以在每个“HTTP Request”面板中把“Server Name or IP”设置为同一个服务器地址。但这个方法灵活性不好,如果我们想换一个服务器进行测试,则需要修改很多地方。

推荐使用“User Defined Variables”,其面板可以通过右击“Thread Group”,选择“Add”->“Config Element”->“User Defined Variables”来找到。在这个面板中,把服务器地址定义为某个变量(如 myServer ),然后在每个“HTTP Request”面板的“Server Name or IP”中使用 ${myServer} 来引用它。这样想换一个服务器进行测试时,只需要修改“User Defined Variables”中的配置即可。

4.2. 用 While Controller 实现“重复测试直到满足某条件”

假设,要测试两个 HTTP 请求:submit 和 retrieve。第一个请求用来提交任务到服务器,第二个请求用来查询任务的执行情况。但第二个请求,有时会返回 pending,有时会返回 done。我们想测试它直到第二个请求返回 done。

这种场景可以使用 While Controller 来实现。这里有个例子:https://stackoverflow.com/questions/39414799/jmeter-send-http-request-by-condition

4.3. 指定 pkcs12 格式的 client 证书(TLS 双向认证时需要)

当服务器启用了 TLS 双向认证时,我们需要指定 client 证书。这可以通过下面方法来实现。

方法一:在 JMeter 安装目录下的 ./bin/system.properties 文件中指定:

javax.net.ssl.keyStoreType=pkcs12
javax.net.ssl.keyStore=/path/to/your/certificate.p12
javax.net.ssl.keyStorePassword=your_certificate_password_here

方法二:启动 jmeter 时指定:

$ jmeter -Djavax.net.ssl.keyStoreType=pkcs12 -Djavax.net.ssl.keyStore=/path/to/your/certificate.p12 -Djavax.net.ssl.keyStorePassword=your_certificate_password_here

Author: cig01

Created: <2018-02-25 Sun>

Last updated: <2020-11-05 Thu>

Creator: Emacs 27.1 (Org mode 9.4)