1. 概述

性能测试往往被安排在软件开发周期的后期阶段,我们通常依赖 Java Profiler 来排查性能瓶颈。

在本教程中,我们将介绍 SPF4J(Simple Performance Framework for Java)。它提供了一套可以直接嵌入代码的 API,使得性能监控成为组件开发中不可或缺的一部分,从而实现早期性能问题的发现与定位。

2. 指标采集与可视化基础概念

在深入 SPF4J 之前,我们先通过一个简单的例子来理解指标采集与可视化的概念。

假设我们要监控某款新上线 App 在应用商店的下载量。为了便于理解,我们可以手动模拟这个过程。

2.1 指标采集

首先,我们需要确定要测量的内容。我们关心的是 下载量/分钟,因此我们会记录每分钟的下载次数。

其次,采集频率是多少?我们决定 每分钟采集一次

最后,监控持续多久?我们设定为 持续监控一小时

根据这些规则,我们开始实验,结束后可以得到如下数据:

时间    Cumulative 下载量    下载量/分钟
----------------------------------------------
T       497                     0  
T+1     624                     127
T+2     676                     52
...     
T+14    19347                   17390
T+15    19427                   80
...  
T+22    27195                   7350
...  
T+41    41321                   11885
...   
T+60    43395                   40

前两列(时间、累计下载量)是直接观测到的值。第三列(下载量/分钟)是通过当前与前一时间点的差值得出的。

2.2 指标可视化

我们可以绘制一个时间 vs 下载量/分钟的线性图:

Downloads per Minute Chart

图中可以看到一些峰值,但由于 Y 轴使用线性刻度,低值部分看起来像是直线。

我们换用对数刻度(base 10)来绘制 log/linear 图:

graph2

此时我们能更清晰地看到低值部分,它们大致在 100 左右。线性图的平均值为 703,是因为包含了峰值。

如果我们把峰值视为异常值剔除,从 log/linear 图中可以得出结论:

平均每分钟下载量约为 100 次

3. 方法调用的性能监控

理解了如何采集和分析简单指标后,我们将其应用到一个 Java 方法中:isPrimeNumber()

private static boolean isPrimeNumber(long number) {
    for (long i = 2; i <= number / 2; i++) {
        if (number % i == 0)
            return false;
    }
    return true;
}

SPF4J 提供了两种方式来采集指标,我们将在下文中详细介绍。

4. 环境搭建与配置

4.1 Maven 依赖配置

SPF4J 提供多个模块,我们仅需几个核心模块即可完成实验。

  • spf4j-core:核心库,提供基本功能
  • spf4j-aspects:基于 AspectJ 的性能监控
  • spf4j-ui:图形化界面用于数据可视化
<dependency>
    <groupId>org.spf4j</groupId>
    <artifactId>spf4j-core</artifactId>
    <version>8.6.10</version>
</dependency>

<dependency>
    <groupId>org.spf4j</groupId>
    <artifactId>spf4j-aspects</artifactId>
    <version>8.6.10</version>
</dependency>

<dependency>
    <groupId>org.spf4j</groupId>
    <artifactId>spf4j-ui</artifactId>
    <version>8.6.10</version>
</dependency>

4.2 输出文件配置

SPF4J 支持写入时间序列数据库(TSDB)和文本文件,我们配置如下:

public static void initialize() {
  String tsDbFile = System.getProperty("user.dir") + File.separator + "spf4j-performance-monitoring.tsdb2";
  String tsTextFile = System.getProperty("user.dir") + File.separator + "spf4j-performance-monitoring.txt";
  LOGGER.info("\nTime Series DB (TSDB) : {}\nTime Series text file : {}", tsDbFile, tsTextFile);
  System.setProperty("spf4j.perf.ms.config", "TSDB@" + tsDbFile + "," + "TSDB_TXT@" + tsTextFile);
}

4.3 Recorder 与 Source

SPF4J 核心能力是记录、聚合和保存指标,无需后期处理。主要类包括:

  • MeasurementRecorder:可手动调用记录指标
  • MeasurementRecorderSource:用于注解方式记录指标

工厂类 RecorderFactory 提供了多种聚合方式:

  • createScalableQuantizedRecorder() / Source
  • createScalableCountingRecorder() / Source
  • createScalableMinMaxAvgRecorder() / Source
  • createDirectRecorder() / Source

我们选择 ScalableQuantized 聚合方式。

4.4 创建 Recorder

public static MeasurementRecorder getMeasurementRecorder(Object forWhat) {
    String unitOfMeasurement = "ms";
    int sampleTimeMillis = 1_000;
    int factor = 10;
    int lowerMagnitude = 0;
    int higherMagnitude = 4;
    int quantasPerMagnitude = 10;

    return RecorderFactory.createScalableQuantizedRecorder(
      forWhat, unitOfMeasurement, sampleTimeMillis, factor, lowerMagnitude, 
      higherMagnitude, quantasPerMagnitude);
}

参数说明:

  • unitOfMeasurement:单位,如 ms
  • sampleTimeMillis:采样频率
  • factor:绘图使用的对数底数
  • lowerMagnitude:对数最小值(如 0 表示 10^0)
  • higherMagnitude:对数最大值(如 4 表示 10^4)
  • quantasPerMagnitude:每个量级的细分区间数

4.5 创建 Source

