实现接口性能压测工具类
手撸一个接口性能压测工具类
常用的好用的压测工具
Apache服务器安装目录的 ab.exe
Jmeter
LoadRunner
为什么要自己实现一个压测工具?
高并发有很多知识点,工具类(如:线程池、JUC四个常用工具类【CountDownLatch、CyclicBarrier、Semaphore、ExChange】等),通过不同的场景案例去实战演练,才能深入掌握这些知识点,然后才能够灵活地去解决业务问题。
本文目标
手写一个接口性能压测工具类,能够加深对下面3个知识点的理解,同时可以得到一个常用的压测工具类,方便自己或他人使用。
涉及的知识点
线程池(ThreadPoolExecutor)
CountDownLatch
AtomicInteger
要实现的功能:写一个通用的压测工具类
类名
LoadRunnerUtils
类中定义一个通用的压测方法
方法定义如下,提供3个参数,可以对第3个参数需要执行的业务进行压测,最终将压测的结果返回。
/**
* 对 command 执行压测
*
* @param requests 总请求数
* @param concurrency 并发数量
* @param command 需要执行的压测代码
* @param <T>
* @return 压测结果 {@link LoadRunnerResult}
* @throws InterruptedException
*/
public static <T> LoadRunnerResult run(int requests, int concurrency, Runnable command)
方法返回压测结果(LoadRunnerResult)
LoadRunnerResult 包含了压测结果,定义如下,主要有下面这些指标
public static class LoadRunnerResult {
// 请求总数
private int requests;
// 并发量
private int concurrency;
// 成功请求数
private int successRequests;
// 失败请求数
private int failRequests;
// 请求总耗时(ms)
private int timeTakenForTests;
// 每秒请求数(吞吐量)
private float requestsPerSecond;
// 每个请求平均耗时(ms)
private float timePerRequest;
// 最快的请求耗时(ms)
private float fastestCostTime;
// 最慢的请求耗时(ms)
private float slowestCostTime;
}
压测工具类
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 压测工具类
*/
@Slf4j
public class LoadRunnerUtils {
@Data
public static class LoadRunnerResult {
// 请求总数
private int requests;
// 并发量
private int concurrency;
// 成功请求数
private int successRequests;
// 失败请求数
private int failRequests;
// 请求总耗时(ms)
private int timeTakenForTests;
// 每秒请求数(吞吐量)
private float requestsPerSecond;
// 每个请求平均耗时(ms)
private float timePerRequest;
// 最快的请求耗时(ms)
private float fastestCostTime;
// 最慢的请求耗时(ms)
private float slowestCostTime;
}
/**
* 对 command 执行压测
*
* @param requests 总请求数
* @param concurrency 并发数量
* @param command 需要执行的压测代码
* @param <T>
* @return 压测结果 {@link LoadRunnerResult}
* @throws InterruptedException
*/
public static <T> LoadRunnerResult run(int requests, int concurrency, Runnable command) throws InterruptedException {
log.info("压测开始......");
//创建线程池,并将所有核心线程池都准备好
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(concurrency, concurrency,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
poolExecutor.prestartAllCoreThreads();
// 创建一个 CountDownLatch,用于阻塞当前线程池待所有请求处理完毕后,让当前线程继续向下走
CountDownLatch countDownLatch = new CountDownLatch(requests);
//成功请求数、最快耗时、最慢耗时 (这几个值涉及到并发操作,所以采用 AtomicInteger 避免并发修改导致数据错误)
AtomicInteger successRequests = new AtomicInteger(0);
AtomicInteger fastestCostTime = new AtomicInteger(Integer.MAX_VALUE);
AtomicInteger slowestCostTime = new AtomicInteger(Integer.MIN_VALUE);
long startTime = System.currentTimeMillis();
//循环中使用线程池处理被压测的方法
for (int i = 0; i < requests; i++) {
poolExecutor.execute(() -> {
try {
long requestStartTime = System.currentTimeMillis();
//执行被压测的方法
command.run();
//command执行耗时
int costTime = (int) (System.currentTimeMillis() - requestStartTime);
//请求最快耗时
setFastestCostTime(fastestCostTime, costTime);
//请求最慢耗时
setSlowestCostTimeCostTime(slowestCostTime, costTime);
//成功请求数+1
successRequests.incrementAndGet();
} catch (Exception e) {
log.error(e.getMessage());
} finally {
countDownLatch.countDown();
}
});
}
//阻塞当前线程,等到压测结束后,该方法会被唤醒,线程继续向下走
countDownLatch.await();
//关闭线程池
poolExecutor.shutdown();
long endTime = System.currentTimeMillis();
log.info("压测结束,总耗时(ms):{}", (endTime - startTime));
//组装最后的结果返回
LoadRunnerResult result = new LoadRunnerResult();
result.setRequests(requests);
result.setConcurrency(concurrency);
result.setSuccessRequests(successRequests.get());
result.setFailRequests(requests - result.getSuccessRequests());
result.setTimeTakenForTests((int) (endTime - startTime));
result.setRequestsPerSecond((float) requests * 1000f / (float) (result.getTimeTakenForTests()));
result.setTimePerRequest((float) result.getTimeTakenForTests() / (float) requests);
result.setFastestCostTime(fastestCostTime.get());
result.setSlowestCostTime(slowestCostTime.get());
return result;
}
private static void setFastestCostTime(AtomicInteger fastestCostTime, int costTime) {
while (true) {
int fsCostTime = fastestCostTime.get();
if (fsCostTime < costTime) {
break;
}
if (fastestCostTime.compareAndSet(fsCostTime, costTime)) {
break;
}
}
}
private static void setSlowestCostTimeCostTime(AtomicInteger slowestCostTime, int costTime) {
while (true) {
int slCostTime = slowestCostTime.get();
if (slCostTime > costTime) {
break;
}
if (slowestCostTime.compareAndSet(slCostTime, costTime)) {
break;
}
}
}
}
/**
* 此Filter用于记录接口耗时
*/
@Order(Ordered.HIGHEST_PRECEDENCE)
@WebFilter(urlPatterns = "/**", filterName = "CostTimeFilter")
@Component
public class CostTimeFilter extends OncePerRequestFilter {
private static final Logger LOGGER = LoggerFactory.getLogger(CostTimeFilter.class);
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
long st = System.currentTimeMillis();
try {
filterChain.doFilter(request, response);
} finally {
long et = System.currentTimeMillis();
LOGGER.info("请求地址:{},耗时(ms):{}", request.getRequestURL().toString(), (et - st));
}
}
}
2个测试案例
案例1:总请求1000个,并发100,压测一个简单的接口
接口代码如下,test1接口,很简单,没有任何逻辑,这个接口效率很高
@GetMapping("/test1")
public String test1() {
log.info("test1");
return "ok";
}
对应的压测用例代码
LoadRunnerUtilsTest 中的 test1方法
@Test
public void test1() throws InterruptedException {
//需要压测的接口地址,这里我们压测test1接口
//压测参数,总请求数量1000,并发100
int requests = 1000;
int concurrency = 100;
String url = "http://localhost:8080/test1";
System.out.println(String.format("压测接口:%s", url));
RestTemplate restTemplate = new RestTemplate();
//调用压测工具类开始压测
LoadRunnerUtils.LoadRunnerResult loadRunnerResult = LoadRunnerUtils.run(requests, concurrency, () -> {
restTemplate.getForObject(url, String.class);
});
//输出压测结果
print(loadRunnerResult);
}
运行test1用例,效果如下
压测接口:http://localhost:8080/test1
11:47:56 - 压测开始......
11:47:57 - 压测结束,总耗时(ms):601
压测结果如下:
==============================
请求总数: 1000
并发量: 100
成功请求数: 1000
失败请求数: 0
请求总耗时(ms): 601
每秒请求数(吞吐量): 1663.8936
每个请求平均耗时(ms): 0.601
最快的请求耗时(ms): 0.0
最慢的请求耗时(ms): 565.0
案例2:总请求1000个,并发100,压测一个耗时的接口
接口代码如下,test2接口,内部休眠了100毫秒,用于模拟业务耗时操作
@GetMapping("/test2")
public String test2() throws InterruptedException {
//接口中休眠100毫秒,用来模拟业务操作
TimeUnit.MILLISECONDS.sleep(100);
return "ok";
}
对应的压测用例代码
LoadRunnerUtilsTest 中的 test2方法
@Test
public void test2() throws InterruptedException {
//需要压测的接口地址,这里我们压测test2接口
//压测参数,总请求数量10000,并发100
int requests = 1000;
int concurrency = 100;
String url = "http://localhost:8080/test2";
System.out.println(String.format("压测接口:%s", url));
RestTemplate restTemplate = new RestTemplate();
//调用压测工具类开始压测
LoadRunnerUtils.LoadRunnerResult loadRunnerResult = LoadRunnerUtils.run(requests, concurrency, () -> {
restTemplate.getForObject(url, String.class);
});
//输出压测结果
print(loadRunnerResult);
}
运行test2用例,效果如下
压测接口:http://localhost:8080/test2
11:48:20 - 压测开始......
11:48:22 - 压测结束,总耗时(ms):1231
压测结果如下:
==============================
请求总数: 1000
并发量: 100
成功请求数: 1000
失败请求数: 0
请求总耗时(ms): 1231
每秒请求数(吞吐量): 812.34766
每个请求平均耗时(ms): 1.231
最快的请求耗时(ms): 100.0
最慢的请求耗时(ms): 281.0
==============================