「如何实现流动式软件发布」线上课堂开课啦,快来报名参与课堂抽奖吧~ 了解详情
写点什么

性能调优利器:火焰图

2020 年 6 月 17 日

性能调优利器:火焰图

本文主要分享火焰图使用技巧,介绍 systemtap 的原理机制,如何使用火焰图快速定位性能问题原因,同时加深对 systemtap 的理解。


让我们回想一下,曾经作为编程新手的我们是如何调优程序的?通常是在没有数据的情况下依靠主观臆断来瞎蒙,稍微有些经验的同学则会对差异代码进行二分或者逐段调试。这种定位问题的方式不仅耗时耗力,而且还不具有通用性,当遇到其他类似的性能问题时,需要重复踩坑、填坑,那么如何避免这种情况呢?


俗语有曰:兵欲善其事必先利其器,个人认为,程序员定位性能问题也需要一件“利器”。 如同医生给病人看病,需要依靠专业的医学工具(比如 X 光片、听诊器等)进行诊断,最后依据医学工具的检验结果快速精准的定位出病因所在。性能调优工具(比如 perf / gprof 等)之于性能调优就像 X 光之于病人一样,它可以一针见血的指出程序的性能瓶颈。


但是常用的性能调优工具 perf 等,在呈现内容上只能单一的列出调用栈或者非层次化的时间分布,不够直观。这里我推荐大家配合使用火焰图,它将 perf 等工具采集的数据呈现得更为直观。


初识火焰图

火焰图(Flame Graph)是由 Linux 性能优化大师 Brendan Gregg 发明的,和所有其他的 profiling 方法不同的是,火焰图以一个全局的视野来看待时间分布,它从底部往顶部,列出所有可能导致性能瓶颈的调用栈。



火焰图整个图形看起来就像一个跳动的火焰,这就是它名字的由来。


火焰图有以下特征(这里以 on-cpu 火焰图为例):


  • 每一列代表一个调用栈,每一个格子代表一个函数

  • 纵轴展示了栈的深度,按照调用关系从下到上排列。最顶上格子代表采样时,正在占用 cpu 的函数。

  • 横轴的意义是指:火焰图将采集的多个调用栈信息,通过按字母横向排序的方式将众多信息聚合在一起。需要注意的是它并不代表时间。

  • 横轴格子的宽度代表其在采样中出现频率,所以一个格子的宽度越大,说明它是瓶颈原因的可能性就越大。

  • 火焰图格子的颜色是随机的暖色调,方便区分各个调用信息。

  • 其他的采样方式也可以使用火焰图, on-cpu 火焰图横轴是指 cpu 占用时间,off-cpu 火焰图横轴则代表阻塞时间。

  • 采样可以是单线程、多线程、多进程甚至是多 host,进阶用法可以参考附录进阶阅读


火焰图类型

常见的火焰图类型有 On-CPU,Off-CPU,还有 Memory,Hot/Cold,Differential 等等。他们分别适合处理什么样的问题呢?


这里笔者主要使用到的是 On-CPU、Off-CPU 以及 Memory 火焰图,所以这里仅仅对这三种火焰图作比较,也欢迎大家补充和斧正。



火焰图类型横轴含义纵轴含义解决问题采样方式
cpu 火焰图cpu占用时间调用栈找出 cpu 占用高的问题函数;分析代码热路径固定频率采样 cpu 调用栈
off-cpu 火焰图阻塞时间调用栈i/o、网络等阻塞场景导致的性能下降;锁竞争、死锁导致的性能下降问题固定频率采样 阻塞事件调用栈
内存火焰图内存申请/释放函数调用次数调用栈内存泄露问题;内存占用高的对象/申请内存多的函数;虚拟内存或物理内存泄露问题有四种方式: 跟踪malloc/free;跟踪brk;跟踪mmap;跟踪页错误
Hot/Cold 火焰图on-CPU 火焰图和 off-CPU 火焰图结合在一起综合展示调用栈需要结合 cpu 占用以及阻塞分析的场景;off-CPU 火焰图无法直观判断问题的场景on-CPU 火焰图和 off-CPU 火焰图结合


