背景
根据帕累托法则,只有优化处于性能瓶颈的那些少量代码,才能用最小的成本获得最大的收益。
单机系统中有一种最直接有效的方式,就是从代码层面直接寻找调用次数最频繁、耗时最长的函数,通常它就是性能瓶颈。但达到这个目标需要三个约束条件:
- 没有代码侵入性。比如,在函数执行的前后,分别打印一行日志记录时间,这当然能获取到函数的调用频次和执行时长,但并不可取,它的开发、维护成本太高了。
- 覆盖到代码中的全部函数。如果只是监控事先猜测的、有限的几个函数,往往会因为思维死角,遗漏掉真正的瓶颈函数。因此,只有监控所有函数的执行情况,并以一种全局的、直观的方式分析聚合后的监控结果,才能快速、准确地找到性能瓶颈。
- 搭建环境的成本足够低。最好使用现成的包,比如Brendan Gregg 发明的火焰图
关于火焰图的设计思路和详细使用方式,可以看他的论文:https://queue.acm.org/detail.cfm?id=2927301
FlameGraph
火焰图中最重要的信息,就是表示函数执行时间的 X 轴,以及表示函数调用栈的 Y 轴。函数方块的颜色是随机的,并没有特别的意义,只是为了在视觉上更容易区分开。
X 轴由多个方块组成,每个方块表示一个函数,其长度是定时采样得到的函数调用频率,因此可以简单粗暴地把它近似为执行时间。需要注意的是,如果函数 A 中调用了函数 B、C、D,监控采样时会形成 A->B、A->C、A->D 这 3 个函数调用栈,但火焰图会将 3 个调用栈中表示 A 函数的方块合并,并将 B、C、D 放在上一层中,并以字母表顺序排列它们。这样更有助于你找到耗时最长的函数。
Y 轴,它表示函数的调用栈。既可以看到内核 API 的函数调用栈,也可以看到用户态函数的调用栈
如何查看函数是否消耗过多CPU资源
- 首先,火焰图很容易从全局视角,通过寻找长方块,找到调用频率最高的几个函数。
-
函数方块长,有些是因为函数自身执行了很消耗 CPU 的代码,而有些则是调用的子函数耗时长造成的;如果函数方块的长度,远大于调用栈中子函数方块的长度之和,那么这个函数就执行了比较耗费 CPU 的计算。比如,下图中执行 TLS 握手的 ngx_ssl_handshake_handler 函数,自身并没有消耗 CPU 多少计算力。
而更新 Nginx 时间的 ngx_time_update 函数就不同,它在做时间格式转换时消耗了许多 CPU,如下图所示:
使用perf-FlameGraph生成On CPU火焰图
该火焰图只能找到消耗 CPU 计算力最多的函数,因此它也叫做 On CPU 火焰图,由于 CPU 工作时会发热,所以色块都是暖色调。
# 安装
yum install perf
git clone --depth 1 https://github.com/brendangregg/FlameGraph.git
# 接着,针对运行中的进程 PID,使用 perf 采样函数的调用频率(对于 C/C++ 语言,为了能够显示完整的函数栈,你需要在编译时加入 -g 选项)
perf record -F 99 -p 进程PID -g --call-graph dwarf
或者全部进程
perf record -F 99 -a --call-graph dwarf
# 更多参数:https://www.brendangregg.com/perf.html
# 再将二进制信息转换为 ASCII 格式的文件,方便 FlameGraph 处理:
perf script > out.perf
# 再然后,需要汇聚函数调用栈,转化为 FlameGraph 生成火焰图的数据格式:
FlameGraph/stackcollapse-perf.pl out.perf > out.folded
# 最后一步,生成 SVG 格式的矢量图片:
FlameGraph/flamegraph.pl out.folded > out.svg
生成Off CPU 火焰图
有些使用了阻塞 API 的代码,则会导致进程休眠,On CPU 火焰图无法找到休眠时间最长的函数,此时可以使用 Off CPU 火焰图,它按照函数引发进程的休眠时间的长短,确定函数方块的长度。由于进程休眠时不使用 CPU,所以色块会使用冷色调。如下图所示:
详见:https://www.brendangregg.com/FlameGraphs/offcpuflamegraphs.html
发表回复