https://www.dazhuanlan.com/smallnight/topics/1040103
在做性能调优的时候,我们通常会借助一些性能分析工具(比如 perf,DTrace)分析系统资源的使用情况,比如 CPU、内存等,但这些工具分析的结果通常是文本形式,不够直观,不便于快速定位系统瓶颈。Brendan Gregg开发了一种可视化的性能分析工具–火焰图,这种工具通过分析包含 stack traces 的 profile 数据,以可视化的形式展现出来,通过它可以快速准确地定位到热点代码。
本文内容涵盖了如何理解火焰图、如何生成火焰图、火焰图有哪些缺陷、如何解决这些缺陷、以及 Java 混合模式火焰图的具体生成步骤和具体实例。
针对不同的资源和事件类型,有不同类型的火焰图,主要包括:CPU、Memory、Off-CPU、Hot/Cold以及Differential
我们先看一个 CPU 火焰图是什么样的,整个火焰图看起来是不是像一团火焰,这正是以火焰图命名的原因。本文中所描述的火焰图默认是指 CPU 火焰图。
Brendan Gregg 在Blazing Performance with Flame Graphs一文中对如何解读火焰图有详细的介绍。
栈帧:用于记录函数的活动记录,保存函数局部变量,函数参数等信息
因此要想分析出函数调用堆栈关系,就需要采样到栈帧信息。
生成火焰图只需要两步操作:(1)采集 stack traces 数据,(2)使用 FlameGraph 脚本生成火焰图。
perf是最常用的采集 stack traces 数据的工具。下图显示了 perf 的工作流程,其中 perf record 展示了生成火焰图的主要步骤。
由于火焰图是根据任何包含 stack traces 的 profile 数据生成的,所以首先必须采集 stack traces 数据。以下列举的 profiling 工具都可以采集 profile 数据
Linux: perf_events(简称 perf) , eBPF, SystemTap, and ktap
Solaris, illumos, FreeBSD: DTrace
Mac OS X: DTrace and Instruments
Windows: Xperf.exe
perf是最常用的采集 stack 数据工具,通过 perf 采集数据时会在当前目录下生成 perf.data 文件。执行以下命令就可以完成数据采集。
1 |
sudo perf record -F 99 -a -g -- sleep 30 |
如果 Linux 系统未安装 perf,可参考以下命令安装 perf
1 |
sudo yum install perf |
更多 perf 的使用示例可参考perf-examples
1 |
sudo perf script | ./stackcollapse-perf.pl | ./flamegraph.pl > out.svg |
perf script 会默认在当前目录下查找通过 perf 生成的 perf.data 文件。FlameGraph提供了很多脚本,具体的使用方式可以参考官网示例。
perf script: 读取 perf.data 的文件内容,并以多行的形式生成 stacks
stackcollapse-perf.pl: 将多行形式的 stacks 折叠成一行记录
flamegraph.pl: 根据折叠之后的 stacks 信息生成 svg 格式的火焰图
该 svg 格式的火焰图内嵌了 JS 代码,通过浏览器打开,自带了交互功能:鼠标移动显示方法信息、可以点击放大或者缩小调用栈以及通过表达式查找对应的方法名。
通过 perf 等工具生成的火焰图只显示内核函数的调用栈,并不显示 Java 的调用栈(stack)和方法 (method),所以火焰图无法直接定位 Java 出是哪个方法比较耗 CPU,这正是火焰图的缺陷。
为了解决火焰图不显示 Java 的调用栈(stack)和方法 (method) 这个缺陷,Brendan Gregg 对火焰图工具进行了改进,创造出一种名为 Java 混合模式火焰图,这种火焰图即显示系统调用栈又显示 Java 调用栈和方法,将这两类信息都混合在一起,展现在一个图中,混合模式火焰图因此而命名。
Brendan Gregg 是如何解决这个问题的呢?如果你对这个部分的内容不感兴趣,可以直接跳过。
究竟是什么原因导致无法显示 method 以及完整的 stack 呢?
要想解决显示 method 名称这个问题,首先要能够收集到 symbols 信息,然后再从这些信息中识别出 method 名称
perf-map-agent 是一个开源的 Java 代理,它包括一个用 C 写的 Java 代理以及一个可以将这个代理 attach 到 Java 进程的简易 Java 代码
如果找不到 function symbol 信息,当前版本的 perf 脚本的处理方式是采用 library 的名字或者 perf-$PID.map 文件名作为 method 的名字
所以结合 perf 以及 perf-map-agent 就可以在火焰图上显示出 Java method。
多年来编译器一直在不断地持续优化,gcc 编译器使用了 frame pointer,这种技术打断了 stack 的调用栈,使得 stack 不完整。然而在使用 gcc 时,如果使用了-fno-omit-frame-pointer 选项,就会显示完整的 stack。但是 JVM 并没有对应的选项对否显示完整的 stack 进行控制。
如果 JVM 能够实现一个选项,那么就可以解决 stack 不完整的问题。
这个特征能不能在 JDK 中实现呢?技术大咔 Brendan Gregg 出于好奇,他在 OpenJDK8 上开发了一个可以工作的原型,并期望能够在 Oracle JDK 中实现这个特征,于是他在 hotspot-compiler-dev 开发邮件组中发了一封邮件,低调地表达了自己不是资深的 hotspot 工程师,希望有人可以继续优化或者重写他写的这个原型。
hotspot-compiler-dev 社区将这个特性分别记录在 JDK9 的JDK-8068945和 JDK8 的JDK-8072465中。Oracle 的工程师 Zoltán Majó为了重写了这个 patch 做了大量的工作,四个月后终于完成,将这个特性集成在 JDK9 和 JDK8(JDK8 update 60 build 19)的发布版本里,使用选项 -XX:+PreserveFramePointer 控制是否显示完整的 stack。
因此通过打开 -XX:+PreserveFramePointer 就解决了 stack 不完整的问题,前提是 JDK 的版本必须大于或者等于Java 8 update 60 build 19
安装 perf 之前,先 check OS(Linux 环境)是否已经安装过
1 |
perf --version |
如果 perf 已经安装过,可以跳过安装这一步;如果未安装,执行以下命令完成安装
1 |
sudo yum install perf |
12 |
sudo yum install gccsudo yum install gcc-c++ |
如果系统中已安装 gcc 和 gcc-c++,这一步可以跳过。
下载 cmake2.8.6,解压到/home/q/perf-tools/下
1234 |
sudo mkdir /home/q/perf-toolscd /home/q/perf-tools/cmake-2.8.6sudo ./configure --prefix=/home/q/cmake-2.8.6sudo make && make install |
最好安装 cmake2.8.6,因为我曾尝试过安装最新版本的 cmake,但是在使用 cmake 编译 perf-map-agent 时失败,后来使用 cmake2.8.6 可以成功安装 perf-map-agent。
123 |
git clone https://github.com/jvm-profiling-tools/perf-map-agentcd /home/q/perf-tools/perf-map-agentsudo /home/q/cmake-2.8.6/bin/cmake . && sudo make |
配置 Java 选项
JDK>=Java 8 update 60 build 19,选型 **-XX:+PreserveFramePointer** 才能生效
1 |
java -version |
JDK 版本是否大于或者等于Java 8 update 60 build 19
1 |
ps wwp `pgrep -n java`|grep PreserveFramePointer --color |
检查 JVM 参数是否已经配置过选型 **-XX:+PreserveFramePointer**
1 |
-Xms24g -Xmx24g -server -XX:+DisableExplicitGC -Dqunar.logs=$CATALINA_BASE/logs -Dqunar.cache=$CATALINA_BASE/cache -verbose:gc -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:+PrintGCApplicationStoppedTime -XX:+PrintGCApplicationConcurrentTime -XX:+PrintGC -XX:+UseConcMarkSweepGC -XX:CMSFullGCsBeforeCompaction=1 -Xloggc:$CATALINA_BASE/logs/gc.log -XX:+PrintSafepointStatistics -XX:ReservedCodeCacheSize=512m -XX:CMSInitiatingOccupancyFraction=50 -XX:+UseCMSInitiatingOccupancyOnly -XX:MaxTenuringThreshold=14 -XX:+PreserveFramePointer |
1 |
git clone https://github.com/brendangregg/FlameGraph.git |
1 |
sudo ./gen-flame-graph.sh $SLEEP_SECONDS $PID |
具体的脚本内容:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475 |
#!/bin/bashSLEEP_SECONDS=$1PID=$2PERF_TOOLS_HOME=$(pwd)GEN_RESULTS_DIR=$PERF_TOOLS_HOME/gen-flame-graphs-resultAGENT_HOME=$PERF_TOOLS_HOME/perf-map-agentAGENT_JAR=$AGENT_HOME/attach-main.jarAGENT_OUT=$AGENT_HOME/outFLAME_GRAPH_PL_HOME=$PERF_TOOLS_HOME/FlameGraphFLAME_GRAPH_GENERAGED_FILE=$PERF_TOOLS_HOME/flamegraph-mixed-model-`date +%Y%m%d-%H%M%S`.svgMAP_FILE=/tmp/perf-$PID.mapfunction check_env(){ if [[ ! -x $JAVA_HOME ]]; then echo "ERROR: JAVA_HOME not set correctly; edit $0 and fix" exit fi if [[ ! -x $AGENT_HOME ]]; then echo $AGENT_HOME echo "ERROR: AGENT_HOME not set correctly; edit $0 and fix." exit fi if [[ ! -x $GEN_RESULTS_DIR ]]; then echo "ERROR: '$GEN_RESULTS_DIR' not found;Please mkdir a file :'$GEN_RESULTS_DIR'" exit fi}#generate perf.data with command: perf recordfunction perf_record(){ echo "Perf record for all processors with sleep 30 seconds." cmd_perf="sudo perf record -F 99 -a -g -- sleep 30" eval $cmd_perf if [[ -e "./perf.data" ]]; then echo "SUCCESS: perf.data was generated." else echo "ERROR: perf.data not generated;edit $0 and fix." exit fi}#agent mapping pidfunction gen_map_file(){ user=$(ps ho user -p $PID) echo "Agent mapping PID $PID with user $user" cmd_agent="cd $AGENT_HOME;java -cp attach-main.jar:$JAVA_HOME/lib/tools.jar net.virtualvoid.perf.AttachOnce $PID" cmd_agent="sudo -u $user sh -c '$cmd_agent'" eval $cmd_agent if [[ -e "$MAP_FILE" ]]; then echo "generate map file" chown root $MAP_FILE chmod 666 $MAP_FILE else echo "ERROR: $MAP_FILE not created." exit fi}#generate flame graphfunction gen_flame_graph(){ cmd_stack="sudo perf script|$FLAME_GRAPH_PL_HOME/stackcollapse-perf.pl --pid|$FLAME_GRAPH_PL_HOME/flamegraph.pl --color=java --width=800 --minwidth=0.5 --title="$PID flame graph" --hash > $FLAME_GRAPH_GENERAGED_FILE" eval $cmd_stack}check_envperf_recordgen_map_filegen_flame_graphecho "SUCCESS: generate flame graph." |
在根据上面的步骤安装完对应的工具之后,我们可以通过上面提供的脚本生成火焰图。下面这个实例是对 Java 进程 8708 采样 30 秒,然后生成名为 flamegraph-mixed-model-date +%Y%m%d-%H%M%S
.svg 的 Java 混合模式火焰图。
1 |
sudo ./gen-flame-graph.sh 30 8708 |
执行脚本过程中,如果遇到以下错误
1 |
Expected libperfmap.so at '/home/q/flamegraph/perf-map-agent/libperfmap.so' but it didn't exist. |
就需要 copy 文件 libperfmap.so,然后再次执行生成火焰图的脚本
1 |
cp /home/q/flamegraph/perf-map-agent/out/libperfmap.so /home/q/flamegraph/perf-map-agent |
生成的 Java 混合模式火焰图如下:
-XX:+PreserveFramePointer 对性能的影响
Orcale 的性能测试团队测试的结果是性能下降 2%-5%(SPECjvm2008-Derby* -2% to -5%),出于对性能的考虑,他们建议默认配置不启用 PreserveFramePointer
Netflex 的性能测试结果是性能下降 0-3%
下图是我们常见的 Linux 性能优化图,大家肯定见过多次。没错,这个图出自于Brendan Gregg,他正是火焰图的作者。
他现在是 Netflix 的高级性能架构师,主要工作是做大规模计算的性能设计、分析以及调优。之前在 Sun Microsystems 做一名 kernel engineer
也是《Systems Performance》(中文版本《性能之巅:洞悉系统、企业与云计算》) 一书的作者,该书荣获 2013 年的 USENIX LISA 大奖
也开发了很多性能分析工具,比如 DTrace
如果你对性能调优感兴趣,可以关注他的blog和个人网站
参考资料
1.https://medium.com/netflix-techblog/java-in-flames-e763b3d32166
2.https://www.slideshare.net/brendangregg/javaone-2015-java-mixedmode-flame-graphs?qid=3245fe6e-33e7-41c8-b9d8-8bbefab56671&v=&b=&from_search=1