一、使用perf工具时,看到的是16进制地址而不是函数名?

在 CentOS 系统中,使用 perf 工具看不到函数名,只能看到一些 16 进制格式的函数地址。

观察perf界面最下面一行,就会发现一个警告信息:

Failed to open /opt/bitnami/php/lib/php/extensions/opcache.so, continuing without symbols

说明,perf找不到待分析进程依赖的库。

这个问题,其实也是在分析 Docker 容器应用时,容器应用依赖的库都在镜像里面。

1.1、解决方法

1.1.1、法一:

在容器外面构建相同路径的依赖库。这种方法从原理上可行,但是并不推荐,一方面是因为找出这些依赖库比较麻烦,更重要的是,构建这些路径,会污染容器主机的环境。

1.1.2、法二:

在容器内部运行 perf。不过,这需要容器运行在特权模式下,但实际的应用程序往往只以普通容器的方式运行。所以,容器内部一般没有权限执行 perf 分析。

比方说,在普通容器内部运行 perf record ,将会看到下面这个错误提示:

$ perf_4.9 record -a -g
perf_event_open(..., PERF_FLAG_FD_CLOEXEC) failed with unexpected error 1 (Operation not permitted)
perf_event_open(..., 0) failed unexpectedly with error 1 (Operation not permitted)

当然,其实还可以通过配置 ​​/proc/sys/kernel/perf_event_paranoid​​ (比如改成 -1),来允许非特权用户执行 perf 事件分析。

为了安全起见,这种方法也不推荐

1.1.3、法三:

指定符号路径为容器文件系统的路径

$ mkdir /tmp/foo
$ PID=$(docker inspect --format {{.State.Pid}} phpfpm)
$ bindfs /proc/$PID/root /tmp/foo
$ perf report --symfs /tmp/foo

# 使用完成后不要忘记解除绑定
$ umount /tmp/foo/
  • 注意
    ​​​bindfs​​​这个工具需要额外安装。bindfs 的基本功能是实现目录绑定(类似于 mount --bind),这里需要你安装的是 1.13.10 版本(这也是它的最新发布版)。
    如果是旧版本,可以​​Github​​上下载源码,编译安装。

1.1.4、法四:

在容器外面把分析纪录保存下来,再去容器里查看结果。这样,库和符号的路径也就都对了。

如:

先运行​​perf record -g -p <pid>​​​,执行一会(比如15秒)后,按​​Ctrl + C​​停止。

然后把生成的perf.data文件,拷贝到容器里面来分析

$ docker cp perf.data phpfpm:/tmp 
$ docker exec -i -t phpfpm bash

接下来,在容器的 bash 中继续运行下面的命令,安装 perf 并使用 ​​perf report​​ 查看报告

$ cd /tmp/ 
$ apt-get update && apt-get install -y linux-tools linux-perf procps
$ perf_4.9 report

注意:

  1. perf工具的版本问题。在最后一步中,运行的工具是容器内部安装的版本 perf_4.9,而不是普通的 perf 命令。这是因为, perf 命令实际上是一个软连接,会跟内核的版本进行匹配,但镜像里安装的 perf 版本跟虚拟机的内核版本有可能并不一致。
  2. php-fpm 镜像是基于 Debian 系统的,所以安装 perf 工具的命令,跟 Ubuntu 也并不完全一样。比如, Ubuntu 上的安装方法是下面这样
$ apt-get install -y linux-tools-common linux-tools-generic linux-tools-$(uname -r)

在php-fpm容器里,执行下面命令来安装perf:

$ apt-get install -y linux-perf

当按照前面几种方法操作后,就可以在容器内部看到sqrt堆栈

如何用perf工具分析Java程序_perf工具

实际上,抛开案例,即使在非容器化应用中,也可能会碰到这个问题,假如,应用程序在编译时,使用strip删除了ELF二进制文件的符号表,那么同样也只能看到函数的地址。

现在的磁盘空间,其实已经足够大。保留这些符号,虽然会导致编译后的文件变大,当对整个磁盘空间来说已经不是什么大问题。所以为了调试方便,建议保留它们。

二、如何用perf工具分析Java程序?

像是 Java 这种通过 JVM 来运行的应用程序,运行堆栈用的都是 JVM 内置的函数和堆栈管理。所以,从系统层面你只能看到 JVM 的函数堆栈,而不能直接得到 Java 应用程序的堆栈。

perf_events 实际上已经支持了 JIT,但还需要一个 ​​/tmp/perf-PID.map​​​ 文件,来进行符号翻译。当然,开源项目 ​​perf-map-agent​​ 可以帮你生成这个符号表。

此外,为了生成全部调用栈,你还需要开启 JDK 的选项 ​​-XX:+PreserveFramePointer​​​。涉及到大量Java知识这里不展开说,如果你的应用刚好基于 Java ,那么你可以参考 Netflix 的技术博客 ​​Java in Flames​​ ,来查看详细的使用步骤。

三、为什么perf报告中,很多符号都是不显示调用栈?

perf report 是一个可视化展示 perf.data 的工具。在​​这个案例​​中,直接给出了最终结果,并没有详细介绍它的参数。

如何用perf工具分析Java程序_perf工具_02

这个界面可以清楚看到,perf report 的输出中,只有 swapper 显示了调用栈,其他所有符号都不能查看堆栈情况,包括我案例中的 app 应用。

这种情况以前也遇到过,当你发现性能工具的输出无法理解时,应该怎么办呢?当然还是查工具的手册。比如,可以执行 man perf-report 命令,找到 -g 参数的说明:

