1、介绍
Async-profiler是一个对系统性能影响很少的Java采样分析器,它的实现是基于HotSpot特有的API,通过这些特有的API收集堆栈跟踪和跟踪内存分配,因而其可以和OpenJDK、Oracle JDK和其他基于HotSpot JVM的Java应用在运行时协同工作。
Github项目链接地址:https://github.com/jvm-profiling-tools/async-profiler
Async-profiler可以跟踪以下类型的事件:
- CPU周期;
- 硬件和软件性能计数器,如缓存未命中、分支未命中、页面错误、上下文切换等;
- Java堆中的分配;
- 满足的锁定尝试,包括Java对象监视器和可重入锁;
支持的平台
- Linux / x64 / x86 / ARM / AArch64
- macOS / x64
注意:macOS分析仅限于用户空间代码。
2、CPU性能分析
在此模式下,profiler收集堆栈跟踪示例,其中包括Java方法、native调用、JVM代码和内核函数。
为了能够准确的生成Java和native代码的确切性能报告,常用的方法是接收perf_events生成的调用堆栈,并将它们与AsyncGetCallTrace生成的调用堆栈进行匹配。此外Async-profiler还提供了一种可以在AsyncGetCallTrace失败的某些情况下,恢复堆栈跟踪的解决方法。
与将地址转换为Java方法名称的Java代理相比较,使用perf_eventst的方式具有以下优点:
- 它适用于较旧的Java版本,因为它不需要-XX:+PreserveFramePointer,这个参数只在JDK 8u60和更高版本中可用;
- 不需要引入-XX:+PreserveFramePointer,因为它可能导致较高的性能开销,在极少数情况下可能高达10%;
- 不需要生成映射文件来将Java代码地址映射到方法名;
- 使用解释器帧;
- 不需要为了后续的进一步分析而需要生成perf.data文件;
3、堆内存分配分析
async-profiler使用的分析技术对系统的性能影响不大,它不像字节码检测或DTrace探测等会对系统性能可能产生较大的影响。它也不影响转义分析或防止JIT优化,如分配消除,async-profiler只测量实际的堆分配。
分析器具有TLAB(Thread Local Allocation Buffer,即线程本地分配缓存区)驱动的采样功能,它依赖于HotSpot特定的回调来接收以下两种TLAB通知:
- 新创建的TLAB中分配对象时;
- 在TLAB外部的慢速路径上分配对象时。
这意味分析器不会对每个TLAB分配进行计算,而只会计算每N kB的分配,其中N是TLAB的平均大小。这使得堆采样非常轻量,也适合于生产环境使用。虽然这种收集方式也可能会导致收集的数据可能是不完整的,不过根据实践经验,这种收集方式通常能够反映顶级分配来源。
采样间隔可以使用-i选项进行调整,例如,-i 500k将在平均分配了500kb空间后获取一个样本。但是,小于TLAB大小的间隔将不会生效。
与使用类似方法的Java任务控制不同,async-profiler不需要Java Flight Recorder或任何JDK的其他商业特性,它完全基于开源技术,并与OpenJDK一起工作。
注:如需要收集TLAB的相关信息,JDK的最低版本要求是7u40,大于等于这些版本的JDK才有TLAB回调功能。
堆分析器需要HotSpot调试符号,Oracle JDK已经将它们嵌入到libjvm.so中,但是OpenJDK在构建时,被打包到了单独的包中,如要在Debian/Ubuntu上安装OpenJDK调试符号,请运行:
apt install openjdk-8-dbg
或者对于OpenJDK在CentOS、RHEL和其他一些基于RPM的发行版上,这可以使用debuginfo-install来安装:
debuginfo-install java-1.8.0-openjdk
4、Wall-clock分析
选项-e wall告诉async-profiler在给定的时间段内对所有线程进行平均采样,可以对运行、休眠或阻塞的线程进行采样,如需要分析应用程序启动时间时,可以使用该选项。
在per-thread模式下,Wall-clock分析更能够发挥其作用,通过加入-t参数开启该模式,示例:
./profiler.sh -e wall -t -i 5ms -f result.svg 8983
5、编译
编译async-profiler,需要以下条件:
- JAVA HOME环境变量,且指向JDK安装路径;
- GCC(可以通过如apt install gcc等进行安装)。
然后通过make命令进行打包,编译后的代理二进制文件将位于生成子目录中,与此同时,可以将代理加载到目标进程中的小型应用程序jattach也将编译到build子目录中。
6、基本用法
从Linux4.6开始,如果需要使用非root用户启动的进程中的perf_events,捕获内核调用堆栈的信息,需要设置两个系统运行时变量,可以使用sysctl或按如下方式设置它们:
echo 1 > /proc/sys/kernel/perf_event_paranoid
echo 0 > /proc/sys/kernel/kptr_restrict
async-profiler通过profiler.sh脚本进行启动,并向需要分析的应用程序传递命令,典型的工作流:
- 启动Java应用程序;
- 附加代理并开始分析;
- 运行性能场景;
- 停止分析。
代理的输出(包括分析结果)将显示在Java应用程序的标准输出中。
示例:
$ jps
9234 Jps
8983 Computey
$ ./profiler.sh start 8983
$ ./profiler.sh stop 8983
也可以通过-d(duration)参数指定分析的时间,以秒为单位:
$ ./profiler.sh -d 30 8983
默认情况下,分析频率为100Hz(每10ms CPU时间),下面是输出到Java应用程序终端的输出示例:
--- Execution profile ---
Total samples: 687
Unknown (native): 1 (0.15%)
--- 6790000000 (98.84%) ns, 679 samples
[ 0] Primes.isPrime
[ 1] Primes.primesThread
[ 2] Primes.access$000
[ 3] Primes$1.run
[ 4] java.lang.Thread.run
... a lot of output omitted for brevity ...
ns percent samples top
---------- ------- ------- ---
6790000000 98.84% 679 Primes.isPrime
40000000 0.58% 4 __do_softirq
... more output omitted ...
这表明受影响最大的方法是Primes.isPrime,其是被Primes.primesThread线程调用的。
7、以Agent的方式启动
如果需要在JVM启动后立即分析一些代码,而不是待应用程序启动完成后再使用profiler.sh脚本进行分析,则可以在命令行上附加async-profiler作为代理。例如:
$ java -agentpath:/path/to/libasyncProfiler.so=start,file=profile.svg ...
Agent库是通过JVMTI参数接口配置的,参数字符串的格式在源代码中描述,profiler.sh脚本实际上将命令行参数转换为该格式。
例如:
-e alloc会被转换为event=alloc;
-f profile.svg会被转换为file=profile.svg等等。
但是有些参数是由profiler.sh脚本直接处理的。例如参数-d 5将导致3个操作:
- 使用start命令附加探查器agent;
- 休眠5秒;
- 然后使用stop命令再次附加代理。
8、查看火焰图
async-profiler提供开箱即用的Flame图形支持,指定参数-o svg以将分析结果转储为可在所有主流浏览器中查看的交互式svg图像。另外,如果目标文件名以.SVG结尾,则会自动选择SVG输出格式。
如下命令:
$ jps
9234 Jps
8983 Computey
$ ./profiler.sh -d 30 -f /tmp/flamegraph.svg 8983
可能会生成如下火焰图:
9、分析选项参数
下面是profiler.sh脚本接受的命令行选项的完整列表:
start - 以半自动模式开始分析,即在显式调用stop命令之前,分析程序将会一直运行;
resume - 启动或恢复先前已停止的分析会话,前面所有收集的数据仍然有效,分析选项不会在会话之间保留,应再次指定;
stop - 停止分析并打印报告;
status - 打印分析状态:分析程序是否处于活动状态以及持续多长时间;
list - 显示可用分析事件的列表,此选项仍然需要PID,因为支持的事件可能因JVM版本而异;
-d N - 分析持续时间,以秒为单位。如果未提供start、resume、stop或status选项,则探查器将在指定的时间段内运行,然后自动停止,示例:./profiler.sh - d 30 8983
-e event - 指定要分析的事件,如:cpu、alloc、lock、cache misses等。使用list参数查看可用事件的完整列表。
在分配(alloc)分析模式中,每个调用跟踪的顶部框架是已分配对象的类,计数器是堆中的记录(已分配TLAB或TLAB之外的对象的总大小)。
在锁(lock)分析模式下,顶部框架是锁/监视器的类,计数器是进入此锁/监视器所需的纳秒数。
Linux上支持两种特殊事件类型:硬件断点和内核跟踪点:
-e mem:<func>[:rwx] 在函数<func>处设置读/写/执行断点。mem事件的格式与perf-record相同。执行断点也可以由函数名指定,例如-e malloc将跟踪本地malloc函数的所有调用;
-e trace:<id> 设置内核跟踪点。可以指定跟踪点符号名,例如-e syscalls:sys_enter_open将跟踪所有打开的系统调用;
-i N - 后面跟ms(毫秒)、us(微秒)或s(秒),则以纳秒或其他单位设置分析间隔。仅计算CPU活动的时间,CPU空闲时不收集样本,默认值为10000000(10ms)。
示例:./profiler.sh - i 500us 8983
-j N - 设置Java堆栈分析深度。如果N大于默认值2048,则将忽略此选项。
示例:./profiler.sh - j 30 8983
-b N - 设置帧缓冲区大小,以缓冲区中应该容纳的Java方法id的数量为单位。如果接收到有关帧缓冲区大小不足的消息,请将此值从默认值增加,示例:./profiler.sh - b 5000000 8983
-t - 对每个线程进行单独分析,每个堆栈跟踪都将以表示单个线程的帧结束,示例:./profiler.sh - t 8983
-s - 打印简单类名而不是FQN(Full qulified name全类名);
-g - 打印方法签名;
-a - 通过添加_[j]后缀来注释Java方法名;
-o fmt - 指定分析结束时要转储的信息。fmt可以是以下选项之一:
summary - 转储基本配置统计信息;
traces[=N] - 转储调用跟踪(最多N个样本);
flat[=N] - dump flat profile(调用最多的前面N个方法);
jfr - 以Java Mission Control可读的Java Flight Recorder格式转储事件,这不需要启用JDK商业功能;
collapsed[=C] - 以FlameGraph脚本使用的格式转储调用跟踪的结果,这是调用堆栈的集合,其中每一行是一个分号分隔的帧列表,后跟一个计数器。
svg[=C] - 生成svg格式的火焰图。
tree[=C] - 以HTML格式生成调用树。
--reverse 该选项将生成回溯视图。
C是计数器类型:
samples - 计数器是给定跟踪的若干样本;
total - 计数器是收集的度量的总值,例如总分配大小。
小结,跟踪和展开可以结合在一起。
默认格式是summary,traces=200,flat=200。
--title TITLE,--width PX,--height PX,-- minwidth PX,--reverse -FlameGraph参数;
示例:./profiler.sh - f profile.svg--title "示例CPU配置文件" --minwidth 0.58983
-f FILENAME - 要将配置文件信息转储到的文件名。
%p - 被扩展到目标JVM的PID;
%t - 到命令调用时的时间戳。
示例: ./profiler.sh -o collapsed -f /tmp/traces-%t.txt 8983
--all-user - 仅包括用户模式事件。当内核分析受perf_event_paranoid设置限制时,此选项非常有用。
--all-kernel 表示只包含内核模式事件。
--sync-walk - 首选同步JVMTI堆栈walker,而不是AsyncGetCallTrace。此选项可以提高分析JVM运行时函数(如VMThread::execute、G1CollectedHeap::humongus_obj_allocate等)时Java堆栈跟踪的准确性,除非您绝对确定,否则不要使用!如果使用不当,此模式将导致JVM崩溃!
-v,--version - 打印探查器库的版本,如果指定了PID,则获取加载到给定进程中的库的版本。
10、分析容器中的Java应用程序
可以从容器内部和主机系统分析在Docker或LXC容器中运行的Java进程。
从主机进行分析时,pid应该是主机命名空间中的Java进程ID。使用ps aux | grep java或docker top<container>查找进程ID。
async-profiler应该由特权用户从主机运行 - 它将自动切换到正确的pid/装载命名空间,并更改用户凭据以匹配目标进程。还要确保目标容器可以通过与主机上相同的绝对路径访问libasyncProfiler.so。
默认情况下,Docker container限制对perf_event_open syscall的访问。因此,为了允许在容器中进行分析,您需要修改seccomp配置文件,或者使用--security-opt seccomp=unconfined选项完全禁用它。此外,可能需要,--cap-add SYS_ADMIN。
或者,如果无法更改Docker配置,则可以返回到-e itimer分析模式,请参阅疑难解答。
11、限制
- 在大多数Linux系统中,perf-events捕获最大堆栈深度为127帧的调用堆栈,在最新的Linux内核上,这可以使用sysctl kernel.perf_event_max_stack或通过写入/proc/sys/kernel/perf_event_max_stack文件来配置;
- Profiler为目标进程的每个线程分配8kB性能事件缓冲区,在非特权用户下运行时,请确保/proc/sys/kernel/perf_event_mlock_kb值足够大(大于8*总的线程数),否则将打印消息“perf_event mmap failed:Operation not allowed”,并且不会收集本机堆栈跟踪;
- 无法保证perf_events overflow信号以保证没有其他代码运行的方式传递到Java线程,这意味着在某些罕见的情况下,捕获的Java堆栈可能与捕获的本机(用户+内核)堆栈不匹配;
- 在堆栈的Java帧之前,您不会看到非Java帧,例如,如果start_thread调用JavaMain,然后Java代码开始运行,则在生成的堆栈中不会看到前两个帧。另一方面,您将看到Java代码调用的非Java框架(用户和内核);
- 如果-XX:MaxJavaStackTraceDepth参数设置为0或为负数,则不会收集Java堆栈;
- 分析间隔太短,可能会被clone()之类占用系统资源较多的方法给中断,因此它起不到收集数据并分析的目的,请参阅#97 issue,解决办法只是增加间隔;
- 如果在JVM启动时未加载代理(通过使用-agentpath选项),强烈建议使用-XX:+UnlockDiagnosticVMOptions -XX:+DebugNonSafepoints JVM标志,如果没有这些标志,分析器仍然可以正常工作,但结果可能不太准确,例如,如果没有-XX:+DebugNonSafepoints,则很有可能简单的内联方法不会出现在概要文件中。当代理在运行时附加CompiledMethodLoad时,JVMTI事件启用调试信息,但仅适用于在事件打开后编译的方法;
12、常见问题
1)Failed to change credentials to match the target process: Operation not permitted
由于HotSpot动态附加机制的限制,Profiler必须由与目标JVM进程所有者完全相同的用户(和组)运行,如果探查器由其他用户运行,它将尝试自动更改当前用户和组,对于根用户这可能会成功,但对于其他用户则不会成功,从而导致上述错误。
2)Could not start attach mechanism: No such file or directory
Profiler无法通过UNIX域套接字与目标JVM建立通信,通常发生在下列情况之一:
- socket套接字连接/tmp/.java_pidNNN被删除Attach,可能由于其中/tmp/目录下,被其它的系统清除程序给删除了,检查可通过如下命令:
lsof -p PID | grep java_pid
如果它列出了一个套接字文件,但是文件不存在,那么这就是所描述的问题;
- JVM以-XX:+DisableAttachMechanism选项启动;
- Java进程的/tmp目录在物理上与shell的/tmp目录不同,因为Java是在容器或chroot环境中运行的。jattach试图自动解决这个问题,但它可能缺少这样做所需的权限可通过如下命令进行检查:
strace build/jattach PID properties
- JVM正忙,无法到达安全点,例如,JVM正在进行长时间的垃圾收集,检查当前JVM是否繁忙的命令:
kill-3 PID
运行良好的JVM进程应该在其控制台中打印线程转储和堆信息;
3)Failed to inject profiler into <pid>
已建立与目标JVM的连接,但JVM无法加载探查器共享库,确保JVM进程的用户具有访问libasyncProfiler.so的权限,访问的绝对路径完全相同。有关详细信息,请参见#78 Issue。
4)Perf events unavailble. See stderr of the target process.
perf_event_open()系统调用失败,错误消息被打印到目标JVM的错误流中。
典型原因包括:
- /proc/sys/kernel/perf_event_paranoid设置为受限模式(>=2);
- seccomp禁用容器中的perf_event_open API;
- 操作系统在不虚拟化性能计数器的管理程序下运行;
- 当前系统不支持perf_event_open API,例如WSL。
如果无法更改配置,则可以返回到使用-e itimer分析模式。它类似于cpu模式,但不需要性能事件支持,但是其存在一个不足,不能够收集内核堆栈跟踪的信息;
5)No AllocTracer symbols found. Are JDK debug symbols installed?
可能需要安装带有OpenJDK调试符号的包,有关详细信息,请参阅分配分析。
注意,除了HotSpot(例如Zing)JVM支持之外,其它的JVM都不支持分配分析。
6)VMStructs unavailable. Unsupported JVM?
JVM共享库未导出gHotSpotVMStructs*符号-显然这不是一个HotSpot JVM。有时,错误构建的JDK也可能导致相同的消息(请参见218 Issue),在这些情况下,安装JDK调试符号可以解决问题;
7)Could not parse symbols due to the OS bug
Async-profiler无法分析非Java函数名,因为/proc/[pid]/maps中的内容已损坏,众所周知,使用Linux内核5.x运行Ubuntu时,容器中会出现此问题。这是操作系统错误,请参阅https://bugs.launchpad.net/Ubuntu/+source/Linux/+bug/1843018。
8、[frame_buffer_overflow]
输出中的此消息表示没有足够的空间存储所有调用跟踪,考虑使用-b选项增加帧缓冲区大小。