我还年轻的时候,经常听一些大会或者演讲。有些人说,思路逻辑非常重要。我那时就想,你肯定是瞎忽悠的,因为我怎么就没听懂你说的思路呢?而现在轮到自己来写或者讲一些东西的时候,才发现他们说得对,而我之所以不理解,也是有原因的。性能分析思路和具体实现之间,有一道鸿沟,那就是操作的能力。之前我为什么听不懂那些人的思路,其实是因为我没有操作的功底。而有了操作的功底之后,还有一个大的鸿沟要越过去,那就是从操作到对监控计数器的理解。这一步可以说让很多性能测试人员都望而却步了。但是这还不算完,这一步迈过去之后,还有一个跳跃,就是相关性分析和证据链分析的过程。

如此一来,就会得到一张性能测试分析的能力阶梯视图,如下:

性能分析能力阶梯视图

  1. 工具操作:包括压力工具、监控工具、剖析工具、调试工具
  2. 数值理解:包括上面工具中所有输出的数据
  3. 趋势分析、相关性分析、证据链分析:就是了解了工具产生的数值之后,还要把它们的逻辑关系明白。这才是性能测试分析中最重要的一环
  4. 最后才是调优:有了第 3 步之后,调优的方案策略就有很多种了,具体选择取决于调优成本和产生的效果

那么怎么把这些内容都融会贯通呢?下面我们就来说说性能测试分析的几个重要环节。

应该说,从我十几年的性能工作中,上面讲的这些内容是我觉得最有价值的内容了。在今天的文章中,我们将对它做一次系统的说明。我先把性能分析思路大纲列在这里:

  1. 瓶颈的精确判断
  2. 线程递增的策略
  3. 性能衰退的过程
  4. 响应时间的拆分
  5. 构建分析决策树
  6. 场景的比对

瓶颈的精确判断

TPS 曲线

对性能瓶颈做出判断是性能分析的第一步,有了问题才能分析调优。之前有很多人在描述性能测试的过程中,说要找到性能测试中曲线上的“拐点”。我也明确说,大部分系统其实没有明确的拐点的。举例来说,TPS 的试图如下:

TPS图 1

显然,这是一个阶梯式增加的场景,非常好。但是拐点在哪呢?有人说,显然在 1200TPS左右的时候。也有人说了,显然是到 1500TPS 才是拐点呀。但是也有人说,这都已经能到2000TPS 了,显然 2000TPS 是拐点。

我们再来看一下这张图对应的响应时间视图:

响应时间图 1

是不是有人要说响应时间为 4.5ms 时是拐点了?其实这些对拐点的判断,都是不合理的。如果我们对 TPS 的增加控制得更为精确的话,那么这个 TPS 的增加是是有一个有清晰的弧度,而不是有一个非常清晰的拐点。

但是至少我们可以有一个非常明确的判断,那就是瓶颈在第二个压力阶梯上已经出现了。因为响应时间增加了,TPS 增加得却没有那么多,到第三个阶梯时,显然增加的 TPS 更少了,响应时间也在不断地增加,所以,性能瓶颈在加剧,越往后就越明显。

那么我们的判断就是:

  1. 有瓶颈!
  2. 瓶颈和压力有关
  3. 压力呈阶梯,并且增长幅度在衰减

如果你觉得上面的瓶颈还算清晰的话,那么我们再来看一张图:

TPS图 2

在这个 TPS 的曲线中,你还能判断出拐点在哪吗?

显然是判断不出来拐点的,但是我们根据图得出以下几个结论:

  1. 有瓶颈!
  2. 瓶颈和压力有关
  3. 压力也是阶梯的,但是并没有明确的拐点

我们再来看一个 TPS 图:

TPS图 3

看到这张图,是不是明显感觉系统有瓶颈呢?那么瓶颈是不是和压力大小有关呢?

这种比较有规律的问题,显然不是压力大小的原因。为什么呢?因为 TPS 周期性地出现降低,并且最大的 TPS 也都恢复到了差不多的水位上。所以,即使是压力降低,也最多降低最大的 TPS 水位,会让问题出现得更晚一点,但是不会不出现。

综合以上,如果画一个示意图的话,TPS 的衰减过程大概会如下所示:

示意图

  1. 随着用户数的增加,响应时间也在缓慢增加
  2. TPS 前期一直都在增加,但是增加的幅度在变换,直至变平