-g, --call-graph=<print_type,threshold[,print_limit],order,sort_key[,branch],value> 
Display call chains using type, min percent threshold, print limit, call order, sort key, optional branch and value. Note that
ordering is not fixed so any parameter can be given in an arbitrary order. One exception is the print_limit which should be
preceded by threshold.

print_type can be either:
- flat: single column, linear exposure of call chains.
- graph: use a graph tree, displaying absolute overhead rates. (default)
- fractal: like graph, but displays relative rates. Each branch of
the tree is considered as a new profiled object.
- folded: call chains are displayed in a line, separated by semicolons
- none: disable call chain display.

threshold is a percentage value which specifies a minimum percent to be
included in the output call graph. Default is 0.5 (%).

print_limit is only applied when stdio interface is used. It's to limit
number of call graph entries in a single hist entry. Note that it needs
to be given after threshold (but not necessarily consecutive).
Default is 0 (unlimited).

order can be either:
- callee: callee based call graph.
- caller: inverted caller based call graph.
Default is 'caller' when --children is used, otherwise 'callee'.

sort_key can be:
- function: compare on functions (default)
- address: compare on individual code addresses
- srcline: compare on source filename and line number

branch can be:
- branch: include last branch information in callgraph when available.
Usually more convenient to use --branch-history for this.

value can be:
- percent: diplay overhead percent (default)
- period: display event period
- count: display event count

通过这个说明可以看到,-g 选项等同于 --call-graph,它的参数是后面那些被逗号隔开的选项,意思分别是输出类型、最小阈值、输出限制、排序方法、排序关键词、分支以及值的类型。

这里默认的参数是 graph,0.5,caller,function,percent

现在再回过头来看我们的问题,堆栈显示不全,相关的参数当然就是最小阈值 threshold。通过手册中对 threshold 的说明,我们知道,当一个事件发生比例高于这个阈值时,它的调用栈才会显示出来。

threshold 的默认值为 0.5%,也就是说,事件比例超过 0.5% 时,调用栈才能被显示。再观察我们案例应用 app 的事件比例,只有 0.34%,低于 0.5%,所以看不到 app 的调用栈就很正常了。

这种情况下,只需要给 perf report 设置一个小于 0.34% 的阈值,就可以显示我们想看到的调用图了。比如执行下面的命令:

$ perf report -g graph,0.3

这样就可以得到下面这个新的输出界面,展开 app 后,就可以看到它的调用栈了。

如何用perf工具分析Java程序_perf工具_03

四、怎么理解perf report报告

上图中,swapper 高达 99% 的比例。直觉来说,我们应该直接观察它才对,为什么没那么做呢?

其实,当你清楚了 swapper 的原理后,就很容易理解为什么可以忽略它了。

看到 swapper,你可能首先想到的是 SWAP 分区。实际上, swapper 跟 SWAP 没有任何关系,它只在系统初始化时创建 init 进程,之后,它就成了一个最低优先级的空闲任务。也就是说,当 CPU 上没有其他任务运行时,就会执行 swapper 。所以,可以称它为“空闲任务”。

回到我们的问题,在 perf report 的界面中,展开它的调用栈,你会看到, swapper 时钟事件都耗费在了 do_idle 上,也就是在执行空闲任务。

如何用perf工具分析Java程序_perf工具_04

所以,分析案例时,直接忽略了前面这个 99% 的符号,转而分析后面只有 0.3% 的 app。其实从这里你也能理解,为什么我们一开始不先用 perf 分析。

因为在多任务系统中,次数多的事件,不一定就是性能瓶颈。所以,只观察到一个大数值,并不能说明什么问题。具体有没有瓶颈,还需要你观测多个方面的多个指标,来交叉验证。

关于 Children 和 Self 的含义,手册里其实有详细说明,还很友好地举了一个例子,来说明它们的百分比的计算方法。简单来说

  • Self 是最后一列的符号(可以理解为函数)本身所占比例;
  • Children 是这个符号调用的其他符号(可以理解为子函数,包括直接和间接调用)占用的比例之和。

很多性能工具确实会对系统性能有一定影响。就拿 perf 来说,它需要在内核中跟踪内核栈的各种事件,那么不可避免就会带来一定的性能损失。这一点,虽然对大部分应用来说,没有太大影响,但对特定的某些应用(比如那些对时钟周期特别敏感的应用),可能就是灾难了。

所以,使用性能工具时,确实应该考虑工具本身对系统性能的影响。而这种情况,就需要你了解这些工具的原理。比如,

  • ​perf​​ 这种动态追踪工具,会给系统带来一定的性能损失。
  • ​vmstat、pidstat​​ 这些直接读取 proc 文件系统来获取指标的工具,不会带来性能损失。

五、性能优化书籍和参考资料

Brendan Gregg写的《Systems Performance: Enterprise and the Cloud》中文版《性能之巅:洞悉系统、企业与云计算》

他个人网站​​http://www.brendangregg.com/​​​特别是​​Linux Performance​​这个页面,包含了很多 Linux 性能优化的资料,如:

  • Linux性能工具图谱
  • 性能分析参考资料
  • 性能优化的演讲视频

不过,这里很多内容会涉及到大量的内核知识,对初学者来说并不友好。但是,如果你想成为高手,辛苦和坚持都是不可避免的。所以,在查看这些资料时,不要一遇到不懂的就打退堂鼓。任何东西的第一遍学习有不懂的地方很正常,忍住恐惧别放弃,继续往后走,前面很多问题可能会一并解决掉,再看第二遍、第三遍就更轻松了。

抓住主线不动摇,先从最基本的原理开始,掌握性能分析的思路,然后再逐步深入,探究细节,不要试图一口吃成个大胖子。