火焰图分析技巧

  1. 纵轴代表调用栈的深度(栈桢数),用于表示函数间调用关系:下面的函数是上面函数的父函数。

  2. 横轴代表调用频次,一个格子的宽度越大,越说明其可能是瓶颈原因。

  3. 不同类型火焰图适合优化的场景不同,比如 on-cpu 火焰图适合分析 cpu 占用高的问题函数,off-cpu 火焰图适合解决阻塞和锁抢占问题。

  4. 无意义的事情:横向先后顺序是为了聚合,跟函数间依赖或调用关系无关;火焰图各种颜色是为方便区分,本身不具有特殊含义

  5. 多练习:进行性能优化有意识的使用火焰图的方式进行性能调优(如果时间充裕)


如何绘制火焰图?

要生成火焰图,必须要有一个顺手的动态追踪工具,如果操作系统是 Linux 的话,那么通常通常是 perf 或者 systemtap 中的一种。其中 perf 相对更常用,多数 Linux 都包含了 perf 这个工具,可以直接使用;SystemTap 则功能更为强大,监控也更为灵活。网上关于如何使用 perf 绘制火焰图的文章非常多而且丰富,所以本文将以 SystemTap 为例。


SystemTap 是动态追踪工具,它通过探针机制,来采集内核或者应用程序的运行信息,从而可以不用修改内核和应用程序的代码,就获得丰富的信息,帮你分析、定位想要排查的问题。SystemTap 定义了一种类似的 DSL 脚本语言,方便用户根据需要自由扩展。不过,不同于动态追踪的鼻祖 DTrace ,SystemTap 并没有常驻内核的运行时,它需要先把脚本编译为内核模块,然后再插入到内核中执行。这也导致 SystemTap 启动比较缓慢,并且依赖于完整的调试符号表。


使用 SystemTap 绘制火焰图的主要流程如下:


  • 安装 SystemTap 以及 操作系统符号调试表

  • 根据自己所需绘制的火焰图类型以及进程类型选择合适的脚本

  • 生成内核模块

  • 运行 SystemTap 或者运行生成的内核模块统计数据

  • 将统计数据转换成火焰图


本文演示步骤将会基于操作系统 Tlinux 2.2


安装 SystemTap 以及 操作系统符号调试表

使用 yum 工具安装 systemtap:


yum install systemtap systemtap-runtime
复制代码


由于 systemtap 工具依赖于完整的调试符号表,而且生产环境不同机器的内核版本不同(虽然都是 Tlinux 2.2 版本,但是内核版本后面的小版本不一样,可以通过 uname -a 命令查看)所以我们还需要安装 kernel-debuginfo 包、 kernel-devel 包


我这里是安装了这两个依赖包


kernel-devel-3.10.107-1-tlinux2-0046.x86_64kernel-debuginfo-3.10.107-1-tlinux2-0046.x86_64
复制代码


根据自己所需绘制的火焰图类型以及进程类型选择合适的脚本

使用 SystemTap 统计相关数据往往需要自己依照它的语法,编写脚本,具有一定门槛。幸运的是,github 上春哥(agentzh)开源了两组他常用的 SystemTap 脚本:openresty-systemtap-toolkit 和 stapxx,这两个工具集能够覆盖大部分 C 进程、nginx 进程以及 Openresty 进程的性能问题场景。


我们这里需要绘制 off-cpu 火焰图,所以使用 sample-bt-off-cpu 脚本即可


生成内核模块

现在我们有了统计脚本,也安装好了 systemtap,正常来说就可以使用了,但由于 systemtap 是通过生成内核模块的方式统计相关探针的统计数据,而 tlinux 要求所有运行的内核模块需要先到 tlinux 平台签名才可以运行,所以:


故需要先修改 off-cpu 脚本,让其先生成内核模块;之后对该内核模块作签名;最后使用 systemtap 命令手工运行该脚本,统计监控数据


Systemtap 执行流程如下:



  • parse:分析脚本语法

  • elaborate:展开脚本 中定义的探针和连接预定义脚本库,分析内核和内核模块的调试信息

  • translate:.将脚本编译成 c 语言内核模块文件放 在 $HOME/xxx.c 缓存起来,避免同一脚本多次编译

  • build:将 c 语言模块文件编译成.ko 的内核模块,也缓存起来。

  • 把模块交给 staprun,staprun 加载内核模块到内核空间,stapio 连接内核模块和用户空间,提供交互 IO 通道,采集数据。


