Java Flight Recorder and JFR Event Streaming in Java 14(java中jvm事件监视和分析技术,性能调优)

文章来自期刊: Java Flight Recorder and JFR Event Streaming in Java 14
翻译采用了软件,部分语句及翻译进行了调整

原文没有再贴,以下为翻译及自己的一些理解

在本文中,我将讨论Java 14带来的一个新特性,这个特性称为JFR事件流(JEP 349),是具有悠久历史的一组成熟的分析和监视技术的最新版本。

最初的Java飞行记录器(JFR)和Java任务控制(JMC)工具是Oracle在2008年收购BEA系统时获得的。这两个组成部分一起工作。JFR是一个低开销的、基于事件的分析引擎,其后端将事件以二进制格式写入文件,切具有非常高的性能,而JMC是一个GUI工具,用于检查JFR从单个JVM的遥测中创建的数据文件。

简单点说,就是JFR先生成了文件,再由JMC来图形化的展示,以便确定线程、方法等性能上的瓶颈。后文也有讲到,JFR可以不生成文件,而是以流(stream)的形式进行传输;同时,也可以自己实现程序来解析和监控JFR的产出,以便实现自动监控等企业级应用。

这些工具最初是BEA的JRockit JVM工具的一部分,并作为将JRockit与Java HotSpot VM合并的过程的一部分转移到Oracle JDK的商业版本。JDK 9发布后,Oracle改变了Java的发布模型,并宣布JFR和JMC将成为开源工具。

JFR是对OpenJDK的贡献,并在JDK 11中作为Jep 328交付。JMC被拆分成一个独立的开源项目,现金可以单独下载。

Java 14的出现为JFR引入了一个新特性:JFR产生连续事件流的能力。此更改还提供了一个API,可以立即处理事件,而不是在事后解析文件。这一变化为构建监视和可观察工具提供了良好的基础。

然而,有一个问题是,由于JFR和JMC只是最近才成为开放源码工具,许多Java开发人员并不了解他们的大量功能。因此,在我进入新的Java 14特性之前,让我们从一开始就开始研究JMC和JFR。

介绍JFR

因为JFR最初是作为OpenJDK 11的一部分作为开放源码提供的,所以您需要运行该版本(或最近的版本),或者成为运行商用JDK的Oracle客户。

创建jfr记录有多种方法,但我将特别介绍两种方法:在启动jvm时使用命令行参数,以及使用jcmd.

首先,让我们看看启动JFR所需的命令:
-XX:StartFlightRecording:
此开关允许在进程启动时进行JFR记录。直到Java 14之前,JFR生成了一个分析数据文件。现在产出为一个一次性转储文件或一个连续的环形缓冲区。有大量单独的选项来控制想捕获的数据。

此外,JFR可以捕获超过100个不同的可能指标。大部分指标的开销都是非常低,但有些确实会引起明显的开销。单独管理所有这些指标的配置是一项艰巨的任务。为了简化过程,JFR使用配置文件。这些文件是简单的XML文件,包含每个指标,以及是否需要对其捕获。标准JDK下载包含两个基本文件:default.jfc和profile.jfc.

默认记录级别,由default.jfc,它被设计成非常低的开销,并且基本上可以用于每个Java产品。这个profile.jfc配置包含更详细的信息,但这当然需要更高的运行时成本。

这里说的更详细的信息,有可能指的是更多的指标,或者指标的粒度更细。

除了提供的两个文件之外,还可以创建一个自定义配置文件,只包含所需数据点。JMC工具(在下一节中介绍)有一个模板管理器,可以方便地创建这些文件。

命令行上可以传递其他参数,包括存储记录数据的文件名,以及要保留的数据量(按数据点的年龄计算)。例如,整个JFR命令行可能如下所示:

-XX:StartFlightRecording:disk=true,filename=/sandbox/service.jfr,maxage=12h,settings=profile
这将创建一个12小时的滚动缓冲区,其中包含深度分析信息。没有关于这个文件能得到多大的规定。