在这样的曲线图中,我们是看不到明确的观点的。但是我们能做的清晰的判断就是:有瓶颈!

所以对 TPS 曲线来说,它可以明确告诉我们的就是:

  1. 有没有瓶颈:其实准确来说所有的系统都有性能瓶颈,只看我们在哪个量级上做性能测试了
  2. 瓶颈和压力有没有关系:TPS 随着压力的变化而变化,那就是有关系。不管压力增不增加,TPS 都会出现曲线趋势问题,那就是无关

这时你可能会问,为什么不看响应时间就武断地下此结论呢?其实响应时间用来判断业务有多快的,而 TPS 才是用来判断容量有多大的。

响应时间的曲线

我们还是来看看响应时间,下面看一张响应时间图:

响应时间图

它对应的线程图是:

线程图

多明显的问题,随着线程的增多,响应时间也在增加,是吧。再来看它们对应的 TPS 图:

TPS图

到第 40 个线程时,TPS 基本上达到上限,为 2500 左右。响应时间随着线程数的增加而增加了,系统的瓶颈显而易见地出现了。

但是,如果只让你看 TPS 曲线,你是不是也会有同样的判断?那就是:有瓶颈!并且和压力有关?所以说,其实 TPS 就可以告诉我们系统有没有瓶颈了,而响应时间是用来判断业务有多块的。

后面我们还会提到响应时间会是性能分析调优地重要分析对象。

线程递增的策略

讲完响应时间之后,我们再来看下线程递增。在见识了很多性能测试人员做的场景之后,必须得承认,有些场景地问题太多了。

首先,我们来看两个场景地执行对比。

场景 1 的线程图:

线程图 1

场景 1 的 TPS 图:

TPS图 1

场景 1 的响应时间图:

响应时间图 1

场景 2 的线程图:

线程图 2

场景 2 的 TPS 图:

TPS图 2

场景 2 的响应时间图:

响应时间图 2

这两个场景的比对如下:

比对图

对比项场景 1场景 2
线程数一次性加到50010线程递增,并且递增中也是有梯度的
TPS最大值达到400最大值达到400,但递增过程中有抖动
响应时间在600ms~660ms之间在20ms~150ms之间
错误率
粒度以分钟为粒度以2秒为粒度

有了这些对比数据之后,你是不是觉得哪里似乎是有问题的?

对的!

TPS 都是达到 400,但两个场景中线程递增的策略不同。产生的响应时间完全不同。虽然都没有报错,但是第一种场景是完全不符合真实的业务场景的。这是为什么呢?

在场景的执行过程中,首先,响应时间是从低到高的,而在场景 1 中不是这样。其次,线程应该是递增的,而场景 1 并没有这样做(这里或许有人会想到秒杀的场景,认为场景 1 符合秒杀的业务设定,这个问题我们稍后提及)。最后,在两个场景中,TPS 的上限都达到了 400 TPS。但是你可以看到,在场景 2 中,只要 40 线程即可达到,但场景 1 中居然用到了 500 线程,显然压力过大,所以响应时间才那么长。

其实在生产环境中,像场景 1 这样的情形是不会出现的。如果它出现了,那就是你作为性能测试的责任,因为你没有给出生成环境中应该如何控制流量的参数配置说明。

同时,我们从上面的场景对比中可以看到,对一个系统来说,如果仅在改变压力策略(其他的条件比如环境、数据、软硬件配置等都不变)的情况下,系统的最大 TPS 上限是固定的。

场景 2 使用了递增的策略,在每个阶梯递增的过程中,出现了抖动,这就明显是系统设置的不合理导致的。设置不合理,有两种可能性:

  1. 资源的动态分配不合理,向后端线程池、内存、缓存等等
  2. 数据没有预热

我们再回到之前说的秒杀场景。说到秒杀场景,有人觉得用大线程并发是合理的,其实这属于认知上的错误。因为即使线程数增加得再多,对已经达到 TPS 上限的系统来说,除了会增加响应时间之外,并无其他作用。所以他们描述系统的容量是用系统当前能处理的业务量(你用 TPS 也好,RPS 也好,HPS 也好,它们都是用来描述服务端的处理能力的),而不是压力工具中的线程数。

