小编说:当程序出现性能瓶颈时,我们通常通过表象并结合代码去推测可能出问题的地方,却不知道问题是由什么引起的。如果有一个可视化的工具能直观地展现程序的性能瓶颈就好了,幸好 Brendan D. Gregg 发明了火焰图。 本文选自《Node.js调试指南》
火焰图(Flame Graph)看起来就像一团跳动的火焰,因此得名,它可以将 CPU 的使用情况可视化,使我们直观地了解到程序的性能瓶颈。我们通常要结合操作系统的性能分析工具(Profiling Tracer)使用火焰图,常见的操作系统的性能分析工具如下。
perf_events(简称 perf)是 Linux Kernal 自带的系统性能分析工具,能够进行函数级与指令级的热点查找。它基于事件采样原理,以性能事件为基础,支持针对处理器与操作系统相关的性能指标的性能剖析,常用于查找性能瓶颈及定位热点代码。
测试机器:
$ uname -a Linux nswbmw-VirtualBox 4.10.0-28-generic #32~16.04.2-Ubuntu SMP Thu Jul 20 10:19:48 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux
注 意
非 Linux 用户需要在通过虚拟机安装 Ubuntu 16.04 和 node@8.9.4 后再进行后面的操作。
安装 perf :
$ sudo apt install linux-tools-common $ perf # 根据提示安装对应的内核版本的 tools, 如下 $ sudo apt install linux-tools-4.10.0-28-generic linux-cloud-tools-4.10.0-28- generic
创建测试目录 ~/test 和测试代码。
app.js :
添加 --perf_basic_prof(或者 --perf-basic-prof)参数运行此程序,会对应生成一个 /tmp/perf-<PID>.map 文件。命令如下:
map 文件中的三列依次为:16 进制的符号地址(Symbol Address)、大小(Size)和符号名(Symbol Names)。perf 会尝试查找 /tmp/perf-<PID>.map 文件,用来做符号转换,即把 16 进制的符号地址转换成人能读懂的符号名。
当然,在这里使用 --perf_basic_prof_only_functions 参数也可以,但笔者在尝试后发现生成的火焰图信息不全(不全的地方显示 [perf-<PID>.map]),所以使用了--perf_basic_prof。但是,使用 --perf_basic_prof 有个缺点,就是会导致 map 文件一直增大,这是由于符号(symbols)地址的不断变换导致的,用 --perf_basic_prof_only_functions 可以缓解这个问题。关于如何取舍,还请读者自行尝试。
接下来 clone(克隆) 用来生成火焰图的工具:
$ git clone http://github.com/brendangregg/FlameGraph ~/FlameGraph
我们先用 ab 压测:
$ curl "http://localhost:3000/newUser?username=admin&password=123456" $ ab -k -c 10 -n 2000 "http://localhost:3000/auth?username=admin&passwo rd=123456"
重新打开另一个终端,在 ab 开始压测后立即运行:
$ sudo perf record -F 99 -p 3590 -g -- sleep 30 $ sudo chown root /tmp/perf-3590.map $ sudo perf script > perf.stacks $ ~/FlameGraph/stackcollapse-perf.pl --kernel < ~/perf.stacks | ~/FlameGraph/ flamegraph.pl --color=js --hash> ~/flamegraph.svg
注 意
第 1 次生成的 svg 可能不太准确,最好重复几次以上步骤,使用第 2 次及以后生成的 flamegraph.svg。
有几点需要解释一下。
(1)关于perf record :
(2)sudo chown root /tmp/perf-3009.map 用于将 map 文件更改为 root 权限,否则会报如下错误:
> File /tmp/perf-PID.map not owned by current user or root, ignoring it (use -f to override). > Failed to open /tmp/perf-PID.map, continuing without symbols
(3)perf record 会将记录的信息保存到当前执行目录的 perf.data 文件中,将使用perf script 读取的 perf.data 的 trace 信息写入 perf.stacks。
(4)--color=js 指定生成针对JavaScript 配色的 svg,其中,green 代表 JavaScript,blue 代表 Builtin,yellow 代表 C++,red 代表 System。
ab 压测用了 30s 左右,用浏览器打开 flamegraph.svg,截取关键的部分,如图。
火焰图的含义如下。
从上面的火焰图可以看出, 最上面的绿色小块( 即 JavaScript 代码) 指向 test/app.js 的第 18 行, 即 GET /auth 这个路由;再往上看, 黄色的小块( 即 C++ 代码)node::crypto::PBKDF2 占用了大量的 CPU 时间。
解决方法:将同步改为异步,即将 crypto.pbkdf2Sync 改为 crypto.pbkdf2。修改如下:
用 ab 重新压测,结果用了 16s。重新生成的火焰图如下图。
可以看出,只有在左侧极窄的绿色小块中可以看到 JavaScript 代码,我们不关心也无法优化红色的部分。那么,为什么异步比同步的 QPS 要高呢?原因是 Node.js 底层的libuv 用了多个线程进行计算,这里就不再深入介绍了。
svg 火焰图的其他小技巧如下。
虽然我们有了火焰图,但要处理性能回退问题,还需要在修改代码前后的火焰图之间不断切换和对比,来找出问题所在,很不方便。于是 Brendan D. Gregg 又发明了红蓝差分火焰图(Red/Blue Differential Flame Graphs),如下图所示,其中,红色表示增长,蓝色表示衰减。
红蓝差分火焰图的工作原理如下。
(1)抓取修改前的栈 profile1 文件。
(2)抓取修改后的栈 profile2 文件。
(3)使用 profile2 来生成火焰图,这样栈帧的宽度就是以 profile2 文件为基准的。
(4)将profile2 与profile1 进行比较,并根据其差异来生成新的火焰图,并对新的火
焰图重新上色。上色的原则是:如果栈帧在 profile2 中出现的次数更多,则将该栈帧标为红色,否则将其标为蓝色。色彩是根据修改前后的差异来填充的。
这样,通过红蓝差分火焰图,我们就可以清楚地看到当前系统的性能差异。
生成红蓝差分火焰图的流程如下。
(1)在修改代码前运行:
$ sudo perf record -F 99 -p <PID> -g -- sleep 30 $ sudo chown root /tmp/perf-<PID>.map $ sudo perf script > perf_before.stacks
(2)在修改代码后运行:
$ sudo perf record -F 99 -p <PID> -g -- sleep 30 $ sudo chown root /tmp/perf-<PID>.map $ sudo perf script > perf_after.stacks
(3)将 profile 文件进行折叠(fold),然后生成差分火焰图:
其中的缺点是:如果一个代码执行路径完全消失了,在火焰图中就找不到地方来标注蓝色,我们只能看到当前的 CPU 使用情况,却不知道为什么会变成这样。
一种解决办法是:生成一个相反的差分火焰图,即基于 profile1 生成 profile1 与profile2 的差分火焰图。对应的命令如下:
$ ./FlameGraph/difffolded.pl perf_after.folded perf_before.folded | ./ FlameGraph/flamegraph.pl --negate > flamegraph_diff2.svg
其中,--negate 用于颠倒红、蓝配色。我们最终得到flamegraph_diff.svg 和flamegraph_diff2.svg,如下所述。
总之,红蓝差分火焰图可能只在代码变化不大的情况下使用时效果明显,在代码变化较大的情况下使用时效果可能就不明显了。
本文选自《Node.js调试指南》