Skip to content

JEP 349: JFR Event Streaming | JFR 事件流

摘要

为持续监控暴露 JDK Flight Recorder 数据。

目标

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

非目标

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

动机

HotSpot VM 使用 JFR 发出超过 500 个数据点,其中大部分除了解析日志文件外,无法通过其他方式获得。

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

创建记录会产生一些开销,例如:

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

如果有一种方法可以从磁盘存储库中读取正在记录的数据而不创建新的记录文件,那么可以避免大部分这些开销。

描述

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

以下示例打印了总体 CPU 使用情况和锁等待时间超过 10 毫秒的情况。

java
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 接口,该接口提供了一种统一的方式来过滤和消费事件,无论事件的来源是实时流还是磁盘上的文件。

java
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" 中,可以使用 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 收集数据方面投入了大量时间。将所有这些探测点重写为 JMX 将是一项非常巨大的工作。
  • JMX 没有在发送事件之前过滤事件的机制,这意味着系统很容易被淹没。
  • 复杂的数据结构(如带有引用的堆栈跟踪)不能有效地使用 Open MBean 类型来表示。

测试

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

风险和假设

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