性能分析方法论

2018-10-19

有个项目 POC,性能达不到要求。一个小朋友抓了一下火焰图,感觉 parser 占的挺高,就吭哧吭哧要优化 parser。这做事方式,没有讲究方法论。

用数据说话,拒绝先入为主

首先没有分析系统的瓶颈。影响系统整体性能的因素很多,瓶颈有可能在 CPU,当然也有可能是网络,磁盘,甚至调度器,锁等等等。抓火焰图看的只是 CPU。CPU 是不是打满了,瓶颈是不是在 CPU,这是首先要确认的。

其次没有用数据说话,parse 占用的 CPU 高,并不能代表 parse 消耗的时间最长,是系统瓶颈。就算 parse 是耗时,那它耗时究竟是多长呢?跟其它步骤相比呢,比如和 optimize,execute 相比呢,这需要量化。做事情呐,一定要拒绝先入为主,要用数据说话,不能够 "我觉得 parser 占 CPU,所以要优化它"。

假设系统处理一个任务要经过三个主要步骤,耗时比较分别占 10% 20% 70%,将第一个步骤优化 30%,那对系统整体的提升也只有 3%。而如果我们找到那个瓶颈的 70%,将它提升 30%,对系统整体的提升却有 21%。一定要先抓住主要矛盾,先定位瓶颈。

培养数据敏感性

有时候做压测,就觉得结果不如自己想象中好,总觉得应该哪里还能有提升,但是又不知道如何定位到瓶颈。这是缺乏数据敏感性,说白了是平时太懒得思考和分析,没养成好习惯。

假设我们得到的是 8000 QPS,80 并发连接。那么大脑马上应该反应,平均每个连接上面,是每秒处理 100 个请求。所以,平均处理一个请求需要 10ms。 如果我们有监控,是不是应该去看看,80% 95% 99% 999% 这些时间分别是多少,数据是否能对上。80 跟平均应该是相差不多的。

假设我们看到,80 跟 99 差得特别多,比如 80 在几百 us,而 99 到了几十 ms,我们是不是应该立刻思考哪些因素影响响应时间的分布情况,考虑长尾问题。 会不会统计的请求类型不同,大部分请求类型都很快,而特定几类很慢,将所以将整体 99 响应时间拖长了?或者拿我们自己的数据库监控来说,是不是应该马上看一下,有没有事务冲突重试,看下锁的监控是什么样子的?

假设我们算出平均处理一个请求需要 10ms,并且我们知道系统各阶段会经历什么。还是拿我们的系统举例,一个 SQL 请求进到数据库以后,要做 parse 生成 AST,然后 optimize 生成执行计划,接着 execute 执行它。另外我们的事务模型需要取一个全局唯一时钟 tso。那么我们就可以算一算,各步骤分别占用的耗时,加起来跟整个请求处理时间是否吻合。

parse 正常情况下落在 100 us,optimize 也是 几百 us,execute 对简单点查就等于走一次网络时间,tso 也是一次网络时间,我们在同数据中心内,一次网络也就 500 us,小于 1ms。 取 tso 跟 parse + optimize 是并行的,parse + optimize 正常小于 tso,制约因素会落在 tso,那么经过分析,点查的理论处理时间应该在两次网络请求,2ms。

如果跟理论算的不一样,就应该看监控定位问题。是不是应该想到 tso 的问题。也要修正自己的理论,比如 SQL 特别复杂,那 parse 时间会不会升高严重,或者 parse + optimize 超过了 tso 时间成为制约因素?这些都是需要数据敏感性的。怕就怕,没有数据敏感性,多快叫快?多慢叫慢?没有分析方法,拿到监控也是大脑一片空白。

数据敏感性一定要建立起来。有一个 jeff dean 的 what are the numbers that every computer engineer should know,网上可以搜到,推荐每个程序员都应该了解一下:

Latency Comparison Numbers (~2012)
----------------------------------
L1 cache reference                           0.5 ns
Branch mispredict                            5   ns
L2 cache reference                           7   ns                      14x L1 cache
Mutex lock/unlock                           25   ns
Main memory reference                      100   ns                      20x L2 cache, 200x L1 cache
Compress 1K bytes with Zippy             3,000   ns        3 us
Send 1K bytes over 1 Gbps network       10,000   ns       10 us
Read 4K randomly from SSD*             150,000   ns      150 us          ~1GB/sec SSD
Read 1 MB sequentially from memory     250,000   ns      250 us
Round trip within same datacenter      500,000   ns      500 us
Read 1 MB sequentially from SSD*     1,000,000   ns    1,000 us    1 ms  ~1GB/sec SSD, 4X memory
Disk seek                           10,000,000   ns   10,000 us   10 ms  20x datacenter roundtrip
Read 1 MB sequentially from disk    20,000,000   ns   20,000 us   20 ms  80x memory, 20X SSD
Send packet CA->Netherlands->CA    150,000,000   ns  150,000 us  150 ms
另外,我更推荐自己实验,自己平时总结数据,自己动手得到的数据,印象更加深刻!平时多积累。比如说当我看到这个 repo,就知道这个程序员的数据敏感性肯定挺好,那基本素质绝对不会差。

