跳到主要内容

JEP 349:JFR 事件流

概括

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

目标

  • 提供一个 API,用于持续消耗磁盘上的 JFR 数据,适用于进程内和进程外应用程序。
  • 记录与非流式传输情况相同的事件集,如果可能的话,开销小于 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();
}

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();
}

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

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

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

备择方案

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

  • JVM 中收集的数据点通常发生在无法调用 Java 代码的地方,例如在 GC 引发的安全点期间。
  • 开发人员已经投入时间使用 JFR 收集数据。为 JMX 重写所有这些探测点将是一项非常艰巨的工作。
  • JMX 不提供在发送事件之前过滤事件的机制,这意味着系统很容易被淹没。
  • 带有引用的复杂数据结构(例如堆栈跟踪)无法使用 Open MBean 类型有效地表示。

测试

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

风险和假设

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