其他
必须拿捏:测试和性能监控神器 JMH & Arthas
最近的工作日并不算太平,各种大大小小的case和解case,发现已经有好久好久没有静下心来专心写点东西了。不过倒还是坚持利用业余时间学习了不少微课上的东西,发现大佬们总结的东西还是不一样,相比于大学时的那些枯燥的课本,大佬们总结出来的内容更活,更加容易理解。自己后面也会把大佬们的东西好好消化吸收,变成自己的东西用文字性的东西表达出来。
对这两个工具,我都会首先简单介绍一下这些工具的大致使用场景,然后会使用一个在工作中真正遇到的性能问题排查为例详细讲解这两个工具的实际用法。话不多说,直奔主题。
问题描述
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class AppenderTest {
private static final String LOGGER_NAME_DEFAULT = "defaultLogger";
private static final String LOGGER_NAME_INCLUDE = "includeLocationLogger";
private static final Logger LOGGER = LoggerFactory.getLogger(LOGGER_NAME_INCLUDE);
public static final long BATCH = 10000;
public static void main(String[] args) throws InterruptedException {
while(true) {
long start, end;
start = System.currentTimeMillis();
for (int i = 0; i < BATCH; i++) {
LOGGER.info("msg is {}", i);
}
end = System.currentTimeMillis();
System.out.println("duration of " + LOGGER_NAME_INCLUDE + " is " + (end - start) + "ms");
Thread.sleep(1000);
}
}
}
logger.info
每次打印10000条日志,然后记录耗时。两者的对比如下上面的代码测试是否标准,规范
如果真的是性能问题,那么这两个代码到底在哪个方法上有了这么大的差距导致了最终的性能差异
JMH简介
JMH 是一个面向 Java 语言或者其他 Java 虚拟机语言的性能基准测试框架
private LogBenchMarkWorker() {}
private static class LogBenchMarkWorkerInstance {
private static final LogBenchMarkWorker instance = new LogBenchMarkWorker();
}
public static LogBenchMarkWorker getInstance() {
return LogBenchMarkWorkerInstance.instance;
}
private static final String LOGGER_DEFAULT_NAME = "defaultLogger";
private static final String LOGGER_INCLUDE_LOCATION = "includeLocationLogger";
private static final Logger LOGGER = LoggerFactory.getLogger(LOGGER_DEFAULT_NAME);
private static long BATCH_SIZE = 10000;
public void logBatch() {
for (int i = 0; i < BATCH_SIZE; i++) {
LOGGER.info("msg is {}", i);
}
}
}
@Benchmark
@BenchmarkMode(Mode.AverageTime)
@Fork(value = 1)
@Threads(1)
public void testLog1() {
LogBenchMarkWorker.getInstance().logBatch();
}
@Benchmark
@BenchmarkMode(Mode.AverageTime)
@Fork(value = 1)
@Threads(4)
public void testLog4() {
LogBenchMarkWorker.getInstance().logBatch();
}
@Benchmark
@BenchmarkMode(Mode.AverageTime)
@Fork(value = 1)
@Threads(8)
public void testLog8() {
LogBenchMarkWorker.getInstance().logBatch();
}
@Benchmark
@BenchmarkMode(Mode.AverageTime)
@Fork(value = 1)
@Threads(16)
public void testLog16() {
LogBenchMarkWorker.getInstance().logBatch();
}
Benchmark注解:标识在某个具体方法上,表示这个方法将是一个被测试的最小方法,在JMH中成为一个OPS BenmarkMode:测试类型,JMH提供了几种不同的Mode Throughput:整体吞吐量 AverageTime:调用的平均时间,每次OPS执行的时间 SampleTime:取样,给出不同比例的ops时间,例如99%的ops时间,99.99%的ops时间 fork:fork的次数,如果设置为2,JMH会fork出两个进程来测试 Threads:很容易理解,这个参数表示这个方法同时被多少个线程执行
BenchmarkMode
均为测试单次OPS的平均时间,但4个方法的线程数不同。public static void main(String[] args) throws RunnerException {
Options options = new OptionsBuilder()
.include(LogBenchMarkMain.class.getSimpleName())
.warmupIterations(5)
.measurementIterations(5)
.output("logs/BenchmarkCommon.log")
.build();
new Runner(options).run();
}
}
include:benchmark所在类的名字,可以使用正则表达 warmupIteration:预热的迭代次数,这里为什么要预热的原因是由于JIT的存在,随着代码的运行,会动态对代码的运行进行优化。因此在测试过程中需要先预热几轮,让代码运行稳定后再实际进行测试 measurementIterations:实际测试轮次 output:测试报告输出位置
LogBenchMarkMain.testLog16 avgt 5 0.062 ± 0.026 s/op
LogBenchMarkMain.testLog4 avgt 5 0.006 ± 0.002 s/op
LogBenchMarkMain.testLog8 avgt 5 0.040 ± 0.004 s/op
INCLUDE_LOCATION
的loggerLogBenchMarkMain.testLog16 avgt 5 1.784 ± 0.670 s/op
LogBenchMarkMain.testLog4 avgt 5 0.378 ± 0.003 s/op
LogBenchMarkMain.testLog8 avgt 5 0.776 ± 0.070 s/op
Arthas 我的代码在运行时到底做了什么
实际操作
反编译代码 监控某个方法的调用 查看某个方法的调用和返回值 trace某个方法
监控方法调用
org.slf4j.Logger
,方法时info,我们的监控语句为include appender
include appeder
的打印日志方法要比普通的appender高出了3倍,这就不禁让我们有了疑问,究竟这两个方法各个步骤耗时如何呢。下面我们就介绍第二条命令,trace方法。trace命令 & jad命令
<!--status:日志等级 monitorInterval:更新配置文件的时间间隔,单位秒-->
<configuration status="warn" monitorInterval="30">
<!--定义所有的appender -->
<appenders>
<!--这个输出控制台的配置 -->
<Console name="console" target="SYSTEM_OUT">
<!--控制台只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch) -->
<ThresholdFilter level="warn" onMatch="ACCEPT" onMismatch="DENY"/>
<!--日志打印格式 -->
<PatternLayout pattern="[%d{HH:mm:ss.SSS}] [%-5p] %l - %m%n"/>
</Console>
<Async name="AsyncDefault" blocking="false" includeLocation="false">
<AppenderRef ref="fileAppender"/>
</Async>
<Async name="AsyncIncludeLocation" blocking="false" includeLocation="true">
<AppenderRef ref="fileAppender"/>
</Async>
<!--文件会打印出所有信息,这个log每次运行程序会自动清空,由append属性决定,这个也挺有用的,适合临时测试用 -->
<!--append为TRUE表示消息增加到指定文件中,false表示消息覆盖指定的文件内容,默认值是true -->
<File name="fileAppender" fileName="log/test.log" append="false">
<PatternLayout pattern="[%d{HH:mm:ss.SSS}] [%-5p] %l - %m%n"/>
</File>
<!--添加过滤器ThresholdFilter,可以有选择的输出某个级别以上的类别 onMatch="ACCEPT" onMismatch="DENY"意思是匹配就接受,否则直接拒绝 -->
<File name="ERROR" fileName="logs/error.log">
<ThresholdFilter level="error" onMatch="ACCEPT" onMismatch="DENY"/>
<PatternLayout pattern="[%d{yyyy.MM.dd 'at' HH:mm:ss z}] [%-5p] %l - %m%n"/>
</File>
<!--这个会打印出所有的信息,每次大小超过size,则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩,作为存档 -->
<RollingFile name="rollingFile" fileName="logs/app.log"
filePattern="logs/$${date:yyyy-MM}/web-%d{yyyy-MM-dd}.log.gz">
<PatternLayout pattern="[%d{yyyy-MM-dd HH:mm:ss}] [%-5p] %l - %m%n"/>
<Policies>
<TimeBasedTriggeringPolicy modulate="true" interval="1"/>
</Policies>
<DefaultRolloverStrategy>
<Delete basePath="logs" maxDepth="2">
<IfFileName glob="*/*.log.gz" />
<IfLastModified age="7d" />
</Delete>
</DefaultRolloverStrategy>
</RollingFile>
</appenders>
<!--然后定义logger,只有定义了logger并引入的appender,appender才会生效 -->
<loggers>
<!--过滤掉spring和mybatis的一些无用的DEBUG信息-->
<logger name="defaultLogger" additivity="false">
<appender-ref ref="AsyncDefault"></appender-ref>
</logger>
<logger name="includeLocationLogger" additivity="false">
<appender-ref ref="AsyncIncludeLocation"></appender-ref>
</logger>
<!--建立一个默认的root的logger -->
<root level="INFO">
</root>
</loggers>
</configuration>
AsyncAppender
套用了一个FileAppender
。查看fileAppender
,发现二者相同完全没区别,只有asyncAppender
中的一个选项有区别,这就是includeLocation
,一个是false,另一个是true。至于这个参数的含义,我们暂时不讨论,我们现在知道问题可能出在AsyncAppender
里面,但是我们该如何验证呢。trace命令就有了大用场。include appender
org.apache.logging.log4j.core.impl.Log4jLogEvent
类中的createMemento
方法,那么怎么才能知道这个方法到底做了啥呢,那就请出我们下一个常用命令jad,这个命令能够反编译出对应方法的代码。这里我们jad一下上面说的那个createMemento
方法,命令很简单includeLocation
参数,这个和我们的看到的两个appender的唯一不同的配置相吻合,我们此时应该有这个猜想,会不会就是这个参数导致的呢?为了验证这个猜想,我们引出下一个命令,watchwatch命令
createMemento
方法的入参,如果两个程序的入参不同,那基本可以断定是这个原因引起命令如下-x
参数展开层次-n
执行次数-b
查看方法调用前状态-f
方法调用后
AsyncAppender
的append方法includeLocation
正是appender
的一个属性,也就是我们xml中配置的那个属性。查看官网的相关分析,我们看到这个参数会使log的性能下降5–10倍includeLocation
会在event的createMemento
中被用到,在序列化生成对象时会创建一个LogEventProxy
,代码如下this.loggerFQCN = event.getLoggerFqcn();
this.marker = event.getMarker();
this.level = event.getLevel();
this.loggerName = event.getLoggerName();
final Message msg = event.getMessage();
this.message = msg instanceof ReusableMessage
? memento((ReusableMessage) msg)
: msg;
this.timeMillis = event.getTimeMillis();
this.thrown = event.getThrown();
this.thrownProxy = event.getThrownProxy();
this.contextData = memento(event.getContextData());
this.contextStack = event.getContextStack();
this.source = includeLocation ? event.getSource() : null;
this.threadId = event.getThreadId();
this.threadName = event.getThreadName();
this.threadPriority = event.getThreadPriority();
this.isLocationRequired = includeLocation;
this.isEndOfBatch = event.isEndOfBatch();
this.nanoTime = event.getNanoTime();
}
includeLocation
为true,那么就会调用getSource
函数,跟进去查看,代码如下if (source != null) {
return source;
}
if (loggerFqcn == null || !includeLocation) {
return null;
}
source = Log4jLogEvent.calcLocation(loggerFqcn);
return source;
}
public static StackTraceElement calcLocation(final String fqcnOfLogger) {
if (fqcnOfLogger == null) {
return null;
}
// LOG4J2-1029 new Throwable().getStackTrace is faster than Thread.currentThread().getStackTrace().
final StackTraceElement[] stackTrace = new Throwable().getStackTrace();
StackTraceElement last = null;
for (int i = stackTrace.length - 1; i > 0; i--) {
final String className = stackTrace[i].getClassName();
if (fqcnOfLogger.equals(className)) {
return last;
}
last = stackTrace[i];
}
return null;
}
trace crateMemento
方法org.apache.logging.log4j.core.impl.Log4jLogEvent
的serialize()
,继续trace下去org.apache.logging.log4j.core.impl.Log4jLogEvent:LogEventProxy
的构造方法,继续traceorg.apache.logging.log4j.core.impl.Log4jLogEvent
的calcLocation
函数,和我们静态跟踪的代码一样。END
往期精彩Nacos、Apollo、Config配置中心如何选型?这10个维度告诉你!
Chrome 优化内存占用,新增内存释放开关
Spring Boot 应用该如何预防 XSS 攻击 ?
你知道 Spring lazy-init 懒加载的原理吗?
关注后端面试那些事,回复【2022面经】
获取最新大厂Java面经