登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  java教程

Java JFR 如何只录制一次慢请求窗口

来源:17golang原创

时间:2026-09-07 18:31:19 395浏览 收藏

Java 服务出现偶发慢请求时,不必先把 JFR 常驻成一份巨大的连续录制。更稳妥的做法是:从 default.jfc 复制一份临时配置,给关心的阻塞事件设置阈值,再用 jcmd JFR.start 只录制 30 秒,最后用 jfr summaryjfr print 读取同一个 .jfr 文件。这样得到的是一次有边界的现场,既能保留调用栈,又不会把所有短事件都混进结果。

要点速览
  • duration=30s 决定这次录制何时自动结束,filename 决定结果落到哪个文件。
  • threshold 只过滤事件,不代表整个 HTTP 请求耗时;应用路由需要自定义 JFR 事件或结合其他证据。
  • 先看事件总量,再看 jdk.SocketReadjdk.JavaMonitorWait 和调用栈,才能判断慢点落在哪个边界。

先把一次录制的边界定下来

这类排查至少有四个边界:业务请求事件、事件阈值、录制窗口和结果文件。JFR 的 duration 事件只有达到 threshold 才会被记录;窗口结束后,数据写入一个可复查的 slow-window.jfr。如果只观察网络或锁,可以直接使用 JFR 内置事件;如果必须按路由定位,就在服务代码里增加一个自定义请求事件,让它的字段包含路由、状态码和耗时。

Java JFR 慢请求排查中业务请求事件、threshold、30 秒录制与 slow-window.jfr 的静态边界关系
图1:把业务请求事件、阈值、一次录制窗口和结果文件放在同一组边界里,避免把事件阈值误认为接口总耗时。

从 default.jfc 生成只保留慢事件的配置

不要直接修改 JDK 自带的配置文件。用 jfr configure 从默认配置生成一份项目专用文件,只调整本次要看的 duration 事件。例如下面把网络读写和锁等待都设为 20 毫秒;短于这个值的事件不会进入这次录制。

# 从默认配置派生临时配置,不改动 JDK 自带文件
jfr configure --input "$JAVA_HOME/lib/jfr/default.jfc" \
  --output /opt/jfr/slow-request.jfc \
  jdk.SocketRead#threshold=20ms \
  jdk.SocketWrite#threshold=20ms \
  jdk.JavaMonitorWait#threshold=20ms

如果运行环境的 JDK 没有这个路径,先用 jfr configure --help 和实际的 JAVA_HOME 确认配置位置。阈值越低,捕获的短事件越多;阈值越高,文件更干净,但可能漏掉由大量短等待叠加形成的请求问题。

用 jcmd 启动一次性慢请求窗口

找到目标 JVM 的 PID 后,显式指定名称、配置、时长和文件名。duration=30s 到期后自动结束,适合让问题复现一次;不要省略 duration,否则默认值可能让录制一直运行。

# 先确认目标进程,再启动唯一一个 30 秒录制
jcmd  VM.version
jcmd  JFR.start name=slow-window \
  settings=/opt/jfr/slow-request.jfc \
  duration=30s \
  filename=/tmp/slow-window.jfr

# 复现慢请求期间查看录制状态
jcmd  JFR.check name=slow-window

需要按路由和状态码判断时,可以在请求入口包住一个应用事件。事件类上的 @Threshold 是默认过滤线,commit() 会结束本次事件的计时;下面的字段会随事件写入 JFR。

import jdk.jfr.Category;
import jdk.jfr.Event;
import jdk.jfr.Label;
import jdk.jfr.Name;
import jdk.jfr.Threshold;

@Name("demo.HttpRequest")
@Label("HTTP Request")
@Category({"Demo", "HTTP"})
@Threshold("200 ms")
final class HttpRequestEvent extends Event {
    @Label("Route") String route;
    @Label("Status") int status;
}

// 在请求入口开始,在响应状态确定后提交事件
HttpRequestEvent event = new HttpRequestEvent();
event.route = "/orders/{id}";
event.begin();
try {
    // 这里调用实际业务处理,并在结束前写入状态码
    event.status = 200;
} finally {
    event.commit();
}

这段自定义事件只说明“哪个请求超过 200 毫秒”,不替代网络、锁和 CPU 事件。真实项目中应把 route 做成低基数模板,避免把用户参数直接写进事件字段。

用 jfr summary 与 print 读取同一个文件

录制结束后先做摘要,再做定向读取。摘要用来确认文件确实包含目标事件,定向输出用来抓 duration、线程和调用栈。

# 先看事件数量和录制基本信息
jfr summary /tmp/slow-window.jfr

# 再读取可能造成慢请求的阻塞事件与调用栈
jfr print --events jdk.SocketRead,jdk.SocketWrite,jdk.JavaMonitorWait \
  --stack-depth 20 /tmp/slow-window.jfr

# 如果写入了应用级事件,再单独过滤它
jfr print --events demo.HttpRequest /tmp/slow-window.jfr
Java JFR 结果读取中 slow-window.jfr 连接到 jfr summary、jfr print、SocketRead、JavaMonitorWait 与调用栈
图2:从 slow-window.jfr 分出摘要、事件过滤和调用栈三条读取关系,先确认数量再解释慢点。

判断时不要把一个 jdk.SocketRead 直接当成接口总耗时:它说明线程在 socket 读取上等待过;jdk.JavaMonitorWait 更接近锁或条件等待;自定义 demo.HttpRequest 才能给出路由级的请求窗口。三者的线程、时间范围和调用栈能够相互印证时,结论才足够可靠。

常见问题

为什么设置了 threshold 仍然看不到某个慢请求?

threshold 只作用于对应事件类型,且事件必须真的被启用。路由级耗时如果没有自定义事件,单看 SocketRead 不能覆盖全部业务执行时间。

duration 到期后还需要执行 JFR.stop 吗?

不需要。duration 到期会结束这次录制并写入 filename;只有提前结束或需要立即落盘时,才用带 name 的 JFR.stop

为什么不直接使用 profile?

profile 适合短时间获取更多信息,但仍可能产生更多事件。先从 default 配置派生并提高目标事件阈值,更适合一次只看慢请求窗口的排查。

最终检查只看三件事:录制是否只有一个明确的 name,文件是否按预期生成,以及结果中是否同时出现事件类型、duration 和可解释的调用栈。满足这三点,就可以把 JFR 现场交给后续的代码或链路排查。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>