🎯 本文档详细介绍了如何使用Apache JMeter进行压力测试,以评估预定接口在高并发场景下的性能表现。通过创建线程组模拟不同数量的用户并发请求,利用CSV文件动态配置时间段ID和用户token,确保了测试数据的真实性和有效性。文档中还展示了如何设置JMeter的各项参数、添加HTTP请求头、查看结果树和聚合报告等操作步骤。最终,通过一次针对4000用户并发的压测实例,分析了样本数、响应时间、异常率及吞吐量等关键指标,验证了系统的稳定性和可靠性。
文章目录
压力测试
为了评估和测量接口在高负载情况下的性能表现。压力测试通常用于确定系统在预期的最大负载下的运行情况,识别系统可能存在的性能瓶颈,以及验证系统的稳定性和可靠性。压力测试对于确保应用程序能够支持特定数量的并发用户或操作至关重要。
Jmeter介绍
Apache JMeter 是一款开源的、基于Java的性能测试工具,主要用于测试静态和动态资源(如静态文件、Java Servlets、CGI脚本、数据库和其他基于Web的应用程序资源等)的性能。它最初设计用于Web应用测试但后来扩展到其他测试领域。JMeter可以用来模拟大量用户并发访问目标服务,以此来分析在不同负载条件下应用的性能表现。此外,它支持多种协议和技术,包括HTTP、HTTPS、FTP、SOAP、REST、LDAP、TCP、SMTP等,极大地增强了其灵活性和适用范围。JMeter的一大优点是它能够以图形界面或命令行模式运行,而且由于它是用Java编写的,因此可以在任何安装了Java虚拟机的平台上使用,具有很好的跨平台性。
- 官网:https://jmeter.apache.org/
- 下载地址:https://jmeter.apache.org/download_jmeter.cgi
测试目标
测试时间段预定接口在不同并发用户下的吞吐量。
数据准备
为了让测试结果更加有参考性,需要尽量模拟现实生活中的预定逻辑,即肯定是有多个用户同时进行预定,且预定的场馆、分区、时间段都可能不同。因此我们需要先模拟生成一些数据,其中包括场馆、分区、时间段模板、时间段。最终需要传给Jmeter的数据有:
- 可接受预定的不同时间段 id
- 不同用户登录之后的 token
Jmeter如何进行操作
Jmeter设置
修改为白色外观
设置为简体中文,方便操作,如果你英语好,当我没说,哈哈哈
创建线程组
创建线程组是一个基础且关键的步骤。线程组主要用来模拟用户对服务器或应用程序发起请求的行为。具体来说,它定义了虚拟用户的数目(即线程数)、这些用户将如何行动以及它们执行动作的时间安排(如启动时间、持续时间和关闭时间)。
初步设置如下参数,后续在进行压力测试的时候,可以从小到大调整线程数等参数
- 一个线程代表一个用户,每个用户对不同时间段发起多次请求,可以先从50个用户开始,逐步增加到500或1000个用户,观察接口的性能变化
- 循环次数:每个线程发请求的数量,相当于一个用户发起多少次预定
- Ramp-Up时间:设置为5秒,表示这1000个线程,会在5秒内均匀启动起来,每个线程之间的启动间隔大约为 5/1000 秒
创建 HTTP 请求
预订接口如下:
@GetMapping("/v1/reserve")
public Result reserve(@RequestParam("timePeriodId") Long timePeriodId) {
OrderDO orderDO = timePeriodService.reserve(timePeriodId);
return Results.success(orderDO);
}
这里需要设置服务所在IP、端口,以及请求的接口路径。因为预定的时候,需要指明是哪个时间段,所以需要在参数中进行设置
添加请求头
由于用户在预定时间段的时候,需要先从用户登录之后的 token 信息中获知用户是谁,所以我们需要将 token 设置到请求头中
注意:除了设置 token 之外,还需要添加Content-type
为application/json
,后端接口才能正常解析 json 数据
添加查看结果树、聚合报告
- 结果树:用来查看请求的请求参数、响应结果
- 聚合报告:用来查看这些请求的统计信息
动态参数
因为我们需要模拟不同用户预定不同时间段的行为,这期间用户、时间段都有多个,因此,我们不能写死 HTTP 请求中的请求参数,而是需要使用动态参数,从 CSV 文件中读取数据,然后动态设置到不同的请求中
给请求参数配置不同的时间段ID
依赖
CSV导出直接写一个单元测试类即可,首先引入测试相关的依赖
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
CSV生成代码
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.vrs.domain.entity.TimePeriodDO;
import com.vrs.service.TimePeriodService;
import com.vrs.utils.TxtUtil;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import java.io.File;
import java.nio.file.Paths;
import java.time.LocalDate;
import java.util.List;
/**
* 可预定时间段id CSV 导出
*
* @Author dam
* @create 2025/1/12 15:06
*/
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = {VrsVenueApplication.class})
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class ReserveTestCSVGenerateTest {
@Autowired
private TimePeriodService timePeriodService;
/**
* csv地址
*/
private final String csvPath = Paths.get("").toAbsolutePath().getParent().getParent() + File.separator + "tmp" + File.separator + "场馆预定时间段.csv";
@Test
public void generate() throws Exception {
StringBuilder stringBuilder = new StringBuilder();
QueryWrapper<TimePeriodDO> queryWrapper = new QueryWrapper<>();
// 只查询在今天和今天之后的可预订时间段
queryWrapper.ge("period_date", LocalDate.now());
List<TimePeriodDO> timePeriodDOList = timePeriodService.list(queryWrapper);
for (TimePeriodDO timePeriodDO : timePeriodDOList) {
stringBuilder.append(timePeriodDO.getId() + "\n");
}
TxtUtil.write(new File(csvPath), stringBuilder.toString(), "utf-8");
}
}
通过下面的注解为基于Spring Boot的应用程序提供全面的测试支持,包括依赖注入、应用上下文的配置以及Web环境的模拟等,这样我们才可以注入TimePeriodService来进行查询数据库等操作
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = {VrsVenueApplication.class})
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
导出的CSV文件如下图所示
绑定CSV文件
最后一步是给请求绑定 CSV 文件
设置CSV文件路径
最后在请求的参数中使用,通过表头列名来绑定数据,使用方式为${列名}
配置不同的用户token
CSV 生成
import com.vrs.domain.entity.UserDO;
import com.vrs.service.UserService;
import com.vrs.utils.TxtUtil;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import java.io.File;
import java.nio.file.Paths;
import java.util.List;
/**
* 模拟用户数据生成
*
* @Author dam
* @create 2025/1/12 15:06
*/
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = {VrsAdminApplication.class})
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class UserTokenCSVGenerateTest {
@Autowired
private UserService userService;
/**
* csv地址
*/
private final String csvPath = Paths.get("").toAbsolutePath().getParent().getParent() + File.separator + "tmp" + File.separator + "用户token.csv";
@Test
public void generate() throws Exception {
StringBuilder stringBuilder = new StringBuilder();
List<UserDO> userDOList = userService.list();
for (UserDO userDO : userDOList) {
// 登录并返回一个token
stringBuilder.append(userService.handleLogin(userDO).getToken() + "\n");
}
TxtUtil.write(new File(csvPath), stringBuilder.toString(), "utf-8");
}
}
绑定 CSV 文件
简单测试
完成上面的操作之后,启动压力测试即可,在查看结果树中,可以看到每个请求是否成功,响应结果是什么
接口错误的原因
在汇总报告中,可以查看压力测试的统计数据,例如接口调用时间的平均值、最小值、最大值,吞吐量……,这里异常那么高的原因是:用户已经购买过相应时间段或者时间段已经售罄
正式压测
测试环境
【测试机器】
- 名称:MacBook Pro 2023
- 尺寸:14英寸
- CPU:m2 pro丐版芯片
- 内存:16GB
【服务启动方式】
为了模拟真实分布式环境下的性能表现,项目使用微服务方式启动
内存预热
为了在预定的时候可以快速查询,首先对需要使用到缓存进行预热,这里涉及的缓存有时间段信息、时间段库存、时间段位图
package com.vrs;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.vrs.constant.RedisCacheConstant;
import com.vrs.domain.entity.PartitionDO;
import com.vrs.domain.entity.TimePeriodDO;
import com.vrs.service.PartitionService;
import com.vrs.service.TimePeriodService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import java.time.LocalDate;
import java.util.List;
/**
* 时间段预定缓存预热
*
* @Author dam
* @create 2025/1/12 15:06
*/
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = {VrsVenueApplication.class})
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class TimePeriodCacheLoadTest {
@Autowired
private TimePeriodService timePeriodService;
@Autowired
private PartitionService partitionService;
@Test
public void generate() throws Exception {
QueryWrapper<TimePeriodDO> queryWrapper = new QueryWrapper<>();
// 只查询在今天和今天之后的可预订时间段
queryWrapper.ge("period_date", LocalDate.now());
List<TimePeriodDO> timePeriodDOList = timePeriodService.list(queryWrapper);
for (TimePeriodDO timePeriodDO : timePeriodDOList) {
timePeriodService.getTimePeriodDOById(timePeriodDO.getId());
PartitionDO partitionDO = partitionService.getPartitionDOById(timePeriodDO.getPartitionId());
// 首先检测空闲场号缓存有没有加载好,没有的话进行加载
timePeriodService.checkBitMapCache(String.format(RedisCacheConstant.VENUE_TIME_PERIOD_FREE_INDEX_BIT_MAP_KEY, timePeriodDO.getId()), timePeriodDO.getId(), partitionDO.getNum());
// 其次检测时间段库存有没有加载好,没有的话进行加载
timePeriodService.getStockByTimePeriodId(timePeriodDO.getId());
}
}
}
预热之后的缓存如下:
压力测试
测试参数如下:
- 线程数:4000
- 循环次数:10
- Ramp-Up时间
即模拟4000个用户进行场馆预定,每个用户分别发送20次预定请求,线程在20秒内启动完成
测试结果如下:
- 样本数量:总共有40,000个样本,这表示在测试期间进行了40,000次请求或操作。
- 响应时间:
- 平均值:6036毫秒,表示所有请求的平均响应时间。
- 中位数:6442毫秒,表示50%的请求响应时间低于此值。
- 90%百分位:7155毫秒,表示90%的请求响应时间低于此值。
- 95%百分位:7264毫秒,表示95%的请求响应时间低于此值。
- 99%百分位:7482毫秒,表示99%的请求响应时间低于此值。
- 最小值:2毫秒,表示最快的请求响应时间。
- 最大值:26692毫秒,表示最慢的请求响应时间。
- 异常率:7.84%的请求出现了异常,当然这里的异常是:时间段售罄、时间段过期之类的。
- 吞吐量:508.3次请求/秒,表示系统在测试期间每秒处理的请求数量。
- 网络流量:
- 接收速率:284.68 KB/sec,表示系统每秒接收的数据量。
- 发送速率:248.40 KB/sec,表示系统每秒发送的数据量。