Java混合模式火焰图

在做性能调优的时候,我们通常会借助一些性能分析工具(比如perf,DTrace)分析系统资源的使用情况,比如CPU、内存等,但这些工具分析的结果通常是文本形式,不够直观,不便于快速定位系统瓶颈。Brendan Gregg开发了一种可视化的性能分析工具–火焰图,这种工具通过分析包含stack traces的profile数据,以可视化的形式展现出来,通过它可以快速准确地定位到热点代码。

本文内容涵盖了如何理解火焰图、如何生成火焰图、火焰图有哪些缺陷、如何解决这些缺陷、以及Java混合模式火焰图的具体生成步骤和具体实例。

火焰图(Flame Graph)

针对不同的资源和事件类型,有不同类型的火焰图,主要包括:CPUMemoryOff-CPUHot/Cold以及Differential
我们先看一个CPU火焰图是什么样的,整个火焰图看起来是不是像一团火焰,这正是以火焰图命名的原因。本文中所描述的火焰图默认是指CPU火焰图

理解火焰图

Brendan Gregg在Blazing Performance with Flame Graphs一文中对如何解读火焰图有详细的介绍。

  • 小方块
    每个小方块代表一个函数,对应一个栈帧(stack frame),小方块的宽度表示该函数在采样期间出现的频率,宽度越宽表示出现的频率越高

栈帧:用于记录函数的活动记录,保存函数局部变量,函数参数等信息
因此要想分析出函数调用堆栈关系,就需要采样到栈帧信息。

  • Y轴
    表示函数的调用栈,体现出了stack深度,每个垂直方向的顶端小方块表示正在使用CPU(on-CPU)的函数
  • X轴
    表示采样数据的总量,X轴显示的数据是在对调用栈进行合并之后的结果,然后根据函数的字母顺序进行排列,X轴并不代表使用CPU的时间长短,而只是说明该方法出现的次数,宽度越宽表示该方法出现的次数越多。国内不少文章将X轴直接看作为使用CPU的时间长短,这样理解并不准确。
  • 颜色
    小方块的颜色并没有特定的含义,是随机选取的暖色色调而已。

如何生成火焰图

生成火焰图只需要两步操作:(1)采集stack traces数据,(2)使用FlameGraph脚本生成火焰图。
perf是最常用的采集stack traces数据的工具。下图显示了perf的工作流程,其中perf record展示了生成火焰图的主要步骤。

图中的脚本stackcollapse-perf.pl和flamegraph.pl由火焰图工具FlameGraph提供

  • 采集stack traces数据

由于火焰图是根据任何包含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

  • 生成火焰图
    stack traces数据采集完之后,通过FlameGraph提供的python脚本就可以生成火焰图。
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调用栈和方法,将这两类信息都混合在一起,展现在一个图中,混合模式火焰图因此而命名。

Java混合模式火焰图

Brendan Gregg是如何解决这个问题的呢?如果你对这个部分的内容不感兴趣,可以直接跳过。
究竟是什么原因导致无法显示method以及完整的stack呢?

  • 为什么不显示method
    原因在于JIT在编译method时,并没有暴露出一个symbol table,导致profiler工具无法获取具体的method。
  • 为什么statck不完整
    原因在于在x86 (RBP on x86–64)上,JVM使用frame指针寄存器作为通用的寄存器,这种寄存器打破了传统的stack的运行方式,导致stack不完整。

如何解决显示method名称的问题

要想解决显示method名称这个问题,首先要能够收集到symbols信息,然后再从这些信息中识别出method名称

  • 如何收集symbols信息
    2014年Johannes Rudolph写了一个名为perf-map-agent的开源Java代理,通过这个代理可以对Java应用生成/tmp/perf-PID.map文件。这个文件里面记录了所有symbol的地址(十六进制)、大小和名称。所以通过perf-map-agent就可以解决收集symbols信息的问题。

