perf-map-agent 使用步骤
使用 asyncProfiler 能捕获 Java 的栈, 使用 perf 能捕获操作系统栈, 由于 JVM 使用自己单独的虚拟机, 所以不能同时访问 2 部分栈. Netflix 的教程(https://netflixtechblog.com/java-in-flames-e763b3d32166), 能捕获 2 个在一起的栈. 需要做 2 件事情:
- 对 Java 进程添加
-XX:+PreserveFramePointer
flag - 生成当前正在运行的 Java 进程的 符号表文件. asyncProfiler 和 bpftrace 都需要这个文件在
/tmp/
目录下, 并且以perf-<pid>.map
的形式存在.
如 ls -lah /tmp/perf-2868448.map
假如不使用 Java 运行时符号表
你将看到如下的栈信息, 毫无用处:
0x7f72beee3337
0x7f72be72e399
0x7f72be72ed3e
0x7f72be1a7d98
0x7f72be1aacb1
下面就是关于如何产生符号表文件的教程.
clone 或者下载最新版本 https://github.com/jvm-profiling-tools/perf-map-agent
curl -vvv 'https://github.com/jvm-profiling-tools/perf-map-agent/archive/refs/heads/master.zip' --output perf-map-agent.zip # or git clone https://github.com/jvm-profiling-tools/perf-map-agent.git
设置 JAVA_HOME 环境变量
export JAVA_HOME=/home/supra/work/tools/myjdk/jdk11/jdk
编译
# 解压并进入该文件夹 unzip perf-map-agent.git && cd perf-map-agent cmake . make
产生 perf-
.map ./bin/create-java-perf-map.sh $(pgrep java) # 到 /tmp 目录查看对应的 perf-<pid>.map 是不是存在了 ls /tmp/ | grep .map -rw-rw-r-- 1 root root 29K Nov 13 06:29 perf-2868448.map
使用 bpftrace 的例子
sudo bpftrace -e 'tracepoint:syscalls:sys_enter_pread /comm=="java"/{ @[ustack(20)] = count(); }'
# or
sudo bpftrace -e 'tracepoint:syscalls:sys_enter_pread /pid==2868448/{ @[ustack(20)] = count(); }'
可能遇到问题:
如果遇到下面:
-- The C compiler identification is GNU 7.5.0 -- The CXX compiler identification is unknown -- Check for working C compiler: /usr/bin/cc -- Check for working C compiler: /usr/bin/cc -- broken CMake Error at /usr/share/cmake-3.10/Modules/CMakeTestCCompiler.cmake:52 (message): The C compiler "/usr/bin/cc" is not able to compile a simple test program. It fails with the following output:
则安装编译工具
sudo apt-get cmake sudo apt-get install build-essential
如果遇到下面:
Sorry, user xxx is not allowed to execute '/yyyy/bin/java -cp /home/xxx/perf-map-agent-master/bin/../out/attach-main.jar:/yyyy/lib/tools.jar net.virtualvoid.perf.AttachOnce 23545 ' as xxx on hostzzzz.txh.com.
那么我们可以到 perf-map-agent-master/out/ 目录下去执行这个命令就好了, 不是执行 create-java-perf-map.sh.
- 关于要如何使用 root 的, 查看 Netflix 的那个文档
- 创建 perf-
.map 要使用 java 应用启动相同的用户名, 因为 java 应用的 agent 要使用同样的用户才能访问对应的 java 程序. 如果你发现即便使用了运行时 Java 进程的符号表, 可是还是很多翻译的栈, 如下:
__GI___recv+110 Java_java_net_SocketInputStream_socketRead0+434 Interpreter+28336 Interpreter+4352 Interpreter+4352 Interpreter+4352 Interpreter+4352 Interpreter+5875 Interpreter+4352 Interpreter+4352 Interpreter+3728 Interpreter+3728 Interpreter+5184 Interpreter+5184 Interpreter+4352 call_stub+138 JavaCalls::call_helper(JavaValue*, methodHandle const&, JavaCallArguments*, Thread*)+883
因为只有编译的代码才有符号表, 翻译的是没有的, 所以要让它尽快尽早JIT编译, 因此, 要通过下面2个参数:
-XX:-TieredCompilation -XX:CompileThreshold=1
2个参数分别是: 禁止分层编译, 然后只要运行1次就JIT编译. 所以完整的命令是:
java -XX:+PreserveFramePointer -XX:-TieredCompilation -XX:CompileThreshold=1 myJavaApp
.
同时要确保你的编译cache 足够大.
这样, 你就得到了如下的代码栈:__GI___recv+110 Java_java_net_SocketInputStream_socketRead0+434 Ljava/net/SocketInputStream;::socketRead0+244 Ljava/net/SocketInputStream;::read+224 Ljava/io/BufferedInputStream;::fill+784 Ljava/io/BufferedInputStream;::read1+176 Ljava/io/BufferedInputStream;::read+252 Lsun/net/www/http/HttpClient;::parseHTTPHeader+444 Lsun/net/www/http/HttpClient;::parseHTTP+1004 Lsun/net/www/protocol/http/HttpURLConnection;::getInputStream0+1420 Lsun/net/www/protocol/http/HttpURLConnection;::getInputStream+196 Ljava/net/HttpURLConnection;::getResponseCode+96