跳到主要内容

JEP 349: JFR 事件流

QWen Max 中英对照 JEP 349: JFR Event Streaming

概述

公开 JDK Flight Recorder 数据以进行持续监控。

目标

  • 为磁盘上的 JFR 数据的持续消费提供一个 API,适用于进程内和进程外的应用程序。
  • 记录与非流式传输情况下相同的一组事件,如果可能的话,开销小于 1%。
  • 事件流必须能够与基于磁盘和内存的非流式录制共存。

非目标

  • 为消费者提供同步回调。
  • 允许消费内存中的记录。

动机

HotSpot VM 使用 JFR 发出 500 多个数据点,其中大多数数据点无法通过解析日志文件以外的其他方式获取。

今天,要使用这些数据,用户必须启动记录,停止记录,将内容转储到磁盘,然后解析记录文件。这对于应用程序分析来说非常有效,因为通常每次至少记录一分钟的数据,但并不适用于监控目的。监控使用的例子是一个显示数据动态更新的仪表板。

创建录制内容时会产生一些开销,例如:

  • 触发在创建新录制时必须发生的事件,
  • 写入事件元数据,例如字段布局,
  • 写入检查点数据,例如堆栈跟踪,以及
  • 将数据从磁盘存储库复制到单独的录制文件。

如果有一种方法可以在不创建新的录制文件的情况下读取从磁盘存储库记录的数据,那么就可以避免许多这样的开销。

描述

模块 jdk.jfr 中的包 jdk.jfr.consumer 增加了异步订阅事件的功能。用户可以直接从磁盘存储库中读取记录数据,或者进行流式传输,而无需转储记录文件。与流交互的方式是注册一个处理器(例如 lambda 函数),以便在事件到达时调用。

以下示例打印整体 CPU 使用率和争用时间超过 10 毫秒的锁。

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", event -> {
System.out.println(event.getFloat("machineTotal"));
});
rs.onEvent("jdk.JavaMonitorEnter", event -> {
System.out.println(event.getClass("monitorClass"));
});
rs.start();
}
java

RecordingStream 类实现了 jdk.jfr.consumer.EventStream 接口,该接口提供了一种统一的方式来过滤和消费事件,无论源是实时流还是磁盘上的文件。

public interface EventStream extends AutoCloseable {
public static EventStream openRepository();
public static EventStream openRepository(Path directory);
public static EventStream openFile(Path file);

void setStartTime(Instant startTime);
void setEndTime(Instant endTime);
void setOrdered(boolean ordered);
void setReuse(boolean reuse);

void onEvent(Consumer<RecordedEvent> handler);
void onEvent(String eventName, Consumer<RecordedEvent handler);
void onFlush(Runnable handler);
void onClose(Runnable handler);
void onError(Runnable handler);
void remove(Object handler);

void start();
void startAsync();

void awaitTermination();
void awaitTermination(Duration duration);
void close();
}
java

有三种工厂方法可以创建流。EventStream::openRepository(Path) 从磁盘存储库构建流。这是一种通过直接针对文件系统来监控其他进程的方式。磁盘存储库的位置存储在系统属性 "jdk.jfr.repository" 中,可以使用 attach API 来读取该属性。也可以使用 EventStream::openRepository() 方法进行进程内监控。与 RecordingStream 不同,它不会启动录制。相反,只有当通过外部手段(例如使用 JCMD 或 JMX)启动录制时,流才会接收事件。EventStream::openFile(Path) 方法从录制文件创建流。它补充了目前已有的 RecordingFile 类。

该接口还可用于设置缓冲的数据量,以及事件是否应按时间顺序排列。为了尽量减少分配压力,还有一个选项可以控制是否应该为每个事件分配一个新的事件对象,或者是否可以重用之前的对象。流可以在当前线程或异步启动。

存储在线程本地缓冲区中的事件会由 Java 虚拟机 (JVM) 每秒定期刷新到磁盘存储库。一个单独的线程解析最近的文件,直到数据写入的点,并将事件推送给订阅者。为了保持开销较低,仅从文件中读取当前活跃订阅的事件。要接收刷新完成时的通知,可以使用 EventStream::onFlush(Runnable) 方法注册一个处理器。这是在 JVM 准备下一组事件的同时聚合或推送数据到外部系统的机会。

替代方案

JMX 通知为 JDK 和第三方应用程序提供了一种暴露信息以进行持续监控的方式。然而,JMX 存在一些缺点,使其不适合用于此 JEP 的目的。

  • 在 JVM 中收集的数据点通常出现在无法调用 Java 代码的地方,例如在 GC(垃圾回收)引发的安全点期间。
  • 开发者已经投入时间使用 JFR(Java Flight Recorder)收集数据。将所有这些探测点重写为 JMX(Java Management Extensions)将会是一项非常庞大的工作。
  • JMX 没有提供在事件发送之前进行过滤的机制,这意味着系统很容易被淹没。
  • 包含引用的复杂数据结构(例如堆栈跟踪)无法使用 Open MBean 类型高效表示。

测试

  • 验证该功能没有任何内存泄漏。
  • 验证该功能随着时间的推移具有稳定的性能(适当的压测)。
  • 为所有导出的方法编写单元测试。
  • 验证事件订阅是否可以与其他同时运行的录制功能正常工作。
  • 验证该 API 开箱即用的效果良好。
  • 验证该 API 适合转发事件数据,以供其他框架使用。
  • 验证该 API 适合低延迟要求的环境(最小的 GC 暂停)。
  • 验证该 API 适合工具供应商,即数据到达速率适合用于绘制图表。
  • 验证该 API 是安全的,不应在特权线程上下文中触发回调。
  • 验证开销是可接受的。
  • 验证在订阅者中无法创建无限递归。

风险与假设

  • API 回调中的操作可能会引发 JFR 事件,这可能导致无限递归。在这种情况下,可以通过不记录事件来缓解此问题。