Using Java Flight Recorder
本指南说明如何扩展 Java Flight Recorder (JFR),以便为 Quarkus 应用程序提供更多洞察力。JFR 将来自 Java 标准 API 和 JVM 的各种信息记录为事件。通过添加 Quarkus JFR 扩展,你可以向 JFR 添加自定义 Quarkus 事件。这将帮助你解决应用程序中潜在的问题。 JFR 可以预先配置为转储文件,当应用程序退出时,JFR 将输出此文件。该文件将包含 JFR 事件流的内容,其中还添加了 Quarkus 自定义事件。当然,你可以在任何时候获取此文件,即使应用程序意外退出。
Prerequisites
如要完成本指南,您需要:
-
Roughly 15 minutes
-
An IDE
-
安装了 JDK 17+,已正确配置
JAVA_HOME
-
Apache Maven ${proposed-maven-version}
-
如果你想使用 Quarkus CLI, 则可以选择使用
-
如果你想构建一个本机可执行文件(或如果你使用本机容器构建,则使用 Docker),则可以选择安装 Mandrel 或 GraalVM 以及 configured appropriately
Creating the Maven project
首先,我们需要一个新项目。使用以下命令创建一个新项目:
quarkus create app {create-app-group-id}:{create-app-artifact-id} \
--no-code
cd {create-app-artifact-id}
要创建一个 Gradle 项目,添加 --gradle
或 --gradle-kotlin-dsl
选项。
有关如何安装和使用 Quarkus CLI 的详细信息,请参见 Quarkus CLI 指南。
mvn {quarkus-platform-groupid}:quarkus-maven-plugin:{quarkus-version}:create \
-DprojectGroupId={create-app-group-id} \
-DprojectArtifactId={create-app-artifact-id} \
-DnoCode
cd {create-app-artifact-id}
要创建一个 Gradle 项目,添加 -DbuildTool=gradle
或 -DbuildTool=gradle-kotlin-dsl
选项。
适用于 Windows 用户:
-
如果使用 cmd,(不要使用反斜杠
\
,并将所有内容放在同一行上) -
如果使用 Powershell,将
-D
参数用双引号引起来,例如"-DprojectArtifactId={create-app-artifact-id}"
此命令生成 Maven 项目并导入 Quarkus JFR 扩展,其中包含默认的 JFR 支持。
如果你已经配置了 Quarkus 项目,可以通过在项目基目录中运行以下命令,将 JFR 扩展添加到项目:
quarkus extension add {add-extension-extensions}
./mvnw quarkus:add-extension -Dextensions='{add-extension-extensions}'
./gradlew addExtension --extensions='{add-extension-extensions}'
这会将以下内容添加到构建文件中:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jfr</artifactId>
</dependency>
implementation("io.quarkus:quarkus-jfr")
Examine the Jakarta REST resource
使用以下内容创建一个 src/main/java/org/acme/jfr/JfrResource.java
文件:
package org.acme.jfr;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
@Path("/hello")
public class JfrResource {
@GET
@Produces(MediaType.TEXT_PLAIN)
public String hello() {
return "hello";
}
}
请注意,应用程序中不包含特定的 JFR 代码。默认情况下,发送到此端点的请求将被记录到 JFR 中,无需进行任何必需的代码更改。
Open JFR dump file
我们可以使用两个工具打开一个 JFR 转储: jfr
CLI 和 JDK Mission Control (JMC)。也可以使用 JFR API 阅读它们,但我们不会在本指南中涉及这一点。
jfr CLI
jfr
CLI 是 OpenJDK 中包含的一个工具。可执行文件是 $JAVA_HOME/bin/jfr
。我们可以使用 jfr CLI 查看转储文件中仅限于与 Quarkus 相关的事件列表,方法是运行以下命令:
jfr print --categories quarkus myrecording.jfr
JDK Mission Control
JMC 实际上是 JFR 的一个 GUI。某些 Java 发行版包含 JMC,但如果没有,你需要手动下载它。
要使用 JMC 查看事件列表,我们首先按照以下步骤在 JMC 中加载 JFR 文件。
jmc -open myrecording.jfr
打开 JFR 文件后,我们有两个选项。一个是以表格列表的形式查看事件,另一个是按时间顺序查看事件在其发生的线程上。
要以表格样式查看 Quarkus 事件,请选择 JMC 左侧的事件浏览器,然后打开 JMC 右侧的 Quarkus 事件类型树。
要在线程上按时间顺序查看 Quarkus 事件,请选择 JMC 左侧的 Java application
和 Threads
。
标准配置不显示 Quarkus 事件。我们要完成三个任务才能看到 Quarkus 事件。
-
右键单击并选择
Edit Thread Activity Lanes…​
。 -
选择加号按钮以在左侧添加一个新通道,然后选中显示该通道。
-
选择 Quarkus 作为该通道将显示的事件类型,然后按确定。
现在我们可以按线程查看 Quarkus 事件了。
此扩展能够同时记录多个 JFR 事件(由不同的线程或在响应式执行模型的情况下由同一个线程发出)。因此,事件可能在 JMC 中重叠。这可能会让你难以看到你想要看到的事件。为了避免这种情况,我们建议使用 Request IDs 过滤事件,以便你只能看到你想要看到的有关请求的信息。 |
Events
Identifying Requests
此扩展与 OpenTelemetry 扩展协作。此扩展记录的事件具有跟踪 ID 和跨度 ID。这些分别使用 OpenTelemetry ID 记录。
这意味着在从 OpenTelemetry 实现提供的 UI 识别出感兴趣的跟踪和跨度 ID 后,我们可以使用这些 ID 立即跳转到 JFR 中的详细信息。
如果我们尚未启用 OpenTelemetry 扩展,此扩展会为每个请求创建一个 ID,并将其作为 traceId 链接到 JFR 事件。在这种情况下,跨度 ID 将为 null。
目前,Quarkus 仅记录 REST 事件,但我们计划随着我们将来添加更多事件,使用此 ID 将每个事件相互链接。
Event Implementation Policy
当 JFR 开始记录事件时,该事件尚未记录到 JFR,但当它提交该事件时,该事件就被记录了。因此,在转储时间开始记录但尚未提交的事件不会被转储。这是由 JFR 的设计决定的,无法避免。这意味着如果长时间处理,事件不会永远被记录。因此,你不会意识到长时间处理。
为了解决此问题,Quarkus 还可以分别在处理的开始和结束时,记录开始和结束事件。这些事件在默认情况下处于禁用状态。但是,我们可以按照以下说明在 JFR 中启用这些事件。
REST API Event
记录此事件是在启用 Quarkus REST 或 RESTEasy 扩展时。REST 服务器处理结束后会立即记录以下三个 JFR 事件。
- REST
-
Records the time period from the start of the REST server process to the end of the REST server process.
- RestStart
-
Records the start of the REST server process.
- RestEnd
-
Records the end of the REST server process.
这些事件包含以下信息。
-
HTTP Method
-
URI
-
Resource Class
-
Resource Method
-
Client
HTTP 方法会记录客户端访问的 HTTP 方法。这会记录一个字符串,包含 GET、POST、DELETE 等 HTTP 方法以及其他通用 HTTP 方法。
URI 记录客户端访问的 URI 路径。这并不包含主机名或端口号。
资源类会记录已执行的类。我们可以检查按预期通过 HTTP 方法和 URI 执行了资源类。
资源方法会记录已执行的方法。我们可以检查按预期通过 HTTP 方法和 URI 执行了资源方法。
客户端会记录有关访问客户端的信息。
Native Image
本机可执行文件支持 Java Flight Recorder。此扩展也支持本机可执行文件。
在要为本机可执行文件启用 JFR 时,通常是用 --enable-monitoring
构建的。但是,我们可以通过将 jfr
添加到配置文件属性 quarkus.native.monitoring
中来为 Quarkus 本机可执行文件启用 JFR。有两种方法可以设置此配置文件:通过将其包含在 application.properties
中或在构建时指定它。
第一种方法是首先在 application.properties
中设置配置文件。
quarkus.native.monitoring=jfr
然后,像往常一样构建你的本机可执行文件:
quarkus build --native
./mvnw install -Dnative
./gradlew build -Dquarkus.native.enabled=true
第二种方法是构建时传递 -Dquarkus.native.monitoring=jfr
:
include::./_includes/devtools/build-native.adoc[] :构建-附加-参数:
在构建完本机可执行文件后,你可以通过以下方式使用 JFR 运行本机应用程序:
target/your-application-runner -XX:StartFlightRecording=name=quarkus,dumponexit=true,filename=myrecording.jfr
请注意,此时 Mandrel 和 GraalVM 无法为 Windows 本机可执行文件记录 JFR。 |
JFR configuration
我们可以使用 JFR CLI 来配置 JFR 将要记录的事件。配置文件 JFC 文件采用 XML 格式,所以我们可以使用文本编辑器对其进行修改。但是,建议使用 jfr configure
,它默认包含在 OpenJDK 中。
这里我们创建一个配置文件,其中记录了 REST Start 和 REST End 事件(它们默认不会记录):
jfr configure --input default.jfc +quarkus.RestStart#enabled=true +quarkus.RestEnd#enabled=true --output custom-rest.jfc
这会创建 custom-rest.jfc
作为配置文件,并启用对 RestStart 和 RestEnd 的记录。
现在,我们准备用新设置运行我们的应用程序。我们使用 JFR 配置来启动应用程序,以便在启动 Java 虚拟机时启用它。
include::./_includes/devtools/dev.adoc[]:dev-additional-parameters:
Adding new events
本部分针对希望向此扩展添加新事件的用户。
我们建议将新事件与现有事件进行关联。对于查看一个耗时较长的进程的详细信息而言,关联非常有用。例如,一个常规 REST 应用程序从数据存储获取处理所需的数据。如果 REST 事件未与数据存储事件关联,则不可能知道每个 REST 请求处理了哪些数据存储事件。当这两个事件关联时,我们就能立即知道每个 REST 请求处理了哪个数据存储事件。
数据存储事件尚未实现。 |
Quarkus JFR 扩展为事件关联提供一个请求 ID。有关请求 ID 的更多信息,请参见 Identifying Requests。
在具体代码中,需要以下两个步骤。首先,在新事件上按如下所示实现 traceId`和 `spanId
。附加到这些事件上的 `@TraceIdRelational`和 `@SpanIdRelational`将提供关联。
import io.quarkus.jfr.runtime.SpanIdRelational;
import io.quarkus.jfr.runtime.TraceIdRelational;
import jdk.jfr.Description;
import jdk.jfr.Event;
import jdk.jfr.Label;
public class NewEvent extends Event {
@Label("Trace ID")
@Description("Trace ID to identify the request")
@TraceIdRelational
protected String traceId;
@Label("Span ID")
@Description("Span ID to identify the request if necessary")
@SpanIdRelational
protected String spanId;
// other properties which you want to add
// setters and getters
}
然后,从 `IdProducer`对象的 `getTraceId()`和 `getSpanId()`获取信息并存储在事件中。
import io.quarkus.jfr.runtime.IdProducer;
public class NewInterceptor {
@Inject
IdProducer idProducer;
private void recordedInvoke() {
NewEvent event = new NewEvent();
event.begin();
// The process you want to measure
event.end();
if (endEvent.shouldCommit()) {
event.setTraceId(idProducer.getTraceId());
event.setSpanId(idProducer.getSpanId());
// call other setters which you want to add
endEvent.commit();
}
}
}