24. Java 性能测试与压测工具链

从 JMH 微基准到 Gatling 协议压测,从 Arthas 线上诊断到全链路压测,构建完整的 Java 性能工具体系

性能是软件系统的关键质量属性之一。在 Java 生态中,性能测试涵盖多个层面:微基准测试验证代码片段性能、协议压测模拟用户请求、全链路压测验证系统整体容量。本文系统梳理从开发到生产的性能测试工具链。

1. JMH:Java 微基准测试

1.1 为什么需要 JMH

手写基准测试极易出错:

  • JIT 预热:JVM 需要数千次执行才会编译优化
  • 死代码消除:未使用的结果可能被 JVM 优化掉
  • 常数折叠:编译期计算出常量结果
  • GC 干扰:测试期间 GC 导致停顿

JMH(Java Microbenchmark Harness)是 OpenJDK 官方基准测试框架,正确处理了上述问题。

1.2 JMH 基础用法

<dependency>
    <groupId>org.openjdk.jmh</groupId>
    <artifactId>jmh-core</artifactId>
    <version>1.37</version>
    <scope>test</scope>
</dependency>
@BenchmarkMode(Mode.Throughput)        // 吞吐量模式 (ops/s)
@OutputTimeUnit(TimeUnit.MILLISECONDS) // 输出单位
@State(Scope.Thread)                   // 每个线程一个实例
@Warmup(iterations = 3, time = 1)      // 预热3轮
@Measurement(iterations = 5, time = 1) // 正式测量5轮
@Fork(2)                               // 2个独立 JVM 进程
public class StringConcatBenchmark {

    @Param({"10", "100", "1000"})
    private int count;

    private List<String> strings;

    @Setup
    public void setup() {
        strings = new ArrayList<>();
        for (int i = 0; i < count; i++) {
            strings.add(UUID.randomUUID().toString());
        }
    }

    @Benchmark
    public String stringBuilder() {
        StringBuilder sb = new StringBuilder();
        for (String s : strings) {
            sb.append(s);
        }
        return sb.toString();
    }

    @Benchmark
    public String stringJoin() {
        return String.join("", strings);
    }

    @Benchmark
    public String stringConcat() {
        String result = "";
        for (String s : strings) {
            result += s;  // 每次创建新 String
        }
        return result;
    }

    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
            .include(StringConcatBenchmark.class.getSimpleName())
            .build();
        new Runner(opt).run();
    }
}

1.3 关键注解速查

注解功能常用值
@BenchmarkMode测量指标Throughput, AverageTime, SampleTime, SingleShotTime, All
@OutputTimeUnit输出时间单位TimeUnit.NANOSECONDS ~ DAYS
@State实例作用域Thread, Benchmark, Group
@Setup / @TearDown初始化/清理Level.Trial, Iteration, Invocation
@Param参数化测试任意基本类型/字符串
@ForkJVM 进程数≥2 消除单 JVM 偏差
@Threads并发线程数压测并发场景
@Group线程组组合多个方法为线程组
@CompilerControl编译器控制INLINE, DONT_INLINE, EXCLUDE

1.4 避免常见陷阱

// ❌ 陷阱 1: 死代码消除(结果未被使用,JVM 直接优化掉)
@Benchmark
public void wrong() {
    Math.log(Math.PI);  // 结果丢弃,可能被完全消除
}

// ✅ 正确做法: 返回结果
@Benchmark
public double correct() {
    return Math.log(Math.PI);
}

// ❌ 陷阱 2: 常数折叠
@Benchmark
public double constantFold() {
    return Math.log(3.14159);  // 编译期计算,测试无意义
}

// ✅ 正确做法: 使用 @State 存储变量
@State(Scope.Thread)
public class Bench {
    private double value;
    @Setup
    public void setup() {
        value = System.currentTimeMillis();  // 运行期确定
    }
    @Benchmark
    public double correct() {
        return Math.log(value);
    }
}

// ❌ 陷阱 3: 测试包含非目标任务
@Benchmark
public List<String> wrong() {
    List<String> list = new ArrayList<>();  //  allocation 干扰
    for (int i = 0; i < 100; i++) {
        list.add("item" + i);
    }
    return list;
}