public static final class RecorderSourceForIsPrimeNumber extends RecorderSourceInstance {
    public static final MeasurementRecorderSource INSTANCE;
    static {
        Object forWhat = App.class + " isPrimeNumber";
        String unitOfMeasurement = "ms";
        int sampleTimeMillis = 1_000;
        int factor = 10;
        int lowerMagnitude = 0;
        int higherMagnitude = 4;
        int quantasPerMagnitude = 10;
        INSTANCE = RecorderFactory.createScalableQuantizedRecorderSource(
          forWhat, unitOfMeasurement, sampleTimeMillis, factor, 
          lowerMagnitude, higherMagnitude, quantasPerMagnitude);
    }
}

4.6 配置类封装

public class Spf4jConfig {
    public static void initialize() { /* ... */ }

    public static MeasurementRecorder getMeasurementRecorder(Object forWhat) { /* ... */ }

    public static final class RecorderSourceForIsPrimeNumber extends RecorderSourceInstance { /* ... */ }
}

4.7 配置 aop.xml

使用 AspectJ 实现注解式性能监控,需配置 aop.xml 文件:

<aspectj>
    <aspects>
        <aspect name="org.spf4j.perf.aspects.PerformanceMonitorAspect" />
    </aspects>
    <weaver options="-verbose">
        <include within="com..*" />
        <include within="org.spf4j.perf.aspects.PerformanceMonitorAspect" />
    </weaver>
</aspectj>

将该文件放在 META-INF 目录下。

5. 使用 MeasurementRecorder

5.1 指标记录

生成 100 个随机数并循环调用 isPrimeNumber(),使用 MeasurementRecorder 记录耗时:

Spf4jConfig.initialize();
MeasurementRecorder measurementRecorder = Spf4jConfig
  .getMeasurementRecorder(App.class + " isPrimeNumber");
Random random = new Random();
for (int i = 0; i < 100; i++) {
    long numberToCheck = random.nextInt(999_999_999 - 100_000_000 + 1) + 100_000_000;
    long startTime = System.currentTimeMillis();
    boolean isPrime = isPrimeNumber(numberToCheck);
    measurementRecorder.record(System.currentTimeMillis() - startTime);
    LOGGER.info("{}. {} is prime? {}", i + 1, numberToCheck, isPrime);
}

5.2 运行代码

运行后输出如下:

Time Series DB (TSDB) : E:\Projects\spf4j-core-app\spf4j-performance-monitoring.tsdb2
Time Series text file : E:\Projects\spf4j-core-app\spf4j-performance-monitoring.txt
1. 406704834 is prime? false
...
9. 507639059 is prime? true
...
...
100. 841159884 is prime? false

5.3 查看结果

运行 SPF4J UI:

java -jar target/dependency-jars/spf4j-ui-8.6.9.jar

打开 spf4j-performance-monitoring.tsdb2 文件,点击 Plot,生成以下图表:

  • 测量分布图(log-linear)
    graph3

  • 聚合数据图(min/max/avg)
    graph4

  • 测量次数 vs 时间图
    graph5

6. 使用 MeasurementRecorderSource

6.1 指标记录(注解方式)

使用注解方式可避免手动记录时间,只需在方法上添加 @PerformanceMonitor

@PerformanceMonitor(
  warnThresholdMillis = 1,
  errorThresholdMillis = 100, 
  recorderSource = Spf4jConfig.RecorderSourceForIsPrimeNumber.class)
private static boolean isPrimeNumber(long number) {
    // ...
}

参数说明:

  • warnThresholdMillis:警告阈值(毫秒)
  • errorThresholdMillis:错误阈值(毫秒)
  • recorderSource:使用的 MeasurementRecorderSource

调用代码:

Spf4jConfig.initialize();
Random random = new Random();
for (int i = 0; i < 50; i++) {
    long numberToCheck = random.nextInt(999_999_999 - 100_000_000 + 1) + 100_000_000;
    isPrimeNumber(numberToCheck);
}

6.2 运行代码

构建并运行时需指定 Java Agent:

java -javaagent:target/dependency-jars/aspectjweaver-1.8.13.jar -jar target/spf4j-aspects-app.jar

输出日志:

Time Series DB (TSDB) : E:\Projects\spf4j-aspects-app\spf4j-performance-monitoring.tsdb2
Time Series text file : E:\Projects\spf4j-aspects-app\spf4j-performance-monitoring.txt

[DEBUG] Execution time 0 ms for execution(App.isPrimeNumber(..)), arguments [555031768]
...
[ERROR] Execution time  2826 ms for execution(App.isPrimeNumber(..)) exceeds error threshold of 100 ms, arguments [464032213]
...

6.3 查看结果

与前一种方式相同,通过 SPF4J UI 查看结果。

7. 总结

本文介绍了指标采集与可视化的基础概念,并通过 SPF4J 实现了对 isPrimeNumber() 方法的性能监控。

我们使用了两种方式:

  • ✅ 手动创建 MeasurementRecorder 并调用 record()
  • ✅ 使用 @PerformanceMonitor 注解实现自动监控

两种方式各有优劣,适用于不同场景。同时,我们也利用 SPF4J 自带的 UI 工具对性能数据进行了可视化展示。

SPF4J 的最大优势在于其轻量、易集成、支持多种聚合方式,并能与 AOP 无缝结合,非常适合在开发阶段就集成性能监控能力。


原始标题:Introduction to SPF4J | Baeldung