1. 总体介绍
性能测试是一项在软件生命开发周期中总是被置于最后一环的活动。我们经常依靠 Java profilers 去帮助发现性能问题。
在这篇文章中,我们将会学习关于 Java 的简单性能测试框架 - SPF4J。它提供了可以加在我们代码中的 API。因此,我们可以将 性能监视变为我们组件的一部分。
2. 度量捕获和可视化的基本概念
在我们开始之前,让我们用一个简单的例子来理解度量捕获和可视化的基本概念。
让我们想象一下:我们正关注于一款新发布的 App 在应用商店的下载量,出于学习的目的,让我们手工的做这件事情。
2.1. 捕捉度量
首先我们需要决定要测量什么,我们感兴趣的是每分钟下载量。 因此, 我们将会测量下载量的数量。
第二,我们多久需要执行一次测量?让我们 “每分钟测量一次”吧。
最后,我们应该监控多长时间?让我们 “监控一小时吧”。
有了以上的这些规则,我们就可以实施这个实验了。当实验完成的时候,我们可以看到以下的结果:
时间 累积下载量 每分钟下载数
----------------------------------------------
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每分钟下载量的线形图。
我们可以看到,有一些峰值表明大量的下载发生在一些场合。因为使用线性比例作为下载量轴,所以较低的值以直线出现。
让我们用以 10 为底的对数作为下载量轴的标度,并绘制一个对数/线性图。
现在我们就可以看到那些更低的值了。这些值在 100 上下浮动。注意,线性图中的平均值为 703 ,因为它也包含峰值。
如果我们将峰值从图像中移除,我们可以使用对数/线形图从我们的实验中得到结论:
- 每分钟下载量的平均值在 100 左右
3. 函数调用的性能监视
在理解了如何捕获一个简单的度量并从前面的例子中分析之后,让我们现在将它应用于一个简单的 Java 方法 - 是否是素数:
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,它为我们提供了大部分必要的功能。
让我们将其添加到 Maven 依赖:
org.spf4j
spf4j-core
8.6.10
有一个更适合性能监控的库 - spf4j-aspects,它使用的是 AspectJ。
我们将在我们的示例中探讨这一点,所以我们也添加它:
org.spf4j
spf4j-aspects
8.6.10
最后,SPF4J 还带有一个对数据可视化非常有用的简单 UI,所以让我们添加 spf4j-ui 如下:
org.spf4j
spf4j-ui
8.6.10
4.2. 输出文件的配置
SPF4J 框架将数据写入时间序列数据库(TSDB),也可以选择写入文本文件。
让我们配置它们并设置系统属性 spf4j.perf.ms.config:
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. 记录器和来源
SPF4J 框架的核心功能是记录,聚合和保存指标,以便在分析时不需要进行后置处理。它通过使用MeasurementRecorder 和 MeasurementRecorderSource 类来实现。
这两个类提供了两种记录度量的方法。关键的区别在于 MeasurementRecorder 可以从任何地方调用,而MeasurementRecorderSource 仅用于注解。
该框架为我们提供了一个 RecorderFactory 工厂类,用于为不同类型的聚合创建记录器和记录器源类的实例:
- createScalableQuantizedRecorder() 和 createScalableQuantizedRecorderSource()
- createScalableCountingRecorder() 和 createScalableCountingRecorderSource()
- createScalableMinMaxAvgRecorder() 和 createScalableMinMaxAvgRecorderSource()
- createDirectRecorder() 和 createDirectRecorderSource()
对于我们的示例,让我们选择可扩展的量化聚合。
4.4. 创建记录器
首先,让我们创建一个辅助方法来创建 MeasurementRecorder 的实例:
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 – 被测量的单位价值 - 对于性能检测方案,它通常是一个时间单位
- sampleTimeMillis – 进行测量的时间段 - 换句话说,进行测量的频率
- factor – 用于绘制测量值的对数标度的底数
- lowerMagnitude – 对数刻度的最小值 - 对于底数 10,lowerMagnitude = 0 表示 10 的 0 次幂 = 1
- higherMagnitude – 对数刻度上的最大值 - 对于底数 10,higherMagnitude = 4 表示 10 的 4 次幂 = 10,000
- quantasPerMagnitude – 幅度范围内的部分数量 - 如果幅度范围从 1,000 到 10,000,则 quantasPerMagnitude = 10 表示范围将被划分为 10 个子范围
我们可以看到可以根据需要更改值。因此,为不同的测量创建单独的 MeasurementRecorder 实例可能是个好主意。
4.5. 创建资源
接下来,让我们使用另一个辅助方法创建 MeasurementRecorderSource 的实例:
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. 创建配置类
现在让我们创建一个 Spf4jConfig 类并将所有上述方法放入其中:
public class Spf4jConfig {
public static void initialize() {
//...
}
public static MeasurementRecorder getMeasurementRecorder(Object forWhat) {
//...
}
public static final class RecorderSourceForIsPrimeNumber extends RecorderSourceInstance {
//...
}
}
4.7. 配置 aop.xml
SPF4J 为我们提供了基于注解的性能测量和监控方法的选项。它使用 AspectJ 库,它允许在不修改代码本身的情况下向现有代码添加性能监视所需的其他行为。
让我们使用 load-time weaver 编织我们的类和切面,并将 aop.xml 放在 META-INF 文件夹下:
5. 使用 MeasurementRecorder
现在让我们看看如何使用 MeasurementRecorder 来记录测试功能的性能指标。
5.1. 记录指标
让我们生成 100 个随机数并在循环中调用是否为素数方法。在此之前,让我们调用我们的 Spf4jConfig 类来进行初始化并创建 MeasureRecorder 类的实例。使用此实例,让我们调用 record() 方法来保存调用 100 次 isPrimeNumber() 所需的时间:
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. 运行代码
我们已经准备好测试我们的简单函数 isPrimeNumber() 的性能。
让我们运行代码并查看结果:
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
...
20. 557385397 is prime? true
...
26. 152042771 is prime? true
...
100. 841159884 is prime? false
5.3. 查看结果
让我们在项目文件夹下通过运行命令来启动 SPF4J UI:
java -jar target/dependency-jars/spf4j-ui-8.6.9.jar
这将打开桌面UI应用程序。然后,从菜单中选择 File> Open 。之后,让我们使用浏览窗口找到 spf4j-performance-monitoring.tsdb2 文件并打开它。
我们现在可以看到一个新窗口,其中有一个包含我们的文件名和子项目的树状图。让我们点击子项目,然后点击它上面的 Plot 按钮。
这会生成一系列图表。
第一个图表测量分布是我们之前看到的对数线性图的变体。该图还显示了基于计数的热图。
第二个图表显示聚合数据,如最小值,最大值和平均值:
最后一张图显示了测量次数与时间的关系:
6. 使用 MeasurementRecorderSource
在上一节中,我们必须围绕我们的功能编写额外的代码来记录测量值。在本节中,让我们使用另一种方法来避免这种情况。
6.1. 记录指标
首先,我们将删除为捕获和记录指标而添加的额外代码:
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);
}
接下来,让我们用 @PerformanceMonitor 来注解 isprimenumber() 方法,而不是所有的样板文件:
@PerformanceMonitor(
warnThresholdMillis = 1,
errorThresholdMillis = 100,
recorderSource = Spf4jConfig.RecorderSourceForIsPrimeNumber.class)
private static boolean isPrimeNumber(long number) {
//...
}
让我们看看这些参数:
- warnThresholdMillis – 允许该方法在没有警告消息的情况下运行的最长时间
- errorThresholdMillis – 允许该方法在没有错误消息的情况下运行的最长时间
- recorderSource – MeasurementRecorderSource 的一个实例
6.2. 运行代码
让我们先做一个 Maven 构建,然后通过传递 Java 代理来执行代码:
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]
...
我们可以看到 SPF4J 框架记录了每次方法调用所花费的时间。只要它超过 errorThresholdMillis 值100毫秒,它就会将其记录为错误。传递给该方法的参数也会被记录。
6.3. 查看结果
我们可以使用与之前使用 SPF4J UI 相同的方式查看结果,因此我们可以参考上一节。
7. 结论
在本文中,我们讨论了捕获和可视化指标的基本概念。然后,我们借助一个简单的例子了解了 SPF4J 框架的性能监控功能。我们还使用内置的 UI 工具来可视化数据。与往常一样,本文中的示例均可用 over on GitHub。
原文:https://www.baeldung.com/spf4j
作者:Mohan Sundararaju
译者:KeepGoingPawn