Arthas 实战指南(二):profiler生成火焰图实战

目录

一、火焰图原理:从"采集"到"解读"

很多人会用火焰图,但并不一定真正理解火焰图的原理。理解原理以后,才能避免误读。


1. 火焰图解决什么问题

火焰图本质上是在回答一个问题:

程序运行过程中,CPU 时间主要花在哪些函数和调用路径上?

如果没有火焰图,我们看到的往往只是零散的方法耗时、线程栈或日志,很难从全局判断真正的热点。

火焰图的价值就在于:

  • 把大量采样数据压缩成一张图
  • 快速看到最热的函数
  • 快速看到热点出现在哪条调用链上

2. 数据是怎么来的:调用栈采样

火焰图并不是"逐行记录每次函数调用",而是采样

例如,Profiler 每隔 10ms 采集一次当前线程调用栈:

  • 某次采样拿到:A -> B -> C
  • 下一次采样拿到:A -> B -> D
  • 再下一次采样拿到:A -> E

随着采样次数越来越多,就可以统计出:

  • 哪些栈路径出现频率最高
  • 哪些函数在热点路径中反复出现

这些采样栈会被聚合,最后绘制成火焰图。

因此:

火焰图展示的不是"某一次调用",而是"统计意义上的热点分布"。


3. 火焰图的图形语义

这是理解火焰图最关键的部分。

3.1 X 轴:宽度代表热点程度
  • 某个函数框越宽,表示该函数(或包含它的路径)出现的样本越多
  • 样本越多,通常意味着消耗的 CPU 时间越多

所以:

宽,才是热。

3.2 Y 轴:表示调用栈深度
  • 最底层通常是入口或调用链较上游的方法
  • 越往上表示调用越深入
  • 上层函数是下层函数调用出来的

所以:

下方是调用者,上方是被调用者。

3.3 颜色:通常只用于区分,不一定有固定含义

很多人会误以为"越红越热",这通常并不准确。

在大多数火焰图工具里:

  • 颜色更多是为了视觉区分
  • 不同颜色不一定代表不同耗时等级
  • 除非某个工具明确赋予颜色特殊语义,否则不要过度解读颜色

所以:

颜色不是重点,宽度才是重点。

3.4 X 轴通常不是时间线

这是最容易误解的一点。

很多人看到图是横向展开的,就下意识认为:

  • 左边是前面执行的
  • 右边是后面执行的

这通常是错的。

火焰图中的左右位置,一般只是聚合后排布的位置,不表示真实的时间顺序。

所以:

火焰图通常不是时序图,而是聚合统计图。


4. 为什么叫"火焰图"

因为它看起来像一簇一簇向上燃烧的火焰:

  • 每个矩形是一个函数
  • 相同路径向上堆叠
  • 热点路径形成宽而高的"火焰柱"

视觉上非常适合快速识别瓶颈路径。


5. 正确解读火焰图的方法

解读火焰图时,建议按下面顺序来:

第一步:找最宽的块

最宽的函数块,通常就是整体热点入口或热点路径的一部分。

第二步:顺着最宽块往上看

看它调用了谁,热点进一步落在哪个子函数。

第三步:区分"自身耗时"和"子调用耗时"

有时候某个函数很宽,并不代表它自己的代码慢,而是它调用的下游慢。

第四步:找"宽且可优化"的点

不是所有宽块都值得优化。比如:

  • JDK 内部调度逻辑
  • 框架底层调度
  • 不可控的中间件等待

真正值得优先优化的是:

  • 宽度大
  • 业务上可改
  • 改动收益明显

6. 常见误区

误区一:把 X 轴当时间轴

通常错误。火焰图不是按时间先后展开的。

误区二:看到红色就以为最热

不一定。颜色很多时候只是装饰性区分。

误区三:只看最顶层函数

顶层函数可能只是"最后被采样到的点",真正的耗时根因可能在更下层。

误区四:只看单个函数,不看整条调用路径

火焰图强调的是路径,不是孤立函数。

误区五:把采样结果当成绝对精确时间

火焰图是统计近似,不是逐条精确计时。但在定位热点上通常足够有效。


二、使用Arthas Profiler生成火焰图

当你不再满足于单方法观测,而是想从全局视角看"CPU 时间到底烧在哪",就该使用 profiler 了。

示例:

bash 复制代码
[arthas@3792116]$ profiler start  
Profiling started  

[arthas@3792116]$ profiler stop  
OK
profiler output file: /root/myapp/arthas-output/20260327-144254.html  

这个过程可以理解为:

  1. 开始采样程序运行时的调用栈
  2. 运行一段时间,让业务流量进来
  3. 停止采样并输出分析结果
  4. 生成 HTML 火焰图文件
  5. 分析火焰图,查看性能热点

Profiler 适合什么场景

  • CPU 高,但线程栈看不出明确原因
  • 接口整体慢,但 trace 看的是局部,不够全局
  • 想找真正的热点方法和热点路径
  • 做性能优化前的基线分析

三、火焰图优化实战

我这边收到的问题是线上测试数据权限场景时,CPU的负载近乎100%,具体的数据权限处理场景是sql条件拼接了in {集合},集合包含2000个部门ID,按照常理理解这个场景的瓶颈应该在数据库的sql查询上,但实际现象就是服务器的CPU负载跑满了,问题出在了数据权限组件的sql解析上。我先是review了一遍组件代码,对组件内的字符串拼接处理进行了优化(字符串替换优化、添加本地缓存等),但实测下来优化效果非常不明显。

生成火焰图

针对这种CPU负载过高,但却无法定位热点的问题,火焰图就很适用。

