# 性能分析工具的分类
性能分析的技术和工具可以分为以下几类:
- Counters
内核维护着各种统计信息,被称为Counters,用于对事件进行计数。例如,接收的网络数据包数量,发出的磁盘I/O请求,执行的系统调用次数。常见的这类工具有:
- vmstat: 虚拟和物理内存统计
- mpstat: CPU使用率统计
- iostat:磁盘的I/O使用情况
- netstat:网络接口统计信息,TCP/IP协议栈统计信息,连接统计信息
- Tracing
Tracing是收集每个事件的数据进行分析。Tracing会捕获所有的事件,因此有比较大的CPU开销,并且可能需要大量存储来保存数据。
常见的Tracing工具有:
- tcpdump: network packet tracing
- blktrace: block I/O tracing
- perf: Linux Performance Events, 跟踪静态和动态探针
- strace: 系统调用tracing
- gdb: 源代码级调试器
- Profiling
Profiling 是通过收集目标行为的样本或快照,来了解目标的特征。Profiling可以从多个方面对程序进行动态分析,如CPU、Memory、Thread、I/O等,其中对CPU进行Profiling的应用最为广泛。
CPU Profiling原理是基于一定频率对运行的程序进行采样,来分析消耗CPU时间的代码路径。可以基于固定的时间间隔进行采样,例如每10毫秒采样一次。也可以设置固定速率采样,例如每秒采集100个样本。
CPU Profiling经常被用于分析代码的热点,比如“哪个方法占用CPU的执行时间最长”、“每个方法占用CPU的比例是多少”等等,然后我们就可以针对热点瓶颈进行分析和性能优化。
Linux上常用的CPU Profiling工具有:
- perf的 record 子命令
- BPF profile
- Monitoring
系统性能监控会记录一段时间内的性能统计信息,以便能够基于时间周期进行比较。这对于容量规划,了解高峰期的使用情况都很有帮助。历史值还为我们理解当前的性能指标提供了上下文。
监控单个操作系统最常用工具是sar(system activity reporter,系统活动报告)命令。sar通过一个定期执行的agent来记录系统计数器的状态,并可以使用sar命令查看它们,例如:
|
|
本文主要讨论如何使用perf和BPF进行CPU Profiling。
# perf
perf最初是使用Linux性能计数器子系统的工具,因此perf开始的名称是Performance Counters for Linux(PCL)。perf在Linux2.6.31合并进内核,位于tools/perf目录下。
随后perf进行了各种增强,增加了tracing、profiling等能力,可用于性能瓶颈的查找和热点代码的定位。
perf是一个面向事件(event-oriented)的性能剖析工具,因此它也被称为Linux perf events (LPE),或perf_events。
perf的整体架构如下:

perf 由两部分组成:
- perf Tools:perf用户态命令,为用户提供了一系列工具集,用于收集、分析性能数据。
- perf Event Subsystem:Perf Events是内核的子系统之一,和用户态工具共同完成数据的采集。
内核依赖的硬件,比如说CPU,一般会内置一些性能统计方面的寄存器(Hardware Performance Counter),通过软件读取这些特殊寄存器里的信息,我们也可以得到很多直接关于硬件的信息。perf最初就是用来监测CPU的性能监控单元(performance monitoring unit, PMU)的。
# perf Events分类
perf支持多种性能事件:

这些性能事件分类为:
- Hardware Events: CPU性能监控计数器performance monitoring counters(PMC),也被称为performance monitoring unit(PMU)
- Software Events: 基于内核计数器的底层事件。例如,CPU迁移,minor faults,major faults等。
- Kernel Tracepoint Events: 内核的静态
Tracepoint,已经硬编码在内核需要收集信息的位置。 - User Statically-Defined Tracing (USDT): 用户级程序的静态
Tracepoint。 - Dynamic Tracing: 用户自定义事件,可以动态的插入到内核或正在运行中的程序。
Dynamic Tracing技术分为两类:
可以使用perf的list子命令查看当前可用的事件:
|
|
# perf的使用
如果还没有安装perf,可以使用apt或yum进行安装:
|
|
perf的功能强大,支持硬件计数器统计,定时采样,静态和动态tracing等。本文只介绍几个常用的使用场景,如果想全面的了解perf的使用,可以参考perf.wiki。
- CPU Statistics
使用perf的stat命令可以收集性能计数器统计信息,精确统计一段时间内 CPU 相关硬件计数器数值的变化。例如:
|
|
- CPU Profiling
可以使用perf record以任意频率收集快照。这通常用于CPU使用情况的分析。
sudo perf record -F 99 -a -g sleep 10
对所有CPU(-a)进行call stacks(-g)采样,采样频率为99 Hertz(-F 99),即每秒99次,持续10秒(sleep 10)。
sudo perf record -F 99 -a -g -p PID sleep 10
对指定进程(-p PID)进行采样。
sudo perf record -F 99 -a -g -e context-switches -p PID sleep 10
perf可以和各种instrumentation points一起使用,以跟踪内核调度程序(scheduler)的活动。其中包括software events和tracepoint event(静态探针)。
上面的例子对指定进程的上下文切换(-e context-switches)进行采样。
- report
perf record的运行结果保存在当前目录的perf.data文件中,采样结束后,我们使用perf report查看结果。
- 交互式查看模式
|
|