那么,对于场景中线程(有些工具中叫虚拟用户)递增的策略,我们要做到以下几点:

  1. 场景中的现场递增一定是连续的,并且在递增的过程中也是有梯度的。
  2. 场景中的线程递增一定要和 TPC 的递增有比例关系,而不是突然达到最上限。
  3. 上面两点针对的是常规的性能场景。对于秒杀类的场景,我们前期一定要做好了系统预热的工作的,在预热后,线程突增产生的压力,也是在可处理范围的。这是,我们可以设计线程徒增的场景来看系统瞬间的处理能力。如果不能模拟出秒杀的徒增,就是不合理的场景。

这里给出我做性能场景递增的经验值:

响应时间递增幅度
0-50ms1
50-100ms1-3
100-200ms3-5
200-500ms5-10

当然这里也不会放在哪个系统中合适的递增幅度,你还是要根据实际的测试过程来做相应的判断。

性能衰减的过程

有瓶颈的判断能力,也有了线程递增的意识,那么下面在场景执行,我们就要有判断性能衰减的能力了吧。来,我们先看一个压力过程中产生的结果图。

压力结果图

在递增的压力过程中,随着用户数的增加。我们可以做几次计算。

第一次计算,在线程达到 24 时,TPS 为 1810.6,也就是每线程每秒发出 75.44 个请求。

第二次计算,在线程达到 72 时,TPS 为 4375.1,也就是每线程每秒发出 60.77 个请求。

第三次计算,在线程达到 137 时,TPS 为 5034,也就是每线程每秒发出 36.74 个请求。

通过这三次计算,我们是不是可以看到,每线程每秒发出的请求数在变少,但是整体 TPS 是在增加的。

我们很多做性能测试的人,基本上,只看 TPS 和响应时间的时候,在上面这个示例中,肯定会一直往上加用户。虽然响应时间在增加,但是增加得也不多嘛。

但实际上,通过我们的计算可以知道,性能是在不断地衰减的。我们来看一张统计图:

统计图

通过红线德大致比对可以知道,当每线程每秒的请求数降到 55 左右的时候,TPS 就达到上线了,大概在 5000 左右,再接着往上增加线程已经没有用了,响应时间开始往上增加了。

这就是性能衰减的过程(题外话,再上图中,其实还有一个问题,就是在红线前面,性能在上升的过程中有几次抖动,这个抖动到后面变大了,也变频繁了,如果这是必然出现的抖动,那也是配置问题)

为什么要这么细致地描述性能衰退的过程呢?

其实我就是想告诉你,只要每线程每秒的 TPS 开始变少,就意味着性能瓶颈已经出现了。但是瓶颈出现之后,并不是说服务器的处理能力(这里我们用 TPS 来描述)会下降,应该说 TPS 仍然会上升,在性能不断衰退的过程中,TPS 就会达到上限。

这也是前面说的,性能瓶颈其实在最大 TPS 之前早就已经出现了。

那么我们是不是应该在性能衰减到最大 TPS 时就停止场景呢?这个不一定的哦。

因为停不停场景,取决于我们的场景目标,如果我们只是为了得到最大 TPS,那确实可以停止场景了。但是,如果我们要扩大化性能瓶颈,也就是说为了瓶颈更为明显,就完全不需要停止场景,只要不报错,就接着往上压,一直压到我们能要说的下一个话题 ———— 响应时间变长,需要拆分。

响应时间的拆分

在性能分析中,响应时间的差分通常是一个分析起点。因为在性能场景中,不管是什么原因,只要系统达到了瓶颈,再接着增加压力,肯定会导致响应时间的上升,直至达到超时为止。

在判断了瓶颈之后,我们需要找到问题出现在什么地方。在压力工具上看到的响应时间,都是经过了后端的每一个系统的。

那么,当响应时间变长,我们就要知道,它在哪个阶段时间变长了,来看下面这个图:

压力测试逻辑图

这应该是最简单的一个压力测试逻辑图。一个应用,一个 DB,结果也拆分出了 8 个时间段,这还是在我没有加上压力工具自己所消耗的时间的情况下。但是在真实的场景中,基本上不是这样的。如果是内网,那基本上都是连在一个交换机上,所以通常是这样的:

拓扑图1

在这样的拓扑中,我们仍然可以拆出来 t1 到 t8 的时间。只是实际动手的时候,思路一定要清晰,时间拆分是从哪里到哪里,要画出来,不能混乱。