我本地采用Jmeter对线上环境进行了压测(100并发、5分钟),然后通过Arthas Profiler命令抓取并生成了压测时间段内的应用程序的CPU火焰图:

注:由于是在docker容器中执行的应用程序,

所以需要先将arthas相关文件通过docker cp拷贝到容器内::docker cp ./arthas xxx:./arthas

然后再进入到docker容器内执行相应的抓取操作:docker exec -it xxx /bin/sh

bash 复制代码
# 开始抓取
[arthas@3792116]$ profiler start  
Profiling started  

# 结束抓取,并生成火焰图
[arthas@3792116]$ profiler stop  
OK
# 结束后,会提示相应的结果文件位置
profiler output file: /root/myapp/arthas-output/20260327-144254.html  

可将上述的输出结果文件拷贝到本地,然后通过浏览器即可查看相应的火焰图:

分析火焰图

可通过浏览器查看相应的火焰图(即前文提到的/root/myapp/arthas-output/20260327-144254.html ),具体的火焰图效果如下:


上面的火焰图看着很高,但其实最下面的都是框架级的调用代码,比如undertow、spring framework、mybaits等,并且最下层的这些行的宽度都一样,可以顺着这些行 往上看 直到看到我们需要调试的组件代码,可以发现真正涉及到数据权限相关的要从这行开始看起:
com/neusoft/oscoe/osmium/data/permission/mybatis/interceptor/DataPermissionInterceptor.intercept

这个DataPermissionInterceptor是数据权限组件的核心处理拦截器,从这行往上看才真正涉及到数据权限组件的相关处理,同时这行也是火焰图中最宽的众行之一(具体定位时可优先定位到业务相关 (而不是框架或jdk相关)、最宽的 核心实现代码)。


DataPermissionInterceptor往上看,可以发现从com/github/pagehelper/PageInterceptor.intercept开始,调用链拆分成:

  • com/github/pagehelper/PageInterceptor.count
  • com/github/pagehelper/util/ExecutorUtil.pageQuery

DataPermissionInterceptor组件之后调用了PageHelper组件的PageInterceptor,其中PageInterceptor又拆分为PageInterceptor.countExecutorUtil.pageQuery 2部分,即分别处理分页计数和具体的分页查询,如此便可基本确认PageHelper组件的PageInterceptor的执行基本占用了全部的CPU资源。


再顺着PageInterceptor.countExecutorUtil.pageQuery 往上看,可以看出满屏充斥着大量的net/sf/jsqlparser/parser/...包名前缀,即下面中的粉色部分(可通过页面的CTRL+F搜索指定关键字即可高亮显示为粉色),结论就是PageHelperPageInterceptor拦截器底层调用了jsqlparser解析sql时耗费了大量的CPU资源,此处的jsqlparser解析sql便是此次CPU高负载的根源。

分析问题代码

前文我们已经定位到PageHelperPageInterceptor拦截器底层的jsqlparser解析sql便是此次CPU高负载的根源。
PageHepler组件负责为DAO方法添加分页查询逻辑,它会通过jsqlparser解析sql并附加分页查询条件(比如limit..order by ...),如果被解析的sql过长则会出现性能问题,而出现问题的场景正是由于数据权限组件DataPermissionInterceptor给sql拼接了条件 in { 2000个部门ID}(数据权限组件底层采用了字符串拼接,所以没有jsqlparser解析慢的问题),而PageHelperPageInterceptor拦截器在其之后执行,此时的PageInterceptor拦截器便需要解析拼接了 in { 2000个部门ID}的sql语句,即解析sql字符串并转换为java对象,此处转换耗费了大量的CPU资源,导致了CPU负载飙升到100%。

解决方案

定位到了问题根源,接下来说说解决办法。既然PageHelper解析长sql有性能问题,那么我就将PageHelperPageInterceptor拦截器的执行顺序提前,即在数据权限组件DataPermissionInterceptor之前执行。如此PageHelper解析的通常都是短sql,即没有拼接数据权限条件的sql,而DataPermissionInterceptorPageHelper拼接完分页sql后再拼接数据权限相关查询条件,综上便可避免PageHelper解析长sql的性能问题。但需要额外处理的就是DataPermissionInterceptor需要自行识别limit..order by ...(需兼容众多数据库分页语法)等分页条件,并将数据权限相关的sql条件 拼接到正确的位置上(原方案DataPermissionInterceptor无需关注分页,由PageHepler统一兜底处理,有利有弊吧):

注: 关于mybatis 拦截器顺序相关的介绍可参见我的另一篇博客:
MyBatis Interceptor执行顺序详解(plugin机制、责任链模式)

sql 复制代码
select 
*
from my_table
col = 'val'
and '数据权限相关的sql条件'
limit 10, 20
order by my_name desc

经过上述调整后,再次压测CPU负载如愿降下来了,打卡下班...

相关推荐
nvvas2 小时前
IDEA安装并且使用Roo Code工具
java·ide·人工智能
菜鸟小九2 小时前
JVM垃圾回收
java·jvm·算法
曹牧3 小时前
JDK 1.6 ,无法通过安全套接字层(SSL/TLS)加密建立数据库安全连接
java·开发语言·ssl
book123_0_993 小时前
Redis四种模式在Spring Boot框架下的配置
java
IT成长史3 小时前
Windows D盘安装Docker Desktop全流程(避坑+ECR镜像推送实战)
java·docker
一定要AK3 小时前
java基础
java·开发语言·笔记
splage3 小时前
Java进阶之泛型
java·开发语言
Meepo_haha3 小时前
python爬虫——爬取全年天气数据并做可视化分析
java
xiaohe073 小时前
JAVA系统中Spring Boot 应用程序的配置文件:application.yml
java·开发语言·spring boot