注:当JFR是商业构建的一部分时,它被解锁在-XX:+UnlockCommercialFeatures切换。但是,当使用此选项时,Oracle JDK 11和更高版本会发出警告。发出警告是因为所有的商业特性都是开源的,而且标志从来都不是OpenJDK的一部分,所以继续使用它是没有意义的。在OpenJDK构建中,使用商业特性标志会导致错误。

JFR的一个重要特性是它不需要在JVM启动时进行配置。相反,可以使用jcmd从UNIX命令行控制正在运行的JVM的程序:
$ jcmd JFR.start name=Recording1 settings=default
$ jcmd JFR.dump filename=recording.jfr
$ jcmd JFR.stop
不仅如此,还可以在应用程序已经启动之后附加到JFR。您可以看到,JMC(参见下一节)提供了在本地机器上运行的JVM中动态控制JFR的功能。

JFR激活的方式有很多,可以通过命令行,配置文件,jcmd,应用程序与JFR的启动顺序也不影响,并且可以动态控制JFR

无论如何激活JFR,结果都是一样的:每个JVM每次运行一个分析文件。该文件包含大量二进制数据,并且不具有人类可读性,因此您需要某种工具来提取和可视化数据,例如JMC。

介绍JMC 7

使用JMC图形工具显示JFR输出文件中包含的数据。由jmc命令启动。以前JMC与Oracle JDK下载捆绑在一起,但现在可单独使用.

图1显示JMC的启动界面。加载文件后,JMC对其进行一些自动分析,以检测记录运行中发现的明显问题。
图1.JMC启动屏幕
图1.JMC启动屏幕
注:若要配置文件,必须在目标应用程序上启用JFR。除了使用以前创建的文件外,JMC还在标记为JVM浏览器的左上角提供了一个选项卡,用于动态地附加到本地应用程序。
您将在JMC中遇到的第一个界面是概述遥测屏幕(图2),它显示了JVM总体健康状况的高级仪表板。
图2.初始遥测仪表板
图2.初始遥测仪表板
JVM的主要子系统都有专门的屏幕来进行深入分析。例如,垃圾收集有一个概览屏幕(图3)显示JFR文件生存期内的垃圾收集事件。通过最长的暂停显示,您可以查看时间线上,任何异常的长时间的垃圾收集事件的位置。
图3.垃圾收集概述
图3.垃圾收集概述
在详细的配置文件配置中,还可以看到将新分配缓冲区(TLabs)分发给应用程序线程的各个事件(图4)。这使您可以更准确地查看流程中的资源。
图4.Tlab拨款
图4.Tlab拨款
Tlab分配使您可以轻松地看到哪些线程分配的内存最多;图4,它是一个使用ApacheKafka主题的数据的线程。
JVM的另一个主要子系统是JIT编译器,图5如图所示,JMC允许您深入了解编译器是如何工作的。
图5.监视编译器
图5.监视编译器

其中一个关键资源是JIT编译器的代码缓存中的可用内存,它存储方法的编译版本。对于具有许多编译方法的进程,这个内存区域可能会耗尽,导致进程无法达到峰值性能。图6显示与此缓存相关的数据。

图6.监视代码缓存
图6.监视代码缓存

编译方法是什么?
JIT技术是优化java执行速度产生出来的。早期,编译器javac将源代码编译成字节码,然后再由解释器来解释执行。后来为了提高速度,将执行频率高的语句块或方法,通过JIT编译成本地机器码,可以直接被执行。所以,不能再单纯的说java是解释型语言或编译型语言。

JMC还包括一个方法级分析器(图7),它的工作方式与VisualVM或商业工具(如JProfiler或YourKit)中的工作方式非常相似。
Figure 7. Profiling pane
Figure 7. Profiling pane

