async-profiler 非 Java 应用性能剖析实战:LD_PRELOAD 注入与 C API 集成指南
【免费下载链接】async-profilerSampling CPU and HEAP profiler for Java featuring AsyncGetCallTrace + perf_events项目地址: https://gitcode.com/GitHub_Trending/as/async-profiler
导读
async-profiler 不仅能为 Java 应用提供 CPU/内存剖析,还支持通过LD_PRELOAD注入或 C API 编程式调用的方式,对原生(Native)应用进行 CPU、wall-clock、native 内存分配等剖析。本文基于 docs/ProfilingNonJavaApplications.md 展开,结合仓库源码(src/asprof.h、src/asprof.cpp、src/zInit.cpp、src/hooks.cpp),讲解非 Java 场景下的两种接入方式、可用的剖析模式与输出格式,以及不稳定扩展 API(线程级采样计数、自定义 JFR 事件)的使用方法。读完本文,你将能够为任意原生可执行程序接入 async-profiler,并在自己的 C/C++ 代码中直接驱动剖析器启停与数据采集。
适用范围与重要边界
首先明确 async-profiler 对非 Java 应用的剖析范围:
- 受支持的方式只有两种:在进程内部编程式控制剖析器(C API),以及通过
LD_PRELOAD预加载注入; - 与 Java 场景不同,动态 Attach(dynamic attach)不适用于非 Java 剖析。Java 下通过
-agentpath/jattach附加的方式(详见 IntegratingAsyncProfiler.md 中的 Launching as an Agent)依赖 JVM 的 attach 机制,非 Java 进程不存在该机制,因此必须采用上述两种注入途径。
这一边界也体现在源码中:Hooks::init(bool attach)的attach参数区分了两种初始化路径(src/hooks.cpp),其中 attach 路径(Profiler::instance()->updateSymbols(false)、patchLibraries())专为 JVM 场景设计。
通过 LD_PRELOAD 注入原生应用
基本用法
async-profiler 的共享库libasyncProfiler.so可以通过LD_PRELOAD注入到任意原生应用中:
LD_PRELOAD=/path/to/libasyncProfiler.so ASPROF_COMMAND=start,event=cpu,file=profile.jfr NativeApp [args]其原理是:注入后库的构造函数(见 src/zInit.cpp 中的LateInitializer)会在进程启动阶段执行。构造函数会先检查进程内是否已加载 JVM(checkJvmLoaded()),若未加载 JVM,则读取环境变量ASPROF_COMMAND;当满足ASPROF_COMMAND已设置、且检测到进程确实通过预加载方式注入了该库(OS::checkPreloaded())时,便调用Hooks::init(false)初始化剖析器,随后解析ASPROF_COMMAND中的命令并启动剖析(startProfiler(command))。这一机制意味着ASPROF_COMMAND是 LD_PRELOAD 场景下传递剖析参数的标准入口。
可用的剖析模式与输出格式
注入后,async-profiler 的全部基础功能保持可用:
- 剖析模式:
cpu、wall、nativemem以及其他基于 perf_events 的模式(如cycles、instructions等硬件事件)均可使用; - 输出格式:支持 Flame Graph(
.html)与 JFR(.jfr)格式。需要注意的是,非 Java 进程产出的 JFR 文件不会包含 Java 特有的事件(如分配、锁竞争、Java 线程信息等 Java 专属事件)。
更完整的命令示例见 ProfilingModes.md。其中值得特别关注的是nativemem模式与 LD_PRELOAD 的组合,可用于原生内存泄漏定位:
LD_PRELOAD=/path/to/libasyncProfiler.so ASPROF_COMMAND=start,nativemem,total,loop=10m,cstack=dwarf,file=profile-%t.jfr NativeApp [args]该命令以nativemem模式记录malloc、realloc、calloc、free调用,loop=10m表示每 10 分钟滚动生成一个带时间戳(%t)的 JFR 文件,之后可用jfrconv --total --nativemem --leak <profile>.jfr <profile>-leak.html生成火焰图以分析泄漏(详见 ProfilingModes.md 的 nativemem 章节)。
LD_PRELOAD 下的底层 Hook 机制
从源码看,LD_PRELOAD注入时 async-profiler 会以弱符号(WEAK)覆盖三个关键 libc/POSIX 接口(src/hooks.cpp):
pthread_create/pthread_exit:拦截线程创建与退出,从而为新线程注册剖析所需的线程本地数据;dlopen:拦截动态库加载,以便在运行时加载的库上及时安装剖析 Hook(如 malloc/native lock 追踪)。
在Hooks::init中还会调用Profiler::setupSignalHandlers()安装剖析信号处理器(如 perf 事件、itimer 等采样引擎的信号)。这意味着注入的剖析器与常规 Java 场景共用同一套信号采样与栈回溯框架。
通过 C API 在原生应用内部控制剖析器
除了LD_PRELOAD,async-profiler 还提供了与 Java API 对等的C API(Java 侧用法参见 IntegratingAsyncProfiler.md 的 Using Java API),允许原生程序在自身代码中按需启动、停止剖析。
头文件与动态加载
C API 的头文件随发布包一起提供,位于仓库的 src/asprof.h。该头文件以extern "C"包裹声明,同时兼容 C 与 C++;所有导出函数都以DLLEXPORT标记默认可见,便于dlsym动态解析。
完整示例
下面是一个使用 C API 的完整示例(原文档示例,与 src/asprof.h 中的函数签名一一对应):
#include "asprof.h" #include <dlfcn.h> #include <stdio.h> #include <stdlib.h> void test_output_callback(const char* buffer, size_t size) { fwrite(buffer, sizeof(char), size, stderr); } int main() { void* lib = dlopen("/path/to/libasyncProfiler.so", RTLD_NOW); if (lib == NULL) { printf("%s\n", dlerror()); exit(1); } asprof_init_t asprof_init = (asprof_init_t)dlsym(lib, "asprof_init"); asprof_execute_t asprof_execute = (asprof_execute_t)dlsym(lib, "asprof_execute"); asprof_error_str_t asprof_error_str = (asprof_error_str_t)dlsym(lib, "asprof_error_str"); if (asprof_init == NULL || asprof_execute == NULL || asprof_error_str == NULL) { printf("%s\n", dlerror()); dlclose(lib); exit(1); } asprof_init(); printf("Starting profiler\n"); char cmd[] = "start,event=cpu,loglevel=debug,file=profile.jfr"; asprof_error_t err = asprof_execute(cmd, test_output_callback); if (err != NULL) { fprintf(stderr, "%s\n", asprof_error_str(err)); exit(1); } // ... some meaningful work ... printf("Stopping profiler\n"); err = asprof_execute("stop", test_output_callback); if (err != NULL) { fprintf(stderr, "%s\n", asprof_error_str(err)); exit(1); } return 0; }C API 核心函数与底层行为
对照头文件 src/asprof.h 与实现 src/asprof.cpp,各函数语义如下:
| 函数 | 签名 | 语义与实现要点 |
|---|---|---|
asprof_init | void asprof_init() | 在任何其他 API 调用前仅需调用一次。实现为Hooks::init(true),即执行剖析器初始化(安装信号处理器、patch 库等,见 src/asprof.cpp)。 |
asprof_execute | asprof_error_t asprof_execute(const char* command, asprof_writer_t output_callback) | 执行一条 async-profiler 命令(如start,event=cpu,...、stop),output_callback为可选的输出接收回调。返回NULL表示成功,非NULL为错误码(本质是const char*错误消息)。 |
asprof_error_str | const char* asprof_error_str(asprof_error_t err) | 将错误码转换为可读的错误消息字符串(实现即直接返回该字符串)。 |
asprof_writer_t | void (*)(const char* buf, size_t size) | 输出回调类型,剖析器的文本输出会按缓冲区片段回调给该函数。 |
在asprof_execute的实现中(src/asprof.cpp),命令会先经Arguments::parse(command)解析,再根据输出目标选择不同的写入路径:
- 未指定输出文件时,使用
CallbackWriter将输出逐段回传给output_callback; - 指定
file=时,写入本地文件(打开失败返回"Could not open output file"); - 输出目标为 URL(http/https)时,先写入内存缓冲区,再由
HttpClient::send推送到远程端点。
这解释了示例中loglevel=debug与file=profile.jfr的配合:剖析器日志经output_callback输出,JFR 数据写入文件。
不稳定扩展 API(Unstable APIs)
以下 API 在 src/asprof.h 中被明确标注为UNSTABLE,可能在下一版本中变更或移除,使用时需做好兼容性评估。
高级采样检测:asprof_get_thread_local_data
asprof_get_thread_local_data返回指向 async-profiler线程本地数据结构asprof_thread_local_data的指针,该结构保证与线程同生命周期:
typedef struct { // 该线程最近一次剖析采样发生时刻的时间戳(使用 async-profiler 内部时钟, // 与 JFR 事件时间戳同源)。单调递增,每次新采样记录时更新。 // 首次采样前为 0;该字段可能被延迟初始化,仅在首次调用 // asprof_get_thread_local_data 时创建。 volatile uint64_t sample_counter; } asprof_thread_local_data;结构体中的sample_counter字段会在每次采样事件发生时递增,为原生代码提供了一种检测采样事件是否发生的简便手段——当程序被采样到时,可在该线程的采样回调逻辑中记录"此刻程序正在做什么"的元数据,从而把采样点与业务状态关联起来。
从实现看(src/threadLocalData.h、src/threadLocalData.cpp),线程本地数据基于 POSIX TLS(pthread_getspecific/pthread_setspecific)实现:
- 数据按线程惰性分配(首次调用时
calloc分配并绑定到 pthread key),分配失败时返回NULL,不会中断宿主程序; - 在剖析器尚未初始化(pthread key 未创建)时同样返回
NULL; - 该函数不是 async-signal-safe的,但可与其他剖析器操作(包括初始化)并发调用。
自定义 JFR 事件:asprof_register_jfr_event与asprof_emit_jfr_event
同一头文件中还提供了两个相关的扩展函数,用于向 JFR 输出写入自定义事件:
asprof_register_jfr_event(const char* name):注册一个用户自定义事件名,返回asprof_jfr_event_key标识(失败返回-1)。实现上通过线程安全的Dictionary::lookup将名称映射为整数键(src/userEvents.cpp);asprof_emit_jfr_event(key, data, len):以指定键发射一个自定义 JFR 事件。数据为任意二进制,长度上限ASPROF_MAX_JFR_EVENT_LENGTH(2048 字节),超限会返回错误(见 src/asprof.cpp 中的长度校验与 src/asprof.h 的宏定义)。
事件发射后,会以profiler.UserEvent类型写入 JFR,包含startTime(发射时刻的时钟 tick,取自TSC::ticks())、eventThread、type(常量池索引的事件类型名)与data等字段。这使非 Java 应用也能在 JFR 时间线上标注业务关键点,与采样数据对齐分析。
如何选择:LD_PRELOAD 与 C API
两种方式服务于不同的集成场景:
| 维度 | LD_PRELOAD | C API |
|---|---|---|
| 集成成本 | 无需改代码,仅设置环境变量 | 需要在源码中引入asprof.h并编写调用逻辑 |
| 控制时机 | 进程启动即按ASPROF_COMMAND自动开始 | 可在程序任意时刻按需启停 |
| 适用对象 | 已编译、不便改动的原生可执行程序 | 拥有源码、希望将剖析能力内置进产品的原生应用 |
| 输出接收 | 固定输出到文件 | 可指定文件、URL 或自定义回调 |
在实际工程中,两者也常结合使用:LD_PRELOAD适合快速复现与诊断线上问题(尤其配合nativemem定位原生内存泄漏),而 C API 适合将剖析能力作为可编程组件嵌入自身应用(例如按请求/阶段动态开关剖析、用asprof_emit_jfr_event打点)。
小结
- 非 Java 剖析仅支持
LD_PRELOAD注入与进程内 C API 两种途径,不支持 dynamic attach; LD_PRELOAD场景通过ASPROF_COMMAND环境变量传递启动命令,底层由 src/zInit.cpp 的构造逻辑在进程启动时完成初始化,并通过 src/hooks.cpp 的pthread_create/dlopenHook 支撑多线程与动态加载场景;- C API 的头文件位于 src/asprof.h,核心流程为
dlopen加载 →dlsym解析 →asprof_init→asprof_execute,错误通过asprof_error_str获取可读信息; cpu、wall、nativemem等模式与 Flame Graph、JFR 输出在非 Java 场景下同样可用,仅 JFR 中缺少 Java 专属事件;- 需要精确感知采样时机时,可借助不稳定的
asprof_get_thread_local_data读取线程级sample_counter,或用asprof_emit_jfr_event写入自定义 JFR 事件。
进一步阅读:ProfilingModes.md(各模式与完整命令示例)、IntegratingAsyncProfiler.md(Java API 与 agent 接入方式)、OutputFormats.md(输出格式细节)。
【免费下载链接】async-profilerSampling CPU and HEAP profiler for Java featuring AsyncGetCallTrace + perf_events项目地址: https://gitcode.com/GitHub_Trending/as/async-profiler
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考