以+开头的行可以回车,展开详细信息。
- 使用
--stdio选项打印所有输出
|
|

context-switches的采样报告:

后面我们会介绍火焰图,以可视化的方式展示stack traces,比perf report更加直观。
# BPF
BPF是Berkeley Packet Filter的缩写,最初是为BSD开发,第一个版本于1992年发布,用于改进网络数据包捕获的性能。BPF是在内核级别进行过滤,不必将每个数据包拷贝到用户空间,从而提高了数据包过滤的性能。tcpdump使用的就是BPF。

2013年BPF被重写,被称为Extended BPF (eBPF),于2014年包含进Linux内核中。改进后的BPF成为了通用执行引擎,可用于多种用途,包括创建高级性能分析工具。
BPF允许在内核中运行mini programs,来响应系统和应用程序事件(例如磁盘I/O事件)。这种运作机制和JavaScript类似:JavaScript是运行在浏览器引擎中的mini programs,响应鼠标点击等事件。BPF使内核可编程化,使用户(包括非内核开发人员)能够自定义和控制他们的系统,以解决实际问题。
BPF可以被认为是一个虚拟机,由指令集,存储对象和helper函数三部分组成。BPF指令集由位于Linux内核的BPF runtime执行,BPF runtime包括了解释器和JIT编译器。BPF是一种灵活高效的技术,可以用于networking,tracing和安全等领域。我们重点关注它作为系统监测工具方面的应用。

和perf一样,BPF能够监测多种性能事件源,同时可以通过调用perf_events,使用perf已有的功能:

BPF可以在内核运行计算和统计汇总,这样大大减少了复制到用户空间的数据量:

BPF已经内置在Linux内核中,因此你无需再安装任何新的内核组件,就可以在生产环境中使用BPF。
# BCC和bpftrace
直接使用BPF指令进行编程非常繁琐,因此很有必要提供高级语言前端方便用户使用,于是就出现了BCC和bpftrace。

BCC(BPF Compiler Collection) 提供了一个C编程环境,使用LLVM工具链来把 C 代码编译为BPF虚拟机所接受的字节码。此外它还支持Python,Lua和C++作为用户接口。
bpftrace 是一个比较新的前端,它为开发BPF工具提供了一种专用的高级语言。bpftrace适合单行代码和自定义短脚本,而BCC更适合复杂的脚本和守护程序。
BCC和bpftrace没有在内核代码库,它们存放在GitHub上名为IO Visor的Linux Foundation项目中。
# BCC的安装
BCC可以参考官方的安装文档。以Ubuntu 18.04 LTS为例,建议从源码build安装:
- 安装build依赖
|
|
- 编译和安装
|
|
- build python3 binding
|
|
make install完成后,BCC自带的工具都安装在了/usr/share/bcc/tools目录下。BCC已经包含70多个BPF工具,用于性能分析和故障排查。这些工具都可以直接使用,无需编写任何BCC代码。

我们试用其中一个工具biolatency,跟踪磁盘I/O延迟:
|
|
biolatency展示的直方图比iostat的平均值能更好的理解磁盘I/O性能。
BCC已经自带了CPU profiling工具:
- tools/profile: Profile CPU usage by sampling stack traces at a timed interval.
此外,BCC还提供了Off-CPU的分析工具:
- tools/offcputime: Summarize off-CPU time by kernel stack trace
一般的CPU profiling都是分析on-CPU,即CPU时间都花费在了哪些代码路径。off-CPU是指进程不在CPU上运行时所花费的时间,进程因为某种原因处于休眠状态,比如说等待锁,或者被进程调度器(scheduler)剥夺了 CPU 的使用。这些情况都会导致这个进程无法运行在 CPU 上,但是仍然花费了时间。