所以我们这里修改下 off-cpu 的 stap 脚本,让其只运行完第四阶段,只生成一个内核模块


// 在 stap 命令后增加 -p4 参数,告诉systemtap,当前只需要执行到第四阶段open my $in, "|stap -p4 --skip-badvars --all-modules -x $pid -d '$exec_path' --ldd $d_so_args $stap_args -"or die "Cannot run stap: $!\n";
复制代码


修改好之后运行脚本,会生成一个内核模块


// -p 8682 是需要监控的进程的进程号// -t 30 是指会采样30秒./sample-bt-off-cpu -p 8692 -t 30
复制代码


生成的内核模块名称形如 stap_xxxxx.ko模块名称


由于读者并不需要关心内核模块签名,故章节略过


运行内核模块统计数据

内核模块签名完成后,便可以使用 staprun 命令手工运行相关内核模块了


命令:


// 注意:签名脚本会将生产的内核模块重命名,需要将名字改回去……(脚本bug)staprun -x {进程号} {内核模块名} > demo.bt
复制代码


值得注意的是,监控的进程要有一定负载 systemtap 才可以采集到相关数据,即在采集时,同时需要要有一定请求量(通常是自己构造请求,压测进程)


将统计数据转换成火焰图

获得了统计数据 demo.bt 后,便可以使用火焰图工具绘制火焰图了


下载 FlameGraph,链接:https://github.com/brendangregg/FlameGraph


命令:


./stackcollapse-stap.pl demo.bt > demo.folded./flamegraph.pl demo.folded > demo.svg
复制代码


这样便获得了 off-cpu 火焰图:



看图说话

趁热打铁,通过几张火焰图熟悉下如何使用火焰图


图片来自于春哥微博或者个人近期定位的问题


on-cpu 火焰图

Apache APISIX QPS 急剧下降问题


Apache APISIX 是一个开源国产的高性能 API 网关,之前在进行选型压测时,发现当 Route 匹配不中场景下, QPS 急剧下降,在其 CPU (四十八核)占用率几乎达到 100%的情况下只有几千 QPS,通过绘制火焰图发现,其主要耗时在一个 table 插入阶段(lj_cf_table_insert),分析代码发现是该 table 一直没有释放,每次匹配不中路由会插入数据,导致表越来越大,后续插入耗时过长导致 QPS 下降。


off-cpu 火焰图

nginx 互斥锁问题


这是一张 nginx 的 off-cpu 火焰图,我们可以很快锁定到 ngx_common_set_cache_fs_size -> ngx_shmtx_lock -> sem_wait 这段逻辑使用到了互斥锁,它让 nginx 进程绝大部分阻塞等待时间花费在获取该锁。


agent 监控上报断点问题


这是一张 agent 的 off-cpu 火焰图,它是一个多线程异步事件模型,主线程处理各个消息,多个线程分别负责配置下发或者监控上报的职责。当前问题出现在监控上报性能差,无法在周期(一分钟)内完成监控数据上报,导致监控断点,通过 off-cpu 火焰图我们可以分析出,该上报线程花费了大量的时间使用 curl_easy_perform 接口收发 http 监控数据消息中。


依据火焰图将发送 http 消息的逻辑改为异步非阻塞后,该问题解决。


附录

进阶阅读


FAQ

使用 perf 或者 systemtap 的方式采集数据,会对后台服务有性能影响吗?


有,但是很小,可以基本忽略不计。


它们使用系统的探针或者使用一些自定义的动态探针进行数据采集,第一对代码无侵入性,它既不需要停止服务,也不需要修改应用程序的代码;第二,它们是以内核模块/内核原生的方式跟踪用户态和内核态的所有事件,并通过一系列优化措施,进行采样统计,对目标服务性能影响极小,大概在 5%左右或者更低的性能损耗。相较于将进程运行在沙箱的 valgrind 工具或静态调试工具 gdb 来说,动态追踪 perf 或者 systemtap 或者 ebpf 的性能损耗基本可以忽略不计。


