背景
随着jdk的发展,对Java问题诊断的方式越来越便捷,在openjdk11上开源了javaFlightRecording(后面简称jfr),这个框架提供了一种能力把jdk运行的内部状态记录下来,在需要的时候拿到文件进行分析,以此了解到了运行时的jvm状态,这个功能在排查问题方便特别方便,在社区的努力下backport回到了jdk8。
jfr提供除了提供已经定制好的事件,还提供了扩展的能力,用户可以自己增加事件来做扩展。
jfr事件
了解jfr事件需要从2个维度入手。
第一个维度是收集的类型。分为以下三种。
- instant event。就是发生的时候就立刻记录,至于记录到什么级别后面讲。
- duration event。是可以设置采集阈值,只有达到一定阈值才会被记录。和instat的区别就是这个有持续的状态,例如锁的等待超过一定时间。
- sample event。采样事件,我们常见的堆栈采样就是这种类型。
第二个维度是基于jdk工程的分类。了解这个是为了更好的理解,我们能更好的理解我们的能力范围。
- native event。这个是hotspot里的埋点。声明见github.com/openjdk/jdk...
- jdk内部event。这个埋点在jdk.internal里。主要是jdk工程自己的event,相当于jdk自身提供给外部观测的事件。声明见github.com/openjdk/jdk...
- java event。这个埋点在jdk.jfr, java event相当于我们的core lib中的实现的事件。声明见github.com/openjdk/jdk...
2和3的区别仅限于工程的层次。2边自定义了不同的框架方便编码。在实现的角度看是一样的。
基于上面的介绍。我们如果自定义的话,基本都是在java event中增加自己的java事件。
自定义event
自定义event我们就参考jdk自己的代码来学习。不过我们自己得结合2和3的方式来做,我们自己写代码接近于2,但是声明方式接近于3。 我们先基于2的实现来看。
java
public final class ProcessStartEvent extends Event {
public long pid;
public String directory;
public String command;
}
首先是构造一个event,上面是启动子进程的一个事件,不过这里是event是内部实现,我们自定义的时候导入的包是不一样的。
在使用的时候在代码中直接埋点。
java
Process process = ProcessImpl.start(cmdarray,
environment,dir,redirects,redirectErrorStream);
ProcessStartEvent event = new ProcessStartEvent();
if (event.isEnabled()) {
event.directory = dir;
event.command = String.join(" ", cmdarray);
event.pid = process.pid();
event.commit();
}
和我们常见的埋点方式没有什么不同,在执行完之后,new一个对应的event,然后把需要采集的内容设置进去。然后调用commit就可以了。这里同时解释了上面instant event记录的力度,完全是采集代码写的。只要是能获取到的都可以。
我们也介绍3的声明方式以及为什么这么不如2直接。
java
@Name(Type.EVENT_NAME_PREFIX + "FileForce")
@Label("File Force")
@Category("Java Application")
@Description("Force updates to be written to file")
public final class FileForceEvent extends AbstractJDKEvent {
// The order of these fields must be the same as the parameters in
// commit(..., String, boolean)
@Label("Path")
@Description("Full path of the file")
public String path;
@Label("Update Metadata")
@Description("Whether the file metadata is updated")
public boolean metaData;
public static void commit(long start, long duration, String path, boolean metaData) {
// Generated
}
}
这是file force的事件,可以看到他有一堆的lable。这个地方是方便我们后续解析jfr event使用的。还有对应的说明。可维护性更好。我们自定义的时候,希望是加上各种lable。
这里也解释一下他的写法为什么是这样的。 这里是为了让事件和实现解耦。也方便做裁剪,openjdk后续不提供jre了,需要自己裁剪jdk生产自己想要的jre。这里就有可能有人把jfr给裁剪掉。他虽然声明了这么一个event。在使用的时候,依靠动态字节码生成技术。注入对应的方法。所以我们看到commit方法是空的实现,这里也是动态加入的。这里简单介绍一下他们的对应关系。
java
@JIInstrumentationTarget("sun.nio.ch.FileChannelImpl")
final class FileChannelImplInstrumentor {
private FileChannelImplInstrumentor() {
}
private String path;
@SuppressWarnings("deprecation")
@JIInstrumentationMethod
public void force(boolean metaData) throws IOException {
EventConfiguration eventConfiguration = EventConfigurations.FILE_FORCE;
if (!eventConfiguration.isEnabled()) {
force(metaData);
return;
}
long start = 0;
try {
start = EventConfiguration.timestamp();
force(metaData);
} finally {
long duration = EventConfiguration.timestamp() - start;
if (eventConfiguration.shouldCommit(duration)) {
FileForceEvent.commit(start, duration, path, metaData);
}
}
}
}
这里是使用event的地方,我们可以看到他的标签是注入sun.nio.ch.FileChannelImpl。然后JIInstrumentationMethod表示注入的方法。等于是对FileChannelImpl的force做了一层代理,在正常执行完之后,就会调用原来的force(metaData);这里就看到了其实2和3的实现是一样的。只不过3做了一层控制,把采集代码和使用代码做了解耦。最终靠字节码增强技术拼接出了方法调用。
我们自己的工程就不用这么麻烦了,都是自己的代码,埋点直接加入即可。
小结
当我们没有特别复杂的观测系统的时候,可以直接把事件记录到jfr中。这么记录的模式非常接近debug日志,对比日志的优势是他可以低overhead的获取到堆栈,例如想知道谁调用了这个方法,jfr是比日志更好的选择。