在做性能调优的时候,我们通常会借助一些性能分析工具(比如perf,DTrace)分析系统资源的使用情况,比如CPU、内存等,但这些工具分析的结果通常是文本形式,不够直观,不便于快速定位系统瓶颈。Brendan Gregg开发了一种可视化的性能分析工具–火焰图,这种工具通过分析包含stack traces的profile数据,以可视化的形式展现出来,通过它可以快速准确地定位到热点代码。
本文内容涵盖了如何理解火焰图、如何生成火焰图、火焰图有哪些缺陷、如何解决这些缺陷、以及Java混合模式火焰图的具体生成步骤和具体实例。
火焰图(Flame Graph)
针对不同的资源和事件类型,有不同类型的火焰图,主要包括:CPU、Memory、Off-CPU、Hot/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文件。执行以下命令就可以完成数据采集。
|
|
如果Linux系统未安装perf,可参考以下命令安装perf
|
|
更多perf的使用示例可参考perf-examples
- 生成火焰图
stack traces数据采集完之后,通过FlameGraph提供的python脚本就可以生成火焰图。
|
|
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的效果对比
如何解决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环境)是否已经安装过
|
|
如果perf已经安装过,可以跳过安装这一步;如果未安装,执行以下命令完成安装
|
|
-
- 安装gcc和gcc-c++
12sudo yum install gccsudo yum install gcc-c++如果系统中已安装gcc和gcc-c++,这一步可以跳过。
- 安装cmake
下载cmake2.8.6,解压到/home/q/perf-tools/下
1234sudo 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。
- 安装perf-map-agent
123git 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**才能生效- 检查JDK版本
1java -versionJDK版本是否大于或者等于Java 8 update 60 build 19
- 检查JVM参数
1ps 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
|
|
- 生成脚本
下面是一个包含数据采样和生成火焰图的脚本,因此通过一个脚本就能生成Java混合模式火焰图。使用方法:
|
|
具体的脚本内容:
|
|
Java混合模式火焰图实例
在根据上面的步骤安装完对应的工具之后,我们可以通过上面提供的脚本生成火焰图。下面这个实例是对Java进程8708采样30秒,然后生成名为flamegraph-mixed-model-date +%Y%m%d-%H%M%S
.svg的Java混合模式火焰图。
|
|
执行脚本过程中,如果遇到以下错误
|
|
就需要copy文件libperfmap.so,然后再次执行生成火焰图的脚本
|
|
生成的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