// ✅ 分离 setup
@State(Scope.Thread)
public class Bench {
    List<String> list;
    @Setup
    public void setup() {
        list = new ArrayList<>();
        for (int i = 0; i < 100; i++) list.add("item" + i);
    }
    @Benchmark
    public int correct() {
        return list.size();  // 只测量 size() 调用
    }
}

2. Gatling:协议级压测

2.1 Gatling 特点

  • Scala DSL:简洁的 API 描述场景
  • 异步非阻塞:基于 Netty + Akka,单机可达万级并发
  • 丰富的报告:HTML 报告含响应时间分布、吞吐量趋势

2.2 实战脚本

import io.gatling.core.Predef._
import io.gatling.http.Predef._
import scala.concurrent.duration._

class OrderApiSimulation extends Simulation {

  val httpProtocol = http
    .baseUrl("https://api.example.com")
    .acceptHeader("application/json")
    .contentTypeHeader("application/json")

  //  feeders: 数据驱动
  val userFeeder = csv("users.csv").random
  val orderFeeder = csv("orders.csv").circular

  // 场景定义
  val browseScenario = scenario("Browse Orders")
    .feed(userFeeder)
    .exec(http("Login")
      .post("/auth/login")
      .body(StringBody("""{"username":"${username}","password":"${password}"}"""))
      .check(jsonPath("$.token").saveAs("authToken"))
    )
    .pause(1, 3)
    .repeat(5) {
      exec(http("List Orders")
        .get("/orders?page=${randomInt(1,10)}")
        .header("Authorization", "Bearer ${authToken}")
        .check(status.is(200))
      )
      .pause(500.millis, 2.seconds)
    }

  val createScenario = scenario("Create Order")
    .feed(userFeeder)
    .feed(orderFeeder)
    .exec(http("Create Order")
      .post("/orders")
      .header("Authorization", "Bearer ${authToken}")
      .body(StringBody("""
        {"userId":"${userId}","items":[{"sku":"${sku}","quantity":${quantity}}]}
      """))
      .check(status.in(200, 201))
      .check(jsonPath("$.orderId").saveAs("orderId"))
    )

  // 注入负载模型
  setUp(
    browseScenario.inject(
      rampUsersPerSec(10).to(100).during(60.seconds),  // 逐步加压
      constantUsersPerSec(100).during(300.seconds)      // 稳定压测
    ),
    createScenario.inject(
      rampUsersPerSec(5).to(50).during(60.seconds),
      constantUsersPerSec(50).during(300.seconds)
    )
  ).protocols(httpProtocol)
   .assertions(
     global.responseTime.max.lt(2000),          // 最大响应时间 < 2s
     global.successfulRequests.percent.gt(99.9), // 成功率 > 99.9%
     global.requestsPerSec.gt(500)              // 吞吐量 > 500 rps
   )
}

2.3 负载模型选择

// 开放式模型(默认): 固定请求速率,不限制并发数
constantUsersPerSec(100).during(60.seconds)

// 封闭式模型: 固定并发用户数
constantConcurrentUsers(100).during(60.seconds)

// 尖峰测试
atOnceUsers(1000)  // 瞬间 1000 用户涌入

// 阶梯测试
incrementUsersPerSec(5)
  .times(20)
  .eachLevelLasting(10.seconds)
  .startingFrom(10)

3. JMeter:经典压测工具

3.1 JMeter vs Gatling

特性JMeterGatling
GUI✅ 强大❌ 无(代码即脚本)
脚本方式GUI 拖拽 / XMLScala/Kotlin 代码
学习曲线
并发能力中等(线程模型)高(异步事件驱动)
报告原生图表精美 HTML
扩展性插件丰富自定义代码
CI/CD命令行执行Maven/Gradle 插件

3.2 JMeter CLI 执行

# GUI 设计脚本后,使用 CLI 模式执行(推荐)
jmeter -n -t order-api-test.jmx \
    -l results.jtl \
    -e -o report-output \
    -Jthreads=100 \
    -Jduration=300 \
    -Jrampup=30

# -n: 非 GUI 模式
# -t: 测试脚本
# -l: 结果日志
# -e -o: 生成 HTML 报告
# -J: 覆盖脚本中的属性

4. Arthas:线上诊断

4.1 核心功能

# 安装并启动
java -jar arthas-boot.jar

