13.3 性能测试

响应式系统性能测试方法论完整指南:WebTestClient 并发请求压测与 QPS P99 延迟计算、JMH 微基准测试编写与火焰图热点分析、HDrHistogram 延迟分布分位数统计、连接池饱和与背压溢出诊断识别、性能回归基线对比与瓶颈定位。

为什么要测响应式性能

响应式≠高性能。错误的写法(如链中阻塞、错误背压配置)会导致:

  • 吞吐量低于阻塞实现
  • 内存泄漏(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 性能目标

以下是团队设定的验收目标,不是实测值,应按实际硬件和数据集调整:

场景目标 QPSP99
纯网关转发> 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操作符性能对比
延迟分布HdrHistogramP99/P99.9 毛刺识别
回归检测基线对比防止性能退化
瓶颈分析线程 dump / heap dump / GC 日志定位根因

第 13 章结束。下一章进入生产准备:线程池、连接池、超时、监控的上线前核查。

继续阅读

探索更多技术文章

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

全部文章 返回首页

「java」更多文章

  1. 15.3 全书总结与学习路径回顾
  2. 15.2 响应式流处理的演进趋势
  3. 15.1 引擎选型指南