我们有很多手段可以进行时间的拆分,当然要看我们的应用支持哪一种。如果我们是这样的架构,拆分时间应该是比较清楚的。

拓扑图2

首先我们需要查看 Nginx 上的时间。日志里就可以通过配置$request_time $upstream_response_time得到日志如下信息:

1
14.131.17.129 - - [09/Dec/2019:08:08:09 +0000] "GET / HTTP/1.1" 200 25317 0.028 0.028

最后两列中,前面是请求时间的 28ms,后面是后端响应时间的 28ms。同时,我们再到 Tomcat 上去看时间。

1
172.18.0.1 - - [09/Dec/2019:08:08:09 +0000] "GET / HTTP/1.1" 200 25317 28 27 http-nio-8080-exec-1

请求时间消耗了 28ms,响应时间消耗了 27ms。接着再来看一下前端的时间消耗。

前端耗时图

从这里可以看到,从发出请求到接收到第一个字节,即 TTFB 是 55.01ms,内容下载用了 11.75ms。从这就可以看得出 Nginx 基本上没消耗时间,因为它和 Tomcat 上的请求响应时间非常接近。

那么网络上的消耗时间怎么样呢?我看到有很多人用 TTFB 来描述网络的时间。先来说明一下,TTFB 中显然包括了后端一系列处理和网络传输的时间。如下图所示。

TTFB 图1

下面的红色点是指要接收的内容。上面的红色线就是 TTFB。如果接收完了呢?就是这个状态。

TTFB 图2

所以,我觉得用 TTFB 描述网络的健康状态并不合理。如果用 Content Download 来描述会更为合理。比如我们上面的这个例子中,那就是 11.75ms 下载了 25317 Bytes 的内容。

Tomcat 上基本上是消耗了处理的所有时间,当然这中间也包括了 MySQL 花费的时间。而前端看到的其他时间就消耗在了网络中。

在这个例子中,主要说明了响应时间怎么一步步拆。当然,如果你是下面这种情况的话,再一个个拆就比较辛苦了,需要换另一种方式。

拓扑图3

你肯定想知道每个系统消耗了多长时间,那么我们就需要链路监控工具来拆分时间了。比如像这样来拆分:

拆分图

从 User 开始,每个服务之间的调用时间,都需要看看时间消耗的监控。这就是时间拆分的一种方式。其实不管我们用什么样的工具来监控,最终我们想要得到的无非是每个环节消耗了多长时间。用日志也好,用链路监控也好,甚至抓包都可以。当我们拆分到了某个环节之后,就有了下一步的动作:构建分析决策树。

构建分析决策树

分析决策树,对性能测试分析人员实在太重要了,是性能分析中不可或缺的一环。它是对架构的梳理,是对系统的梳理,是对问题的梳理,是对查找证据链过程的梳理,是对分析思路的梳理。它起的是纵观全局,高屋建瓴的指导作用。

性能做到了艺术的层级之后,分析决策树就是提炼出来的,可以触及旁通的方法论。而我要在这里跟你讲的,就是这样的方法论。应该说,所有的技术行业在面对自己的问题时,都需要分析决策树。再广而推之,所有的问题都要有分析决策树来协助。

通过上面的几个步骤,我们就会知道时间消耗在了哪个节点上。那么之后呢?又当如何?总要找到根本的原因才可以把,我画了如下的分析决策图:

分析决策图

从压力工具中,只需知道 TPS、响应时间和错误率三条曲线,就可以明确判断瓶颈是否存在。再通过分段分层策略,结合监控平台、日志平台,或者其他实时分析平台,知道架构中哪个环节有问题,然后再根据更细化的架构图一一拆解下去。

我在这里,以数据库分析和操作系统分析举一下例子。首先我们看一下数据库分析决策树,比如针对 RDBMS 中的 MySQL,我们就可以画一下如下的决策树:

mysql分析决策图

由于这里面的内容实在过多,无法一次性展现在这里。我举几个具体的例子给你说明一下。

MySQL 中的索引统计信息有配置值,有状态值。我们要根据具体的结果来判断是否需要增加 key_buffer_size 值的大小。比如这种就无所谓了。

1
Buffer used     3.00k of   8.00M  %Used:   0.0004

从上面的数据可以看到,key buffer size 就用到了 4%,显然不用增加。

