如何用火焰图进行 Java 性能分析,这一篇文章就够了

Linux下用火焰图进行性能分析(Ubuntu18 操作系统中演示)

关注Java方面,移步最下面:生成 JAVA 堆栈火焰图

软件的性能分析,往往需要查看 CPU 耗时, 了解瓶颈在哪里,而火焰图(flame graph) 是性能分析的利器,快速定位分析为啥 CPU 飙升。

一、火焰图简介

很多人感冒发烧的时候, 往往会模仿神农氏尝百草的路子: 先尝尝抗病毒的药, 再试试抗细菌的药, 甭管家里有什么药挨个试, 什么中药西药, 瞎猫总会碰上死耗子, 如此做法自然是不可取的, 正确的做法应该是去医院验个血, 确诊后再对症下药.

让我们回想一下我们一般是如何调试程序的: 通常是在没有数据的情况下依靠主观臆断来瞎蒙, 而不是考虑问题到底是什么引起的!

毫无疑问, 调优程序性能问题的时候, 同样需要对症下药. 好消息是 Brendan D. Gregg 发明了火焰图

二、火焰图可视化生成器实战

1、安装 perf 命令

perf(performance 的缩写) 它是 Linux 系统原生提供的性能分析工具, 会返回 CPU 正在执行的函数名以及调用栈(stack)

# 安装perf命令
sudo apt install linux-tools-common

在Ubuntu系统的Termial下,用 apt install 安装软件的时候,如果在未完成下载的情况下将 terminal close。此时 apt 进程可能没有结束。结果可能会发生下面的提示:

1.png

无法获得锁 /var/lib/dpkg/lock - open (11: 资源暂时不可用)
无法锁定管理目录(/var/lib/dpkg/),是否有其他进程正占用它?

解决方法

# 强制解锁命令
sudo rm /var/cache/apt/archives/lock
sudo rm /var/lib/dpkg/lock

测试安装,输入命令:perf -v

# 查看版本
liurenkui@ubuntu:~$ perf -v
perf version 4.18.20

2、下载 FlameGraph

GitHub源码:https://github.com/brendangregg/FlameGraph

3、运行 java 项目

我准备了一个非常简单的SpringBoot项目,GitHub地址:https://github.com/X-rapido/springboot-hello

将 FlameGraph 和 springboot-hello 项目,放在一个目录中。使用 java -jar springboot-hello-0.0.1-SNAPSHOT.jar 启动项目

2.png

主要是运行下面的慢方法

/**
 * 测试慢执行
 */
@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";
}

3、运行 perf 采集数据

(1)查看java进程号

运行命令:jps -l

liurenkui@ubuntu:~/MyLib/Demo$ jps -l
5218 sun.tools.jps.Jps
5149 springboot-hello-0.0.1-SNAPSHOT.jar

(2)开始采集

# 命令
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 命令可以统计每个调用栈出现的百分比, 然后从高到低排列.

# 统计每个调用栈出现的百分比
sudo perf report -fn --stdio

3.png

4、生成 svg 火焰图

首先用 perf script 工具对 perf.data 进行解析

# 生成折叠后的调用栈
sudo perf script -i perf.data &> perf.unfold

将解析出来的信息存下来, 供生成火焰图.

首先用 stackcollapse-perf.pl 将 perf 解析出的内容 perf.unfold 中的符号进行折叠,最后生成 svg 图.

# 生成火焰图
sudo FlameGraph/stackcollapse-perf.pl perf.unfold &> perf.folded

# 生成 svg
sudo FlameGraph/flamegraph.pl perf.folded > perf.svg

我们可以使用管道将上面的流程简化为一条命令

# 简化上面命令
sudo perf script | FlameGraph/stackcollapse-perf.pl | FlameGraph/flamegraph.pl > process.svg

4.png

5、打开 svg 火焰图

将 svg 文件,使用浏览器打开。

5.png

三、解析火焰图

1、火焰图的含义

火焰图是基于 stack 信息生成的 SVG 图片, 用来展示 CPU 的调用栈。

  • y 轴表示调用栈,每一层都是一个函数. 调用栈越深,火焰就越高,顶部就是正在执行的函数,下方都是它的父函数.

  • x 轴表示抽样数,如果一个函数在 x 轴占据的宽度越宽,就表示它被抽到的次数多,即执行的时间长,注意, x 轴不代表时间, 而是所有的调用栈合并后, 按字母顺序排列的.

火焰图就是看顶层的哪个函数占据的宽度最大. 只要有 “平顶”(plateaus), 就表示该函数可能存在性能问题。

