性能是软件系统的关键质量属性之一。在 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 | 参数化测试 | 任意基本类型/字符串 |
@Fork | JVM 进程数 | ≥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
| 特性 | JMeter | Gatling |
|---|---|---|
| GUI | ✅ 强大 | ❌ 无(代码即脚本) |
| 脚本方式 | GUI 拖拽 / XML | Scala/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 → 内存泄漏 / 堆分析
│ └── 限流 → 容量不足 / 扩容
│
└── 资源不均衡
└── 热点数据 → 缓存分片 / 本地缓存
延伸阅读
- JVM 性能调优与 GC 优化 — JVM 层面的深度调优
- Java 高并发编程精要 — 并发性能优化
- 监控诊断与可观测性 — 生产环境性能监控
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。