再比如,我们看到这样的数据:

1
2
3
   __Tables_______________________
Open 2000 of 2000 %Cache: 100.00
Opened 15.99M 4.1/s

这就明显有问题了。配置值为 2000 的 Open Table Cache,已经被占满了。显然这里需要分析。但是,看到状态值达到配置值并不意味着我们需要赶紧加大配置值,而是要分析是否合理,再做相应的处理。比如说上面这个,Table 确实打开得多,但是如果我们再对应看下这一条。

1
Slow 2 s        6.21M     1.6/s

你是不是觉得应该先去处理慢 SQL 的问题了?

关于数据库的我们就不举更多的例子了。在这里只是为了告诉你,在分析决策树的创建过程中,有非常多的相互依赖关系。

然后我们再来看一下操作系统分析决策树,我在这里需要强调一下,操作系统的分析决策树,不可以绕过。

操作系统分析决策图

如果你想到操作系统架构图就头大,那么这时候应该觉得有了希望。那就是我觉得操作系统上的问题判断是比较清晰的,所以基于此决策树,每个人都可以做到对操作系统中性能问题的证据链查找。

但是!对嘛,总得有个但是。

对操作系统的理解是个必然的前提。我看过很多人写的操作系统性能分析方面的书籍或资料,发现大部分人把描述计数器的数值当成性能分析。怎么理解这句话呢?比如说

“CPU 使用率在 TPS 上升的过程中,从 10% 增加到 95%,超过了预期值。” “内存使用率达到 99%,所以是瓶颈点。” “I/O 使用率达到 100%。” 等等。

像这样的描述,在我的性能团队中,一定会被骂回去重写。我要这些描述有什么用?我要的是为什么达到了这样的值,原因在哪?怎么解决?

就像分析决策树中所描述的那样,性能工程师要做的是一步步地细化分析,给出最终的原因。

有人说,如果按这个路子,似乎操作系统的分析并不复杂嘛。大概三五个命令就可以跳到代码层了。是的,对于操作来说,确实不多,但是对于判断来说,那就复杂了。举个例子来说明一下:

TOP图

看到这样的图,你是不是有种手足无措的感觉?中断能占 40%,sy CPU 也能占 40%。这系统还用干业务的事吗?全干自己的事去了,可见操作系统有问题!你是不是要做这个判断了?

而实际情况是,这个主机上只有一个网卡队列,而请求量又比较大。

网卡队列图

所以要解决的是网卡队列的问题,至于怎么解决,那手段就多了。可以换个服务器,可以多加几个队列,可以多接几个节点…

以上只是给出几个性能分析过程中常见的决策树示例。在后续的分析过程实例中,我们将秉承着这种分析思路,一步步地走到瓶颈的面前。

场景的比对

为什么要写这一部分呢?因为我看到很多人对瓶颈的判断,并不那么精确,所以想写一下场景对比的建议。其实简单来说,就一句话:当你觉得系统中哪个环节不行的话,又没能力分析她,你可以直接做该环节的增加。举例来说,我们现在有一个如下的架构:

架构图

可以得到这样的结果:

结果图

从 TPS 曲线中,我们明显看到系统是有瓶颈的,但是并不知道在哪里。鉴于系统架构如此简单,我们索性直接再某环节上加上一台服务器,变成这样:

架构图

然后得到如下数据:

结果图

哟,没好使!

怎么办?再接着加其他节点,我加了更多的 JMeter 机器。

架构图

再来看下结果:

结果图

真巧,TPS 增加了!

看到了吧,这就是我说的场景比对。

当我们不知道系统中哪个环节存在性能瓶颈时,对架构并不复杂的系统来说,可以使用这样的手段,来做替换法,以快速定位问题。

总结

在这一篇中,我说到了瓶颈的精准判断、线程递增的策略、性能衰减的过程、响应时间的拆分、构建分析决策树以及场景的比对,这几个环节,是性能分析过程中非常重要的环节。

从我的经验上来说,这一篇文章可能是我工作十几年的精华所在了。而这里的每一个环节,又有非常多的细分,特别是构建分析决策树这一块,它需要太多的架构知识、系统知识、数据库知识等等。鉴于本文只是想起到一个提纲挈领的作用,所以无法展开描述,希望在后续的篇幅中,我们尽量细致拆解。