颜色没有特殊含义, 因为火焰图表示的是 CPU 的繁忙程度, 所以一般选择暖色调.

2、互动性

火焰图是 SVG 图片,可以与用户互动.

(1)鼠标悬浮

火焰的每一层都会标注函数名,鼠标悬浮时会显示完整的函数名、抽样抽中的次数、占据总抽样次数的百分比

(2)点击放大

在某一层点击,火焰图会水平放大,该层会占据所有宽度,显示详细信息。

左上角会同时显示 Reset Zoom,点击该链接,图片就会恢复原样.

(3)搜索

按下 Ctrl + F 会显示一个搜索框,用户可以输入关键词或正则表达式,所有符合条件的函数名会高亮显示.

3、局限

两种情况下, 无法画出火焰图, 需要修正系统行为.

(1)调用栈不完整

当调用栈过深时,某些系统只返回前面的一部分(比如前10层)。

(2)函数名缺失

有些函数没有名字,编译器只用内存地址来表示(比如匿名函数)。

4、浏览器的火焰图

Chrome 浏览器可以生成页面脚本的火焰图, 用来进行 CPU 分析.

打开开发者工具, 切换到 Performance 面板. 然后, 点击 录制 按钮, 开始记录数据. 这时, 可以在页面进行各种操作, 然后停止”录制”,这时, 开发者工具会显示一个时间轴. 它的下方就是火焰图.

6.png

浏览器的火焰图与标准火焰图有两点差异: 浏览器是倒置的(即调用栈最顶端的函数在最下方); x 轴是时间轴, 而不是抽样次数.

四、红蓝分叉火焰图

幸亏有了 CPU 火焰图(flame graphs), CPU 使用率的问题一般都比较好定位. 但要处理性能回退问题, 就要在修改前后或者不同时期和场景下的火焰图之间, 不断切换对比, 来找出问题所在, 这感觉就是像在太阳系中搜寻冥王星. 虽然, 这种方法可以解决问题, 但我觉得应该会有更好的办法.

所以, 下面就隆重介绍 红/蓝差分火焰图(red/blue differential flame graphs)

参考:http://www.brendangregg.com/blog/2014-11-09/differential-flame-graphs.html

1、红蓝差分火焰图示例

7.png

这是一副交互式 SVG 格式图片. 图中使用了两种颜色来表示状态, 红色表示增长, 蓝色表示衰减.

这张火焰图中各火焰的形状和大小都是和第二次抓取的 profile 文件对应的 CPU 火焰图是相同的. (其中,y 轴表示栈的深度,x 轴表示样本的总数,栈帧的宽度表示了 profile 文件中该函数出现的比例,最顶层表示正在运行的函数,再往下就是调用它的栈).

在下面这个案例展示了,在系统升级后,一个工作载荷的 CPU 使用率上升了。下面是对应的 CPU 火焰图(SVG 格式)

8.png

通常,在标准的火焰图中栈帧和栈塔的颜色是随机选择的,而在红/蓝差分火焰图中,使用不同的颜色来表示两个 profile 文件中的差异部分.

在第二个 profile 中 deflate_slow() 函数以及它后续调用的函数运行的次数要比前一次更多,所以在上图中这个栈帧被标为了红色。可以看出问题的原因是 ZFS 的压缩功能被启用了, 而在系统升级前这项功能是关闭的.

这个例子过于简单,我们甚至可以不用差分火焰图也能分析出来,但想象一下,如果是在分析一个微小的性能下降,比如说小于5%,而且代码也更加复杂的时候,问题就没那么好处理了。

9.png

2、红蓝差分火焰图简介

红蓝差分火焰图的工作原理是这样的

  • 抓取修改前的堆栈 profile1 文件

  • 抓取修改后的堆栈 profile2 文件

  • 使用 profile2 来生成火焰图. (这样栈帧的宽度就是以 profile2 文件为基准的)

  • 使用 “2-1” 的差异来对火焰图重新上色,原则是,如果栈帧在 profile2 中出现出现的次数更多,则标为红色, 否则标为蓝色,色彩是根据修改前后的差异来填充的.

这样做的目的是,同时使用了修改前后的 profile 文件进行对比,在进行功能验证测试或者评估代码修改对性能的影响时,会非常有用。

新的火焰图是基于修改后的 profile 文件生成(所以栈帧的宽度仍然显示了当前的CPU消耗)。通过颜色的对比,就可以了解到系统性能差异的原因。

只有对性能产生直接影响的函数才会标注颜色(比如说,正在运行的函数),它所调用的子函数不会重复标注。

