为什么要测响应式性能
响应式≠高性能。错误的写法(如链中阻塞、错误背压配置)会导致:
- 吞吐量低于阻塞实现
- 内存泄漏(DataBuffer 未释放)
- 延迟毛刺(GC、线程切换)
性能测试验证「响应式承诺」是否兑现。
WebTestClient 并发压测
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureWebTestClient // 绑定到随机端口上的真实服务器
public class LoadTest {
@Autowired
private WebTestClient webClient;
@Test
void testGetOrdersLatency() {
int concurrency = 100;
int requests = 10_000;
long start = System.nanoTime();
Flux.range(1, requests)
.flatMap(i -> webClient.get().uri("/api/orders")
.exchange()
.expectStatus().isOk()
.returnResult(Order.class)
.getResponseBody()
.collectList(), concurrency)
.blockLast();
long duration = (System.nanoTime() - start) / 1_000_000;
double qps = (double) requests / duration * 1000;
System.out.println("Requests: " + requests);
System.out.println("Duration: " + duration + "ms");
System.out.println("QPS: " + qps);
}
}
flatMap(concurrency) 控制同时在途的内层 Publisher 数量。100 并发可以压出连接池和事件循环的压力,但 WebTestClient 仍然是测试客户端:它和被测应用在同一个 JVM,客户端自身的编码、断言和 GC 都会混进耗时。它适合做冒烟级并发检查,不适合作为正式容量基线;正式压测用 wrk、k6 或 Gatling,把压测机和被测服务分开。
JMH 微基准
JMH(Java Microbenchmark Harness)是 JVM 性能测试的标准工具:
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.37</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.37</version>
<scope>test</scope>
</dependency>
只加 jmh-core 是不够的:@Benchmark 方法需要注解处理器在编译期生成 BenchmarkList 等胶水代码,否则运行时找不到基准。
import org.openjdk.jmh.annotations.*;
import reactor.core.publisher.Flux;
@State(Scope.Thread)
@BenchmarkMode(Mode.Throughput)
@Warmup(iterations = 3)
@Measurement(iterations = 5)
public class FluxBenchmark {
@Benchmark
public int testFluxMap() {
return Flux.range(1, 1000)
.map(i -> i * 2)
.filter(i -> i % 3 == 0)
.collectList()
.block()
.size();
}
@Benchmark
public int testStreamMap() {
return IntStream.range(1, 1000)
.map(i -> i * 2)
.filter(i -> i % 3 == 0)
.boxed()
.collect(Collectors.toList())
.size();
}
}
运行:
mvn test-compile exec:java \
-Dexec.mainClass="org.openjdk.jmh.Main" \
-Dexec.classpathScope=test \
-Dexec.args="FluxBenchmark"
延迟分布
平均延迟会掩盖毛刺。用 HdrHistogram 记录完整分布:
<dependency>
<groupId>org.hdrhistogram</groupId>
<artifactId>HdrHistogram</artifactId>
<version>2.2.2</version>
<scope>test</scope>
</dependency>
import org.HdrHistogram.ConcurrentHistogram;
public class LatencyRecorder {
private final ConcurrentHistogram histogram =
new ConcurrentHistogram(1, 1_000_000, 3); // 1μs ~ 1s,3位有效数字
public void record(long latencyMicros) {
// 超过 highestTrackableValue 会抛 ArrayIndexOutOfBoundsException;
// 先 clamp,或者按业务上界把 1_000_000 调大
histogram.recordValue(Math.min(latencyMicros, 1_000_000));
}
public void print() {
System.out.println("P50: " + histogram.getValueAtPercentile(50) + "μs");
System.out.println("P95: " + histogram.getValueAtPercentile(95) + "μs");
System.out.println("P99: " + histogram.getValueAtPercentile(99) + "μs");
System.out.println("P99.9: " + histogram.getValueAtPercentile(99.9) + "μs");
System.out.println("Max: " + histogram.getMaxValue() + "μs");
}
}
@Test
void testLatencyDistribution() {
LatencyRecorder recorder = new LatencyRecorder();
Flux.range(1, 1000)
.flatMap(i -> {
long start = System.nanoTime();
return webClient.get().uri("/api/orders/1")
.exchange()
.returnResult(String.class)
.getResponseBody()
.collectList()
.doOnNext(r -> recorder.record((System.nanoTime() - start) / 1000));
}, 50)
.blockLast();
recorder.print();
}
识别瓶颈
| 现象 | 可能原因 | 排查 |
|---|---|---|
| QPS 低,CPU 低 | 链中有阻塞调用 | 线程 dump 看阻塞在 socketRead、JDBC、sleep 的线程 |
| QPS 饱和,延迟增长 | 连接池满 | 监控 active/pending connections |
| 内存持续增长 | DataBuffer 泄漏 | heap dump 查看 ByteBuf 引用 |
| P99 高,P50 低 | GC 暂停 | -Xlog:gc*(JDK 9+ 的统一样式) |
| 流迟迟不发数据 | 下游没有请求(request(0)) | 检查订阅端是否真的消费了 Flux |
性能基线对比
每次代码变更后,对比性能指标:
private static double baselineQps;
private static long baselineP99;
@BeforeAll
static void loadBaseline() throws IOException {
// 基线是本机/专用压测机跑出来的结果,写入文件后随版本提交
Properties baseline = new Properties();
try (var in = Files.newInputStream(Path.of("perf/baseline.properties"))) {
baseline.load(in);
}
baselineQps = Double.parseDouble(baseline.getProperty("qps"));
baselineP99 = Long.parseLong(baseline.getProperty("p99Micros"));
}
@Test
void testPerformanceRegression() {
double qps = runBenchmark();
assertTrue(qps > baselineQps * 0.95,
"QPS regression: " + qps + " vs baseline " + baselineQps);
}
基线数字必须来自同一台机器、同一份数据和同一套参数;在共享 CI 上跑性能断言通常只会得到随机失败,回归检测应放在专用环境或 nightly 任务里。
eventd v2 性能目标
以下是团队设定的验收目标,不是实测值,应按实际硬件和数据集调整:
| 场景 | 目标 QPS | P99 |
|---|---|---|
| 纯网关转发 | > 30,000 | < 10ms |
| API + Caffeine | > 15,000 | < 20ms |
| API + R2DBC | > 3,000 | < 50ms |
| Saga 完整流程 | > 300 | < 200ms |
| WebSocket 广播 | > 50,000 msg/s | — |
小结
| 测试类型 | 工具 | 用途 |
|---|---|---|
| 集成压测 | WebTestClient + flatMap(concurrency) | 冒烟级端到端并发检查(正式压测用 wrk/k6) |
| 微基准 | JMH | 操作符性能对比 |
| 延迟分布 | HdrHistogram | P99/P99.9 毛刺识别 |
| 回归检测 | 基线对比 | 防止性能退化 |
| 瓶颈分析 | 线程 dump / heap dump / GC 日志 | 定位根因 |
第 13 章结束。下一章进入生产准备:线程池、连接池、超时、监控的上线前核查。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。