JMC中更高级的屏幕之一是VM操作视图(图8),它显示了JVM执行的一些内部操作以及它们所用的时间。这并不是每次分析都需要的,但它对于检测某些类型的问题非常有用。
图8.查看JVM操作
图8.JVM操作视图
例如,这里可视化的关键数据点之一是偏差撤销时间。大多数程序都得益于JVM的偏颇锁定技术(这是一种广泛采用的技术,锁定对象的第一个线程很可能是唯一可以锁定该对象的线程)。然而,一些工作负载(如ApacheCassandra)的架构方式使它们受到偏颇锁定的负面影响。通过VM操作视图,您非常容易地发现这种情况。

以编程方式解析JFR文件

使用JMC检查单个JVM中的文件是分析和开发应用程序的一个重要方面。然而,在基于服务的环境和企业环境中,使用GUI逐个调查文件的模型并不总是最方便的方法。

您可能希望构建工具来解析JFR捕获的原始数据,并提供更方便的访问和可视化方法。幸运的是,JFR事件的编程API非常容易上手。

关键类是jdk.jfr.consumer.RecordingFile和jdk.jfr.consumer.RecordedEvent。前者是由后者的实例组成的,很容易在简单的Java代码中遍历事件,如下所示:

var recordingFile = new RecordingFile(Paths.get(fileName));
while (recordingFile.hasMoreEvents()) {
    var event = recordingFile.readEvent();
    if (event != null) {
        var details = decodeEvent(event);
        if (details == null) {
            // Log a failure to recognize details
        } else {
            // Process details
            System.out.println(details);
        }
    }
}

每个事件可能包含的数据略有不同,因此您需要根据记录的数据类型进行解码。为了简单起见,我选择了一个RecordedEvent并将其转换为一个简单的字符串映射:

public Map<String, String> decodeEvent(final RecordedEvent e) {
        for (var ent : mappers.entrySet()) {
            if (ent.getKey().test(e)) {
                return ent.getValue().apply(e);
            }
        }
        return null;
    }

真正的魔力发生在解码器中,它们有一个稍微复杂的签名:

private static Predicate<RecordedEvent> testMaker(String s) {
        return e -> e.getEventType().getName().startsWith(s);
    }

    private static final Map<Predicate<RecordedEvent>,
            Function<RecordedEvent, Map<String, String>>> mappers =
                Map.of(testMaker("jdk.ObjectAllocationInNewTLAB"),
                ev -> Map.of("start", ""+ ev.getStartTime(), "end",
                             ""+ ev.getEndTime(),
                             "thread",
                             ev.getThread("eventThread").getJavaName(),
                             "class", ev.getClass("objectClass").getName(),
                             "size", ""+ ev.getLong("allocationSize"),
                             "tlab", ""+ ev.getLong("tlabSize")
               ));

mappers是键值对的集合。每个键值对由一个谓词和一个函数组成。谓词用于测试传入事件。如果它返回TRUE,调用函数来处理这个事件,并将事件转换成字符串集合。

这意味着decodeEvent()方法在mappers上循环,并尝试查找与传入事件匹配的谓词。一旦成功,相应的函数就会被调用来解码事件。

每个Java版本提供的JFR事件稍有不同。在Java 11中,有超过120种不同的可能类型。要很好的支持他们比较困难,因为每个解码器都稍微不同。但是,支持特定的、感兴趣的一部分指标是非常可行的。

Java 14 JFR事件流

使用Java 14,可以使用JFR的新使用模式,即JFR事件流。借助API,程序可以在JFR事件发生时接收回调并立即响应。

开发人员可以通过Java代理的方式来利用这一点,这是非常明显和重要的。这是一个特殊的JAR文件,它使用了TabumingAPI。在这个api中,一个类声明一个特殊的静态方法premain(),将自身注册为检测工具。Java代理示例可能如下所示:

