本文是《如何做好性能压测》系列专题分享的第三期,该专题将从性能压测的设计、实现、执行、监控、问题定位和分析、应用场景等多个纬度对性能压测的全过程进行拆解,以帮助大家构建完整的性能压测的理论体系,并提供有例可依的实战。

该系列专题分享由阿里巴巴 PTS 团队出品,欢迎加入性能压测技术交流钉钉群(推荐):23380915,参与《如何做好性能压测》线上直播分享,若没有钉钉,请添加微信 zjjxg2018,进入微信群。

第一期:《压测环境的设计和搭建》,点击这里
第二期:《性能压测工具选型对比》,点击这里

第三期将分享阿里巴巴 PTS 在 JMeter 上的实践,并结合自研产品一一破解在 JMeter 上遇到的不足。

Apache JMeter 是 Apache 旗下的开源压测工具,创建于 1999 年初,迄今已有超过 20 年历史。JMeter 功能丰富,社区(用户群体)庞大,是主流开源压测工具之一。

性能测试通常集中在新系统上线或大型活动前(如电商大促,春节活动等),以验证系统能力,帮助排查定位性能瓶颈等问题。

一次压测活动可粗略分为几个步骤:

  1. 场景配置。配置压测场景模拟用户(业务)与系统的交互。
  2. 压测执行。按指定压力量级启动压测。
  3. 压测监控分析。压测中通常关注施压 RPS,成功率,业务响应时间(RT),网络带宽等关键指标。
  4. 报告总结。披露系统能力是否符合要求,同时沉淀记录系统性能演变和优化过程。

下面我们讨论如何使用 JMeter 完成上述步骤,及相关的最佳实践建议。

JMeter 使用 Java 开发,需要先安装 JDK 并配置好 PATH 环境变量,然后从官网下载 JMeter 二进制压缩包解压即可。
建议将 JMeter bin 目录也添加到 PATH 环境变量,这样在命令行下输入 jmeter 命令即可启动 JMeter 。

场景配置

简单 HTTP 请求配置

最常见的压测场景即 HTTP 压测。
压测场景在 JMeter 脚本中叫做 Test Plan(压测计划),打开 JMeter 默认即为一个空 Test Plan 。JMeter 使用并发(线程)数控制压力大小,一个线程可看做一个执行请求的虚拟用户。在 Test Plan 上点击右键,添加一个 Thread Group(线程组)。

线程组默认为 1 个线程并只执行一次 1 次,这很方便测试执行脚本,保持此默认值即可。

JMeter 中发送请求的组件叫做 Sampler(采样器)。在 Thread Group 上单击右键,添加一个 HTTP Request 节点(采样器)。