off-CPU分析是对on-CPU的补充,让我们知道线程所有的时间花费,更全面的了解程序的运行情况。
后面会介绍profile,offcputime如何生成火焰图进行可视化分析。
# bpftrace的安装
bpftrace 建议运行在Linux 4.9 kernel或更高版本。根据安装文档的说明,是因为kprobes、uprobes、tracepoints等主要特性是在4.x以上加入内核的:
- 4.1 - kprobes
- 4.3 - uprobes
- 4.6 - stack traces, count and hist builtins (use PERCPU maps for accuracy and efficiency)
- 4.7 - tracepoints
- 4.9 - timers/profiling
可以运行scripts/check_kernel_features.sh脚本进行验证:
|
|
bpftrace对Linux的版本要求较高,以Ubuntu为例,19.04及以上才支持apt安装:
|
|
18.04和18.10可以从源码build,但需要先build好BCC。
- 安装依赖
|
|
- 编译和安装
|
|
make install完成后,bpftrace自带的工具安装在/usr/local/share/bpftrace/tools目录下,这些工具的说明文档可以在项目主页找到。
我们同样试用查看Block I/O延迟直方图的工具:
|
|
关于bpftrace脚本编写不在本文的讨论范围,感兴趣的可以参考reference_guide。
# 火焰图
火焰图是Brendan Gregg发明的将stack traces可视化展示的方法。火焰图把时间和空间两个维度上的信息融合在一张图上,将频繁执行的代码路径以可视化的形式,非常直观的展现了出来。
火焰图可以用于可视化来自任何profiler工具的记录的stack traces信息,除了用来CPU profiling,还适用于off-CPU,page faults等多种场景的分析。本文只讨论 on-CPU 和 off-CPU 火焰图的生成。
要理解火焰图,先从理解Stack Trace开始。
# Stack Trace
Stack Trace是程序执行过程中,在特定时间点的函数调用列表。例如,func_a()调用func_b(),func_b()调用func_c(),此时的Stack Trace可写为:
|
|
# Profiling Stack Traces
我们做CPU profiling时,会使用perf或bcc定时采样Stack Trace,这样会收集到非常多的Stack Trace。前面介绍了perf report会将Stack Trace样本汇总为调用树,并显示每个路径的百分比。火焰图是怎么展示的呢?
考虑下面的示例,我们用perf定时采样收集了多个Stack Trace,然后将相同的Stack Trace归纳合并,统计出次数:
|
|
可以看到,总共收集了10个样本,其中代码路径func_a->func_b->func_c有7次,该路径上的func_c在CPU上运行。 func_a->func_b进行了两次采样,func_b在CPU上运行。func_a->func_b->func_d->func_e一次采样,func_e在CPU上运行。
# 火焰图
根据前面对Stack Trace的统计信息,可以绘制出如下的火焰图:

火焰图具有以下特性:
- 每个长方块代表了函数调用栈中的一个函数
- Y 轴显示堆栈的深度(堆栈中的帧数)。调用栈越深,火焰就越高。顶层方块表示 CPU 上正在运行的函数,下面的函数即为它的祖先。
- X 轴的宽度代表被采集的样本数量,越宽表示采集到的越多,即执行的时间长。需要注意的是,X轴从左到右不代表时间,而是所有的调用栈合并后,按字母顺序排列的。
拿到火焰图,寻找最宽的塔并首先了解它们。顶层的哪个函数占据的宽度最大,说明它可能存在性能问题。
可以使用Brendan Gregg开发的开源项目FlameGraph生成交互式的SVG火焰图。该项目提供了脚本,可以将采集的样本归纳合并,统计出Stack Trace出现的频率,然后使用flamegraph.pl生成SVG火焰图。
我们先把FlameGraph项目clone下来,后面会用到:
|
|
# Java CPU Profiling
虽然有很多Java专用的profiler工具,但这些工具一般只能看到Java方法的执行,缺少了GC,JVM的CPU时间消耗,并且有些工具的Method tracing性能损耗比较大。
perf和BCC profile的优点是它很高效,在内核上下文中对堆栈进行计数,并能完整显示用户态和内核态的CPU使用,能看到native libraries(例如libc),JVM(libjvm),Java方法和内核中花费的时间。

