[Java/Flink/Arthas] Java 性能优化分析工具:(CPU) 火焰图

0 序言

  • 近期,工作项目上某 Flink 程序的数据处理性能问题一直萦绕心头,今日第一次尝试使用火焰图这一工具来分析性能问题。

最早项目上的 Flink 是 1.12 ,尚不支持此功能;但现在已切换到 Flink 1.15 版本,火焰图功能(自 1.13起)是被支持的了。

  • 或不多说,开始正文。

主要参考自第1篇参考文献(故部分操作试验,并未亲测),在此文的基础上,添加了一些个人实践经验。

1 概述: Java 性能优化分析工具——火焰图

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

火焰图的缘起

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

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

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

2 案例实践:火焰图可视化生成器

安装 perf 命令

  • perf(performance 的缩写) 它是 Linux 系统原生提供的性能分析工具, 会返回 CPU 正在执行的函数名以及调用栈(stack)
# 安装perf命令
sudo apt install linux-tools-common
  • Ubuntu系统的Termial下,用 apt install 安装软件的时候,如果在未完成下载的情况下将 terminal close。此时 apt 进程可能没有结束。结果可能会发生下面的提示:

image

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

  • 解决方法

Bash

# 强制解锁命令
sudo rm /var/cache/apt/archives/lock
sudo rm /var/lib/dpkg/lock
  • 测试安装,输入命令:perf -v

Bash

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

下载 FlameGraph

  • Github

https://github.com/brendangregg/FlameGraph

运行 java 应用项目

  • 准备了一个非常简单的SpringBoot项目:

GitHub: GitHub - X-rapido/springboot-hello

一个非常简单的helloworld项目,方便以后在其他项目上调用演示而已。

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

image

  • 主要是运行下面的慢方法
/**
 * 测试慢执行
 */
@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";
}

运行 perf 采集数据

  • 查看java进程号

运行命令: jps -l

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

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

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

image

生成 svg 火焰图

  • 首先,用 perf script 工具对 perf.data 进行解析
# 生成折叠后的调用栈
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
 
# 生成 svg
sudo FlameGraph/flamegraph.pl perf.folded > perf.svg

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

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

image

打开 svg 火焰图

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

image

3 解读火焰图

3.1 火焰图的含义

  • 火焰图是基于 stack 信息生成的 SVG 图片, 用来展示 CPU 的调用栈。
  • y 轴表示调用栈,每一层都是一个函数. 调用栈越深,火焰就越高,顶部就是正在执行的函数,下方都是它的父函数.
  • x 轴表示抽样数,如果一个函数在 x 轴占据的宽度越宽,就表示它被抽到的次数多,即执行的时间长,注意, x 轴不代表时间, 而是所有的调用栈合并后, 按字母顺序排列的.
  • 火焰图就是看【顶层】的哪个【函数】占据的【宽度】最大. 只要有 “【平顶】”(plateaus), 就表示该函数可能存在【性能问题】。

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

3.2 互动性

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

  • (1)鼠标悬浮

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

  • (2)点击放大

在某一层点击,火焰图会水平放大,该层会占据所有宽度,显示详细信息。
左上角会同时显示 Reset Zoom,点击该链接,图片就会恢复原样.

  • (3)搜索

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

3.3 局限

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

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

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

3.4 浏览器的火焰图

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

image

  • 浏览器的火焰图与标准火焰图有两点差异: 浏览器是倒置的(即调用栈最顶端的函数在最下方); x 轴是时间轴, 而不是抽样次数.
  • Flink 1.13还引入了一系列性能分析工具,帮助用户更好地理解和优化作业性能。

这些工具包括用于识别瓶颈节点的负载和反压可视化、分析算子热点代码的CPU火焰图以及分析State Backend状态的State访问性能指标等。

详情参见本文: Flink 火焰图章节

4 红蓝分叉火焰图

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

虽然, 这种方法可以解决问题, 但应该会有更好的办法.
所以, 下面就隆重介绍 红/蓝差分火焰图(red/blue differential flame graphs)

参考: Differential Flame Graphs

1 红蓝差分火焰图示例

image

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

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

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

image

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

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

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

image

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

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 系列脚本完成的。

5 生成 JAVA 堆栈火焰图

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

image

  • 我们看到,上面乱乱麻麻堆栈信息,我们看起来也是非常的迷茫。
  • 哪些是 CPU 堆栈,哪些是Java堆栈?
  • 能不能把 Java 堆栈和 CPU 堆栈明确区分?
  • 有的,使用 jmaps 脚本,自动的为所有Java进程创建符号文件

下载,编译 perf-map

  • GitHub

GitHub - jvm-profiling-tools/perf-map-agent: A java agent to generate method mappings to use with the linux perf tool

# 克隆代码
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程序运行时符号表的关键。

配置 perf-map

  • (1)JAVA_HOMEAGENT_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

这里表明,必须使用Java 8,需要我们手动将 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

生成java火焰图

  • jmapsjava 进程创建符号表 生成 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

示例图:

image

查看火焰图

image

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

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

可以写入一个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

T 最佳实践

Arthas 火焰图

  • 如果你了解 Alibaba Arthas,那么 3.1.5 版本中支持火焰图,快速定位应用热点,你一定不要错过。

https://github.com/alibaba/arthas
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

  • 从 Apache Flink 1.13 版本开始,火焰图成为 Flink 的原生支持功能。

通过启用 rest.flamegraph.enabled 配置项,用户可以生成火焰图来直观分析性能瓶颈

需要在 Flink 的配置文件 flink-conf.yaml (华为云 DLI 产品: 【优化参数】) 中添加以下配置

rest.flamegraph.enabled=true

image

  • 使用建议
  • 火焰图功能默认是【关闭】的。

因为采样堆栈跟踪可能会对生产环境产生一定的性能影响
建议在开发和预生产环境中启用此功能,而在生产环境中将其视为【实验性功能】。

  • 火焰图类型

火焰图支持以下几种模式:

  • On-CPU 火焰图:显示处于 RUNNABLENEW 状态的线程。
  • Off-CPU 火焰图: 可视化阻塞调用,显示处于 TIMED_WAITINGWAITINGBLOCKED 状态的线程。
  • 混合模式火焰图: 结合所有线程状态的堆栈跟踪生成。
  • 注意事项
  • 火焰图的采样过程仅在 JVM 内部进行。

因此只能看到 Java 运行时的方法调用,无法捕获系统调用
此外,【默认情况】下,火焰图是在【算子级别】构建的,所有【并行任务的堆栈跟踪】会被合并。
如果某个方法在某些任务中【占用资源】较多,而在【其他任务】中占用较少,可能会【被平均化】,从而【掩盖瓶颈】。

  • Flink 1.17 版本开始,支持单并发级别的火焰图,可针对【特定子任务】进行更精细的分析。
  • 推荐文献
  • Apache Flink - 火焰图

https://nightlies.apache.org/flink/flink-docs-master/zh/docs/ops/debugging/flame_graphs/

Y 推荐文献

  • async-profiler

X 参考文献

posted @ 2025-11-08 09:19  千千寰宇  阅读(3)  评论(0)    收藏  举报