用bpftrace通过tracepoint:syscalls:sys_enter_read与sys_exit_read配对测read延迟,加/comm==”java”/过滤,@start[tid]记时、exit分支清理,耗时转微秒并直方图分析;tracepoint比kprobe更稳定低开销,遇错先查/sys/kernel/debug/tracing/events/和tracing_on。

怎么用 bpftrace 快速抓到 read 系统调用延迟
直接上手就能定位 Java 服务响应变慢的问题,不用改代码、不重启进程。核心是利用 tracepoint:syscalls:sys_enter_read 和 tracepoint:syscalls:sys_exit_read 配对打点,靠 @start[tid] 记录起始时间,再用 nsecs - @start[tid] 算出耗时。
- 必须加
/comm == "java"/过滤,否则所有进程的 read 都会进来,直奔内存爆炸 -
delete(@start[tid])要写在 exit 分支里,漏掉会导致@start表持续膨胀,几秒后 bpftrace 自己 abort - 除以
1000是转成微秒,hist 默认按 2 的幂分桶,@usecs = hist(...)才能看清延迟分布是否毛刺严重 - 如果目标进程是容器内 Java,
comm可能显示为java,也可能被截断成jav(取决于内核版本),可临时放宽为/comm ~ "java"/
为什么 tracepoint 比 kprobe 更适合系统调用追踪
因为 sys_enter_read 这类 tracepoint 是内核预埋的稳定接口,位置固定、语义清晰;而用 kprobe 挂在 sys_read 函数上,不同内核版本函数名可能变(比如加了 __x64_sys_read 前缀),脚本一升级就失效。
- tracepoint 不依赖符号表,
/proc/kallsyms权限受限时照样工作;kprobe 依赖kallsyms解析地址,无权限就挂不上 - tracepoint 参数结构明确:
args->fd、args->count直接可用;kprobe 只能靠寄存器或栈偏移硬猜,稍有不慎就读错值 - tracepoint 开销更低——它本质是条件跳转+数据拷贝,kprobe 触发时要切内核栈+保存上下文,高频率下可观测性本身就成了性能瓶颈
遇到 “invalid probe” 或 “no such file or Directory” 错误怎么办
这类报错基本都卡在 probe 名称拼写或内核配置上,不是语法问题。
- 确认 tracepoint 是否存在:
ls /sys/kernel/debug/tracing/events/syscalls/,没有sys_enter_read说明内核没开CONFIG_TRACEPOINTS=y(常见于某些定制版 android 内核或旧 LTS 版本) - bpftrace 报
invalid probe但路径存在?检查内核是否禁用了 ftrace:cat /sys/kernel/debug/tracing/tracing_on,是0就echo 1 > /sys/kernel/debug/tracing/tracing_on - 使用
sudo bpftrace -l 'tracepoint:syscalls:*'列出所有可用 tracepoint,别凭记忆敲——sys_enter_read在 5.10+ 内核才统一命名,老版本可能是sys_enter_readv单独存在 - 某些云厂商内核会 patch 掉部分 tracepoint(如阿里云部分 ECS 镜像),此时只能退到
kprobe:SyS_read,但得查对应内核源码确认符号名
想捕获用户态 Java GC 事件,USDT 是唯一靠谱路子
系统调用层看不到 GC,jvm 内部行为必须靠 USDT(User Statically Defined Tracing)。OpenJDK 从 10 起默认编译进 USDT 探针,无需额外参数启动,但得确保 JVM 是带 --enable-dtrace 编译的版本(主流发行版基本都满足)。
- 先用
bpftrace -l 'usdt:/usr/lib/jvm/*/jre/lib/*/libjvm.so:*'查探针列表,常见的是gc:::begin、gc:::end,不是所有 JVM 都暴露完整生命周期 - USDT 探针需指定完整 so 路径,
/usr/lib/jvm/java-17-openjdk-amd64/lib/server/libjvm.so这种路径写错一个字符就加载失败 - Java 进程启动时若加了
-XX:+UseShenandoahGC等新 GC,USDT 探针可能未覆盖,bpftrace -l列不出来就等于不可观测 - 别指望用 uprobe 替代 USDT——uprobe 挂函数名太脆弱,JVM JIT 后函数地址乱跳,且 GC 是多线程协作,单点 uprobe 极易漏事件
实际跑起来才发现,@start[tid] 表键值用 tid 而不是 pid,是因为线程级系统调用才真正对应一次 read,fork 出的子进程 tid 变了但 pid 没变,这里差一个字母就全错。