3、生成红/蓝差分火焰图

作者的 GitHub 仓库 FlameGrdph 中实现了一个程序脚本,difffolded.pl 用来生成红蓝差分火焰图. 为了展示工具是如何工作的,利用刚才的方式抓取两次。

第一次,执行慢方法:curl http://localhost:8080/hello/cycle?cycle=6000,抓取修改前的 profile1 文件

# 抓取数据
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 文件

# 抓取数据
sudo perf record -F 99 -a -g -- sleep 30

# 解析数据生成堆栈信息
sudo perf script > out.stacks2

# 折叠堆栈
sudo FlameGraph/stackcollapse-perf.pl out.stacks2 > out.folded2

最终,生成红蓝差分火焰图

# 生成红蓝差分火焰图
sudo FlameGraph/difffolded.pl out.folded1 out.folded2 | FlameGraph/flamegraph.pl > diff2.svg

difffolded.pl 只能对 “折叠” 过的堆栈 profile 文件进行操作,折叠操作 由前面的 stackcollapse 系列脚本完成的。

五、生成 JAVA 堆栈火焰图

让我们回顾一下,刚才java代码生成的火焰图

5.png

我们看到,上面乱乱麻麻堆栈信息,我们看起来也是非常的迷茫。

  • 哪些是 CPU 堆栈,哪些是Java堆栈?

  • 能不能把 Java 堆栈和 CPU 堆栈明确区分?

有的,使用 jmaps 脚本,自动的为所有Java进程创建符号文件

1、下载,编译 perf-map

GitHub地址:https://github.com/jvm-profiling-tools/perf-map-agent

# 克隆代码
git clone https://github.com/jvm-profiling-tools/perf-map-agent.git 

# 进入目录
cd perf-map-agent/

# 安装cmake、如果本地有就不用安装了
sudo apt install cmake
sudo apt install gcc
sudo apt install gcc-c++

# 编译
cmake .
make

成功编译后会在out目录下生成attach-main.jarlibperfmap.so两个文件,这是获取java程序运行时符号表的关键。

2、配置 perf-map

(1)JAVA_HOME 和 AGENT_HOME

打开 /FlameGraph/jmaps 文件,其中一段儿代码如下:

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/ 目录。修改如下

JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64
AGENT_HOME=/home/liurenkui/MyLib/Demo/perf-map-agent

(2)设置非root用户执行

如果当前用户不是root, 注释掉jmaps脚本中的如下代码:

if [[ "$USER" != root ]]; then
        echo "ERROR: not root user? exiting..."
        exit
fi

(3)sudo权限生成文件

避免执行./jmaps脚本出错,chown: changing ownership of '/tmp/perf-xxx.map': Operation not permitted

将jmaps中的代码

if [[ -e "$mapfile" ]]; then
        chown root $mapfile
        chmod 666 $mapfile
else

改为:

if [[ -e "$mapfile" ]]; then
        sudo chown root $mapfile
        sudo chmod 666 $mapfile
else

(4)修改 rm 权限

重新执行会删除临时文件,非root权限不能删除,增加一个sudo

# 修改前
[[ -e $mapfile ]] && rm $mapfile

# 修改后
[[ -e $mapfile ]] && sudo rm $mapfile

3、生成java火焰图

用 jmaps 为 java 进程创建符号表 生成 java 堆栈的火焰图

# 采集
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

示例图

10.png

4、查看火焰图

11.png

绿色的部分表示 Java 堆栈信息。非常的直观。

补充

1、如果你的火焰图中,没有顶部栈的信息,在启动java项目时,请加上JVM参数:-XX:+PreserveFramePointer

2、可以写入一个shell脚本,一劳永逸

# 查看
liurenkui@ubuntu:~/MyLib/Demo$ cat build.sh 
#!/bin/sh
perf record -F 99 -ag -p `jps -l | grep springboot | awk '{print $1}'` -- sleep 30; ./FlameGraph/jmaps
perf script > out.stacks
cat 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

六、Arthas 火焰图(重点推荐)

如果你了解 Alibaba Arthas,那么 3.1.5版本中支持火焰图,快速定位应用热点,你一定不要错过:https://github.com/alibaba/arthas/issues/951

Arthas解放你的双手,直接可用,相当的方便

  • async-profiler:https://github.com/jvm-profiling-tools/async-profiler

  • arthas使用profiler生成火焰图:https://alibaba.github.io/arthas/profiler.html

七、参考


未经允许请勿转载:程序喵 » 如何用火焰图进行 Java 性能分析,这一篇文章就够了

点  赞 (3) 打  赏
分享到: