在众多性能调优工具中,火焰图(Flame Graph)以直观和全局的视角,为开发人员提供了关于程序运行时性能问题的深刻见解。这一工具最初由 Linux 性能优化大师 Brendan Gregg 发明,后在众多的开源项目中大量使用,它使得开发人员能够准确快速定位问题和解决性能瓶颈,是必不可少的性能分析工具。
火焰图是通过对线程/进程的运行状态采样统计,并将采样结果以直观的矢量图的形式展现的一种图形化性能分析工具,图1 所示为官方网站对某 mysql 服务的性能分析的火焰图结果实例,其中:
每一列代表一个调用栈,每一个格子代表一个函数。
纵轴表示调用栈的深度,按照调用关系从下到上排列。最顶上格子代表采样时,调用栈当前执行到的函数。
1. 优势
传统的工具往往会提供大量的细节信息和数据,数据结构复杂且数量庞大,开发人员需要主动过滤一些并不重要的细节,难以把握全局的性能状况,并且各函数之间的调用关系显示不够直观,难以快速分析调用栈等相关信息,在性能分析过程中需要花费大量的时间和精力。
性能瓶颈识别:火焰图通过展示函数调用栈和时间分布情况,来帮助开发人员快速识别出高耗 CPU 时间的函数或高 IO 的阻塞位置,从而快速定位性能瓶颈位置。
多线程并发问题诊断:在多线程环境中,火焰图可以展示各个线程之间的调用关系和时间占用情况,帮助开发者发现线程竞争、死锁等并发问题。
系统性能调优:火焰图用于分析操作系统、数据库、网络等各个大型软件系统的性能瓶颈,快速定位系统性的性能瓶颈位置,对开发人员进行针对性的优化具有重要的指导作用。
内存性能分析:除了 CPU 性能分析外,火焰图还可以用于内存性能分析。通过 Memory 类型的火焰图,可以找出内存占用较高的函数或数据结构,进而优化内存使用。
代码审查和优化建议:在进行代码审查时,可以使用火焰图来检查代码的性能表现。通过查看火焰图,可以发现代码中可能存在的性能问题,从而提出针对性的优化建议。
Flink 是目前最流行的大数据及流式计算框架之一,用户可以使用 Java 等多种语言的 DataStream 接口或者标准 SQL 语言来快速实现一个分布式高可用的流式应用,并具有完整的 Source、Sink、WebUI、Metrics 等功能集成,这让 Flink 成为了流式计算的事实标准之一,在流式计算中具有重要意义。但是在处理海量数据时,各种异常和性能瓶颈是无可避免的,Flink 从 1.13 版本开始便内置了火焰图分析功能,利用 Profiling 技术采样 Flink 任务的实时运行情况并绘制火焰图,这对开发人员进行性能分析与优化工作具有重要帮助,是数据开发人员的必备技能。
1. 基础知识
Flink 集成的火焰图主要针对 TaskManager 上线程的 CPU 占用情况进行采样分析,图2为 TaskManager 上线程状态转换图,依据该图的线程状态将采样信息分为 On-CPU 和 Off-CPU 两个维度,并基于这两个维度将 Flink 的火焰图分为如下三类。
On-CPU:通过固定频率采样 CPU 的调用栈信息,分析进程/线程运行在 CPU 上的时间占用情况和比例,对应于进程/线程的 NEW 和 RUNNABLE 状态,主要用于找出 CPU 占用较高的问题函数。
Off-CPU:通过固定频率采样阻塞事件调用栈信息,分析运行线程 I/O、锁、计数器、换页等事件阻塞的时间情况和比例,对应于进程/线程的 TIMED_WAITING、BLOCKED、WAITING三种阻塞状态。
env.addSource(new FlinkKafkaConsumer<String>(SOURCE_TOPIC, new SimpleStringSchema(), properties))
.name("Consumer kafka topic")
.returns(String.class)
.setParallelism(2)
.map(HbaseInfoRecord::fromStr)
.name("Transform to HbaseInfoRecord")
.map(new RowkeyGeneratorFunction())
.name("Generate rowkey")
.map(HbaseInfoRecord::toJsonStr)
.name("Deserialize to json str")
.addSink(new FlinkKafkaProducer<>(SINK_TOPIC, new SimpleStringSchema(), properties))
.name("Sink to topic");
env.execute();
(2)问题发现
经过长时间的运行发现,输出 topic 被下游正常消费不存在积压,作业上游 topic 数据存在积压,topic 的 LAG 较大,说明该作业可能存在性能瓶颈,数据消费速度较慢,并通过作业的 DashBoard 页面发现,在数据量高峰时该作业存在较严重的反压,如图3 所示。
(图3 优化前算子运行状态图)
选择一个流量低峰时段重启作业并开启作业火焰图功能(Flink 默认关闭该功能,可以通过 “rest.flamegraph.enabled=true” 参数设置开启火焰图功能)。
开启后,观察火焰图信息,如图4 所示。通过对 Mixed 的图形观察,发现存在大量的热代码,Off-CPU 的占比较少,而 On-CPU 所占比例较大,并且自定义代码中 “com.ppdai.flamegraph.RowkeyGeneratorFunction” 的 map 方法所占宽度较宽,说明该方法是高消耗 CPU 的方法。
(图4 优化前算子火焰图)
public HbaseInfoRecord map(HbaseInfoRecord hbaseInfoRecord) throws NoSuchAlgorithmException {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(String.valueOf(hbaseInfoRecord.getUserId()).getBytes(StandardCharsets.UTF_8));
hbaseInfoRecord.setRowkey(new String(hash));
return hbaseInfoRecord;
}
public HbaseInfoRecord map(HbaseInfoRecord hbaseInfoRecord) {
String userId = String.valueOf(hbaseInfoRecord.getUserId());
hbaseInfoRecord.setRowkey(Hashing.murmur3_32().hashString(userId, StandardCharsets.UTF_8).toString() + "_" + userId);
return hbaseInfoRecord;
}
(图5 优化后算子运行状态图)
作业运行时的火焰图如图6 所示,从该图中可以看出,原先热代码“com.ppdai.flamegraph.RowkeyGeneratorFunction”的 map 方法占 CPU 的时间比例从 70% 降低至 39%,作业运行稳定,Checkpoint 定期执行无报错,作业优化效果达到预期,满足业务要求。
在日常软件开发过程中,大家都会或多或少遇到各种性能问题,有的甚至无从下手。本文从火焰图的基本知识进行介绍,并以一个具体的 Flink 任务来讲解如何通过火焰图发现性能瓶颈,希望对各位开发人员有所帮助。并推荐各位开发人员熟练掌握这一性能分析利器,望在需要进行性能瓶颈分析时可以做到思路清晰,事半功倍,共勉!