public class AgentMain implements Runnable {
    public static void premain(String agentArgs, Instrumentation inst) {
        try {
            Logger.getLogger("AgentMain").log(
                Level.INFO, "Attaching JFR Monitor");
            new Thread(new AgentMain()).start();
        } catch (Throwable t) {
            Logger.getLogger("AgentMain").log(
                Level.SEVERE,"Unable to attach JFR Monitor", t);
        }
    }

    public void run() {
        var sender = new JfrStreamEventSender();
        try (var rs = new RecordingStream()) {
            rs.enable("jdk.CPULoad")
                .withPeriod(Duration.ofSeconds(1));
            rs.enable("jdk.JavaMonitorEnter")
                .withThreshold(Duration.ofMillis(10));
            rs.onEvent("jdk.CPULoad", sender);
            rs.onEvent("jdk.JavaMonitorEnter", sender);
            rs.start();
        }
    }
}

调用premain()方法时,会在一个新线程中,创建一个AgentMain的新实例并运行。对记录流进行配置,当它启动时,它永远不会返回(这就是为什么它是在一个单独的检测线程上创建的)。

在记录流中,可以生成大量事件。幸运的是,JFRAPI提供了一些基本的筛选,以减少需要回调处理的事件数:

Enabled确定事件是否被记录下来
Threshold
指定无需记录事件的时间范围。
Stack trace*确定Event.commit()的堆栈跟踪是否需要记录
Period::如果事件是周期性的,则指定发出事件的间隔。
请注意,应用程序程序员必须实现进一步的过滤功能。因为,每秒分配数GB内存的Java程序(这是一个很高的分配,但对于大型应用程序来说完全是可能的)可以很容易地生成数十万个事件。

示例,Java代理启动一个记录流,记录被发送到对象(JfrStreamEventSender)的每个事件,如下:

public final class JfrStreamEventSender implements Consumer<RecordedEvent> {

    private static final String SERVER_URL = "http://127.0.0.1:8080/events";

    @Override
    public void accept(RecordedEvent event) {
        try {
            var payload = JFRFileProcessor.decodeEvent(event);
            String json = new ObjectMapper().writeValueAsString(payload);

            var client = HttpClient.newHttpClient();

            var request = HttpRequest.newBuilder()
                    .uri(URI.create(SERVER_URL))
                    .timeout(Duration.ofSeconds(30))
                    .header("Content-Type", "application/json")
                    .POST(HttpRequest.BodyPublishers.ofString(json))
                    .build();

            HttpResponse<String> response = 
                client.send(request, HttpResponse.BodyHandlers.ofString());
            Logger.getLogger("JfrStreamEventSender").log(
                Level.INFO, 
                "Server response code: " + response.statusCode() + 
                ", body: " + response.body());
        } catch (IOException | InterruptedException e) {
            Logger.getLogger("JfrStreamEventSender").log(
                Level.SEVERE, "Unable to send JFR event to server", e);
        }
    }
}

这个无状态类只需接收事件,按照文件处理用例中显示的方式对其进行解码,然后使用Jackson库将其编码为JSON。然后使用Java 11 HTTP client将度量负载发送到服务,并进行处理,在本例中,该服务位于localhost:8080.

当然,要从这个简单的示例代码中构建一个生产级监控工具,还有更多的工作要做。按规模构建需要创建能够处理大量传入数据点的HTTP endpoints 。还必须处理诸如惟一标识JVM和确保入站数据正确属性化等问题。

还需要部署数据分析管道系统和中期存储(缓存?)系统,以及前端可视化和显示页面。所有这些系统都必须以最高的可靠性水平运行,因为它们正在监测生产系统。

尽管存在这些挑战,但JFR事件流,在监视和观察正在运行的应用程序的JVM的技术方面,是向前的一大步。人们对新的API非常感兴趣,而且很可能在Java 14发布后不久,对流事件的支持将被添加到开源跟踪和监视库中。JFR方法用于监视和分析的好处很快就会来到你身边。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值