<audio id="audio" title="19 | 如何通过监控找到性能瓶颈？" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/71/c8/71acd68a7f1f14f7b3c33f7c1ca117c8.mp3"></audio>

你好，我是陶辉。

从这一讲开始，我们将进入分布式系统层面，站在更宏观的角度去探讨系统性能的优化。

如果优化系统性能时，只是依据自己的经验，对感觉存在性能提升空间的代码，无一例外地做一遍优化，这既是一件事倍功半的事，也很容易遗漏下关键的优化点，无法大幅提升系统的性能。根据帕累托法则（也叫二八定律），**只有优化处于性能瓶颈的那些少量代码，才能用最小的成本获得最大的收益。**

然而，找到性能瓶颈却不是一件容易的事。我们通常会采用各种监控手段来发现性能瓶颈，但**如果监控动作自身的开发成本过高，或者施行监控时显著降低了业务请求的性能，或者无法全面覆盖潜在的问题，都会影响性能优化目标的实现。**

这一讲，我将介绍2个简单而又行之有效的方案，分别从微观上快速地找出进程内的瓶颈函数，以及从宏观上找出整个分布式系统中的瓶颈组件。这样，我们就可以事半功倍地去优化系统性能。

## 单机：如何通过火焰图找到性能瓶颈？

对于工作在一台主机上的进程而言，有许多监控方案可用于寻找性能瓶颈。比如在Linux下，你可以通过iostat命令监控磁盘状态，也可以通过top命令监控CPU、内存的使用。这些方案都是在旁敲侧击着寻找性能瓶颈，然而，有一种**最直接有效的方式，就是从代码层面直接寻找调用次数最频繁、耗时最长的函数，通常它就是性能瓶颈。**

要完成这样的目标，通常还有3个约束条件。

1. **没有代码侵入性。**比如，在函数执行的前后，分别打印一行日志记录时间，这当然能获取到函数的调用频次和执行时长，但并不可取，它的开发、维护成本太高了。
1. **覆盖到代码中的全部函数。**如果只是监控事先猜测的、有限的几个函数，往往会因为思维死角，遗漏掉真正的瓶颈函数。因此，只有监控所有函数的执行情况，并以一种全局的、直观的方式分析聚合后的监控结果，才能快速、准确地找到性能瓶颈。
1. **搭建环境的成本足够低。**我曾经使用过systemtap来监控函数的执行状况，搭建这样的监控环境需要很多步骤，比如要重新编译Linux内核，这些过于繁琐的成本会阻碍很多应用开发者的性能优化脚步。

带着这3个约束条件，最合适的方案已经呼之欲出了：那就是[Brendan Gregg](https://en.wikipedia.org/wiki/Brendan_Gregg) 发明的[火焰图](http://www.brendangregg.com/flamegraphs.html)，以及为火焰图提供监控数据的工具。我们先来直观感受下火焰图到底是什么样子：

<img src="https://static001.geekbang.org/resource/image/9c/d8/9ca4ff41238f067341d70edbf87c4cd8.png" alt="">

火焰图可以将监控期间涉及到的所有函数调用栈都列出来，就像上图对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-&gt;B、A-&gt;C、A-&gt;D这3个函数调用栈，但火焰图会将3个调用栈中表示A函数的方块合并，并将B、C、D放在上一层中，并以字母表顺序排列它们。这样更有助于你找到耗时最长的函数。

再来看Y轴，它表示函数的调用栈。这里，我们既可以看到内核API的函数调用栈，也可以看到用户态函数的调用栈，非常强大。如果你正在学习开源组件的源码，推荐你先生成火焰图，再对照图中的Y轴调用栈，理解源码中函数的调用关系。注意，**函数方块的颜色是随机的，并没有特别的意义，只是为了在视觉上更容易区分开。**

结合X轴和Y轴，我们再来看如何使用火焰图找到性能瓶颈。

首先，火焰图很容易从全局视角，通过寻找长方块，找到调用频率最高的几个函数。

其次，函数方块长，有些是因为函数自身执行了很消耗CPU的代码，而有些则是调用的子函数耗时长造成的。怎么区分这两种场景呢？很简单，**如果函数方块的长度，远大于调用栈中子函数方块的长度之和，那么这个函数就执行了比较耗费CPU的计算。**比如，下图中执行TLS握手的ngx_ssl_handshake_handler函数，自身并没有消耗CPU多少计算力。

<img src="https://static001.geekbang.org/resource/image/14/c2/14c155f5ef577fa2aa6a127145b2acc2.png" alt="">

而更新Nginx时间的ngx_time_update函数就不同，它在做时间格式转换时消耗了许多CPU，如下图所示：

<img src="https://static001.geekbang.org/resource/image/b8/67/b8b40ab7b4d358b45e600d8721b1d867.png" alt="">

这样，我们可以直观地找到最耗时的函数，再有针对性地优化它的性能。

怎么生成火焰图呢？在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 &gt; out.perf

```

再然后，需要汇聚函数调用栈，转化为FlameGraph生成火焰图的数据格式：

```
FlameGraph/stackcollapse-perf.pl out.perf &gt; out.folded

```

最后一步，生成SVG格式的矢量图片：

```
FlameGraph/flamegraph.pl out.folded &gt; out.svg

```

需要注意，**上面的火焰图只能找到消耗CPU计算力最多的函数，因此它也叫做On CPU火焰图，由于CPU工作时会发热，所以色块都是暖色调。**有些使用了阻塞API的代码，则会导致进程休眠，On CPU火焰图无法找到休眠时间最长的函数，此时可以使用Off CPU火焰图，它按照函数引发进程的休眠时间的长短，确定函数方块的长度。由于进程休眠时不使用CPU，所以色块会使用冷色调。如下图所示：

<img src="https://static001.geekbang.org/resource/image/82/8f/823f2666f8f8b57714e751501f178a8f.png" alt="">

图片来源：[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. 首先，可以通过简单的扩容动作，利用负载均衡机制提升性能。
1. 其次，在云计算和微服务时代，扩容可以在分钟级时间内完成。因此，如果性能瓶颈可以实时检测到，就可以自动化地完成扩容和流量迁移。这可以提升服务器资源的利用率。
1. 最后，既然能够找到瓶颈组件，同样也能找出资源富裕的组件，这样就可以通过反向的缩容，减少服务器资源的浪费。

接下来，我将从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火焰图，看看能带给你哪些新的启发？期待你的总结。

感谢阅读，如果你觉得这节课对你有一些启发，也欢迎把它分享给你的朋友。