先假设再求证

观察到性能问题后,在分析时应该是先假设,再求证的。注意要排除掉外界干扰因素,比如说以前遇到过同机器上部署了其它业务,周期性打满 CPU的

曾经有一次使用过某个 SB 的 bench 工具,可以控制固定流量的负载去观察系统表现。结果发现不太对。把 top 刷新时间调短后,观察 CPU 使用很不稳定。就怀疑 bench 程序有问题。 果然,它是这样控制固定的 QPS 的:在每一秒的前期疯狂的制造很高的并发请求,然后就等待直到下一秒。比如生成 1000 QPS 负载,它实际全部落在每秒的几分之一秒内,完全不均匀。到了系统那边,就变成了一下暴发流量,一下又空了,性能随之波动。根本不是在测试固定 QPS 下的效果。

以前聊过 Go 的调度导致的问题,同样是假设网络问题,假设 runtime 有问题,再一步一步分析,验证。 最近在 tso 问题上面,又有一个新的发现。某系统每小时周期性的跑一些分析型任务,然后观察到 tso 的获取时间会周期性变长。对应机器配置特别高,所以跑周期任务时,负载其实也并不高,不是之前遇到的调度问题。 另外我们观察到,分析任务跑的时候,内存会从平时 300M 以内,上升到 6~7G,内存的上涨波动,跟 tso 的时间波动能正好对得上。

好啦,只聊方法论,基于这个观察,可以做一个假设,内存占用影响到 GC 时间,然后 GC 时间影响到 tso 对应的 goroutine,进而影响了延迟。Go 语言的 GC 的 stop the world 时间是很短的,但是注意到,即使不用 stop the world,扫描某个 goroutine 的时候,还是需要挂起这个 goroutine 的,所以它是 stop the goroutine 的。如果整个系统依赖于这个核心的 goroutine,那系统整体延迟还是受影响的。如果求证怎么做?可以模拟类似的场景,然后看内存使用高和内存使用低时,对比 trace 的区别。

个人经验,在几个用 Go 语言做系统里面,我观察到机器配置较高的时候,开多个 Go 进程比单个大 Go 进程性能好。最后部署都是前面挂 proxy 起多个进程,而不是单台机器上只部署一个大的进程的。那怎么分析这个现象呢? 可以假设,调度那一层的损耗并不是随着负载 O(n) 的事情,当负载 N 越大,其实性能损失并不线性增加。就类似为什么排序使用二分的时候可以获得更好的性能。比如说,程序有单点,有全局的 lock,这些随着线程 CPU 核数增加时,并不会 scalable。另外,在系统调度的这一层,单进程和多进程也不同。多进程相对于单进程,是否会更多的从系统那边获取被调度机会。验证起来并不太好做。

先估算再实现

性能是应该在系统设计阶段就估算的。我们公司有一个架构师(前金山快盘之父),做过一次分享,他的方法论我很认同。他有一个特点,不计较一城一池的得失,而强调宏观把控。 系统有 CPU,网络,磁盘等等很多资源,他强调的是,如何合理的把各项资源吃满,只要这一个点做到位了,系统整体的性能不会差到哪去。

他喜欢把终端窗口切成好多,放到一个屏幕里,然后同时监控 CPU,网络,磁盘等等。如果一会 CPU 一个波峰其它很低,一会儿又磁盘吃满了 CPU 空着,这种系统整体的吞吐就不太好。 反之,如果整体曲线是各项相对平滑,那说明这个系统合理地把各项资源都利用上了,这就是一个设计得好的系统。

举个例子,对一个下载类的任务,从一处下载数据,然后将数据压缩,最后要将结果存盘。这是典型的分阶段任务,不同阶段资源瓶颈不同。下载需要网络,压缩主要是耗 CPU,而存盘需要磁盘 IO。 如果才能达到更好的吞吐呢?假设串行的做,系统就会出现典型的先卡在网络,再卡在 CPU,最后卡在磁盘。如果我们将任务划分成小块,然后流水线的做这些事情,就可以把各阶段的等待时间消除掉。 提升吞吐,就那几个关键字:并行,批量,流式。但这里面的门道却很多,任务切分按多大?切大切小分别有什么问题。不同机器性能不一致,有特定的就比其它慢怎么搞?数据乱序到达如何处理,重传的幂等性。如何确定各级流水线,分别该分配多少线程呢?扯远了。各级流水线生产者消费者之间,用消息队列串起来,最终的结果,肯定只有一个消息队列塞满,其它都空着。塞满的那一个,就对应整个系统的瓶颈所在。

估算,提前要做 benchmark,这个例子里面,网络传输的速度多少,写盘速度多少,然后各种压缩算法的数据处理速度是多少 M/s,这些做到心里有数。按照他的方法,在设计阶段,整体系统大概的吞吐量,就应该能估算出来。 代码写得挫没关系,不计较一城一池的得失,各个环节,哪一步有不符合预期,再去做细节优化,慢慢调整,最终整个系统的性能目标就是可控的。

最后,我发现在系统性能这一块,是区分新手老手的一个很好的试金石。因为系统的性能分析的东西,基本是靠经验积累上来的。看校招,看新人,这些地方就很明显。

性能