如何分析容器内的进程
当使用 perf 工具时,看到 16 进制地址而不是函数名,通常是因为缺少符号信息(symbol information)或调试信息,导致 perf 无法将地址解析为对应的函数名或源代码行。比较常见的场景是分析容器内的子进程,因为容器应用依赖的库都在镜像里面。
针对容器的情况,总结了一下四种解决方法。
第一种方法,再容器外构建相同路径的依赖库。这种方法从原理上可行,但是并不推荐,一方面是因为找出这些依赖库比较麻烦,更重要的是,构建这些路径,会污染容器主机环境
第二种方法,在容器内部运行 perf。不过,这需要容器运行在特权模式下,但实际的应用程序往往只以普通容器的方式运行。所以,容器内部一般没有权限执行 perf 分析。
比方说,如何你在普通容器内运行 perf record,你将会看到下面这个错误提示:
1 | [root@efbd40d93ebf /]# perf record -g -p 1 |
从docker seccomp文档中可以看出,由于安全问题,系统调用 “perf*” 和 “ptrace” 默认被禁止。当然,其实你可以通过配置 /proc/sys/kernel/perf_event_paranoid(比如改成 -1),来允许非特权用户执行 perf 事件分析;不过还是那句话,为了安全起见,这种方式不太推荐。
第三种方法,指定符号路径为容器文件系统的路径。
举个例子:我们要分析一个在 Nginx 容器中运行的 worker 进程,可以执行一下命令
1 | 获取 worker 在宿主机上的进程号 |
不过要注意,bindfs 这个工具需要你额外安装。bindfs 的基本功能是实现目录绑定(类似于 mount –bind)。
第四种方法,在容器外吧分析记录保存下来,再去容器里查看结果。这样,库和符号的路径也就都对了。比如,你可以这么做。先运行 perf record -g -p <pid>
,执行一会儿(比如 15 秒)后,按 Ctrl+C 停止。
然后,把生成的 perf.data 文件,拷贝到容器里边来分析,不过,这里也需要注意 perf 工具的版本问题
1 | docker cp perf.data mynginx:/tmp |
当你按照前面这几种方法操作后,你就可以在容器内部看到:
为何看不到原生的Java的调用栈
分析 Java 应用时,perf 工具面临一个根本挑战:Java 代码并非直接在 CPU 上执行原生机器码,而是运行在一个名为 JVM(Java 虚拟机)的托管环境中:
- JIT 编译的抽象层:JVM 会在运行时通过即时编译(JIT)技术将热点的 Java 字节码动态编译成高效的机器码。这个过程意味着 perf 采样到的是这些动态生成的、无明确符号(函数名)的机器码地址
- 非标准调用栈:perf 默认只能识别操作系统层面的函数调用,因此它能看到 JVM 自身的函数(如 JavaMain),但是无法理解 JVM 内部的、属于 Java 应用程序的调用关系
为了让 perf 发挥最用,必须建立一座桥梁,将 JVM 内的函数调用信息与 perf 的系统级采样数据关联起来。可以采用以下方案解决:
步骤一:保留帧指针(Preserve Frame Pointer),这是确保调用栈能被正常回溯的基础
- 操作:在启动 Java 应用时,添加 JVM 参数
-XX:-PreserveFramePointer
- 原理:处于性能优化,JIT 编译器默认会省略帧指针,这会释放一个寄存器提供其他用途。然后,perf 等多数性能分析工具依赖帧指针来遍历和回溯函数调用栈。启动此选项可以强制 JVM 保留该信息,从而生成完整、可追溯的调用栈。
步骤二:生成 Java 符合映射表
此步骤的目的是将 perf 看到的内存地址翻译成可读的 Java 函数名
- 工具:使用 perf-map-agent,这是一个专门为此设计的开源工具
- 原理:perf-map-agent 会作为一个 Java Agent 附加到目标 JVM 进程上,查询 JIT 编译后的机器码地址与原始 Java 方法名的对应关系,并将其写入一个 perf 能够识别的映射文件(通常是 /tmp/perf-
.map)。perf 在生成报告时会自动检测并使用此文件。
步骤三:使用 perf 采样与分析
完成上述准备后,就可以使用标准的 perf 命令进行性能分析。例如:
1 | sudo perf record -F 99 -p <pid> -g -- sleep 30 |
拓展:目前有现成的工具解决上述问题,async-profiler,是一个专为 Java 设计的低开销、高精度的性能分析器,它集成了 perf 的功能并做了大量优化,是目前 Java 性能分析的优选工具。