关注Java方面,移步最下面:生成 JAVA 堆栈火焰图
软件的性能分析,往往需要查看 CPU 耗时, 了解瓶颈在哪里,而火焰图(flame graph) 是性能分析的利器,快速定位分析为啥 CPU 飙升。
很多人感冒发烧的时候, 往往会模仿神农氏尝百草的路子: 先尝尝抗病毒的药, 再试试抗细菌的药, 甭管家里有什么药挨个试, 什么中药西药, 瞎猫总会碰上死耗子, 如此做法自然是不可取的, 正确的做法应该是去医院验个血, 确诊后再对症下药.
让我们回想一下我们一般是如何调试程序的: 通常是在没有数据的情况下依靠主观臆断来瞎蒙, 而不是考虑问题到底是什么引起的!
毫无疑问, 调优程序性能问题的时候, 同样需要对症下药. 好消息是 Brendan D. Gregg 发明了火焰图
perf(performance 的缩写) 它是 Linux 系统原生提供的性能分析工具, 会返回 CPU 正在执行的函数名以及调用栈(stack)
Bash
# 安装perf命令sudo apt install linux-tools-common
在Ubuntu系统的Termial下,用 apt install
安装软件的时候,如果在未完成下载的情况下将 terminal close。此时 apt 进程可能没有结束。结果可能会发生下面的提示:
无法获得锁 /var/lib/dpkg/lock - open (11: 资源暂时不可用)
无法锁定管理目录(/var/lib/dpkg/),是否有其他进程正占用它?
解决方法
Bash
# 强制解锁命令sudo rm /var/cache/apt/archives/locksudo rm /var/lib/dpkg/lock
测试安装,输入命令:perf -v
Bash
# 查看版本liurenkui@ubuntu:~$ perf -vperf version 4.18.20
GitHub源码:GitHub - brendangregg/FlameGraph: Stack trace visualizer
我准备了一个非常简单的SpringBoot项目,GitHub地址:GitHub - X-rapido/springboot-hello: 一个非常简单的helloworld项目,方便以后在其他项目上调用演示而已。
将 FlameGraph 和 springboot-hello 项目,放在一个目录中。使用 java -jar springboot-hello-0.0.1-SNAPSHOT.jar
启动项目
主要是运行下面的慢方法
/**
* 测试慢执行
*/
@RequestMapping("/hello/cycle")
public String helloCycle(@RequestParam Integer cycle) throws InterruptedException {
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
for (int i = 0; i < cycle; i++) {
TimeUnit.MILLISECONDS.sleep(10);
System.out.println(i + " 嗨,你好 " + LocalDateTime.now().format(dtf));
}
return "hello cycle over";
}
(1)查看java进程号
运行命令:jps -l
Bash
liurenkui@ubuntu:~/MyLib/Demo$ jps -l5218 sun.tools.jps.Jps5149 springboot-hello-0.0.1-SNAPSHOT.jar
(2)开始采集
Bash
# 命令sudo perf record -F 采集次数 -p 进程号 -g -- sleep 采集秒数
1、执行慢方法:curl http://localhost:8080/hello/cycle?cycle=6000
2、执行采集:sudo perf record -F 99 -p 5149 -g -- sleep 30
,采集完之后会在当前目录生成一个 perf.data
文件
perf record
表示采集系统事件, 没有使用 -e
指定采集事件, 则默认采集 cycles
(即 CPU clock 周期), -F 99
表示每秒 99 次, -p 13204
是进程号, 即对哪个进程进行分析, -g
表示记录调用栈, sleep 30
则是持续 30 秒.
-F
指定采样频率为 99Hz(每秒99次), 如果 99次 都返回同一个函数名, 那就说明 CPU 这一秒钟都在执行同一个函数, 可能存在性能问题.
运行后会产生一个庞大的文本文件. 如果一台服务器有 16 个 CPU, 每秒抽样 99 次, 持续 30 秒, 就得到 47,520 个调用栈, 长达几十万甚至上百万行.
为了便于阅读,perf record
命令可以统计每个调用栈出现的百分比, 然后从高到低排列.
Bash
# 统计每个调用栈出现的百分比sudo perf report -fn --stdio
首先用 perf script
工具对 perf.data
进行解析
Bash
# 生成折叠后的调用栈sudo perf script -i perf.data &> perf.unfold
将解析出来的信息存下来, 供生成火焰图.
首先用 stackcollapse-perf.pl
将 perf
解析出的内容 perf.unfold
中的符号进行折叠,最后生成 svg
图.
Bash
# 生成火焰图sudo FlameGraph/stackcollapse-perf.pl perf.unfold &> perf.folded # 生成 svgsudo FlameGraph/flamegraph.pl perf.folded > perf.svg
我们可以使用管道将上面的流程简化为一条命令
Bash
# 简化上面命令sudo perf script | FlameGraph/stackcollapse-perf.pl | FlameGraph/flamegraph.pl > process.svg
将 svg 文件,使用浏览器打开。
火焰图是基于 stack
信息生成的 SVG
图片, 用来展示 CPU 的调用栈。
y
轴表示调用栈,每一层都是一个函数. 调用栈越深,火焰就越高,顶部就是正在执行的函数,下方都是它的父函数.x
轴表示抽样数,如果一个函数在 x
轴占据的宽度越宽,就表示它被抽到的次数多,即执行的时间长,注意, x
轴不代表时间, 而是所有的调用栈合并后, 按字母顺序排列的.火焰图就是看顶层的哪个函数占据的宽度最大. 只要有 “平顶”(plateaus), 就表示该函数可能存在性能问题。
颜色没有特殊含义, 因为火焰图表示的是 CPU 的繁忙程度, 所以一般选择暖色调.
火焰图是 SVG 图片,可以与用户互动.
(1)鼠标悬浮
火焰的每一层都会标注函数名,鼠标悬浮时会显示完整的函数名、抽样抽中的次数、占据总抽样次数的百分比
(2)点击放大
在某一层点击,火焰图会水平放大,该层会占据所有宽度,显示详细信息。
左上角会同时显示 Reset Zoom
,点击该链接,图片就会恢复原样.
(3)搜索
按下 Ctrl + F
会显示一个搜索框,用户可以输入关键词或正则表达式,所有符合条件的函数名会高亮显示.
两种情况下, 无法画出火焰图, 需要修正系统行为.
(1)调用栈不完整
当调用栈过深时,某些系统只返回前面的一部分(比如前10层)。
(2)函数名缺失
有些函数没有名字,编译器只用内存地址来表示(比如匿名函数)。
Chrome 浏览器可以生成页面脚本的火焰图, 用来进行 CPU 分析.
打开开发者工具, 切换到 Performance
面板. 然后, 点击 录制 按钮, 开始记录数据. 这时, 可以在页面进行各种操作, 然后停止”录制”,这时, 开发者工具会显示一个时间轴. 它的下方就是火焰图.
浏览器的火焰图与标准火焰图有两点差异: 浏览器是倒置的(即调用栈最顶端的函数在最下方); x
轴是时间轴, 而不是抽样次数.
幸亏有了 CPU 火焰图(flame graphs), CPU 使用率的问题一般都比较好定位. 但要处理性能回退问题, 就要在修改前后或者不同时期和场景下的火焰图之间, 不断切换对比, 来找出问题所在, 这感觉就是像在太阳系中搜寻冥王星. 虽然, 这种方法可以解决问题, 但我觉得应该会有更好的办法.
所以, 下面就隆重介绍 红/蓝差分火焰图(red/blue differential flame graphs)
这是一副交互式 SVG 格式图片. 图中使用了两种颜色来表示状态, 红色表示增长, 蓝色表示衰减.
这张火焰图中各火焰的形状和大小都是和第二次抓取的 profile
文件对应的 CPU 火焰图是相同的. (其中,y
轴表示栈的深度,x
轴表示样本的总数,栈帧的宽度表示了 profile 文件中该函数出现的比例,最顶层表示正在运行的函数,再往下就是调用它的栈).
在下面这个案例展示了,在系统升级后,一个工作载荷的 CPU 使用率上升了。下面是对应的 CPU 火焰图(SVG 格式)
通常,在标准的火焰图中栈帧和栈塔的颜色是随机选择的,而在红/蓝差分火焰图中,使用不同的颜色来表示两个 profile
文件中的差异部分.
在第二个 profile
中 deflate_slow()
函数以及它后续调用的函数运行的次数要比前一次更多,所以在上图中这个栈帧被标为了红色。可以看出问题的原因是 ZFS
的压缩功能被启用了, 而在系统升级前这项功能是关闭的.
这个例子过于简单,我们甚至可以不用差分火焰图也能分析出来,但想象一下,如果是在分析一个微小的性能下降,比如说小于5%
,而且代码也更加复杂的时候,问题就没那么好处理了。
红蓝差分火焰图的工作原理是这样的
profile1
文件profile2
文件profile2
来生成火焰图. (这样栈帧的宽度就是以 profile2 文件为基准的)2-1
” 的差异来对火焰图重新上色,原则是,如果栈帧在 profile2 中出现出现的次数更多,则标为红色, 否则标为蓝色,色彩是根据修改前后的差异来填充的.这样做的目的是,同时使用了修改前后的 profile 文件进行对比,在进行功能验证测试或者评估代码修改对性能的影响时,会非常有用。
新的火焰图是基于修改后的 profile 文件生成(所以栈帧的宽度仍然显示了当前的CPU消耗)。通过颜色的对比,就可以了解到系统性能差异的原因。
只有对性能产生直接影响的函数才会标注颜色(比如说,正在运行的函数),它所调用的子函数不会重复标注。
作者的 GitHub 仓库 FlameGrdph 中实现了一个程序脚本,difffolded.pl
用来生成红蓝差分火焰图. 为了展示工具是如何工作的,利用刚才的方式抓取两次。
第一次,执行慢方法:curl http://localhost:8080/hello/cycle?cycle=6000
,抓取修改前的 profile1 文件
Bash
# 抓取数据sudo perf record -F 99 -a -g -- sleep 30 # 解析数据生成堆栈信息sudo perf script > out.stacks1 # 折叠堆栈sudo FlameGraph/stackcollapse-perf.pl out.stacks1 > out.folded1
第二次,执行慢方法:curl http://localhost:8080/hello/cycle?cycle=3000
,一段时间后 (或者程序代码修改后),抓取 profile 2 文件
Bash
# 抓取数据sudo perf record -F 99 -a -g -- sleep 30 # 解析数据生成堆栈信息sudo perf script > out.stacks2 # 折叠堆栈sudo FlameGraph/stackcollapse-perf.pl out.stacks2 > out.folded2
最终,生成红蓝差分火焰图
Bash
# 生成红蓝差分火焰图sudo FlameGraph/difffolded.pl out.folded1 out.folded2 | FlameGraph/flamegraph.pl > diff2.svg
difffolded.pl
只能对 “折叠” 过的堆栈 profile
文件进行操作,折叠操作 由前面的 stackcollapse
系列脚本完成的。
让我们回顾一下,刚才java代码生成的火焰图
我们看到,上面乱乱麻麻堆栈信息,我们看起来也是非常的迷茫。
有的,使用 jmaps
脚本,自动的为所有Java进程创建符号文件
Bash
# 克隆代码git clone https://github.com/jvm-profiling-tools/perf-map-agent.git # 进入目录cd perf-map-agent/ # 安装cmake、如果本地有就不用安装了sudo apt install cmakesudo apt install gccsudo apt install gcc-c++ # 编译cmake .make
成功编译后会在out
目录下生成attach-main.jar
和libperfmap.so
两个文件,这是获取java程序运行时符号表的关键。
(1)JAVA_HOME 和 AGENT_HOME
打开 /FlameGraph/jmaps
文件,其中一段儿代码如下:
Python
JAVA_HOME=${JAVA_HOME:-/usr/lib/jvm/java-8-oracle}AGENT_HOME=${AGENT_HOME:-/usr/lib/jvm/perf-map-agent} # from https://github.com/jvm-profiling-tools/perf-map-agent
这里表明,必须使用Java8,需要我们手动将 AGENT_HOME
替换为刚才编译后的 per-map-agent/out/
目录。修改如下
Python
JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64AGENT_HOME=/home/liurenkui/MyLib/Demo/perf-map-agent
(2)设置非root用户执行
如果当前用户不是root, 注释掉jmaps脚本中的如下代码:
Python
if [[ "$USER" != root ]]; then echo "ERROR: not root user? exiting..." exitfi
(3)sudo权限生成文件
避免执行./jmaps
脚本出错,chown: changing ownership of '/tmp/perf-xxx.map': Operation not permitted
将jmaps中的代码
Python
if [[ -e "$mapfile" ]]; then chown root $mapfile chmod 666 $mapfileelse
改为:
Python
if [[ -e "$mapfile" ]]; then sudo chown root $mapfile sudo chmod 666 $mapfileelse
(4)修改 rm 权限
重新执行会删除临时文件,非root权限不能删除,增加一个sudo
Bash
# 修改前[[ -e $mapfile ]] && rm $mapfile # 修改后[[ -e $mapfile ]] && sudo rm $mapfile
用 jmaps
为 java 进程创建符号表 生成 java 堆栈的火焰图
Bash
# 采集sudo perf record -F 99 -ag -p 进程ID -- sleep 30; ./FlameGraph/jmaps # 折叠堆栈sudo perf script > out.stacks # 生成svgsudo cat out.stacks | ./FlameGraph/stackcollapse-perf.pl | grep -v cpu_idle | ./FlameGraph/flamegraph.pl --color=java --hash > out.svg
示例图
绿色的部分表示 Java 堆栈信息。非常的直观。
1、如果你的火焰图中,没有顶部栈的信息,在启动java项目时,请加上JVM参数:-XX:+PreserveFramePointer
2、可以写入一个shell脚本,一劳永逸
Bash
# 查看liurenkui@ubuntu:~/MyLib/Demo$ cat build.sh #!/bin/shperf record -F 99 -ag -p `jps -l | grep springboot | awk '{print $1}'` -- sleep 30; ./FlameGraph/jmapsperf script > out.stackscat out.stacks | ./FlameGraph/stackcollapse-perf.pl | grep -v cpu_idle | ./FlameGraph/flamegraph.pl --color=java --hash > out.svg # 修改权限liurenkui@ubuntu:~/MyLib/Demo$ chmod +x build.sh # 执行liurenkui@ubuntu:~/MyLib/Demo$ sudo ./build.sh
如果你了解 Alibaba Arthas,那么 3.1.5版本中支持火焰图,快速定位应用热点,你一定不要错过:https://github.com/alibaba/arthas/issues/951
Arthas解放你的双手,直接可用,相当的方便
原网址: 访问
创建于: 2024-02-22 16:35:15
目录: default
标签: 无
未标明原创文章均为采集,版权归作者所有,转载无需和我联系,请注明原出处,南摩阿彌陀佛,知识,不只知道,要得到
最新评论