目标进程重启后,systemtap 是否需要重新生成内核模块?


不需要。甚至同一个 Linux 内核版本下的同一个二进制进程(md5 值一致),在安装 kernel 调试符号表后,便可以在生成采集指标的内核模块,并且可以多次使用。


当 Linux 内核版本不一致,符号表有变化,需要重新生成内核模块;当目标进程二进制文件重新编译后,也需要重新生成统计用的 systemtap 内核模块。


2020 年 6 月 17 日 18:146641

评论

发布
暂无评论
发现更多内容

多线程与高并发之锁

彭阿三

多线程 多线程与高并发

oeasy教您玩转linux 010211 牛说 cowsay

o

阿里培训官给新入职程序员的25条建议

Java架构师迁哥

藏在Java数组的背后,你可能忽略的知识点

Java架构师迁哥

一点 Go Web 编程实践经验

Garfield

go Go web

从新浪数字化转型,窥见互联网的“懂行”新十年

脑极体

LeetCode题解:232. 用栈实现队列,使用两个栈 入队 - O(1),出队 - 摊还复杂度 O(1),JavaScript,详细注释

Lee Chen

LeetCode 前端进阶训练营

宅家三个月玩转算法,再战字节跳动,字节跳动面试官朝我比了个“ok”

云流

编程 字节跳动 算法 Java 面试

这篇文章,把中国科技的真实底子讲透了

CECBC区块链专委会

中国 科技 产业竞争

甲方日常 13

句子

工作 随笔杂谈 日常

架构师训练营第十四周

Melo

【高并发】Redis如何助力高并发秒杀系统,看完这篇我彻底懂了!!

冰河

redis 多线程 高并发 秒杀 电商超卖

面试必问亿级流量优化策略之JVM调优,文档视频面试,还不收藏

小Q

Java 程序员 架构 JVM jvm调优

一文解开java中字符串编码的小秘密

程序那些事

java安全编码 java编码指南 UTF编码

基于Goc的Golang代码VSCode实时染色方案

大卡尔

go 测试覆盖率 精准测试

区块链赋能市场监管 浙江上线“黑科技”清除取证固证难题

CECBC区块链专委会

区块链 市场监管 取证难题

合约跟单模式系统开发,交易所合约跟单源码

13530558032

拥抱K8S系列-06-K8S如何解决docker部署的问题

张无忌

Docker Kubernetes 运维 service

大数据任务调度 - 有向无环图(DAG)之拓扑排序

海豚调度

数据结构 大数据任务调度 DAG 拓扑排序 Apache DolphinScheduler

不要以为Bug写的好就是好程序员,其实这只占不到15%

小Q

Java 学习 程序员 架构 面试

从外卖员到拼多多30K+程序员,我付出了三年的青春

小Q

Java 学习 程序员 架构 面试

.NET委托,事件和Lambda表达式

AI代笔

JavaScript引擎的事件循环机制是怎样工作的?

Walker

Java 前端 运行时栈帧 事件循环

[翻译]Scalable Go Scheduler Design Doc[Go可扩展调度设计文档]

卓丁

golang golang scheduler Go scheduler

央行数研所推出贸易金融区块链平台

CECBC区块链专委会

区块链 金融

区块链技术发展的十大趋势

CECBC区块链专委会

区块链 金融 安全问题

澳门金沙玩赢了钱提现不了异常注单未更新有什么办法处理?

丛林里的余光

异常检测 澳门金沙 提现不了

朱嘉明:全球科技革命正在逼近“奇点”,区块链影响未来人类社会的走向

CECBC区块链专委会

人工智能 科技 科技革命

记一种spring框架的想当然但错误的用法

小明同学

源码分析 最佳实践 Spring Framework bug

LeetCode题解:232. 用栈实现队列,使用两个栈 入队 - O(n), 出队 - O(1),JavaScript,详细注释

Lee Chen

前端进阶训练营

Dubbo-go应用维度注册模型

apache/dubbo-go

dubbo dubbo-go dubbogo

性能调优利器:火焰图-InfoQ