You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

142 lines
14 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 19 | 如何通过监控找到性能瓶颈?
你好,我是陶辉。
从这一讲开始,我们将进入分布式系统层面,站在更宏观的角度去探讨系统性能的优化。
如果优化系统性能时,只是依据自己的经验,对感觉存在性能提升空间的代码,无一例外地做一遍优化,这既是一件事倍功半的事,也很容易遗漏下关键的优化点,无法大幅提升系统的性能。根据帕累托法则(也叫二八定律),**只有优化处于性能瓶颈的那些少量代码,才能用最小的成本获得最大的收益。**
然而,找到性能瓶颈却不是一件容易的事。我们通常会采用各种监控手段来发现性能瓶颈,但**如果监控动作自身的开发成本过高,或者施行监控时显著降低了业务请求的性能,或者无法全面覆盖潜在的问题,都会影响性能优化目标的实现。**
这一讲我将介绍2个简单而又行之有效的方案分别从微观上快速地找出进程内的瓶颈函数以及从宏观上找出整个分布式系统中的瓶颈组件。这样我们就可以事半功倍地去优化系统性能。
## 单机:如何通过火焰图找到性能瓶颈?
对于工作在一台主机上的进程而言有许多监控方案可用于寻找性能瓶颈。比如在Linux下你可以通过iostat命令监控磁盘状态也可以通过top命令监控CPU、内存的使用。这些方案都是在旁敲侧击着寻找性能瓶颈然而有一种**最直接有效的方式,就是从代码层面直接寻找调用次数最频繁、耗时最长的函数,通常它就是性能瓶颈。**
要完成这样的目标通常还有3个约束条件。
1. **没有代码侵入性。**比如,在函数执行的前后,分别打印一行日志记录时间,这当然能获取到函数的调用频次和执行时长,但并不可取,它的开发、维护成本太高了。
2. **覆盖到代码中的全部函数。**如果只是监控事先猜测的、有限的几个函数,往往会因为思维死角,遗漏掉真正的瓶颈函数。因此,只有监控所有函数的执行情况,并以一种全局的、直观的方式分析聚合后的监控结果,才能快速、准确地找到性能瓶颈。
3. **搭建环境的成本足够低。**我曾经使用过systemtap来监控函数的执行状况搭建这样的监控环境需要很多步骤比如要重新编译Linux内核这些过于繁琐的成本会阻碍很多应用开发者的性能优化脚步。
带着这3个约束条件最合适的方案已经呼之欲出了那就是[Brendan Gregg](https://en.wikipedia.org/wiki/Brendan_Gregg) 发明的[火焰图](http://www.brendangregg.com/flamegraphs.html),以及为火焰图提供监控数据的工具。我们先来直观感受下火焰图到底是什么样子:
![](https://static001.geekbang.org/resource/image/9c/d8/9ca4ff41238f067341d70edbf87c4cd8.png)
火焰图可以将监控期间涉及到的所有函数调用栈都列出来就像上图对Nginx worker进程的监控一样函数非常密集因此通常采用可缩放、可互动的[SVG矢量格式](https://en.wikipedia.org/wiki/Scalable_Vector_Graphics)图片来展示。由于极客时间暂时还没法直接展示矢量图片,所以我把矢量图片放在了我的个人网站上,你可以点击[这个链接](http://www.taohui.pub/nginx_perf.svg)跳转到矢量图片页面。在SVG图片上你可以用Ctrl+F通过名称搜索函数而且这是支持正则表达式匹配的。你还可以点击函数方块只查看它相关的函数调用栈。
火焰图中最重要的信息就是表示函数执行时间的X轴以及表示函数调用栈的Y轴。
先来看X轴。X轴由多个方块组成每个方块表示一个函数**其长度是定时采样得到的函数调用频率,因此你可以简单粗暴地把它近似为执行时间。**需要注意的是如果函数A中调用了函数B、C、D监控采样时会形成A->B、A->C、A->D这3个函数调用栈但火焰图会将3个调用栈中表示A函数的方块合并并将B、C、D放在上一层中并以字母表顺序排列它们。这样更有助于你找到耗时最长的函数。
再来看Y轴它表示函数的调用栈。这里我们既可以看到内核API的函数调用栈也可以看到用户态函数的调用栈非常强大。如果你正在学习开源组件的源码推荐你先生成火焰图再对照图中的Y轴调用栈理解源码中函数的调用关系。注意**函数方块的颜色是随机的,并没有特别的意义,只是为了在视觉上更容易区分开。**
结合X轴和Y轴我们再来看如何使用火焰图找到性能瓶颈。
首先,火焰图很容易从全局视角,通过寻找长方块,找到调用频率最高的几个函数。
其次函数方块长有些是因为函数自身执行了很消耗CPU的代码而有些则是调用的子函数耗时长造成的。怎么区分这两种场景呢很简单**如果函数方块的长度远大于调用栈中子函数方块的长度之和那么这个函数就执行了比较耗费CPU的计算。**比如下图中执行TLS握手的ngx\_ssl\_handshake\_handler函数自身并没有消耗CPU多少计算力。
![](https://static001.geekbang.org/resource/image/14/c2/14c155f5ef577fa2aa6a127145b2acc2.png)
而更新Nginx时间的ngx\_time\_update函数就不同它在做时间格式转换时消耗了许多CPU如下图所示
![](https://static001.geekbang.org/resource/image/b8/67/b8b40ab7b4d358b45e600d8721b1d867.png)
这样,我们可以直观地找到最耗时的函数,再有针对性地优化它的性能。
怎么生成火焰图呢在Linux上这非常简单因为Linux内核默认支持perf工具你可以用perf生成函数的采样数据再用FlameGraph脚本生成火焰图。当然如果你在使用Java、GoLang、Python等高级语言也可以使用各自语言生态中封装过的Profile类工具生成采样数据。这里我以最基本的perf、FlameGraph为例介绍下生成火焰图的5步流程。
首先你可以通过yum或者apt-get安装perf工具再通过git来下载FlameGraph
```
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
```
上述命令行中各参数的含义,可以参见[这里](http://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
```
需要注意,**上面的火焰图只能找到消耗CPU计算力最多的函数因此它也叫做On CPU火焰图由于CPU工作时会发热所以色块都是暖色调。**有些使用了阻塞API的代码则会导致进程休眠On CPU火焰图无法找到休眠时间最长的函数此时可以使用Off CPU火焰图它按照函数引发进程的休眠时间的长短确定函数方块的长度。由于进程休眠时不使用CPU所以色块会使用冷色调。如下图所示
![](https://static001.geekbang.org/resource/image/82/8f/823f2666f8f8b57714e751501f178a8f.png)
图片来源:[http://www.brendangregg.com/FlameGraphs/offcpuflamegraphs.html](http://www.brendangregg.com/FlameGraphs/offcpuflamegraphs.html),你可以点击[这个链接](http://www.brendangregg.com/FlameGraphs/off-mysqld-busy.svg)查看SVG图片。
生成Off CPU火焰图的步骤稍微繁琐点你可以参照[这个页面](http://www.brendangregg.com/FlameGraphs/offcpuflamegraphs.html)。关于火焰图的设计思路和详细使用方式,推荐你参考[Brendan Gregg](https://en.wikipedia.org/wiki/Brendan_Gregg) 的[这篇论文](https://queue.acm.org/detail.cfm?id=2927301)。
## 分布式系统:如何通过全链路监控找到性能瓶颈?
说完微观上性能瓶颈的定位,我们再来看宏观上的分布式系统,它通过网络把不同类型的硬件、操作系统、编程语言组合在一起,结构很复杂,因此,现在我们的目标变成寻找更大尺度的瓶颈组件。
当然与单机不同的是找到分布式系统的性能瓶颈组件后除了可以继续通过火焰图优化组件中某个进程的性能外还有下面这样3个新收益。
1. 首先,可以通过简单的扩容动作,利用负载均衡机制提升性能。
2. 其次,在云计算和微服务时代,扩容可以在分钟级时间内完成。因此,如果性能瓶颈可以实时检测到,就可以自动化地完成扩容和流量迁移。这可以提升服务器资源的利用率。
3. 最后,既然能够找到瓶颈组件,同样也能找出资源富裕的组件,这样就可以通过反向的缩容,减少服务器资源的浪费。
接下来我将从4个角度介绍分布式系统中的性能监控体系。
首先监控基于日志来做对系统的侵入性最低因此你要把格式多样化的文本日志进行结构化管理。我们知道1个分布式系统会涉及到多种编程语言这样各组件基于完全异构的中间件产生的日志格式也是五花八门如果定义一个统一的日志格式标准再要求每个组件遵照它重新开发一个版本这样代价太高基本无法实现
比较幸运的是几乎每个组件都会输出文本类型的访问日志而且它们通常包括访问对象例如URL、错误码、处理时间等共性信息。因此可以针对每个组件特定格式的文本日志写一个正则表达式将我们需要的结构化信息提取出来。这样结构化数据的提取是在组件之外的对于组件中的代码也没有影响。
其次同时搜集系统中每个组件的日志并汇总在一起这样日志数据的规模就非常可观每天可能会产生TB级别的数据因此关系型数据库并不可取因为它们为了照顾事务、关联查询等操作不具备很好的可伸缩性。同时结构化的日志源于我们必须针对某类请求、某类组件做聚合分析因此纯粹的Key/Value数据库也无法实现这类功能。因此像HBase这样的半结构化列式数据库往往是监控数据的首选落地形式。
再次我们分析日志时首先会从横向上聚合分析成百上千个组件中时延、吞吐量、错误码等数据项的统计值。这样计算力就是一个很大的问题因此通常选择支持Map Reduce算法的系统比如Hadoop进行计算并将结果以可视化的方式展现出来协助性能分析。由于计算时间比较久所以这也称为离线计算。其次会在纵向上监控一个请求在整个生命周期内各个参与组件的性能状态。因此必须设计一个请求ID能够贯穿在各个组件内使用由于分布式系统中的请求数量非常大这个ID的碰撞概率必须足够低。所以请求ID通常会同时包含时间和空间元素比如IP地址。这个方案还有个很威武的名字叫做全链路监控可以参考Google的[这篇论文](https://storage.googleapis.com/pub-tools-public-publication-data/pdf/36356.pdf)),它还可以用于组件依赖关系的优化。
最后你还得对性能瓶颈做实时监控这是实现分布式系统自动化扩容、缩容的前提。由于监控数据规模庞大所以我们通常要把流量在时间维度上分片仅对每个时间小窗口中有限的数据做快速的增量计算。像Storm、Spark、Flink都是这样的实时流计算中间件你可以基于它们完成实时数据的汇聚分析。
以上是我对分布式系统性能监控方案的一些思考。
一提到分布式系统涉及到的知识点就非常多。好在我们后续的课程对MapReduce、实时流计算等还有进一步的分析因此监控方案中对它们就一带而过了。
## 小结
这一讲,我们介绍了单机上如何通过火焰图找到性能瓶颈函数,以及在分布式系统中,如何通过全链路监控找到性能瓶颈组件。
在Linux系统中你可以用内核支持的perf工具快速地生成火焰图。其他高级编程语言生态中都有类似的Profiler工具可生成火焰图。
火焰图中可以看到函数调用栈它对你分析源码很有帮助。图中方块的长度表示函数的调用频率当采样很密集时你可以把它近似为函数的执行时长。父方块长度减去所有子方块长度的和就表示了父函数自身代码对CPU计算力的消耗。因此火焰图可以直观地找到调用次数最频繁且最耗时的函数。
除了上面介绍的On CPU火焰图你也可以用同样的工具生成Off CPU火焰图它用于找到频率引发进程休眠进而降低了性能的瓶颈函数。
对于分布式系统性能监控还有利于系统的扩容和缩容运维。搭建性能监控体系包括以下四个关键点首先要把五花八门的日志用正则表达式提取为结构化的监控数据其次用半结构化的列式数据库存放集群中的所有日志便于后续的汇聚分析第三使用统一的请求ID将各组件串联在一起并使用MapReduce算法对大量的监控数据做离线分析最后通过实时流计算框架对监控数据做实时汇聚分析。
性能监控是一个很大的话题,这节课我只希望能从不同的角度带给你思考,课后还需要你进行更深入的探索。
## 思考题
最后给你留一道练习题请你参考On CPU火焰图的制作对你熟悉的程序做一张Off CPU火焰图看看能带给你哪些新的启发期待你的总结。
感谢阅读,如果你觉得这节课对你有一些启发,也欢迎把它分享给你的朋友。