perf-map-agent是一个开源的Java代理,它包括一个用C写的Java代理以及一个可以将这个代理attach到Java进程的简易Java代码

  • 如何从symbols中识别method
    2009年,perf已经支持了JIT symbol,通过这个特征perf会默认地查找/tmp/perf-$PID.map文件,从该文件中解析出function symbol的信息,从而识别出method名称。

如果找不到function symbol信息,当前版本的perf脚本的处理方式是采用library的名字或者perf-$PID.map文件名作为method的名字

所以结合perf以及perf-map-agent就可以在火焰图上显示出Java method。

  • 显示不显示Java method的效果对比

显示JAVA method

不显示JAVA method

如何解决stack不完整的问题

多年来编译器一直在不断地持续优化,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

  • stack是否完整的火焰图对比


如何生成Java混合模式火焰图

  • 安装perf

安装perf之前,先check OS(Linux环境)是否已经安装过

1
perf --version

如果perf已经安装过,可以跳过安装这一步;如果未安装,执行以下命令完成安装

1
sudo yum install perf
  • 安装perf-map-agent

    • 安装gcc和gcc-c++
    1
    2
    sudo yum install gcc
    sudo yum install gcc-c++

    如果系统中已安装gcc和gcc-c++,这一步可以跳过。

    • 安装cmake

    下载cmake2.8.6,解压到/home/q/perf-tools/下

    1
    2
    3
    4
    sudo mkdir /home/q/perf-tools
    cd /home/q/perf-tools/cmake-2.8.6
    sudo ./configure --prefix=/home/q/cmake-2.8.6
    sudo make && make install

    最好安装cmake2.8.6,因为我曾尝试过安装最新版本的cmake,但是在使用cmake编译perf-map-agent时失败,后来使用cmake2.8.6可以成功安装perf-map-agent。

    • 安装perf-map-agent
    1
    2
    3
    git clone https://github.com/jvm-profiling-tools/perf-map-agent
    cd /home/q/perf-tools/perf-map-agent
    sudo /home/q/cmake-2.8.6/bin/cmake . && sudo make
  • 配置Java选项
    JDK>=Java 8 update 60 build 19,选型**-XX:+PreserveFramePointer**才能生效

    • 检查JDK版本
    1
    java -version

    JDK版本是否大于或者等于Java 8 update 60 build 19

    • 检查JVM参数
    1
    ps wwp `pgrep -n java`|grep PreserveFramePointer --color

    检查JVM参数是否已经配置过选型**-XX:+PreserveFramePointer**

    • 设置**-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
  • 安装FlameGraph

1
git clone https://github.com/brendangregg/FlameGraph.git
  • 生成脚本
    下面是一个包含数据采样和生成火焰图的脚本,因此通过一个脚本就能生成Java混合模式火焰图。使用方法:
1
sudo ./gen-flame-graph.sh $SLEEP_SECONDS $PID

具体的脚本内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
#!/bin/bash
SLEEP_SECONDS=$1
PID=$2
PERF_TOOLS_HOME=$(pwd)
GEN_RESULTS_DIR=$PERF_TOOLS_HOME/gen-flame-graphs-result
AGENT_HOME=$PERF_TOOLS_HOME/perf-map-agent
AGENT_JAR=$AGENT_HOME/attach-main.jar
AGENT_OUT=$AGENT_HOME/out
FLAME_GRAPH_PL_HOME=$PERF_TOOLS_HOME/FlameGraph
FLAME_GRAPH_GENERAGED_FILE=$PERF_TOOLS_HOME/flamegraph-mixed-model-`date +%Y%m%d-%H%M%S`.svg
MAP_FILE=/tmp/perf-$PID.map
function 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 record
function 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 pid
function 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 graph
function 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_env
perf_record
gen_map_file
gen_flame_graph
echo "SUCCESS: generate flame graph."

Java混合模式火焰图实例

在根据上面的步骤安装完对应的工具之后,我们可以通过上面提供的脚本生成火焰图。下面这个实例是对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