但是,perf和BCC profile这种系统级的profiler不能很好地与Java配合使用,它们识别不了Java方法和stack traces。这是因为:
- JVM的
JIT(just-in-time)没有给系统级profiler公开符号表 - JVM还使用帧指针寄存器(frame pointer register,x86-64上的RBP)作为通用寄存器,打破了传统的堆栈遍历
为了能生成包含Java栈与Native栈的火焰图,目前有两种解决方式:
- 使用
JVMTIagent perf-map-agent,生成Java符号表,供perf和bcc读取(/tmp/perf-PID.map)。同时要加上-XX:+PreserveFramePointerJVM 参数,让perf可以遍历基于帧指针(frame pointer)的堆栈。 - 使用async-profiler,该项目将
perf的堆栈追踪和JDK提供的AsyncGetCallTrace结合了起来,同样能够获得mixed-mode火焰图。同时,此方法不需要启用帧指针,所以不用加上-XX:+PreserveFramePointer参数。
下面我们就分别演示这两种方式。
# perf-map-agent
perf期望能从/tmp/perf-<pid>.map中获得在未知内存区域执行的代码的符号表。perf-map-agent可以为JIT编译的方法生成/tmp/perf-<pid>.map文件,以满足perf的要求。
首先下载并编译perf-map-agent:
|
|
#
配合perf使用
perf-map-agent提供了perf-java-flames脚本,可以一步生成火焰图。
perf-java-flames接收perf record命令参数,它会调用perf进行采样,然后使用FlameGraph生成火焰图,一步完成,非常方便。
注意,记得要给被profiling的Java进程加上-XX:+PreserveFramePointer JVM 参数。
设置必要的环境变量:
|
|
./bin/perf-java-flames [PID] -F 99 -a -g -p [PID]
对指定进程(-p PID),在所有CPU(-a)上进行call stacks(-g)采样,采样频率为99 Hertz (-F 99),持续时间为PERF_RECORD_SECONDS秒。命令运行完成后,会在当前目录生成名为flamegraph-pid.svg的火焰图。

./bin/perf-java-flames [PID] -F 99 -g -a -e context-switches -p [PID]
对指定进程的上下文切换(-e context-switches)进行采样,并生成火焰图。
- 当然也可以只为
perf生成Java符号表,然后直接使用perf采样
|
|
#
配合bcc profile使用
FlameGraph项目提供了jmaps脚本,它会调用perf-map-agent为当前运行的所有Java进程生成符号表。
首先为jmaps脚本设置好JAVA_HOME和perf-map-agent的正确位置:
|
|
运行jmaps,可以看到它会为当前所有的Java进程生成符号表:
|
|
我们在做任何profiling之前,都需要调用jmaps,保持符号表是最新的。
- CPU Profiling火焰图
|
|
- off-CPU火焰图
|
|
- off-CPU,并过滤指定的进程状态
Linux的进程状态有:
| 状态 | 描述 |
|---|---|
| TASK_RUNNING | 意味着进程处于可运行状态。这并不意味着已经实际分配了CPU。进程可能会一直等到调度器选中它。该状态确保进程可以立即运行,而无需等待外部事件。 |
| TASK_INTERRUPTIBLE | 可中断的等待状态,主要为恢复时间无法预测的长时间等待。例如等待来自用户的输入。 |
| TASK_UNINTERRUPTIBLE | 不可中断的等待状态。用于因内核指示而停用的睡眠进程。它们不能由外部信号唤醒,只能由内核亲自唤醒。例如磁盘输入输出等待。 |
| TASK_STOPPED | 响应暂停信号而运行中断的状态。直到恢复前都不会被调度 |
| TASK_ZOMBIE | 僵尸状态,子进程已经终止,但父进程尚未执行wait(),因此该进程的资源没有被系统释放。 |
在状态TASK_RUNNING(0)会发生非自愿上下文切换,而我们通常感兴趣的阻塞事件是TASK_INTERRUPTIBLE(1)或TASK_UNINTERRUPTIBLE(2),offcputime可以用--state过滤指定的进程状态:
|
|
# async-profiler
async-profiler将perf的堆栈追踪和JDK提供的AsyncGetCallTrace结合了起来,做到同时采样Java栈与Native栈,因此也就可以同时分析Java代码和Native代码中存在的性能热点。
AsyncGetCallTrace是JDK内部提供的一个函数,它的原型如下:
|
|
可以看出,该函数直接通过ucontext就能获取到完整的Java调用栈。
# async-profiler的使用
下载并解压好async-profiler安装包。
从Linux 4.6开始,从non-root进程使用perf捕获内核的call stacks,需要设置如下两个内核参数:
|
|
async-profiler的使用非常简单,一步就能生成火焰图。另外,也不需要为被profiling的Java进程设置-XX:+PreserveFramePointer参数。
|
|

# 总结
为Java生成CPU profiling火焰图,基本的流程都是:
- 使用工具采集样本
- 使用
FlameGraph项目提供的脚本,将采集的样本归纳合并,统计出Stack Trace出现的频率 - 最后使用
flamegraph.pl利用上一步的输出,绘制SVG火焰图
为了能够生成Java stacks和native stacks完整的火焰图,解决perf和bcc profile不能识别Java符号和Java stack traces的问题,目前有以下两种方式:
perf-map-agent加上perf或bcc profileasync-profiler(内部会使用到perf)
如果只是对Java进程做on-CPU分析,async-profiler更加方便好用。如果需要更全面的了解Java进程的运行情况,例如分析系统锁的开销,阻塞的 I/O 操作,以及进程调度器(scheduler)的工作,那么还是需要使用功能更强大的perf和bcc。