HTTP 请求最关键的配置即 URL,JMeter 允许将 URL 协议类型(Protocol)、服务器名、请求路径(Path)等拆开单独配置。也可以直接将整个 URL(如 JMeter 主页 http://jmeter.apache.org/ )填写到 Path,其他字段保留为空即可。这样,一个最简单的 HTTP 压测脚本就配置好了。

为了方便测试、调试脚本,可在 Test Plan 下添加一个 View Results Tree 监听器(Listener)。这个监听器仅用于编辑脚本时测试、调试脚本,查看请求执行详情,不需要做任何配置。

测试执行脚本

第一次执行脚本前,需要先保存脚本,如保存为 test.jmx 。以后每次执行脚本前,JMeter 默认会自动保存脚本。

连续多次执行脚本时,JMeter 默认不会清理历史记录。为了避免历史执行结果干扰,可先点击 Clear All 按钮手动清空历史记录,再点击 Start 按钮执行脚本,这样看到的执行结果更清爽,方便排查问题。

按照默认线程组配置,脚本执行一次即结束。点击 View Results Tree ,可看到请求执行详细信息,包括请求头,请求体,响应头和完整的响应体等信息。

场景编排

真实压测场景通常不会只有一个请求,而是多个请求按一定顺序和规则的编排组合,即场景编排。场景编排是 JMeter 等压测引擎最重要的功能之一,也是与 apache ab等简单压测工具的重要区别之一。

这里我们假设一个最简单的场景,先访问 JMeter 主页,停留 1 秒钟后跳转到下载页。

一个脚本访问一个网站的不同页面(Path)时,可添加一个 HTTP Request Defaults 节点,配置默认协议类型和服务器名。这样可避免重复配置,需要修改协议类型(如 https 与 http 切换)或压测域名时,只用修改 HTTP Request Defaults 即可。

HTTP Request Defaults 配置服务器名为 jmeter.apache.org(协议类型默认为 http),鼠标可拖动 HTTP Request Defaults 节点移动到 HTTP 请求节点之前。

每个请求节点可设置一个具有业务含义的名字,方便理解和管理。访问 JMeter 主页的 HTTP 请求可改名为 home ,同时 Path 修改为 / 。再添加一个 HTTP 请求节点,命名为 download page ,设置 Path 为 /download_jmeter.cgi 即可。

模拟在 home 页面停顿 1 秒钟。home 节点上右键,添加一个 Constant Timer 子节点,设置延迟时间为 1000 毫秒即可。

再次执行脚本,点击 View Results Tree 可看到两个 HTTP 请求节点的执行详情。

注意:

  1. Timer 节点作为场景编排辅助节点,没有请求执行动作,也没有执行详情显示。
  2. 循环执行脚本时,最后一个节点 download page 执行结束后,会立即跳转到脚本开头,执行第一个节点 home 。

可在 download page 上也添加一个 Timer,模拟停留一秒之后再继续后续请求。

JMeter 的压测执行

编辑、调试脚本时,我们通常设置为 1 个线程并且只执行 1 次。执行压力测试时,通常需要以较高的压力持续执行一段时间。

脚本固定配置压力

如计划以 50 并发执行 2 分钟,可修改脚本 Thread Group 配置如下。

配置说明:

  1. 并发数(Number of Threads (users))设置为 50 。
  2. 循环次数(Loop Count)勾选永远执行(Forever)。
  3. 勾选 Scheduler,设置执行时长(Duration (seconds))为 120 秒。

通常我们在 JMeter 图形界面(GUI)编辑脚本,但执行压力测试时 GUI 占用额外资源可能影响施压性能,而且施压机可能没有图形界面环境(如 ssh 远程登录施压机)。因此脚本编辑完成后,通常以命令行模式执行 JMeter 压力测试。

进入 JMeter 脚本目录,执行 JMeter 压力测试的命令为:

jmeter -n -t <脚本>

如执行上述 test.jmx 脚本,命令如下:

jmeter -n -t test.jmx

输出结果如下:

Creating summariser <summary>
Created the tree successfully using test.jmx
Starting the test @ Tue Jun 25 14:38:32 CST 2019 (1561444712414)
Waiting for possible Shutdown/StopTestNow/Heapdump message on port 4445
summary +    553 in 00:00:27 =   20.3/s Avg:  1378 Min:   252 Max:  8587 Err:     0 (0.00%) Active: 50 Started: 50 Finished: 0
summary +    882 in 00:00:30 =   29.4/s Avg:   685 Min:   222 Max:  4272 Err:     0 (0.00%) Active: 50 Started: 50 Finished: 0
summary =   1435 in 00:00:57 =   25.1/s Avg:   952 Min:   222 Max:  8587 Err:     0 (0.00%)
summary +    829 in 00:00:30 =   27.5/s Avg:   815 Min:   222 Max: 21310 Err:     0 (0.00%) Active: 50 Started: 50 Finished: 0
summary =   2264 in 00:01:27 =   25.9/s Avg:   902 Min:   222 Max: 21310 Err:     0 (0.00%)
summary +    881 in 00:00:30 =   29.5/s Avg:   700 Min:   221 Max:  3896 Err:     0 (0.00%) Active: 50 Started: 50 Finished: 0
summary =   3145 in 00:01:57 =   26.8/s Avg:   845 Min:   221 Max: 21310 Err:     0 (0.00%)
summary +    127 in 00:00:05 =   24.2/s Avg:   797 Min:   224 Max:  3819 Err:     0 (0.00%) Active: 0 Started: 50 Finished: 50
summary =   3272 in 00:02:02 =   26.7/s Avg:   843 Min:   221 Max: 21310 Err:     0 (0.00%)
Tidying up ...    @ Tue Jun 25 14:40:35 CST 2019 (1561444835251)
... end of run

压测过程中默认每 30 秒输出一次统计数据,2 分钟后(实际为 00:02:02,比预设的 2 分钟多出少许误差)压测结束。看最后一行统计数据,平均 RPS(每秒请求数)为 26.7,平均 RT (响应时间)为 843 毫秒。

尝试核对一下统计数据。脚本包含两个请求,每个请求附加 1 秒钟等待时间,发送一个请求平均耗时为 RT 843 毫秒 + 等待 1000 毫秒。单线程理论 RPS 为 1000.0 / (843 + 1000),总共 50 个线程,全场景理论 RPS 为 1000.0 / (843 + 1000) * 50 = 27.13,与统计值 26.7 有一定误差。这是因为除了请求 RT 和等待时间,脚本执行请求之间还可能存在少量时间消耗。

命令行动态设置压力

实际工作中,常常需要以不同的压力大小反复执行压力测试,在脚本中写死压力大小(并发数)和执行时间显然很不方便。

如何动态指定压力大小呢,这里有一个技巧。JMeter 脚本支持使用 JMeter 属性进行配置,JMeter 命令行支持使用 -J 参数动态指定 JMeter 属性。把这两者结合起来,即可实现在命令行通过 -J 参数动态设置压力大小。

修改 JMeter 脚本使用 JMeter 属性配置压力大小,配置如下:

配置说明:

  1. 并发数配置为 ${__P(load.concurrency,1)},循环次数取消勾选 Forever,配置为 ${__P(load.count,1)}。 未设置对应的 JMeter 属性时,默认为 1 ,满足只执行 1 次以测试、调试脚本的需求。
  2. 执行时长配置为 ${__P(load.duration,60)},默认 1 分钟(60 秒)。

测试命令行直接执行脚本:

jmeter -n -t test.jmx

可看到统计输出如下:

summary =      2 in 00:00:03 =    0.6/s Avg:   485 Min:   408 Max:   563 Err:     0 (0.00%)

默认一个并发并且只执行一次,发出 2 个请求(脚本循环一次发出两个请求),约 3 秒后脚本停止。
注意:默认执行时间是 1 分钟,同时配置了循环次数和执行时间时,有一个条件先满足脚本即停止。

为了按指定时长执行,需要将执行次数设置为 Forever 。
在 JMeter 内部实现中,执行次数为 -1 即表示 Forever 。
指定以 50 并发执行 2 分钟,jmeter 命令行如下:

jmeter -n -t test.jmx -Jload.concurrency=50 -Jload.duration=120 -Jload.count=-1

执行结果与前述脚本固定配置 50 并发的结果类似。

云上的 JMeter 实践

阿里巴巴有着非常丰富的业务形态,每一种业务形态背后都由一系列分布式的技术体系提供服务,随着业务的快速发展,特别是在双 11 等大促营销等活动场景下,准确评估整个业务站点的服务能力成为一大技术难题。

在这个过程中,我们打造了自己的全链路压测系统,以应对更复杂、更多样的压测需求,并将此技术输出到 性能测试 PTS 上,同时支持原生 JMeter 压测。

打开 PTS 控制台 主页,左侧导航栏选择 创建压测 > JMeter 压测 ,新建 JMeter 压测场景。填写场景名,如 jmeter-test场景配置 页面点击 上传文件 按钮,上传本地测试通过的 test.jmx 脚本。

施压配置 页面,并发数设置为 50,压测时长设置为 2 分钟。

点击 保存去压测,弹出提示框点击 确认,PTS 即开始在云端引擎执行 JMeter 脚本发起压力。

压测中页面如下:

压测中实时展示场景(及每个请求页面)实时 RPS 和 RT 等信息。可看到场景并发数为 50,RPS 为 26,RT 为 812 毫秒,与本地压测的结果差不多。
注意:因为机器配置和网络环境的差异(PTS 施压机默认为 4 核 8G,BGP 多线路公网),PTS 上压测结果可能与本地压测结果存在一定差异。

针对 JMeter 施压配置,再补充几点说明:

  1. PTS 上的施压配置会覆盖原脚本中的配置。 原脚本无论是写死固定配置还是使用 JMeter 属性配置都没关系。
  2. 循环次数表示每个线程循环执行脚本的次数,可能与用户的直觉理解不一样,如 总请求数 = 脚本执行一次的请求数 * 循环次数 * 并发数
  3. 循环次数与预热时间同时配置时可能导致意外行为(如不能达到最大并发), 因此 PTS 上不允许同时配置预热时间和循环次数(即只有预热时间为 0 时才允许设置循环次数)。

压测监控分析

性能测试不仅仅是简单的发起压力,对压力负载(RPS,网络带宽等)和业务表现(RT,成功率等)的监控和分析也是压测活动的重要组成部分。

JMeter 脚本中每个请求节点(Sampler)可设置一个具有业务含义的名字(如 home 和 download page ),我们可称之为业务 API 。JMeter 监控统计按业务 API 名字汇总,如两个名字相同的请求节点将汇总统计为一个业务 API 。配置脚本时需注意,不同业务 API 节点应配置为不同的名字。

业务 API 压力负载和表现

实际工作中,不同业务 API 的统计数据可能存在巨大差异(如浏览商品 RT 通常比提交订单快很多),因此 PTS 默认将各个业务 API 独立统计展示(如上述压测中页面展示的 homedownload page)。

压测中每个时间点的数据 PTS 都在后台记录了下来,最终将形成完整直观的压测报告。点击业务 API 实时监控趋势图按钮 ,即可查看对应的 RPS,成功率,响应时间,网络带宽等监控数据的变化趋势图。

业务 API 采样日志

很多时候我们还希望看到一个具体请求执行的详细信息。如有 1% 的请求失败,需要查看完整的请求、响应内容,以排查失败原因等。JMeter 图形界面下测试脚本时,可添加 View Results Tree 查看单个请求的详细信息,但执行压力测试时,对每个请求都记录详细信息,不仅没有必要,而且非常耗费资源,影响施压性能。

阿里云 PTS 采取了一个折中的办法,施压引擎每 5 秒对每个业务 API(压测 Sampler)分别采样记录一条成功和失败(如果有)的请求详细信息。在压测中或压测报告页面,点击 查看采样日志 按钮即可查询记录的请求采样信息,并支持按业务 API(压测 Sampler),响应状态(是否成功),请求时间等进行搜索过滤。

点击 查看详情 即可看到单个请求的详细信息。目前对详细信息提供了通用和 HTTP 两种展示模板,HTTP 展示模板可针对 HTTP 请求进行更友好的排版展示,展示内容包括请求 URL,请求方法,返回码,完整的请求头、请求体,响应头、响应体等。

因为页面上只展示文本内容,请求体或响应体包含图片等无法识别为文本的内容时,可能显示为乱码。另外当请求体或响应体很大时,对应的内容可能被截断。

JMeter 日志

本地执行 JMeter 脚本时,默认将日志记录到 jmeter.log 文件。在 PTS 上执行 JMeter 脚本时,可通过 JMeter 日志 页面实时查看 JMeter 日志,并支持根据日志级别、时间或线程名进行查询过滤。

JMeter 日志主要用于脚本执行报错时排查错误原因。一些插件可能通过 JMeter 日志输出一些重要信息,用户在 groovy 脚本等代码中也可以直接打印日志。

日志打印过于频繁时,不仅可读性极差(大量重复日志淹没重要信息),而且影响 JMeter 性能,对 PTS 采集存储 JMeter 日志造成额外开销。因此 PTS 在采集 JMeter 日志时默认进行了限流,每秒钟打印日志条数超过 10 条时部分日志可能会丢失。良好设计的 JMeter 脚本应避免大量打印重复日志。

同样,良好设计的 JMeter 脚本应避免通过标准输出(System.out)或标准错误(System.err)打印输出信息,
需要输出查看的重要信息应使用 JMeter 日志输出(如 groovy 脚本中使用 log.info("") )。
PTS 上不支持查看 JMeter 脚本执行产生的标准输出和标准错误内容。

报告总结

压测结束后,PTS 将汇总监控数据形成压测报告。用户根据压测报告分析评估系统性能是否符合要求,如 RPS,成功率和 RT(响应时间)是否符合期望。并可辅助用户排查分析业务系统性能瓶颈。

PTS 压测报告页面可查询历史压测报告列表。

点击 查看报告 打开查看报告详情。压测报告在 PTS 上默认保存 30 天,可点击 报告导出 按钮,导出保存 PDF 版压测报告到本地。
压测报告概要信息包括压测执行时间,RPS,RT,成功率等概要数据。场景详情包含全场景维度和业务 API 维度的监控统计信息。
注意:受 JMeter 引擎本身统计功能的限制,仅全场景维度包含并发数统计。

此外,全场景维度和业务 API 维度均包含 RPS,成功率,网络带宽等统计。如 home 请求相关监控趋势图如下。

相比手动命令行执行 JMeter 脚本,PTS 更加 简单易用 ,提供 简单直观的监控 ,并提供 海量施压能力

免费 开通 PTS 服务,购买 5000 VU
以上资源包,即可使用 JMeter 压测。PTS 计费单位是 VUM ,即并发(VU)* 分钟(最低消费 100 VUM)。上述场景消费为 50 并发 * 2 分钟 = 100 VUM 。如购买 5000 VU资源包,包含 10 万 VUM,售价 ¥278 ,上述计费可换算为 (100 / 10万) * ¥278 = ¥0.278

使用 CSV 参数文件

上述 JMeter 脚本仅简单请求固定 URL ,真实业务 API 通常带有请求参数。

JMeter 中可使用 CSV Data Set Config 读取 CSV 数据文件,简单实现请求参数化。CSV 文件默认首行为变量名(列名),其余行为 CSV 数据。准备一个 user.csv 文件,包含 idname 两列,内容如下:

id,name
1,ali
2,pts
3,jmeter

注意:手工编辑 CSV 文件容易出错,推荐使用 Excel、Numbers 等软件导出,或编程使用 apache commons-csv 生成。编辑 JMeter 脚本,右键单击 Thread Group 添加一个 CSV Data Set Config 节点。

配置如下:

其中有两个地方需要注意(其他配置保持默认即可):

  1. Filename 配置为文件名 user.csv 即可,不要包含文件路径。 不同施压机上 CSV 文件路径可能不一样,只使用文件名(并在当前路径下执行脚本)以便兼容不同的施压机环境。
  2. Sharing mode 设置为 Current thread group,指定 CSV 文件只被当前线程组使用。

假设 home 请求需要设置参数,配置请求参数直接使用 ${id}${name} 引用对应的变量即可。

测试执行脚本,查看 View Results Tree ,可看到执行请求时即会带上请求参数。

在阿里云 PTS 上执行脚本,只需编辑 JMeter 场景,上传修改后的 test.jmx 脚本文件和 user.csv 数据文件,点击 保存去压测 即可。

查看请求采样,可看到请求 URL 已添加对应的参数。

使用 JMeter 插件和附加 jar 包

JMeter 社区提供了丰富的插件,用户还可以自由添加使用 Java 库 Jar 包。在 PTS 上执行脚本时,只需要将额外添加的插件和 jar 包一起上传到 PTS 上,PTS 执行 JMeter 脚本时即可自动加载这些 Jar 包。详情可参考 如何进行 WebSocket 协议的压测

如果遇到问题,请先确认本地已测试通过,并确认额外使用的 Jar 包已全部上传到 PTS 。

海量施压能力

之前的脚本中,我们在请求后添加了 1 秒钟的等待时间以演示业务场景编排。如果压测场景前后请求没有关联,从服务器的角度看只是不停的收到各种请求,与客户端是否等待无关,因此 JMeter 脚本可去掉这些等待时间,以最大压力向服务器发起极限压测。

JMeter 使用并发(线程)数控制压力大小,通常(并发较小时)线程数越多压力越大。但单台施压机的能力毕竟有限,单机线程数增加到某个阈值时请求压力即达到极限。显然,为了继续增加压力,必须使用多台施压机进行分布式压测。

分布式压测需要注意以下几点:

  1. 每台施压机需要安装、启动 JMeter,拷贝分发脚本、CSV 数据、附加 jar 包等文件。
  2. 协调分配施压并发数。通常施压机配置相同,均分施压并发数即可。
  3. 统一控制启动、停止压测,统一收集聚合监控数据。
  4. 希望 CSV 数据唯一或打散数据时,多台施压机需要拆分 CSV 数据文件 。

使用阿里云 PTS 执行 JMeter 脚本时,用户只需要设置期望的最大并发即可 ,PTS 自动透明处理了分布式压测的问题,用户不用关心是单机压测还是分布式压测。PTS 默认每台 JMeter 施压机最大分配 500 并发,场景并发数超过 500 时即自动分配多台施压机。如果希望多台施压机拆分 CSV 数据文件,只要在上传 CSV 文件后勾选 切分文件 即可。详情参考 切分 CSV 数据文件

单机线程数极限与 JMeter 脚本的复杂程度和资源占用(如 CPU,内存,网络带宽占用等)情况有关。占用内存过高的 JMeter 脚本在并发过高时甚至可能会因内存溢出而导致 JMeter 引擎异常退出。对有特殊需求的高级用户,PTS 允许用户手动指定施压机数量,以更精确的控制总并发数和单机并发数。只需在 施压配置 页面勾选 指定 IP 数,设置相应的施压机数量即可。每台施压机有一个 IP 地址,指定 IP 数即指定施压机数量。

注意:指定 IP 数后,压测执行时独占了指定数量的施压机,每台施压机将按 500 并发计费(无论实际并发多少)。此功能专为有特殊需求的高级用户提供,普通用户通常不需要使用。

本文作者:韩勇,阿里云 PTS 技术专家。


↙↙↙阅读原文可查看相关链接,并与作者交流