# 1. 查看方法耗时( profiler 级)
trace com.myapp.OrderService createOrder '#cost>100' -n 5
# 输出: 每个子调用的耗时分解

# 2. 查看实时 dashboard
dashboard
# 线程、内存、GC、运行时信息一览

# 3. 方法入参/返回值监控
watch com.myapp.OrderService createOrder '{params, returnObj, throwExp}' -x 2
# -x 2: 展开对象层级

# 4. 火焰图(CPU 热点)
profiler start --event cpu
profiler stop --format html --file /tmp/cpu.html

# 5. 内存分析
heapdump /tmp/heap.hprof

# 6. 反编译线上代码
jad com.myapp.OrderService

# 7. 动态修改日志级别
logger --name com.myapp.OrderService --level DEBUG

# 8. 调用链路追踪
tt -t com.myapp.OrderService createOrder
# 记录调用时空隧道,可随时回放

4.2 Spring Boot Actuator + Micrometer JVM 指标

management:
  endpoints:
    web:
      exposure:
        include: metrics, prometheus, health, jhiccup
  metrics:
    jvm:
      gc: true
      memory: true
      threads: true
    distribution:
      slo:
        http.server.requests: 50ms,100ms,200ms,500ms,1s,2s,5s
    tags:
      application: ${spring.application.name}

5. 全链路压测

5.1 压测环境设计

流量隔离方案:
1. 独立压测集群(最彻底,成本最高)
2. 影子库(写操作路由到独立数据库)
3. 染色请求(Header 标记,特定逻辑处理)

推荐方案: 线上生产环境 + 染色隔离
  - 读请求: 正常路由,利用缓存隔离
  - 写请求: 
    - 带压测标头的请求写入影子表
    - 消息队列发送到独立 topic
    - 外部调用 Mock 掉

5.2 压测数据准备

@Service
public class PressureTestDataService {

    public void prepareShadowData() {
        // 1. 创建压测专用用户 (prefix: pt_)
        for (int i = 0; i < 10000; i++) {
            User user = User.builder()
                .username("pt_user_" + i)
                .type(UserType.PRESSURE_TEST)
                .build();
            userRepo.save(user);
        }

        // 2. 商品库存预热到缓存
        List<Product> products = productRepo.findAll();
        products.forEach(p -> {
            redisTemplate.opsForValue().set(
                "pt:stock:" + p.getId(),
                p.getStock().toString()
            );
        });

        // 3. 准备订单数据(用于查询压测)
        // 批量生成历史订单到影子表
    }

    public boolean isPressureTestRequest(HttpServletRequest request) {
        return "1".equals(request.getHeader("X-Pressure-Test")) ||
            request.getHeader("User-Agent") != null &&
            request.getHeader("User-Agent").contains("Gatling");
    }
}

5.3 压测执行与监控

阶段目标指标
基线测单接口容量最大 TPS、响应时间 P99
容量测系统极限错误率突增点
稳定测长时间稳定性内存泄漏、连接池耗尽
恢复测故障恢复能力降级后恢复时间

6. 性能调优决策树

压测发现问题
  ├── 响应时间长
  │     ├── CPU 高 → 代码优化 / 算法优化
  │     ├── CPU 低 → 线程阻塞 → 线程 DUMP 分析
  │     ├── GC 频繁 → JVM 参数 / 对象分配分析
  │     └── IO 等待 → 数据库优化 / 缓存
  │
  ├── 吞吐量低
  │     ├── 连接池满 → 扩大连接池 / 优化 SQL
  │     ├── 线程池满 → 调整线程数 / 异步化
  │     └── 网络瓶颈 → 带宽 / 压缩 / CDN
  │
  ├── 错误率高
  │     ├── 超时 → 上游依赖慢 / 熔断配置
  │     ├── OOM → 内存泄漏 / 堆分析
  │     └── 限流 → 容量不足 / 扩容
  │
  └── 资源不均衡
        └── 热点数据 → 缓存分片 / 本地缓存

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「java-enterprise」更多文章

  1. 限流算法深度解析:令牌桶、漏桶与滑动窗口计数
  2. Java 代码质量:SonarQube、Checkstyle 与 SpotBugs 工程化实践
  3. Spring IoC 容器与依赖注入原理深度剖析