avatar

code-xz

寻桨的舟

  • 首页
  • Java
  • godot入门教程
  • 技术文章收录
  • redis
  • 其他
Home 实现接口性能压测工具类
文章

实现接口性能压测工具类

Posted 2023-08-19 Updated 2024-10- 15
By Administrator
26~33 min read

手撸一个接口性能压测工具类

常用的好用的压测工具

  1. Apache服务器安装目录的 ab.exe

  1. Jmeter

  1. LoadRunner

为什么要自己实现一个压测工具?

高并发有很多知识点,工具类(如:线程池、JUC四个常用工具类【CountDownLatch、CyclicBarrier、Semaphore、ExChange】等),通过不同的场景案例去实战演练,才能深入掌握这些知识点,然后才能够灵活地去解决业务问题。

本文目标

手写一个接口性能压测工具类,能够加深对下面3个知识点的理解,同时可以得到一个常用的压测工具类,方便自己或他人使用。

涉及的知识点

  1. 线程池(ThreadPoolExecutor)

  1. CountDownLatch

  1. 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
==============================

Java
License: 
Share

Further Reading

Aug 19, 2023

实现接口性能压测工具类

手撸一个接口性能压测工具类 常用的好用的压测工具 Apache服务器安装目录的 ab.exe Jmeter LoadRunner

Aug 12, 2023

多线程任务批处理通用工具类

多线程任务批处理通用工具类 需求 使用线程池批量发送短信,当短信发送完毕之后,方法继续向下走。 技术点 线程池:ExecutorService CountDownLatch:可以让一个或者多个线程等待一批任务执行完毕之后,继续向下走

Apr 30, 2023

JMeter性能测试

1、准备工作:安装 Jmeter 安装 Jmeter 之前需要先安装好 JDK 环境。//Jmeter是使用Java编写的工具 下载 Jmeter 安装包(压缩包,windows 环境为 zip 格式):Apache JMeter - Apache JMeter™ 解压安装包,进入 bin 目录,点

OLDER

多线程任务批处理通用工具类

NEWER

Katago调研报告

Recently Updated

  • Katago调研报告
  • 实现接口性能压测工具类
  • 多线程任务批处理通用工具类
  • 异步机制:如何避免单线程模型的阻塞?
  • 消息队列的考验:Redis有哪些解决方案?

Trending Tags

godot redis

Contents

©2025 code-xz. Some rights reserved.

Using the Halo theme Chirpy