This commit is contained in:
louzefeng
2024-07-11 05:50:32 +00:00
parent bf99793fd0
commit d3828a7aee
6071 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,352 @@
<audio id="audio" title="07丨性能测试工具如何录制脚本" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/61/40/6155c7c2b616c24e0f97b9f6a6afbb40.mp3"></audio>
对于一个性能测试工具来说,如果能实现以下几大功能,那么就基本上就满足了性能测试工具的功能。
1. 录制或编写脚本功能
1. 参数化功能
1. 关联功能
1. 场景功能
1. 报告生成功能
但是除此以外,在工作的细节上还有更多要求,就要看工具的实施能力了。
有很多性能测试工程师希望工具能做得非常全面,又人性化,而纵观当前的性能工具,真正能够做到傻瓜式录制完脚本,自动设置好参数化、关联、场景,直接产出结果的工具是没有的。不管是云性能测试平台,还是分布式性能测试工具(当然性能测试工具几乎全部具有分布式能力),都需要性能测试人员来定义参数化数据、设置关联、配置场景。
因此,在性能测试的过程中,对工具的配置就成为了性能测试工程师的基本能力。
今天,我们就来看下在性能测试工具中,如何录制脚本。今天的文章有些特殊,可能是专栏中少有的,有详细操作的文章。
## 性能工具的脚本能力
性能测试工具的脚本编写能力分为两类,一个是录制,另一个是手工编写。
现在市场上的性能测试工具虽然支持录制功能但大部分也只是支持HTTP协议。在我们熟知的工具中也只有LoadRunner支持更多协议的录制能力。不过幸好现在我们所面对的应用大部分是HTTP协议的应用。
对手工编写脚本的部分,因为大部分都取决于业务场景,所以很难提出共性。如果有人提出针对性的场景,我们再做相应的示例就行。
因此今天的文章将着重讲一下测试工具的录制功能。很多人以为性能工具录制功能非常简单,点几下就能生成一个脚本,但是录制完之后,针对脚本的增强完善就做得非常少了。事实上,针对脚本,我们不仅要录制下来,还要了解录制的原理和录制完之后的脚本增强。不然,在场景中还是会遇到各种各样的问题。
## 性能工具中的录制功能
录制功能从原理上来说,分成两种:
1. 本地录制通过截取并解析与服务器的交互协议包生成脚本文件。比如说LoadRunner调起IE的时候不用修改IE的代理设置就可以直接抓取HTTP包并通过自己的解析器解析成脚本。
1. 代理录制通过代理服务器设置转发客户端和服务器的交互协议包生成脚本文件。JMeter中的脚本录制功能就是这样做的。
这两者的不同点主要在于操作上。本地录制相对简单,但有些场景受限,比如说操作只能在某台服务器上,但是这台服务器又不允许安装工具;代理录制操作复杂一些,但可以满足更多的场景。
通过这张图,我们可以简单看到代理录制的逻辑:
<img src="https://static001.geekbang.org/resource/image/2c/49/2c444b52c9b614bd7779394e6ed18849.jpg" alt="">
1. 我们在IP为2.2.2.2上的主机上打开一个代理程序开81端口所有到81端口的都转发到1.1.1.1的80端口。
1. 当3.3.3.3主机要访问1.1.1.1的的80端口可以通过访问2.2.2.2的81端口进行转发。
这里需要你注意的是,代理是用来转发数据包的,并不是重定向哦。不管是在本机用代理,还是远程用代理,这个逻辑都是不会变的。
有了这个逻辑之后,你要明白的一点是,**客户机不一定要和代理服务器在同一台机器上**。
为什么要强调这一点呢因为有很多人用工具来录制时都不知道这个逻辑只知道工具是那么操作的。这也是很多人不能理解Port mapping的原因。
不同的工具录制方式略有不同。今天我们用常见的两个性能测试工具LoadRunner和JMeter做为示例工具。
### JMeter的录制功能
首先打开JMeter添加一个线程组再添加一个HTTP(S) Test Script Recorder。界面如下
<img src="https://static001.geekbang.org/resource/image/df/f8/df8730ffbbfb0e68879a88e8699035f8.png" alt="">
这里有几个关键点说明一下:
- Target Controller这里指定录制出的脚本要放到哪里去。如果你想把不同的脚本放到不同的线程组中去在录制的时候就可以拆分开。
- Grouping分组这个分组功能很实用。但是如何分组就和具体的目标相关了这一点下面我们再细说。
点击start按钮时会提示创建一个根CA证书。这个证书生成在bin目录中文件名是ApacheJMeterTemporaryRootCA.crt七天有效期。这个证书将被用来客户端转发HTTPS的请求。与此同时还有另一个证书在同目录中生成名字是proxyserver.jks这是JMeter自己生成的根证书。
<img src="https://static001.geekbang.org/resource/image/be/19/be8ef3815ed27b60f11aae9679457219.png" alt="">
前面我们说到了JMeter是用代理的方式来录制的。如果服务端用了SSL证书在代理时也要加SSL证书那么代理录制的结构就会变成这样。
<img src="https://static001.geekbang.org/resource/image/8f/c9/8f51e323947ca6b8109241f1c71e0ec9.jpg" alt="">
上面的SSL证书就是用来处理上图中蓝色的这一部分。
我们点击ok之后就会出现这个界面。在这个界面中只有两个配置项。
- Prefix请求名的前缀。
- Create new transaction after request(ms):一个请求完成之后,如果下一个请求超出了这里设置的时间间隔,就创建一个新的事务。
<img src="https://static001.geekbang.org/resource/image/bf/ff/bff35c026d490ec75425a0d6e41a36ff.png" alt="">
然后到主机上设置代理。
注意这里我要敲黑板了呀这里的代理设置是在需要访问的客户机上。这个客户机不一定是压力机所在的机器。这里的localhost也应该设置的是代理服务所在的主机IP。
<img src="https://static001.geekbang.org/resource/image/74/49/74e3652701b526509b410790f0fa2549.png" alt="">
请注意如果你要设置为录制HTTPS还需要做如下两步。
第一步是浏览器代理要把Secure Web Proxy(HTTPS)选择上同时填上相应的代理IP和端口下图是macOS上的图示。
<img src="https://static001.geekbang.org/resource/image/b3/46/b3ef5c0c4ce44566ee31157c17cd1846.png" alt="">
但你会发现这时仍然录制不了HTTPS应用访问时会出现如下提示
<img src="https://static001.geekbang.org/resource/image/35/4f/3530bde553697ac448eac94e26ee524f.png" alt="">
这时就要在客户端机器上导入上面提到的ApacheJMeterTemporaryRootCA.crt。我们打开证书管理软件在macOS上是Keychain AccessWindows上是certmgr.msc。
这里以macOS为例。
首先打开Keychain Access。
<img src="https://static001.geekbang.org/resource/image/68/2e/6861248706928c135e68bed9ee28232e.png" alt="">
点击上图中的Import Items。选择ApacheJMeterTemporaryRootCA.crt导入之后选择证书。会看到如下提示
<img src="https://static001.geekbang.org/resource/image/dd/92/ddfdc4ef2050f922c9ea1b7ed3977d92.png" alt="">
因为这个证书不在系统信任的默认列表里,所以会提示证书不可信。
另外这里我可以再多说一句你注意的是全球的可信任的根证书都是默认添加到系统中的如果你在访问网站时提示你要安装什么证书一定要明确知道证书是从哪来的不要随意安装未知来源的证书。目前国内的HTTPS覆盖度不高仍然有大量的HTTP网页这是需要推进的网络安全之一。
然后我们双击此证书。
<img src="https://static001.geekbang.org/resource/image/f7/16/f779bc63cd421ba92b1de098899b5616.png" alt="">
改为Always Trust即可。提示如下
<img src="https://static001.geekbang.org/resource/image/41/8c/417c1e58d3081ca6515ec8977532b78c.png" alt="">
这时HTTP和HTTPS都会被录制下来。然后在客户机上打开浏览器访问你的页面这样就录制到脚本了。
下面我们再来说下Grouping这个功能。
Grouping的设置有如下几种如果需要将脚本分开先确定需要如何拆分。示例如下
<img src="https://static001.geekbang.org/resource/image/22/8b/2214f144340139acd8c84a15c86c938b.png" alt="">
第一个选项是Do not group samples也就是不分组。
这是很多人使用的默认选项这就相当于没有事务的概念了每个请求都会单独统计TPS和响应时间信息。
<img src="https://static001.geekbang.org/resource/image/41/83/41bb7af036a9910db9d8fd4bb6cf4083.png" alt="">
第二个选项是Add separators between groups在组间添加分隔就为了好看
<img src="https://static001.geekbang.org/resource/image/25/35/251bcebb7e4451c881ba718ca21d6535.png" alt="">
第三个选项是Put each group in a new controller每个组放一个新的控制器。这是一个Simple Controller它的作用也是只有一个就为了好看
因为脚本太长了看起来不方便所以分个组看着清晰一些。话说回来你们见过在JMeter中有很长脚本的吗是不是很多人都没有见过
<img src="https://static001.geekbang.org/resource/image/74/0a/7451c99638e005cfe98d80ed0def060a.png" alt="">
第四个选项是Put each group in a new transaction controller将每个组放入一个新的事务控制器中。
<img src="https://static001.geekbang.org/resource/image/9b/34/9b5766b5a61fe76ed82a33f671ae9c34.png" alt="">
Transaction Controller和Simple Controller的区别就是Transaction Controller会做为事务统计脚本执行的时间而Simple controller不会。
第五个选项是Store 1st sampler for each group only只存储每个组的第一个样本。
网上大部分都只描述了上面这句但是请注意我这里还有一句关键的从HTML文件获取所有内含的资源和自动重定向将开启。也就是说虽说只记录了一个Sampler但是资源也会下载重定向也会开启。
我们把这个过程抓出来看一下因为JMeter没有把这个过程显示出来。所以这里用Chrome Developer Tool抓一下看看。举例来说我们在浏览器里只输入了一个https://www.jd.com。抓出如下结果。
<img src="https://static001.geekbang.org/resource/image/5a/a4/5a24f2adae925faac98b057a507525a4.png" alt="">
在上面的图中你可以看到www.jd.com第一个就是307 Internal Redirect。接着请求Document然后下面是静态资源。在录制时选择Store 1st sampler for each group only之后只会录制到第一个请求而后面这些在回放脚本时也都会访问。
在JMeter的代理录制中还有一个界面如下
<img src="https://static001.geekbang.org/resource/image/96/0d/968fe93634c0f18c806fa922f300d50d.png" alt="">
中文界面中通常将之翻译为包含模式、排除模式。“模式”一词一加就显得格外高大上了。
通常这里都会写上正则表达式,比如说常用的一些:
```
.*
.*.png
.*.gif
.*.jpg
.*.php
.*.jsp
.*.html
.*.htm
.*.js
..(js|css|PNG|jpg|ico|png|gif).
```
由于正则是一个很大的话题,这里我们就不展开了,只要你懂正则,在这里就可以适用。
通过上面的内容我们已经把JMeter录制的原理和操作的过程都详细地描述了一遍关于JMeter的录制功能就介绍到这里。
在此重点提醒你一下,录制是通过代理做的,一定要知道代理的原理,代理就是转发的功能。
### 承上启下的话
为什么JMeter这样的功能单一性能又不好的性能测试工具能这么快的占领市场呢
在我看来工具能不能用取决于它能不能满足需要。在很多的性能测试场景中JMeter已经够用了。因为性能压力工具只需要两条曲线TPS和响应时间如果出错最多就再看一下错误率曲线。这些功能JMeter都可以提供。
现在的性能项目中我们要的压力其实并没有很大并且大部分都是HTTP、TCP之类的常见协议脚本所使用的资源并不多。一般能达到万级TPS的都很少很少所以弄几个机器JMeter也就够用了再加上免费开源何乐而不为呢
而LoadRunner的失败之处就是价格高更新慢。一想到HP糟蹋了LoadRunner我就伤心落泪。
### LoadRunner中的录制功能
我们都知道LoadRunner其实可以录制很多协议这也是它前期扩展市场的很重要的功能。应该说在录制这个功能点上所有的性能测试工具都不如LoadRunner。并且LoadRunner在其他很多功能上都是强大的强大到什么程度呢就是有很多你不需要的不常用的功能它都具备。
很多人都知道LoadRunner中的Vuser Generator只支持Windows。你有没有想过这是为什么其实解释起来也简单LoadRunner一开始是基于WinInet做的就是Windows Internet API。后来可能是觉得WinInet太恶心了于是换成了Windows socket。而Windows socket跟UNIX socket还是有一些小区别。
所以从历史延续下来Vuser Gnenerator就一直在Windows上了。
为什么不做UNIX的版本呢其实在我看来完全没有这个必要。因为Load Generator已经支持UNIX了。从使用的角度说Vuser Gnenerator没有必要做UNIX的版本因为它还有Port Mapping的功能这样在UNIX上的操作也照样录得下来。。
下面我们就单说LoadRunner的录制功能。
### 常规录制
首先我们打开Vuser Generator点击Start Record出现如下界面
<img src="https://static001.geekbang.org/resource/image/c7/67/c79f3d7d072478b223d557987951c767.png" alt="">
在这个图中,首先的选择是:
<img src="https://static001.geekbang.org/resource/image/0c/b7/0c0600701f1c6b71c8389eb243ab8bb7.png" alt="">
这里用IE或者应用程序都可以只要支持我们选择的HTTP协议就行。
Recording into action这里是默认的action请你一定要注意的是init、action、end这三个都是action并没有什么区别。控制init和end只执行一遍和action会重复执行多次的功能也不在它们自己身上而是在run logic里。这一点我将在后面的文章中再细说。
点击Options之后跳出界面如下
<img src="https://static001.geekbang.org/resource/image/21/c9/21e1969112927b0e16adc2e013241cc9.png" alt="">
在这个界面中,有很多可以调的内容。这里举几个重要的点。
首先是HTML-based script和URL-based script。
<img src="https://static001.geekbang.org/resource/image/d5/16/d57f3c15ea014cf1502e136a590b4e16.png" alt="">
这个功能点之所以重要,是因为这两个选项录制出来的脚本有很大差别。
其实这一点和JMeter的Store 1st sampler for each group only是一样的含义。
如果选择了HTML-based script就是一个页面一个请求了而在回放和压力时这个页面的所有资源都会请求。
如果选择了URL-based script就是每个资源一个请求。这个选项有好处是便于控制和查找问题。如果不想要某个资源直接注释掉就好。
其次,我们需要注意关联功能。
<img src="https://static001.geekbang.org/resource/image/52/43/52e00fd4eb4e6dab16f0acee5031d843.png" alt="">
你可以在这里事先设置好关联的规则,比如说这样的:
```
JSESSIONID=5687300192384o4^&amp;&amp;^&amp;890523#123456;
```
你就可以设置左边界为JSESSIONID=,左边界为分号,然后在你录制的时候,如果规则匹配到就会自动创建关联。
点击OK之后就开始录制了。出现一个工具条如下所示
<img src="https://static001.geekbang.org/resource/image/2d/93/2dac210c135e85e99d8e0405d72a3093.png" alt="">
在这个功能条上具有的功能是暂停、停止、新建Action创建集合点、创建事务的起点和终端、加备注、加文检查点。
一般在业务流比较长的脚本中,<br>
性能测试工程师都会通过新建Action把操作区分开也会在录制过程中创建好必要的事务。
最后录制出的脚本如下:
<img src="https://static001.geekbang.org/resource/image/9f/3c/9fe88180c56b27edd542db9ff715d73c.png" alt=""><br>
注意哦URL-based script的时候有一个concurrent group这个并发组是同时发出请求的。<br>
在JMeter中有一个Parallel Downloads你还记得吗
这两者功能一样。
上面就是LR中常规的录制功能。录制前看下readme看LR支持什么浏览器。在版本12.6的readme中已经声明支持Windows 10 + IE了。但是我们在使用的过程中还是遇到各种各样的问题比如调不出浏览器、录不出脚本、卡死的问题。
还有有些应用只支持Chrome而有时有些应用只能在某些特定的机器的执行而那些机器又不能装Vuser Generator。
在这样的场景中我们只能使用Port Mapping的功能。是的在LoadRunner中Port Mapping就是代理录制的方式。
### Port Mapping
首先打开Vuser Generator点击Start Record配置成如下界面
<img src="https://static001.geekbang.org/resource/image/55/37/554a3e2ef78259e73ecc4616bc176c37.png" alt="">
注意这里一定要选择的是LoadRunner安装目录bin中的wplus_init_wsock.exe从这个名字你也能知道它是基于Windows Socket的。
然后点击Options - Port Mapping如下所示
<img src="https://static001.geekbang.org/resource/image/84/29/847bf5c6d80b59f3772cb07d12c95929.png" alt=""><br>
点击New Entry。配置如下
<img src="https://static001.geekbang.org/resource/image/b3/49/b39d5e0317acd982a42f97f5689a0d49.png" alt="">
从上图中你可以看到它的代理功能是很全面和强大的不仅支持不同的Service ID也支持SSL。
这时的访问逻辑是下面这样的:
<img src="https://static001.geekbang.org/resource/image/0a/8e/0ab937e601a8f2029527be1ccb0de08e.jpg" alt="">
一路OK返回之后我们就可以开始录制了。会打开一个代理程序。截图如下
<img src="https://static001.geekbang.org/resource/image/b8/36/b8cc2119b6eeb7164bbb47987d88ed36.png" alt="">
这时候本地会开一个92的端口。
<img src="https://static001.geekbang.org/resource/image/59/74/591d5598781d5a107b2d56f93a2e6f74.png" alt="">
请注意,这时如果是远程访问,要注意不要让防火墙拦截了。
接着打开浏览器输入地址http://10.211.55.3:92/可以看到打开的是http://39.105.21.22:91/的界面。
<img src="https://static001.geekbang.org/resource/image/dc/ca/dc5d435bca5f519905a7e49db49e72ca.png" alt="">
同时,录制工具条中也显示出有事件产生。
<img src="https://static001.geekbang.org/resource/image/5d/84/5d4e718b7cfd98398b4dc915180b5784.png" alt="">
当我们停止录制后,查看脚本如下:
<img src="https://static001.geekbang.org/resource/image/3a/98/3acc0dac595c23bedc5229f966d15398.png" alt="">
看到没有这里的访问IP在直接回放时是不对的。所以要将ip:port换成39.105.21.22:91才能回放。替换后如下
<img src="https://static001.geekbang.org/resource/image/7d/eb/7d699171119d5aab83af784220e421eb.png" alt="">
这样就可以回放成功了。
<img src="https://static001.geekbang.org/resource/image/61/d8/613e6d89e4480263bbce1c2aa08f0dd8.png" alt="">
如果回放不成功,我们就需要根据出错日志判断要做什么样的脚本增强。大部分的脚本都是需要做关联的,所以后面我们将讲一下关联的功能如何做,以及关联的原理。
Loadrunner的Port Mapping还可以支持FTP、SOCKET、POP等协议。这个功能点也不复杂操作起来也简单只要想明白访问链路就可以了。
LR的录制常用功能基本就这些了。
## 总结
这篇文章,应该是我写的所有的文章中,最最基础的一篇了,并且,从操作上,一步步地描述,也比较清晰。如果你有性能工具使用经验,肯定会觉得这篇过于简单。
可是为什么还要写呢?
因为在性能测试的过程中,有很多新手对录制的逻辑并不清楚。代理录制的这个动作他们也可以很快学会。但是很快就忘记了,我曾经给一些人手把手教过如何做代理录制。结果第二天就不记得了。其实并不是不记得动作,而是出了问题,脑子里没有判断问题的逻辑,所以根本无从下手排查。
另外你需要注意的是录制功能并不是性能测试工具必备的功能。对性能测试工具来说关键功能是能实现模拟批量的真实请求逻辑。至于脚本是如何实现的怎么做就是可以的。所以我们可以用其他的工具比如说BadBoby、Fiddler甚至Wireshark抓到交互请求再放到JMeter中实现脚本也完全是可以的。
当然没有脚本就无从实现压力,所以脚本的实现是性能测试工程师必备的基础技术,理解原理也是必须的。
## 思考题
学完今天的文章后,你能用自己的话说一下代理录制的逻辑是什么吗?以及,当访问网页时,为什么第一个请求至关重要?
欢迎你在评论区写下你的思考,也欢迎把这篇文章分享给你的朋友或者同事,一起交流进步一下。

View File

@@ -0,0 +1,556 @@
<audio id="audio" title="08丨案例 手把手教你编写最简单的性能脚本" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/dc/61/dcb52c71ae3055c8d15e60ac13825961.mp3"></audio>
通常我们会遇到要手写脚本的时候,就要针对一些接口编写脚本。这时候,我们需要知道接口规范和后台的数据是什么。而有些性能测试工程师写脚本时,并不知道后端的逻辑,只知道实现脚本,事实上,只知道实现脚本是远远不够的。
在这一篇文章中,我不打算讲复杂的内容,只想针对新手写一步步的操作,描述最简单的脚本编写。如果你已经具有丰富的脚本编写经验,会觉得本文很简单。
我没有打算把JMeter的功能点一一罗列出来作为一个性能测试的专栏不写一下脚本的实现似乎不像个样子。在脚本实现中我们最常用的协议就是HTTP和TCP了吧所以在今天的内容里我简单地说一下如何编写HTTP和TCP脚本以应测试主题。
我先画个图说明一下。
<img src="https://static001.geekbang.org/resource/image/c9/e6/c99990fb1cfce0410b6bb2be24107be6.jpg" alt="">
这样的图做性能的人一定要知道,相信很多人也画的出来。
我们知道HTTP是应用层的协议之一现在很多场景都在用它并且是用的HTTP1.1的版本对应的是RFC2616当然还有补充协议RFC7231、6265。
HTTP中只规定了传输的规则规定了请求、响应、连接、方法、状态定义等。我们写脚本的时候必须符合这些规则。比如为什么要在脚本中定义个HeaderHeader里为什么要那样写这些在RFC中都说得明明白白了。
还有一点也需要注意HTTP是通过Socket来使用TCP的Socket做为套接层API它本身不是协议只规定了API。
而我们通常在JMeter中写TCP脚本就是直接调用Socket层的API。TCP脚本和HTTP脚本最大的区别就是TCP脚本中发送和接收的内容完全取决于Socket server是怎么处理的并没有通用的规则。所以脚本中也就只有根据具体的项目来发挥了。
## 手工编写HTTP脚本
### 服务端代码逻辑说明
我们先自己编写一小段服务端代码的逻辑。现在用Spring Boot写一个示例其实就是分分钟的事情。我们做性能测试的人至少要知道访问的是什么东西。
Controller关键代码如下
```
@RestController
@RequestMapping(value = &quot;pa&quot;)
public class PAController {
@Autowired
private PAService paService;
//查询
@GetMapping(&quot;/query/{id}&quot;)
public ResultVO&lt;User&gt; getById(@PathVariable(&quot;id&quot;) String id) {
User user = paService.getById(id);
return ResultVO.&lt;User&gt;builder().success(user).build();
}
}
```
Service关键代码如下
```
public User getById(String id) {
return mapper.selectByPrimaryKey(id);
}
```
用MyBatis组件实现对Mapper的操作。由于不是基础开发教程这里只是为了说明逻辑如果你感兴趣的话可以自己编写一个接口示例。
逻辑调用关系如下:
<img src="https://static001.geekbang.org/resource/image/5a/42/5a41a086e7756bc2bdb2dc834bd4b942.jpg" alt="">
数据库中表的信息如下:
<img src="https://static001.geekbang.org/resource/image/eb/be/ebdadf0672b21c631f7fc6af41eaefbe.png" alt="">
我们先看这个接口的访问逻辑JMeter——SprintBoot的应用——MySQL。
## 1.编写JMeter脚本
### 1.1 创建线程组
首先创建一个线程组,配置如下:
<img src="https://static001.geekbang.org/resource/image/88/0d/8803e67574df9a393ab4a3f135052e0d.png" alt="">
在这个线程组中,有几个关键配置,我来一一说明一下。
Number of Threads(users)我们都知道这是JMeter中的线程数也可以称之为用户数。但是在[第2篇文章](https://time.geekbang.org/column/article/178076)中我已经说得非常明确了这个线程数是产生TPS的而一个线程产生多少TPS取决于系统的响应时间有多快。所以我们用TPS这个概念来承载系统的负载能力而不是用这里的线程数。
Ramp-up Period(in seconds)递增时间以秒为单位。指的就是上面配置的线程数将在多长时间内会全部递增完。如果我们配置了100线程这里配置为10秒那么就是100/(10s*1000ms)=1线程/100ms如果我们配置了10线程这里配置为1秒则是10/1000=1线程/100ms。这时我们要注意了哦在10线程启动的这个阶段中对服务器的压力是一样的。示意图如下
<img src="https://static001.geekbang.org/resource/image/e4/67/e42a668bbdf2d80968af233dea816267.png" alt="">
Loop Count这个值指的是一个线程中脚本迭代的次数。这里你需要注意这个值和后面的Scheduler有一个判断关系下面我们会提到。
Delay Thread creation until needed这个含义从字面看不是特别清楚。这里有一个默认的知识点那就是JMeter所有的线程是一开始就创建完成的只是递增的时候会按照上面的规则递增。如果选择了这个选项则不会在一开始创建所有线程只有在需要时才会创建。这一点和LoadRunner中的初始化选项类似。只是不知道你有没有注意过基本上我们做性能测试的工程师很少有选择这个选项的。选与不选之间区别到底是什么呢
如果不选择在启动场景时JMeter会用更多的CPU来创建线程它会影响前面的一些请求的响应时间因为压力机的CPU在做其他事情嘛。
如果选择了的话就会在使用时再创建CPU消耗会平均一些但是这时会有另一个隐患就是会稍微影响正在跑的线程。这个选项选择与否取决于压力机在执行过程中它能产生多大的影响。如果你的线程数很多一旦启动压力机的CPU都被消耗在创建线程上了那就可以考虑选择它否则可以不选择。
Scheduler Configuration这里有一句重要的话`If Loop Count is not -1 or Forever, duration will be min(Duration, Loop Count * iteration duration)`。举例来说如果设置了Loop Count 为100而响应时间是0.1秒那么Loop `Count * iteration duration(这个就是响应时间) = 100 * 0.1 = 10秒`
即便设置了Scheduler的Duration为100秒线程仍然会以10秒为结束点。
如果没有设置Scheduler的Duration那么你会看到在JMeter运行到10秒时控制台中会出现如下信息
```
2019-11-26 10:39:20,521 INFO o.a.j.t.JMeterThread: Thread finished: Thread Group 1-10
```
有些人不太理解这一点经常会设置迭代次数同时又设置Scheduler中的Duration。而对TPS来说就会产生这样的图
<img src="https://static001.geekbang.org/resource/image/5e/16/5e341022b296dbda9b75ec504ab80e16.png" alt="">
场景没执行完结果TPS全掉下去了于是开始查后端系统其实和后端没有任何关系。
### 1.2 创建HTTP Sampler
### 1.2.1 GET接口
<img src="https://static001.geekbang.org/resource/image/e5/9b/e550be089497bdbec158d906481ae99b.png" alt="">
看上图我将Method选择为GET。为什么要选择它往上看我们的接口注解这是一个GetMapping所以这里要选择GET。
再看path中这里是`/pa/query/0808050c-0ae0-11ea-af5f-00163e124cff`,对应着`“/query/{id}”`
然后执行:
```
User user = paService.getById(id);
```
返回执行结果:
```
return ResultVO.&lt;User&gt;builder().success(user).build();
```
为什么要解释这一段呢?
做开发的人可能会觉得,你这个解释毫无意义呀,代码里已经写得很清楚了。事实上,在我的工作经历中,会发现很多做性能测试脚本的,实际上并不知道后端采用了什么样的技术,实现的是什么样的逻辑。
所以还是希望你可以自己写一些demo去了解一些逻辑然后在排除问题的时候就非常清楚了。
接着我们执行脚本,就得到了如下结果:
<img src="https://static001.geekbang.org/resource/image/08/c7/08896d4bb8e30b957bf14ae486197ac7.png" alt="">
这样一个最简单的GET脚本就做好了。
前面我们提到过URL中的ID是0808050c-0ae0-11ea-af5f-00163e124cff这个数据来自于数据库中的第一条。
如果我们随便写一个数据,会得到什么结果呢?
<img src="https://static001.geekbang.org/resource/image/3d/55/3d9a128860fc71bdc0132ce0b8350155.png" alt="">
你会看到结果一样得到了200的code但是这个结果明显就不对了明明没有查到还是返回了成功。
所以说,业务的成功,只能靠业务来判断。这里只是查询成功了,没返回数据也是查询成功了。我将在后面给你说明如何加断言。
### 1.2.2 POST接口
下面我将Method改为POSTPOST接口与GET接口的区别有这么几处
1. 要把Path改为/pa/add
1. 输入JSON格式的Body Data。
<img src="https://static001.geekbang.org/resource/image/70/1d/7005421905a969c081be038f7ab5541d.png" alt="">
执行起来,查看下结果。
<img src="https://static001.geekbang.org/resource/image/d0/87/d006b04df0746c5de5a122e8036a1487.png" alt=""><br>
你会发现上来就错了,提示如下:
```
&quot;status&quot;:415,&quot;error&quot;:&quot;Unsupported Media Type&quot;,&quot;message&quot;:&quot;Content type 'text/plain;charset=UTF-8' not supported&quot;
```
这里你需要注意,无论遇到什么问题,都要针对问题来处理。当看不懂问题信息时,先查资料,想办法看懂。这是处理问题的关键,我发现很多做性能测试的新同学,一旦碰到问题就懵了,晕头转向地瞎尝试。
我经常对我的团队成员说,先看懂问题,再处理问题,别瞎蒙!
上面这个问题其实提示得很清楚“不支持的媒体类型”。这里就两个信息一个是Content type一个是charset。它们是JMeter中HTTP Header里默认自带的。我们要发送的是JSON数据而JMeter默认是把它当成text发出去的这就出现了问题。所以我们要加一个Header将Content type指定为JSON。
加一个HTTP Header如下所示
<img src="https://static001.geekbang.org/resource/image/25/4c/25c8208ec31c22c07fc3a5dfd3b77b4c.png" alt="">
如果你不知道加什么样的Header建议你用HTTP抓包工具抓一个看一看比如说用Charles抓到如下信息
<img src="https://static001.geekbang.org/resource/image/b0/57/b02a63bf056052304d1d9969a2556157.png" alt="">
这时你就会知道头里的Content-Type原来是`application/json;charset=UTF-8`。这里的charset=UTF-8可以不用写因为它和默认的一样。
这时再回放,你就会看到如下结果:
<img src="https://static001.geekbang.org/resource/image/e3/19/e399b507bfb2af2996f1186c0d27e419.png" alt="">
到此一个POST脚本就完成了。是不是很简单。
在这里我需要跟你强调的是手工编写HTTP脚本时要注意以下几点
1. 要知道请求的类型,我们选择的类型和后端接口的实现类型要是一致的。
1. 业务的成功要有明确的业务判断在下面的TCP中我们再加断言来判断
1. 判断问题时,请求的逻辑路径要清晰。
编写完HTTP脚本时我们再来看一下如何编写TCP脚本。
## 手工编写TCP脚本
### 服务端代码逻辑说明
我在这里写一个非常简单的服务端接收线程(如果你是开发,不要笑话,我只是为了说明脚本怎么写)。
```
package demo.socket;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class SocketReceiver {
//定义初始
public static final int corePoolSize = 5;
//定义最大线程池
public static final int maximumPoolSize = 5;
//定义socket队列长度
public static final int blockingQueue = 50;
/**
* 初始化并启动服务
*/
public void init() {
//定义线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, 0L,
TimeUnit.MILLISECONDS, new ArrayBlockingQueue(blockingQueue));
//定义serverSocket
ServerSocket serverSocket = null;
try {
//启动serverSocket
serverSocket = new ServerSocket(Constants.PORT);
//输出服务启动地址
System.out.println(&quot;服务已启动:&quot; + serverSocket.getLocalSocketAddress().toString());
//接收信息并传递给线程池
while (true) {
Socket socket = serverSocket.accept();
executor.submit(new Handler(socket));
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (serverSocket != null) {
try {
serverSocket.close(); //释放serverSocket
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
//处理请求类
class Handler implements Runnable {
private Socket socket;
public Handler(Socket socket) {
this.socket = socket;
}
public void run() {
try {
// 接收客户端的信息
InputStream in = socket.getInputStream();
int count = 0;
while (count == 0) {
count = in.available();
}
byte[] b = new byte[count];
in.read(b);
String message = new String(b);
System.out.println(&quot; receive request: &quot; + socket.getInetAddress() + &quot; &quot; + message);
// 睡2秒模拟思考时间这里是为了模拟服务器端的业务处理时间
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 向客户端发送确认消息
//定义输出流outer
OutputStream outer = socket.getOutputStream();
//将客户端发送的信息加上确认信息ok
String response = message + &quot; is OK&quot;;
//将输入信息保存到b_out中
byte[] b_out = response.getBytes();
//写入输入流
outer.write(b_out);
//推送输入流到客户端
outer.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
// 关闭socket
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
//程序入口
public static void main(String[] args) {
//定义服务端
SocketReceiver receiver = new SocketReceiver();
//启动服务端
receiver.init();
}
}
```
### 编写JMeter脚本
首先创建TCP Sampler。右键点击Thread Group - Add - Sampler - TCP Sampler即可创建。
<img src="https://static001.geekbang.org/resource/image/a2/b4/a2d7a5f2b0271416f54070d42719d6b4.png" alt="">
输入配置和要发送的信息。
<img src="https://static001.geekbang.org/resource/image/b6/b0/b6c6435fe31916d27e3934a911d781b0.png" alt="">
IP地址和端口是必须要输入的。对于创建一个TCP协议的JMeter脚本来说简单地说过程就是这样的创建连接 - 发数据 - 关闭连接。
就这样,这个手工的脚本就完成了。
你可能会问,就这么简单吗?是的,手工编写就是这么简单。
但是对嘛但是才是重点通常我们在创建TCP协议的脚本时都是根据业务接口规范来说的**复杂点其实不在脚本本身上,而是在接口的规则上**。
### 添加断言
我回放了一下脚本,发现如下情况:
<img src="https://static001.geekbang.org/resource/image/2c/3d/2cbddf1bc190c167212f14b3c6bf193d.png" alt="">
都执行对了呀,为什么下面的没有返回信息呢?这种情况下只有第一个请求有返回信息,但是下面也没有报错。这里就需要注意了。
**测试工具的成功,并不等于业务的成功**
所以我们必须要做的就是响应断言也就是返回值的判断。在JMeter中断言有以下这些
<img src="https://static001.geekbang.org/resource/image/d4/9b/d4b8c319a6ade1344f1cced91308079b.png" alt="">
因为今天的文章不是工具的教程,所以我不打算全讲一遍。这里我只用最基础的响应断言。什么是断言呢?
<img src="https://static001.geekbang.org/resource/image/34/84/343a993f00b5f289ed2fc76ed735c684.jpg" alt="">
断言指的就是服务器端有一个业务成功的标识,会传递给客户端,客户端判断是否正常接收到了这个标识的过程。
<img src="https://static001.geekbang.org/resource/image/97/c7/972884773c413e5b07ee74bf12a14ac7.png" alt="">
在这里我添加了一个断言用以判断服务器是否返回了OK。 你要注意这个“OK”是从哪来的哦它是从服务端的这一行代码中来的。
```
String response = message + &quot; is OK&quot;;
```
请注意,这个断言的信息,一是可以判断出业务的正确性。我在工作中发现有些人用页面中一些并不必要的文字来判断,这样就不对了,我们应该用有业务含义的判断标识。
如果我们再次回放脚本你会发现除了第一个请求后面9个请求都错了。
<img src="https://static001.geekbang.org/resource/image/31/37/318675c91ad46c36da9b7a9eab215737.png" alt="">
所以,在做脚本时,请你一定要注意,**断言是必须要加的**。
### 长短连接的问题
既然有错肯定是要处理。我们查看一下JMeter的控制台错误信息
```
2019-11-26 09:51:51,587 ERROR o.a.j.p.t.s.TCPSampler:
java.net.SocketException: Broken pipe (Write failed)
at java.net.SocketOutputStream.socketWrite0(Native Method) ~[?:1.8.0_111]
at java.net.SocketOutputStream.socketWrite(SocketOutputStream.java:109) ~[?:1.8.0_111]
at java.net.SocketOutputStream.write(SocketOutputStream.java:141) ~[?:1.8.0_111]
at org.apache.jmeter.protocol.tcp.sampler.TCPClientImpl.write(TCPClientImpl.java:78) ~[ApacheJMeter_tcp.jar:5.1.1 r1855137]
at org.apache.jmeter.protocol.tcp.sampler.TCPSampler.sample(TCPSampler.java:401) [ApacheJMeter_tcp.jar:5.1.1 r1855137]
at org.apache.jmeter.threads.JMeterThread.doSampling(JMeterThread.java:622) [ApacheJMeter_core.jar:5.1.1 r1855137]
at org.apache.jmeter.threads.JMeterThread.executeSamplePackage(JMeterThread.java:546) [ApacheJMeter_core.jar:5.1.1 r1855137]
at org.apache.jmeter.threads.JMeterThread.processSampler(JMeterThread.java:486) [ApacheJMeter_core.jar:5.1.1 r1855137]
at org.apache.jmeter.threads.JMeterThread.run(JMeterThread.java:253) [ApacheJMeter_core.jar:5.1.1 r1855137]
at java.lang.Thread.run(Thread.java:745) [?:1.8.0_111]
```
从字面上来看就是通道瓦塔被破坏Broken pipe。这个提示表明客户端上没有这个连接了而JMeter还以为有这个链接于是接着用这个链接来发显然是找不到这个通道于是就报错了。
这是一个典型的压力工具这边的问题。
而服务端,只收到了一条请求。
<img src="https://static001.geekbang.org/resource/image/b1/d9/b1050e9a5920bdfd84b3c78a700615d9.png" alt="">
为什么会报这个错呢?因为我们代码是短链接的,服务端处理完之后,就把这个链接给断掉了。
这里是压力机上的抓包信息:
```
//从这里开始上面已经看到了有Fin结束包了后面还在发Push发送数据包。显然是通不了还被服务端啪啪抽了两次reset。
11:58:07.042915 IP localhost.57677 &gt; 60.205.107.9.m-oap: Flags [P.], seq 34:67, ack 41, win 4119, options [nop,nop,TS val 163718903 ecr 2122793206], length 33
11:58:07.046075 IP localhost.57677 &gt; 60.205.107.9.m-oap: Flags [FP.], seq 67:331, ack 41, win 4119, options [nop,nop,TS val 163718906 ecr 2122793206], length 264
11:58:07.076393 IP 60.205.107.9.m-oap &gt; localhost.57677: Flags [R], seq 3986768192, win 0, length 0
11:58:07.079156 IP 60.205.107.9.m-oap &gt; localhost.57677: Flags [R], seq 3986768192, win 0, length 0
```
服务端的抓包信息:
```
//服务端也是没有办法只能在看到了Push包之后给回了个Reset包。
11:58:07.047001 IP 124.64.16.240.bones &gt; 7dgroup1.enc-eps-mc-sec: Flags [P.], seq 34:67, ack 41, win 4119, options [nop,nop,TS val 163718903 ecr 2122793206], length 33
11:58:07.047077 IP 7dgroup1.enc-eps-mc-sec &gt; 124.64.16.240.bones: Flags [R], seq 3986768192, win 0, length 0
11:58:07.054757 IP 124.64.16.240.bones &gt; 7dgroup1.enc-eps-mc-sec: Flags [FP.], seq 67:331, ack 41, win 4119, options [nop,nop,TS val 163718906 ecr 2122793206], length 264
11:58:07.054844 IP 7dgroup1.enc-eps-mc-sec &gt; 124.64.16.240.bones: Flags [R], seq 3986768192, win 0, length 0
```
这是为什么呢因为在JMeter中默认是复用TCP连接的但是在我们这个示例中服务端并没有保存这个连接。所以我们应该在脚本中把下图中的Re-use connection给去掉。
<img src="https://static001.geekbang.org/resource/image/71/75/71d1b52fe04ced4b3bd793b268ae5175.png" alt="">
这时再回放脚本你就会发现10次迭代全都对了。如下图所示
<img src="https://static001.geekbang.org/resource/image/d2/e1/d23dddf82d073b6bffb1a25f374b5de1.png" alt="">
但是这里还有一个知识点希望你注意。短连接的时候必然会产生更多的TCP连接的创建和销毁对性能来说这会让系统变得缓慢。
所以你可以看到上面10条迭代全都对了的同时响应时间也增加了。
可能会有人问那这怎么办呢长短连接的选择取决于业务的需要如果必须用短链接那可能就需要更多的CPU来支撑要是长连接就需要更多的内存来支撑用以保存TCP连接
根据业务需要,我们选择一个合适的就好。
### TCP连接超时
这个问题,应该说非常常见,我们这里只做问题的现象说明和解决,不做原理的探讨。原理的部分,我会在监控和分析部分加一说明。
下面这个错误,属于典型的主机连不上。
```
java.net.ConnectException: Operation timed out (Connection timed out)
at java.net.PlainSocketImpl.socketConnect(Native Method) ~[?:1.8.0_111]
at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:350) ~[?:1.8.0_111]
at java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:206) ~[?:1.8.0_111]
at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:188) ~[?:1.8.0_111]
at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:392) ~[?:1.8.0_111]
at java.net.Socket.connect(Socket.java:589) ~[?:1.8.0_111]
at org.apache.jmeter.protocol.tcp.sampler.TCPSampler.getSocket(TCPSampler.java:168) [ApacheJMeter_tcp.jar:5.1.1 r1855137]
at org.apache.jmeter.protocol.tcp.sampler.TCPSampler.sample(TCPSampler.java:384) [ApacheJMeter_tcp.jar:5.1.1 r1855137]
at org.apache.jmeter.threads.JMeterThread.doSampling(JMeterThread.java:622) [ApacheJMeter_core.jar:5.1.1 r1855137]
at org.apache.jmeter.threads.JMeterThread.executeSamplePackage(JMeterThread.java:546) [ApacheJMeter_core.jar:5.1.1 r1855137]
at org.apache.jmeter.threads.JMeterThread.processSampler(JMeterThread.java:486) [ApacheJMeter_core.jar:5.1.1 r1855137]
at org.apache.jmeter.threads.JMeterThread.run(JMeterThread.java:253) [ApacheJMeter_core.jar:5.1.1 r1855137]
at java.lang.Thread.run(Thread.java:745) [?:1.8.0_111]
```
time out是个如果你理解了逻辑就觉得很简单如果没理解逻辑就觉得非常复杂的问题。
要想解决这个问题,就要先确定服务端是可以正常连通的。
如果不能正常连通那么通常都是IP不正确、端口不正确、防火墙阻止之类的问题。解决了网络连通性的问题就可以解决connection timed out的问题。
### 编写LoadRunner脚本
针对上面这个示例如果你要想编写一个LoadRunner的示例脚本也是简单到不行。
首先创建一个空的winsock脚本复制下面代码到action里面。
```
//创建socket1
lrs_create_socket(&quot;socket1&quot;, &quot;TCP&quot;, &quot;RemoteHost=60.205.10.9:5567&quot;, LrsLastArg);
//走socket1, 发送buf1中定义的数据
lrs_send (&quot;socket1&quot;, &quot;buf1&quot;, LrsLastArg );
//走socket1接收数据保存在buf2中
lrs_receive(&quot;socket1&quot;, &quot;buf2&quot;, LrsLastArg);
//关掉socket1
lrs_close_socket(&quot;socket1&quot;);
```
从上面的信息就可以看到socket1这个标识是我们操作的基础。如果你在一个脚本中想处理两个socket也是可以的只要控制好你的标识不会乱就行。
接着再将下面的内容复制到data.ws里面。
```
send buf1 5
&quot;12345&quot;
recv buf2 10
```
你可能会问这个recv怎么不写返回的值是什么
当你手写socket脚本的时候都还没有运行你怎么知道返回值是什么呢所以这里可以不用写。
而recv 后面的10是指接收10个字节。如果多了怎么办截掉不会的LoadRunner还是会把所有信息全部接收并保存下来除非你提前定义了截取字符长度的函数。
最后看下我们回放的结果:
```
Action.c(6): lrs_create_socket(socket1, TCP, ...)
Action.c(7): lrs_send(socket1, buf1)
Action.c(8): lrs_receive(socket1, buf2)
Action.c(8): Mismatch in buffer's length (expected 10 bytes, 11 bytes actually received, difference in 1 bytes)
================================EXPECTED BUFFER================================
===============================================================================
================================RECEIVED BUFFER================================
&quot;12345 is OK&quot;
===============================================================================
Action.c(8): callRecv:11 bytes were received
Action.c(9): lrs_close_socket(socket1)
```
脚本正常执行了只是报了一个Mismatch这是因为我们定义了buf2 是10字节而我们实际上接收了11字节所以这里给出了Mismatch。
到此一个LoadRunner的手工TCP脚本就完成了。后面我们就可以根据需要增强脚本了加个参数化、关联、检查点等等。
## 总结
其实这篇文章只想告诉你一件事情,手工编写脚本,从基础上说,是非常简单的,只是有三点需要特别强调:
1. 涉及到业务规则和逻辑判断之后,编写脚本就复杂了起来。但是了解业务规则是做脚本的前提条件,也是性能测试工程师的第一步。
1. 编写脚本的时候,要知道后端的逻辑。这里的意思不是说,你一开始写脚本的时候,就要去读后端的代码,而是说你在遇到问题的时候,要分析整个链路上每个环节使用到了什么技术,以便快速地分析判断。
1. 写脚本是以**最简为最佳**,用不着故意复杂。
脚本的细节功能有很多,而现在我们可以看到市场上的书籍也好,文档也好,基本上是在教人如何用工具,很少会从前到后地说明一个数据从哪发到哪,谁来处理这样的逻辑。
希望学习性能测试工具的你,不仅知其然,更知其所以然。
## 思考题
学习完今天的内容你不妨思考一下HTTP的GET和POST请求在后端处理中有什么不同断言的作用是什么如何使用断言呢
欢迎你在评论区写下你的思考,也欢迎把这篇文章分享给你的朋友或者同事,一起交流一下。

View File

@@ -0,0 +1,241 @@
<audio id="audio" title="09丨关联和断言:一动一静,核心都是在取数据" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/71/5b/7194bb7d14ed3a283810c86adc5fad5b.mp3"></audio>
对每一个性能测试工具来说,关联和断言都是应该具备的基本功能。
但是有很多新手对关联的逻辑并不是十分理解,甚至有人觉得关联和参数化是一样的,因为它们用的都是动态的数据,并且关联过来的数据也可以用到参数化中,但不一样的点是,关联的数据后续脚本中会用到,参数化则不会。断言倒是比较容易理解,就是做判断。
那么到底该怎样理解关联和断言呢?下面我们通过两个例子来思考一下。
## 关联
现在做性能测试的,有很多都是单纯的接口级测试,这样一来,关联就用得更少了。因为接口级的测试是一发一收就结束了,不需要将数据保存下来再发送出去。
那么什么样的数据需要关联呢?满足如下条件的数据都是需要关联的:
1. 数据是由服务器端生成的;
1. 数据在每一次请求时都是动态变化的;
1. 数据在后续的请求中需要再发送出去。
示意图如下:
<img src="https://static001.geekbang.org/resource/image/6e/c4/6e5a4272e989c2bfe226fddd664a08c4.jpg" alt="">
其实我们可以把关联的功能理解为取服务端返回的某个值。在这样的前提之下,我们可以把它用在很多场景之下。
举个例子我们常见的Session ID就是一个典型的需要关联的数据。它需要在交互过程中标识一个客户端身份这个身份要在后续的交互中一直存在否则服务端就不认识这个客户端了。
再比如我们现在用微服务已经非常多了在Spring Boot中有一个spring-boot-starter-security默认会提供一个基于HTTP Basic认证的安全防护策略。它在登录时会产生一个CSRFCross-Site Request Forgery这个值典型地处于动态变化中。
下面我们来看一下这个值如何处理。
首先,录制登录、退出的脚本。操作如下:
<img src="https://static001.geekbang.org/resource/image/a6/f8/a687642dc767a3c325981946e0004df8.png" alt="">
录出的脚本如下所示:
<img src="https://static001.geekbang.org/resource/image/33/c1/33afd450171be3e83666b44b242332c1.png" alt="">
这时直接回放会得到如下结果:
<img src="https://static001.geekbang.org/resource/image/d2/24/d24ca3100ab59ebd978e2b1f52de2124.png" alt="">
这回你会看到提示了Unauthorized没权限。
在回放的脚本中,我们看到了如下的登录返回信息。
<img src="https://static001.geekbang.org/resource/image/6f/83/6fe988df068982136aa2389ff6c85283.png" alt=""><br>
同时,在脚本中,我们可以看到登录时会使用到这个值。
<img src="https://static001.geekbang.org/resource/image/2b/bd/2b1afb0f4cc27fb0a8464655d290fcbd.png" alt="">
下面我们就把它关联了。
首先添加Cookies Manage。JMeter在处理CSRF时需要添加一个Cookies manager。如下
<img src="https://static001.geekbang.org/resource/image/12/1f/1284db3c2c234cc184c7a62c2d476c1f.png" alt="">
这里的Cookie Policy一定要选择compatibility以兼容不同的cookie策略。
然后取动态值在返回CSRF值的地方加一个正则表达式提取器来做关联。当然还有更多的提取器我将在后面提及。
<img src="https://static001.geekbang.org/resource/image/a4/d1/a4adfb17fad89fe36ddedc0e22a8acd1.png" alt="">
这里的`&lt;input name="_csrf" type="hidden" value="(.+?)" /&gt;`就是要取出这个动态的变化值保存到变量csrfNumber中去。
然后发送动态值出去将发送时的CSRF值替换成变量。
<img src="https://static001.geekbang.org/resource/image/24/fe/242ba58f3eee77fab3def992486c9ffe.png" alt="">
最后,再回放,就会得到如下结果。
<img src="https://static001.geekbang.org/resource/image/1e/86/1e5a6a8638433120139ef3ede5554686.png" alt="">
这样我们就能看到可以正常访问了。
这就是一个典型的关联过程。
上面是用的正则提取器在JMeter中还有其他的提取器如下图所示
<img src="https://static001.geekbang.org/resource/image/9d/5d/9dd9fd915ededee2d9918b0f8bc1d55d.png" alt="">
使用什么样的提取器取决于业务的需要比如说如果你返回的是JSON格式就可以使用上图中的JSON Extractor。
我们在很多的业务中,都可以看到大量的动态数据。所以做关联一定要有耐心,不然就会找得很混乱。
## 断言
在第8篇文章中我们讲到手工编写脚本有一个添加断言的动作。断言就是判断服务端的返回是不是正确的。
它的判断逻辑是这样的:
<img src="https://static001.geekbang.org/resource/image/30/23/30ebaec1c1c07b31d9fbdaca19ffa423.jpg" alt="">
在压力工具中,我们已经知道要比对的值是什么了,接下来就看服务端返回的对不对了。下面我们来详细说一下这个逻辑。
先写一个POST接口脚本。
<img src="https://static001.geekbang.org/resource/image/46/53/46d7d74a7a16753d922d7e4f6e3a3d53.png" alt="">
执行下,看到如下结果:
<img src="https://static001.geekbang.org/resource/image/56/45/56688bcf2fe5eaeefbe78a1d42020a45.png" alt="">
添加断言。
<img src="https://static001.geekbang.org/resource/image/5d/35/5d0cfa549e919607a51354d33a27d135.png" alt="">
关键点来了我们知道图片中的这个“true”服务端返回的可是它到底是从服务端的什么地方产生的呢
下面我们来看一下服务端的代码。处理我们的add请求的是这样的代码段
```
@PostMapping(&quot;/add&quot;)
public ResultVO&lt;Boolean&gt; add(@RequestBody User user) {
Boolean result = paService.add(user);
return ResultVO.&lt;Boolean&gt;builder().success(result).build();
}
```
我们post出去的数据是
```
{
&quot;userNumber&quot;: &quot;00009496&quot;,
&quot;userName&quot;: &quot;Zee_2&quot;,
&quot;orgId&quot;: null,
&quot;email&quot;: &quot;Zee_2@7dtest.com&quot;,
&quot;mobile&quot;: &quot;18600000000&quot;
}
```
代码中对应的是:
```
@Override
public String toString() {
return &quot;User{&quot; +
&quot;id='&quot; + id + '\'' +
&quot;, userNumber='&quot; + userNumber + '\'' +
&quot;, userName='&quot; + userName + '\'' +
&quot;, orgId='&quot; + orgId + '\'' +
&quot;, email='&quot; + email + '\'' +
&quot;, mobile='&quot; + mobile + '\'' +
&quot;, createTime=&quot; + createTime +
'}';
}
```
ID是自增的
```
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY, generator = &quot;select uuid()&quot;)
private String id;
```
然后由paServer.add添加到数据库里去
```
Boolean result = paService.add(user);
```
add的实现是
```
public Boolean add(User user) {
return mapper.insertSelective(user) &gt; 0;
}
```
这就是一个关键了。这里return的是`mapper.insertSelective(user) &gt; 0`的结果也就是一个true也就是说这时在数据库中插入了一条数据
<img src="https://static001.geekbang.org/resource/image/ac/2e/ac60d7974d2d98a2f209604eb3c7f22e.png" alt="">
然后build返回信息
```
public ResultVO&lt;T&gt; build() {
return new ResultVO&lt;&gt;(this.code, this.msg, this.data);
}
```
这个时候,我们才看到下面的提示信息:
```
{&quot;data&quot;:true,&quot;code&quot;:200,&quot;msg&quot;:&quot;成功&quot;}
```
也就是说在数据库中成功插入1条数据之后把1&gt;0的判断结果也就是true返回给result这个变量然后通过`public ResultVO&lt;Boolean&gt; add(@RequestBody User user)`中的ResultVO返回给压力工具。
用图片来说的话,逻辑就是下面这样的:
<img src="https://static001.geekbang.org/resource/image/92/c2/92ccc16a1403668248a88a99672b79c2.png" alt="">
通过这一系列的解释我就是想说明一个问题断言中的true是从哪来的。
知道了这个问题的答案之后我们就能理解为什么这个true很重要了。因为有了它就说明我们在数据库成功插入了数据。
断言是根据需要来设计的,而设计断言的前提就是完全理解这个逻辑。
当然我们也可以直接这样来写Controller
```
public String add(@RequestBody User user) {
Boolean result = paService.add(user);
return &quot;Added successfully!&quot;;
}
```
这时就没有true了。脚本运行结果如下
<img src="https://static001.geekbang.org/resource/image/c5/5a/c5d4e081f01e0fb67058338c076d785a.png" alt="">
这时断言看似是失败的因为我们断言判断的是“true”但服务端没有返回“true”这个字符。而实际上当我们从数据库中查看时插入是成功的。
但是这种写法是有问题的不管数据有没有插入成功只要在add方法执行了就会提示“`Added successfully!`”。
在实际的工作中,也有开发这样写代码,这样的话,断言似乎都是对的,事务也是成功的,但实际上数据库中可能没有插进去数据。
## 总结
实际上,关联和断言的前半部分是一样的,都是从服务器返回信息中取出数据。但不同的是,关联取来的数据每次都会不同;而断言取出来的数据基本上都是一样的,除非出了错。
对服务端生成的,并且每次生成都不一样的动态变化的数据,那么将其**取回来之后,在后续的请求中使用**,这种逻辑就是关联。
对服务端返回的,可标识业务成功与否的数据,将其取回来之后,做判断。这种逻辑就是断言。
## 思考题
最后给你留道思考题吧,你能说一下关联和断言的逻辑是什么吗?它们取数据的特点又是什么呢?
欢迎你在评论区写下你的思考,我会和你一起交流,也欢迎把这篇文章分享给你的朋友或者同事,一起交流进步。

View File

@@ -0,0 +1,231 @@
<audio id="audio" title="10丨案例在JMeter中如何设置参数化数据" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/c0/a7/c04f8d08472fe114d9635ce18f1a12a7.mp3"></audio>
今天我们来做一个实例看下在JMeter中如何合理地设置参数化数据。
## 正式场景前的基准测试
在没有做业务混合场景之前我们需要先做Benchmark测试来确定一个登录业务能支持多少的业务量这样就可以在业务混合场景中根据场景中各业务的比例来确定登录的数据需要多少真实的数据。
我们继续用上一篇文章中用户登录的例子,单独测试登录业务,结果如下:
```
Java
summary + 125 in 00:00:04 = 31.0/s Avg: 28 Min: 0 Max: 869 Err: 0 (0.00%) Active: 1 Started: 1 Finished: 0
summary + 3404 in 00:00:30 = 113.2/s Avg: 31 Min: 0 Max: 361 Err: 0 (0.00%) Active: 6 Started: 6 Finished: 0
summary + 4444 in 00:00:30 = 148.4/s Avg: 57 Min: 0 Max: 623 Err: 10 (0.23%) Active: 11 Started: 11 Finished: 0
```
从上面的结果可以看到登录业务能达到的TPS是113左右这里我们取整为100以方便后续的计算。
## 在测试工具中配置参数
在上面的试探性测试场景中不需要观察系统的资源只需要根据TPS做相应的数据统计即可。
前面我们知道在这个示例中只做了近10万条的用户数据为了方便示例进程。
下面我们从数据库中查询可以支持登录5分钟不重复的用户数据。根据前面的公式我们需要30000条数据。
```
Java
100x5mx60s=30000条
```
接下来连接数据库取30000条数据存放到文本中如下所示
```
Java
username,password
test00001,test00001
test00002,test00002
test00003,test00003
test00004,test00004
test00005,test00005
test00006,test00006
test00007,test00007
...................
test30000,test30000
```
## 参数化配置在JMeter中的使用说明
我们将这些用户配置到测试工具的参数当中这里以JMeter的CSV Data Set Config功能为例。配置如下
<img src="https://static001.geekbang.org/resource/image/d4/a6/d40134621469079dd7b9de6e19165ca6.png" alt="">
在JMeter的参数化配置中有几个技术点在这里说明一下。
“Allow quoted data?”这里有两个选择分别是False和True。它的含义为是否允许带引号的数据比如说在参数化文件中有这样的数据。
```
Java
&quot;username&quot;,&quot;password&quot;
&quot;test00001&quot;,&quot;test00001&quot;
&quot;test00002&quot;,&quot;test00002&quot;
...................
&quot;test30000&quot;,&quot;test30000&quot;
```
如果有引号这个选择必须是True。如果设置为False那么我们在脚本中会看到如下的数据
```
Java
username=%22test00001%22password=%22test00001%22
```
由于设置为FalseJMeter将")转换为了%22的URL编码很显然这个数据是错的。如果选择为True则显示如下
```
Java
username=test00001password=test00001
```
这里就显示对了。
除此之外,还有如下几个功能点需要说明:
- Recycle on EOF? 这里有三个选择False、True和Edit。前两个选择非常容易理解。False是指在没有参数的时候不循环使用True是指在没有参数的时候循环使用。Edit是指在没有参数的时候会根据定义的内容来调用函数或变量。
- Stop thread on EOF?这里有三个选择False、True和Edit。含义和上面一致。
- Sharing mode : 这里有四个选择All threads、Current thread group、Current thread、Edit。
Sharing mode的前三个选择是比较容易理解的参数是在所有线程中生效在当前线程组生效还是在当前线程中生效。但这里的Edit和前两个参数中的Edit相比有不同的含义。这里选择了Edit之后会出现一个输入框就是说这里并不是给引用函数和参数使用的而是要自己明确如何执行Sharing mode。那如何来使用呢
举例来说假设我们有Thread Group 1-5 四个线程组但是参数化文件只想在Thread Group 1、3、5中使用不想在线程组2、4中使用那么很显然前面的几个选项都达不到目的这时我们就可以选择Edit选项在这里输入`SharedWithThreadGroup1and3and5`。而在其他的线程组中配置其他参数化文件。
也就是说同样的一个变量名在线程组1/3/5中取了一组数据在线程组2/4中取了另一组数据。
以上三个参数的选项可以随意组合。于是就会得到如下表。
**需要注意的是EOF是文件结束符的意思。在下面的解释中为了更符合性能测试中的术语特意解释为参数不足时。**
以上三个功能点根据参数设计得不同,会产生不同的组合,我们依次查看一下。
<img src="https://static001.geekbang.org/resource/image/3c/b1/3ce3106d6b079e715cce6fcbf8de72b1.png" alt="">
这个组合显然是矛盾的,没有参数时不让循环,还不让停止线程,这不是耍流氓吗?真实的结果是什么呢?当我们执行时就会发现,参数变成了这样:
```
username=%3CEOF%3E&amp;password=%3CEOF%3E
```
服务端果然返回了:`{"flag":false,"errMsg":"账号不存在"}`
<img src="https://static001.geekbang.org/resource/image/2e/6b/2ea13362d80579cbcc8f941e53ed696b.png" alt=""><br>
这个组合中第二个选项显然是没意义的,既然参数允许重复使用了,又怎么会发生参数不足停止线程的情况呢?
<img src="https://static001.geekbang.org/resource/image/e5/65/e5643ea7e5dd17bb31a715e2aa5bdf65.png" alt=""><br>
这个组合因为第一个选项为“Edit”所以变得不确定了如果在Edit的函数或变量返回为True则和第2种组合一样如果返回为False则和第1种组合一样。
<img src="https://static001.geekbang.org/resource/image/d4/99/d48f109b59767bd99679f4f442494899.png" alt=""><br>
这是一个完全合情合理的组合!
<img src="https://static001.geekbang.org/resource/image/4b/05/4be6a8f830155a6fdfdee518c9220305.png" alt="">
同第二个组合一样,第二个选项显然没有意义。
<img src="https://static001.geekbang.org/resource/image/91/0c/9107142e3f0acd73eb2f350dc698a70c.png" alt=""><br>
这个组合同样因为第一个选项为Edit所以变得不确定了如果在Edit的函数或变量返回为True则和第3种组合一样如果返回为False则和第4种组合一样。
<img src="https://static001.geekbang.org/resource/image/00/e0/008693e916bf65b9cdc6586afd5fcde0.png" alt=""><br>
这个组合因为是否停止线程的不确定性会出现两种可能有可能是第1种组合也有可能是第4种组合。
<img src="https://static001.geekbang.org/resource/image/ed/57/ed552dbc050ec154ac7f4f1b4148e657.png" alt=""><br>
这个组合中是否停止线程的Edit配置没有意义因为可循环使用参数所以不会发生参数不足导致线程停止的情况。
<img src="https://static001.geekbang.org/resource/image/37/f0/37735347120d86198df67dffbaa31ff0.png" alt=""><br>
这是一个古怪的组合具有相当的不确定性有可能变成第1、2、4、5种组合。
下面我们再来看下其他衍生的设置组合。
<img src="https://static001.geekbang.org/resource/image/91/0b/9151da6424095444d56141c5e8c11b0b.jpg" alt="">
## 真实场景下的JMeter参数配置和执行结果
根据以上的描述我们先用10个用户来测试下将Stop `thread on EOF?`改为True`Recycle on EOF?`改为False其他不变。同时将线程组中配置为1个线程循环11次。这样设置的目的是为了看在数据不足时是否可以根据规则停掉线程组。如下所示
<img src="https://static001.geekbang.org/resource/image/d4/a6/d40134621469079dd7b9de6e19165ca6.png" alt="">
线程组配置如下:
<img src="https://static001.geekbang.org/resource/image/d2/3f/d26aeda4baea18631966b15dd5084a3f.png" alt="">
执行之后,我们会在日志中看到如下信息:
```
Java
2019-09-05 22:56:30,171 INFO o.a.j.t.JMeterThread: Stop Thread seen for thread Thread Group 1 1-1, reason: org.apache.jorphan.util.JMeterStopThreadException: End of file:/Users/Zee/Downloads/user10.csv detected for CSV DataSet:CSV Data Set Config configured with stopThread:true, recycle:false
```
可以看到在参数用完又不可循环使用参数的情况下JMeter主动停止了线程。
我们延续使用上文中场景二的条件,即希望场景中每个线程的每次迭代都用不同的数据。
为了能很快地整理出实际的结果我们只使用10条数据来模拟条件设置如下
```
线程组2
线程每线程组6
参数化数据10条
```
执行完场景后,会在日志中看到如下信息:
```
Java
2019-09-07 23:24:25,585 INFO o.a.j.t.JMeterThread: Stop Thread seen for thread Thread Group 1 1-1, reason: org.apache.jorphan.util.JMeterStopThreadException: End of file:/Users/Zee/Downloads/user10.csv detected for CSV DataSet:CSV Data Set Config configured with stopThread:true, recycle:false
2019-09-07 23:24:25,452 INFO o.a.j.t.JMeterThread: Stop Thread seen for thread Thread Group 1 1-2, reason: org.apache.jorphan.util.JMeterStopThreadException: End of file:/Users/Zee/Downloads/user10.csv detected for CSV DataSet:CSV Data Set Config configured with stopThread:true, recycle:false
2019-09-07 23:24:23,406 INFO o.a.j.t.JMeterThread: Stop Thread seen for thread Thread Group 2 2-1, reason: org.apache.jorphan.util.JMeterStopThreadException: End of file:/Users/Zee/Downloads/user10.csv detected for CSV DataSet:CSV Data Set Config configured with stopThread:true, recycle:false
2019-09-07 23:24:25,517 INFO o.a.j.t.JMeterThread: Stop Thread seen for thread Thread Group 2 2-2, reason: org.apache.jorphan.util.JMeterStopThreadException: End of file:/Users/Zee/Downloads/user10.csv detected for CSV DataSet:CSV Data Set Config configured with stopThread:true, recycle:false
```
可见所有的线程都按我们的配置停止了线程,同时各线程取得参数如下表所示:
<img src="https://static001.geekbang.org/resource/image/6c/ab/6cd12cb6c6b0f41dec961379380beaab.png" alt=""><br>
每次执行场景会有不同不同点是线程组1有可能执行6次而线程组2只执行4次或者线程组1中的线程2执行次数比线程1执行次数多。但总体执行次数会是10次。
如果数据可以被线程平均分配,则每个线程的迭代次数会相同。如果数据不能被线程平均分配,则每个线程的迭代次数不会相同,但相差不会大。
## 参数化配置在LoadRunner中的使用说明
在LoadRunner中参数配置页面如下
<img src="https://static001.geekbang.org/resource/image/88/a7/88659e50f65ba8cb8005aa2e82c742a7.png" alt="">
它的取值组合如下所示:<br>
<img src="https://static001.geekbang.org/resource/image/c7/a3/c705bcf1b185248e05a0457ac0a010a3.png" alt="">
以上的组合中组合7对应着上文中JMeter真实场景中每次迭代取不同数据的组合即JMeter中的参数组合4。
## 总结
通过今天的内容,我们对性能测试中的参数化做了一次解析,在执行性能测试时,我们需要根据实际的业务场景选择不同的数据量和参数设置组合。
不同的压力工具在参数化的实现逻辑上也会不同,但是参数化必须依赖业务逻辑,而不是工具中能做到什么功能。所以在参数化之前,我们必须分析真实业务逻辑中如何使用数据,再在工具中选择相对应的组合参数的方式去实现。
这里我总结一下性能工作中参数化的逻辑,希望对你有所启发。
1. 分析业务场景;
1. 罗列出需要参数化的数据及相对应的关系;
1. 将参数化数据从数据库中取出或设计对应的生成规则;
1. 合理地将参数化数据保存在不同的文件中;
1. 在压力工具中设置相应的参数组合关系,以便实现模拟真实场景。
通过以上步骤,我们就可以合理的参数化数据,模拟出真实场景。
## 思考题
你可以思考一下下面几个问题:
1. 为什么参数化数据要符合生产环境的数据分布?
1. 为什么参数化数据要关注组合逻辑关系,而不是随意设置组合?
欢迎你在评论区写下你的思考,也欢迎把这篇文章分享给你的朋友或者同事,一起交流一下。

View File

@@ -0,0 +1,251 @@
<audio id="audio" title="11丨性能脚本用案例和图示帮你理解HTTP协议" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/3b/6e/3bff70e6123127b48f108648fb795c6e.mp3"></audio>
当前使用得最为广泛的应用层协议就是HTTP了。我想了好久还是觉得应该把HTTP协议写一下。
因为做性能测试分析的人来说HTTP协议可能是绕不过去的一个槛。在讲HTTP之前我们得先知道一些基本的信息。
HTTPHyperText Transfer Protocol超文本传输协议显然是规定了传输的规则但是它并没有规定内容的规则。
HTMLHyperText Marked Language超文本标记语言规定的是内容的规则。浏览器之所以能认识传输过来的数据都是因为浏览器具有相同的解析规则。
希望你先搞清楚这个区别。
我们首先关注一下HTTP交互的大体内容。想了很久画了这么一张图我觉得它展示了我对HTTP协议在交互过程上的理解。
<img src="https://static001.geekbang.org/resource/image/5f/ba/5fe0f2607000183eb8375cb66cfd41ba.jpg" alt="">
在这张图中,可以看到这些信息:
1. 在交互过程中数据经过了Frame、Ethernet、IP、TCP、HTTP这些层面。不管是发送和接收端都必须经过这些层。这就意味着任何每一层出现问题都会影响HTTP传输。
1. 在每次传输中每一层都会加上自己的头信息。这一点要说重要也重要说不重要也不重要。重要是因为如果这些头出了问题非常难定位在我之前的一个项目中就曾经出现过TCP包头的一个option因为BUG产生了变化查了两个星期一层层抓包最后才找到原因。不重要是因为它们基本上不会出什么问题。
1. HTTP是请求-应答的模式。就是说,有请求,就要有应答。没有应答就是有问题。
1. 客户端接收到所有的内容之后,还要展示。而这个展示的动作,也就是前端的动作。**在当前主流的性能测试工具中,都是不模拟前端时间的******比如说JMeter。我们在运行结束后只能看到结果但是不会有响应的信息。你也可以选择保存响应信息但这会导致压力机工作负载高压力基本上也上不去。也正是因为不存这些内容才让一台机器模拟成千上百的客户端有了可能。
如果你希望能理解这些层的头都是什么,可以直接抓包来看,比如如下示图:
<img src="https://static001.geekbang.org/resource/image/5c/06/5cb8a2717a30ab54334728e8e7658306.png" alt="">
从这个图中我们就能看到各层的内容都是什么。当然了这些都属于网络协议的知识范围如果你有兴趣可以去看一下《TCP/IP详解 卷1协议》。
我们还是主要来说一说HTTP层的内容。同样我希望通过最简单的示例的方式给你解释一下HTTP的知识而不是纯讲压力工具或纯理论。
在我看来,只有实践的操作和理论的结合,才能真正的融会贯通。只讲压力工具而不讲原理,是不可能学会处理复杂问题的;空有理论没有动手能力是不可能解决实际问题的。
由于压力工具并不处理客户端页面解析、渲染等动作,所以,以下内容都是从协议层出发的,不包括前端页面层的部分。
## JMeter脚本
在这里我写了一个简单的HTTP GET请求由于HTTP2.0在市场上还没有普及所以这里不做特别说明的话就是HTTP1.1)。
<img src="https://static001.geekbang.org/resource/image/d1/21/d1dd869b165bf14a06102d452a3a6921.png" alt="">
在前面的文章中我已经写过了HTTP GET和POST请求。在这里只解释几个重要信息
第一个就是Protocol。
这个当然重要。从“HTTP”这几个字符中我们就能知道这个协议有什么特点。 HTTP的特点是建议在TCP之上、无连接TCP就是它的连接、无状态后来加了Cookies、Session技术用KeepAlive来维持也算是有状态吧、请求-响应模式等。
第二个是Method的选项GET。
HTTP中有多少个Method呢我在这里做个说明。在RFC中的HTTP相关的定义中比如RFC2616、2068定义了HTTP的方法如下GET、POST、PUT、PATCH、DELETE、COPY、HEAD、OPTIONS、LINK、UNLINK、PURGE。
回到我们文章中的选项中来。GET方法是怎么工作的呢
>
The GET method means retrieve whatever information (in the form of an entity) is identified by the Request-URI.
也就是说GET可以得到由URI请求定义的各种信息。同样的其他方法也有清楚的规定。我们要注意的是HTTP只规定了你要如何交互。它是交互的协议就是两个人对话如何能传递过去小时候一个人手上拿个纸杯子中间有根线相互说话能听到这就是协议。
第三个是Path也就是请求的路径。这个路径是在哪里规定的呢在我这个Spring Boot的示例中。
```
@RequestMapping(value = &quot;pabcd&quot;)
public class PABCDController {
@Autowired
private PABCDService pabcdService;
@Autowired
private PABCDRedisService pabcdRedisService;
@Autowired
private PABCDRedisMqService pabcdRedisMqService;
@GetMapping(&quot;/redis_mq/query/{id}&quot;)
public ResultVO&lt;User&gt; getRedisMqById(@PathVariable(&quot;id&quot;) String id) {
User user = pabcdRedisMqService.getById(id);
return ResultVO.&lt;User&gt;builder().success(user).build();
}
```
看到了吧。因为我们定义了request的路径所以我们必须在Path中写上`/pabcd/redis_mq/query`这样的路径。
第四个是Redirect重定向。HTTP 3XX的代码都和重定向有关从示意上来看如下所示。
<img src="https://static001.geekbang.org/resource/image/1d/73/1d03ddd7a269480b0e38dec3c6bb0f73.jpg" alt=""><br>
用户发了个URL A到服务A上服务A返回了HTTP代码302和URL B。 这时用户看到了接着访问URL B得到了服务B的响应。对于JMeter来说它可以处理这种重定向。
第五个是Content-Encoding内容编码。它是在HTTP的标准中对服务端和客户端之间处理内容做的一种约定。当大家都用相同的编码时相互都认识或者有一端可以根据对端的编码进行适配解释否则就会出现乱码的情况。
默认是UTF8。但是我们经常会碰到这种情况。当我们发送中文字符的时候。比如下面的名字。
<img src="https://static001.geekbang.org/resource/image/d5/0e/d56493383d4c80469d9af7960e94830e.png" alt="">
当我们发送出去时,会看到它变成了这种编码。如下图所示:
<img src="https://static001.geekbang.org/resource/image/c1/d0/c1884e6b0a4e071edc0d1e17b8ba3fd0.png" alt="">
如果服务端不去处理,显然交互就错了。如下图所示:
<img src="https://static001.geekbang.org/resource/image/73/3a/73680e9d1fb278dbd0ab144ffe8a373a.png" alt="">
这时,只能把配置改为如下:
<img src="https://static001.geekbang.org/resource/image/6c/49/6c48ebfddff84238fb5ea01a175eb449.png" alt="">
我们这里用GBK来处理中文。就会得到正确的结果。
<img src="https://static001.geekbang.org/resource/image/6d/5f/6dbc3c868b1790c001942bd9d777df5f.png" alt="">
你就会发现现在用了正常的中文字符。在这个例子有人选择用URL编码来去处理会发现处理不了。这是需要注意的地方。
第六个是超时设置。在HTTP协议中规定了几种超时时间分别是连接超时、网关超时、响应超时等。
如下所示JMeter中可以设置连接和响应超时
<img src="https://static001.geekbang.org/resource/image/3b/fd/3b0b351b50b2deeb1cbc6853eeb886fd.png" alt="">
在工具中我们可以定义连接和响应的超时时间。但通常情况下我们不用做这样的规定只要跟着服务端的超时走就行了。但在有些场景中不止是应用服务器有超时时间网络也会有延迟这些会影响我们的响应时间。如果HTTP默认的120s 超时时间不够,我们可以将这里放大。
在这里为了演示我将它设置为100ms。我们来看一下执行的结果是什么样。
<img src="https://static001.geekbang.org/resource/image/ab/50/ab12ae557c1a89cb74de32c0a8ed4a50.png" alt="">
从栈的信息上就可以看到,在读数据的时候,超时了。
超时的设置是为了保证数据可以正常地发送到客户端。做性能分析的时候,经常有人听到“超时”这个词就觉得是系统慢导致的,其实有时也是因为配置。
通常,我们会对系统的超时做梳理,每个服务应该是什么样的超时设置,我们要有全局的考量。比如说:
<img src="https://static001.geekbang.org/resource/image/34/13/34e07921468afe8d5bba61093de97813.jpg" alt="">
超时应该是逐渐放大的(不管你后面用的是什么协议,超时都应该是这个样子)。而我们现在的系统,经常是所有的服务超时都设置得一样大,或者都是跟着协议的默认超时来。在压力小的时候,还没有什么问题,但是在压力大的时候,就会发现系统因为超时设置不合理而导致业务错误。
如果倒过来的话,你可以想像,用户都返回超时报错了,后端还在处理着呢,那就更有问题了。
而我们性能测试人员,都是在压力工具中看到的超时错误。如果后端的系统链路比较长,就需要一层层地往后端去查找,看具体是哪个服务有问题。所以在架构层级来分析超时是非常有必要的。
<img src="https://static001.geekbang.org/resource/image/3b/fd/3b0b351b50b2deeb1cbc6853eeb886fd.png" alt="">
在上图中还有一个参数是客户端实现Client Implementation。其中有三个选项空值、HTTPClient4、Java。
官方给出如下的解释。
JAVA 使用JVM提供的HTTP实现相比HttpClient实现来说这个实现有一些限制这个限制我会在后面提到。
HTTPClient4使用Apache的HTTP组件HttpClient 4.x实现。
空值如果为空则依赖HTTP Request默认方法或在`jmeter.properties`文件中的`jmeter.httpsample`定义的值。
用JAVA实现可能会有如下限制。
1. 在连接复用上没有任何控制。就是当一个连接已经释放之后,同一个线程有可能复用这个已经释放掉的连接。
1. API最适用于单线程但是很多设置都是依赖系统属性值的所以都应用到所有连接上了。
1. 不支持 Kerberos Authentication这是一种计算机网络授权协议用在非安全网络中对个人通信以安全的手段进行身份认证
1. 不支持通过keystore配置的客户端证书。
1. 更容易控制重试机制。
1. 不支持Virtual hosts。
1. 只支持这些方法: GET、POST、HEAD、OPTIONS、PUT、DELETE和TRACE。
1. 使用DNS Cache Manager更容易控制DNS缓存。
第八个就是HTTP层的压缩。我们经常会听到在性能测试过程中因为没有压缩导致网络带宽不够的事情。当我们截获一个HTTP请求时你会看到如下内容。
<img src="https://static001.geekbang.org/resource/image/3a/64/3a339b618e592c19493cd6acce810364.png" alt="">
这就是有压缩的情况。在我们常用的Nginx中会用如下常见配置来支持压缩
```
gzip on; #打开gzip
gzip_min_length 2k; #低于2kb的资源不用压缩
gzip_comp_level 4; #压缩级别【1-9】值越大压缩率就越高但是CPU消耗也越多根据我们在网上看到建议大部分都是建议设置为中间4、5之类的这里我建议大家根据自己的项目实际情况在压力测试之后给出适合的值。
gzip_types text/plain application/javascript; #设置压缩类型
gzip_disable &quot;MSIE [1-6]\.&quot;; # 禁用gzip的条件支持正则
```
在RFC2616中Content Codings部分定义了压缩的格式gzip 和 Deflate不过我们现在看到的大部分都是gzip。
不过在压缩这件事情上,我们在压力工具中并不需要做什么太多的动作,最多也就是加个头。
<img src="https://static001.geekbang.org/resource/image/35/54/3566e5a03656918cc92b9a88596f9054.png" alt="">
第九个就是并发。在RFC2616中的8.1.1节明确说明了为什么要限制浏览器的并发。大概翻译如下,有兴趣的去读下原文:
1. 少开TCP链接可以节省路由和主机客户端、服务端、代理、网关、通道、缓存的CPU资源和内存资源。
1. HTTP请求和响应可以通过Pipelining在一个连接上发送。Pipelining允许客户端发出多个请求而不用等待每个返回一个TCP连接更为高效。
1. 通过减少打开的TCP来减少网络拥堵也让TCP有充足的时间解决拥堵。
1. 后续请求不用在TCP三次握手上再花时间延迟降低。
1. 因为报告错误时没有关闭TCP连接的惩罚而使HTTP可以升级得更为优雅原文使用gracefully
1. 如果不限制的话,一个客户端发出很多个链接到服务器,服务器的资源可以同时服务的客户端就会减少。
我们常见的浏览器有如下的并发限制。
<img src="https://static001.geekbang.org/resource/image/ff/ad/ffea49962bdbf58bb0cea66e222dcead.png" alt="">
在压力工具中,并没有参数来控制这个并发值,如果是在同一个线程中,就是并行着执行下去。
HTTPS只是加了一个S就在访问中加了一层。这一层可以说的话题有很多因为技术原理比较多。还好对性能测试中的脚本部分来说关系并不大需要时导进去就可以了。而在性能分析中基本上除了看下不同产品、不同软件硬件的性能验证之外其他的也没什么可分析的部分。因为证书是个非常标准的产品加在中间就是加密算法和位数也会对性能产生影响。如果执行场景时报`javax.net.ssl.SSLHandshakeException: Remote host closed connection during handshake`,就应该把证书也加载进来。
有了前面这些压力工具中常用的HTTP知识之后有些人肯定会有一种感觉总觉得有什么内容没有讲到。对了就是HTML。前面我们提到了HTML是属于内容的规则前端是个宏大的话题以后有机会详聊。
其实对我们做性能测试的人来说无需关心HTTP的内容我们只要关心数据的流向和处理的逻辑就可以了。至于你是A业务还是B业务在性能分析中都是一样的逻辑仍然没有变化。
从性能测试的角度来看如果你要模拟页面请求最多也就是正常实现HTTP的方法GET、POST之类的。它发送和接收的内容只要符合业务系统的正常流程就可以这样业务才能正常运行。
比如说前面提到的POST请求。如果我们发送了一段JSON。内容如下
```
{
&quot;userNumber&quot;: &quot;${Counter}&quot;,
&quot;userName&quot;: &quot;Zee_${Counter}&quot;,
&quot;orgId&quot;: null,
&quot;email&quot;: &quot;test${Counter}@dunshan.com&quot;,
&quot;mobile&quot;: &quot;18611865555&quot;
}
```
代码中的Service负责接收User对象同时转换它的是如下代码
```
@Override
public String toString() {
return &quot;User{&quot; +
&quot;id='&quot; + id + '\'' +
&quot;, userNumber='&quot; + userNumber + '\'' +
&quot;, userName='&quot; + userName + '\'' +
&quot;, orgId='&quot; + orgId + '\'' +
&quot;, email='&quot; + email + '\'' +
&quot;, mobile='&quot; + mobile + '\'' +
&quot;, createTime=&quot; + createTime +
'}';
}
```
然后通过Service的add方法insert到数据库中这里后面使用的MyBatis
```
Boolean result = paRedisService.add(user);
```
而这些,都属于业务逻辑处理的部分,我们分析时把这个链路都想清楚才可以一层层剥离。
## 总结
对于HTTP协议来说我们在性能分析中主要关心的部分就是传输字节的大小、超时的设置以及压缩等内容。在编写脚本的时候要注意HTTP头部至于Body的内容只要能让业务跑起来即可。
## 思考题
你能说一下为什么压力机不模拟前端吗?
欢迎你在评论区写下你的思考,也欢迎把这篇文章分享给你的朋友或者同事,一起交流一下。

View File

@@ -0,0 +1,227 @@
<audio id="audio" title="12丨性能场景做参数化之前我们需要考虑什么" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/ec/01/ec95d55ab6e1cd115774c4814b070801.mp3"></audio>
在性能测试中,我们要关注的数据主要有以下几类,分别是参数化数据、监控数据和基础铺底数据。
我们今天先描述第一种参数化数据,在后面的文章中再描述其他数据。
首先我们需要了解,为什么要关注性能场景中的参数化数据呢?我以下面的两个例子说明一下。
在我的工作经历中,见过很多初级性能测试工程师不知道如何设置合理的参数化数据,以至于数据会出现这两种情况。
1.数据不均衡
有些人直接用同一个数据执行混合场景测试,在这种情况下对服务器的压力和真实环境下的完全不一样。有时我们不得不造很多参数化数据,也有很多工程师不考虑数据库表中的数据直方图,就直接在少量的参数化数据中创建了大量的相关记录。比如说,在电商系统中造出大量的购买记录;在银行系统中造出大量的个人流水记录。
这些都不能满足真实用户场景的需要,导致的结果就是整个测试结果都毫无意义。
2.参数化数据量不足
有时候如果我们选择用非常少量的数据运行大量业务操作的场景就会导致压力和真实生产环境完全不一致。比如说用100个数据运行出上万甚至上亿的业务操作。
那么到底该怎样才能合理地设置参数化数据呢?
## 参数化数据的疑问
根据我的经验,在参数化测试数据的获取和考虑上,我们一般会有以下四个常见的疑问。
1. 参数化数据应该用多少数据量?
1. 参数化数据从哪里来?
1. 参数多与少的选择对系统压力有什么影响?
1. 参数化数据在数据库中的直方图是否均衡?
接下来,我们对这些问题一一做出解答。
## 参数化数据应该用多少数据量?
首先参数化数据要用到多少取决于场景举例来说对一个压力工具线程数为100TPS有1000的系统如果要运行30分钟则应该取得的参数化数据是下面这样的。
数据类型、限制条件和数据量计算的方式如下表所示:
<img src="https://static001.geekbang.org/resource/image/2e/f1/2e79e6bca7fd9f6c439ef04699d9ecf1.png" alt=""><br>
从技术角度看,根据数据类型就可以确定应该用多少条参数化数据了。但是这样考虑就够了吗?当然是不够的。因为除了技术的限制之外,还有业务场景的需求。
### 根据业务场景计算参数化数据量
在性能场景中,我们需要根据实际的业务场景来分析需要用到什么样的数据,以便计算数据量。这里的数据类型包括可循环使用的数据和不可循环使用的数据。用户登录是一个在各行业中几乎都会遇到的事务,我们拿它来举例说明,下面这张图是一个用户登录的界面。
<img src="https://static001.geekbang.org/resource/image/ad/48/adffd57ad8e77788600f2b1861e03448.png" alt="">
这里需要用到两种数据,一个是帐号,一个是密码。帐号和密码一定是可以真实登录到系统的,不然无法完成后续的业务。很显然对于登录来说,不同的人一定是用不同的用户登录的。
### 场景一
首先我们来看下场景一。有时候我们做脚本时考虑的是有多少线程Thread就配置多少用户让每个线程在同一个用户上循环执行。
如下图所示:
<img src="https://static001.geekbang.org/resource/image/de/c2/dee2bc71f6e5edb659e2c9fe31d3c9c2.jpg" alt="">
需要注意的是在本文中每一个“—user1→”代表一次脚本完全的迭代。
这样的用户参数化配置只能满足一些比较特定的场景。比如说用户在早上登录系统之后一直在系统中带着登录session做业务操作并且不会退出只有在下班时才退出系统。
当我们要模拟一天中的业务峰值时可以像上面这样配置。登录一次循环使用同一用户的Session信息。这就是前面提到的部分可循环数据。
在这样的场景中,有多少线程就需要准备多少用户数据。即:
```
Java
用户数据=线程个数
```
### 场景二
但在有些场景中,这是完全错误的配置方式。比如说电商系统,用同一个用户账号不停循环购买商品,就是不符合真实场景的。
这时侯怎么办?我们可以用在压力测试工具中模拟出来的线程的每一次迭代来代表一个用户,如下所示:
<img src="https://static001.geekbang.org/resource/image/a1/1c/a1780e7bbe6a9ee3bc3729693aa7521c.jpg" alt="">
这就是不可循环使用的数据。在这样的场景中就需要考虑场景的TPS和持续时间了。用户数据的计算方法是
```
Java
tpsx持续时间秒级
```
我们举个例子假如有一个100TPS的场景持续30分钟。那么计算方式如下
```
Java
100x30*60=180000条用户数据
```
这里再多说一句,压力工具的线程数量也并不是常说的并发数的概念。这个我们在[前面](https://time.geekbang.org/column/article/181916)已经说得很清楚了。
### 场景三
但是还有一种情况,就是在一个线程之中,可以循环使用固定条目的数据。如下所示:
<img src="https://static001.geekbang.org/resource/image/47/9b/47184c05e0a6a3120dea42355d101e9b.jpg" alt="">
在这种情况下我们就需要根据实际的业务场景判断了。在同样的100TPS场景中如果准备了1000条数据就可以让每个线程用10个不同的数据。
这样的场景没有固定的条数限制,只能根据实际的业务判断。
所以在配置参数之前,我们需要先判断这个参数是什么类型的数据。
如果是可循环使用的数据,那么它在真实的性能场景中非常少,也就是说只使用一条或几条测试数据的真实业务场景是非常少的。
## 参数化数据从哪里来?
计算了参数化数据量之后,还有一个重要的问题需要解决,就是参数化数据从哪里来呢?这一步的目的是要确保参数的有效性。
参数化数据从大体上划分,主要有两个来源。
### 第一类
用户输入的数据在后台数据库中已存在,比如我们上面示例中的用户数据。这类数据的特点是什么呢?
1. 存在后台数据库中;
1. 需要用户主动输入;
1. 用户输入的数据会和后台数据库中的数据做比对。
这类数据必须查询数据库之后再参数化到工具中。
### 第二类
用户输入的数据在后台数据库中不存在。在业务流中这些数据会Insert或Update到数据库中。这类数据的特点是什么呢
1. 数据库中原本不存在这些数据;
1. 在脚本执行成功后会将这些数据insert或update到数据库中
1. 每个用户输入的数据可能相同,也可能不同,这取决于业务特点。
这类数据必须通过压力工具做参数化,同时也必须满足业务规则。
我同样用前面的用户参数为例,由于用户登录的时候一定要和数据库中的用户数据做比对,只有用户名密码都完全正确的情况下才可以成功登录,所以这样的用户参数一定要从后台数据库中查询得到。
在本例中通过后台数据库用户表的查询真实可用的用户数共有10万。
<img src="https://static001.geekbang.org/resource/image/43/63/43ff3f5a53a85de1061d2bb168b46f63.png" alt="">
如果在业务场景中是不可循环使用的用户数据那么很显然在可以支持100TPS并发的系统中这些用户数量只够使用16.67分钟。
```
Java
100000/100/60=16.67(分钟)
```
总之,参数化时需要确保数据来源以保证数据的有效性,千万不能随便造数据。这类数据应该满足两个条件:
1. 要满足生产环境中数据的分布;
1. 要满足性能场景中数据量的要求。
## 参数取多与少对系统压力有什么影响?
根据上文中的第二个条件,这里就要说一下数据量的要求了。
从经验上判断,对一个系统来说,获取的参数化数据是否合理,会直接影响压力测试的结果有没有意义。
我们根据下面这张图来理解一下数据在系统中的流转。
<img src="https://static001.geekbang.org/resource/image/be/74/bee122f5816738a73e93b33e36304574.jpg" alt="">
这张图中,绿色部分代表数据在各系统中的正常大小,而黑色部分代表压力工具中使用的数据量大小。如果压力工具使用的数据量少,那么应用服务器、缓存服务器、数据库服务器,都将使用少量的缓存来处理。
显然图中所示的黑色部分是很少的,完全不能把后端各类服务器的缓存占用到真实场景中应该有的大小,所以在这种状态之下是测试不出来真实场景下的压力的。
对数据库连接的存储设备来说同样也有影响。如果数据量少则相应的存储的I/O使用就少。对于一个没有被Cache的数据来说首次使用肯定会触发I/O也就是会产生寻址、PageFalut等情况。
参数取得过多,对系统的压力就会大;参数取得过少,不符合真实场景中的数据量,则无法测试出系统真实的压力。
## 参数化数据在数据库中的直方图是否均衡?
对于参数化数据来说,如果数据取自于数据图,我们通常要检查一下数据库中的数据直方图。 对于直接从生产上拿的数据来说,数据的分布更为精准。但是对于一些在测试环境中造的数据,则一定要在造数据之后,检查下数据分布是否与生产一致。
我们以一个案例开始。
在性能场景执行过程中有一个业务的TPS如下图所示
<img src="https://static001.geekbang.org/resource/image/01/00/01a3e647ed05d8930b91b431023a5f00.png" alt="">
很明显图中TPS中间掉下来的情况是非常不合理的。
为什么会导致这个情况呢?在这个示例中,这种现象是由抽取的数据量不合理导致的,我们来看一下数据分布。
```
客户iD客户流水记录数
'客户ID1', '69865'
'客户ID2', '55075'
......
'客户ID5374728, '177'
'客户ID5374729', '176'
'客户ID5374730', '175'
```
显然通过统计之后我们可以发现客户的流水记录数是完全不均衡的而这个业务脚本是会返回客户的流水记录的。当用到记录数多的客户ID时就会导致TPS严重下降这是因为这些数据都要从存储设备中获取一旦数据量多就会导致一系列的资源开销而用到记录数少的客户ID时TPS就很高。
那么针对这种情况,我们该怎么处理呢?
首先分析业务逻辑确认客户流水是否应该这么多。在这个场景中我们分析过业务客户的流水通常情况下都会在100~200之间这是合理的情况而上万的数据量就是完全不合理的。
然后我们过滤掉不合理的数据即可。
这样得到的参数化数据就符合真实场景了。
## 总结
在今天的文章中,需要你领悟到的是,参数化数据的合理性对性能场景有着举足轻重的作用。通常,我们在做参数化数据之前,需要先分析实际业务的逻辑。比如说:
1. 什么数据是唯一的?什么数据是可重复使用的?
1. 数据是客户主动输入,后端只保存即可,还是客户输入后,后端需要比对?
这些都是我们在做参数化之前要分析的部分。而参数化的数据量的重要性,不仅和业务需求相关,也和数据存储和查询的方式相关。这个话题我们在后面也会讨论到。
## 思考题
如果你吸收了这篇文章的内容,不妨思考一下下面这两道题:
1. 参数化数据的分析重点是哪些?在不同的场景中为什么参数化数据有如此大的差异?
1. 参数化数据的来源和获取要符合哪些规则?当不符合获取规则时,会产生什么问题?
欢迎你在评论区写下你的思考,也欢迎把这篇文章分享给你的朋友或者同事,一起交流学习一下。

View File

@@ -0,0 +1,413 @@
<audio id="audio" title="13丨性能测试场景如何进行场景设计" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/ba/c1/bafa113b3c5a029413efe019486471c1.mp3"></audio>
我们在前面屡次强调了场景的重要性,今天终于到了要把实际场景拿出来解析的时候了。
在本篇文章中,为了保证数据的连续性,我用之前的项目资料来作明确地说明。同时为了模糊关键业务信息,以及让场景的描述更通用性,我会把所有的业务名隐去。
根据之前我们所说的,基准性能场景是为了测试出单业务的最大容量,以便在混合容量场景中判断哪个业务对整体容量最有影响。
今天的场景设计需要说明两个前提条件:
1. 这些业务都是实时的业务,不涉及批处理、大数据等业务。
1. 因为本篇着重讲场景的设计和具体项目的操作,所以不加系统资源的分析,避免信息混乱。
在这个场景设计中首先我们要列出自己要测试的业务比例、业务目标TPS和响应时间指标。
<img src="https://static001.geekbang.org/resource/image/44/0a/444dad8faf28ab717da7635d1b9fb20a.png" alt=""><br>
在这个项目中响应时间指标是统一的就是不大于100ms。
其实我们在做项目的时候,经常会这样制定一个统一的响应时间指标,这样做也不是完全因为懒,更多的是根本不知道如何定每个业务的时间。但我们性能测试人员要知道,这显然是不对的,因为业务不同,响应时间指标也应该不同,合理的做法是给出每个业务的响应时间指标。下面我们还会遇到响应时间定得不够细致的问题。
有了这个列表,下一步就是做基准性能测试了。
## 基准性能场景
有很多人做接口测试的时候觉得接口的TPS真是高呀于是就按照最高的TPS跟老板汇报。但我们一定要知道的是接口的TPS再高都无法说明容量场景的情况除非这个服务只有这一个接口并且也只为了测试服务这时就不必有混合的情况了。
首先,我们要知道,每个业务在系统中的最大容量是多少。那么接下来,我们用上面的业务一个一个地做基准,看看结果如何。
### 业务1
场景执行时长17分钟。
先看Statistics。
<img src="https://static001.geekbang.org/resource/image/9d/a2/9de840c849d7527eaba8d61def519aa2.png" alt=""><br>
很多人喜欢用这个表中的数据来做报告特别是90th pct、95th pct、99th pct。我不是说不能用但是我们要先知道这个场景是什么样再来确定这些值是不是可以用。
从上图来看TPS达到573.24平均响应时间是109.83ms发送字节很少这里都没统计到接收字节966.22KB/sec这个值也非常低最小响应时间43ms最大响应时间694ms。
但是!这能说明什么呢?什么都说明不了呀。是好是坏?不知道呀。所以我们还需要看其他图。
我们先看一下线程图。
<img src="https://static001.geekbang.org/resource/image/ad/a4/ad1f3e8282cf0ba6f923d0f2ec6b48a4.png" alt="">
以每分钟15个用户的速度往上递增。
对应的响应时间图是下面这样的。
<img src="https://static001.geekbang.org/resource/image/c4/9d/c465de60017486d43bc4eebeebfc619d.png" alt="">
随着用户的增加,响应时间一直都在增加,显然瓶颈已经出现了。
我们再结合Statistics表格中几个和时间有关的值来想想一想90th pct、95th pct、99th pct、平均响应时间还可以用吗 Statistics的平均响应时间是109.83ms但是你从响应时间图和线程图比对就可以看到在不同的线程阶梯响应时间是有很大差别的。所以Statistics中的响应时间都是针对整个场景来说的然而**在梯度加压的过程中用Statistics中的数据是不合理的**。
接着我们再来看下TPS图
<img src="https://static001.geekbang.org/resource/image/23/65/23030e782738e7d4f1219e38b4fae565.png" alt="">
我们可以从TPS图上看到最大TPS能达到680左右。我再啰嗦一句请你不要再用所谓的”最大TPS拐点“这样的描述来说明TPS曲线我在[第6篇文章](https://time.geekbang.org/column/article/182912)中也说过性能的衰减是逐步的也有突然的情况那是非常明显的性能瓶颈了在最大TPS出现之前就已经可以判断瓶颈是否出现了。
结合上面四个图,我们就有了如下的判断:
1. 场景是递增的。
1. 压力线程上升到55第四个阶梯TPS达到上限680左右但是明显的性能在第三个阶梯就已经接近上限了
1. 在压力线程达到55时响应时间达到85ms左右这个值并不高。
除此之外,其他的似乎不需要我们再做什么判断了。
也许这时候你会想问,那么瓶颈在哪里呢?总有人看到现象就想知道结果。但是这一次呢,我不打算满足这样的好奇心,因为本篇只是为了讲场景的逻辑,而不是瓶颈的分析。哈哈。
### 业务2
从业务2开始我们不做啰嗦的数据解释了只说明一下关键点。我们看图。
Statistics图
<img src="https://static001.geekbang.org/resource/image/b0/69/b0e2ccaf8a1f9714182870a9bbd1d669.png" alt=""><br>
线程数:
<img src="https://static001.geekbang.org/resource/image/ab/20/ab1fb5be7dcfd94a4026af6571c22420.png" alt="">
响应时间图:
<img src="https://static001.geekbang.org/resource/image/17/0a/1783318a63d409ff8f27038aab7b700a.png" alt="">
TPS图
<img src="https://static001.geekbang.org/resource/image/a9/be/a932087dde363209b62c4e5ec8f72bbe.png" alt="">
基于上面的四张图,我们可以看到:
1. 这个单业务的最大TPS在6000以上。
1. 响应时间变化比较小基本上都在10ms以下但也能明显看出在线程增加的过程中响应时间也是在增加的。
这个业务由于TPS太高响应时间太短实在没啥可分析的。
### 业务3
接下来再看一下业务3的情况。
Statistics
<img src="https://static001.geekbang.org/resource/image/f1/b0/f1349673df15293398a85f900143cab0.png" alt=""><br>
线程数:
<img src="https://static001.geekbang.org/resource/image/0a/c0/0a711dfcf44c37b55817521ae5a64ec0.png" alt=""><br>
响应时间图:
<img src="https://static001.geekbang.org/resource/image/f2/7c/f28571859bacf3a5bdd7cbd9bfcd927c.png" alt="">
TPS图
<img src="https://static001.geekbang.org/resource/image/3d/7e/3d90cb45f3b3c6c37835008a5e8bb57e.png" alt="">
基于上面四张图,我们可以看到:
1. 最大TPS将近5000。
1. 响应时间随着用户的增加而增加在达到4500TPS时响应时间在6.5ms左右。
### 业务4
Statistics
<img src="https://static001.geekbang.org/resource/image/fc/9b/fc5296d4a08e590dd04e323e176e679b.png" alt="">
线程数:
<img src="https://static001.geekbang.org/resource/image/5e/7c/5e6992218dd747d4720cad3131058e7c.png" alt="">
响应时间图:
<img src="https://static001.geekbang.org/resource/image/5c/91/5ca47351a7c712f25222b2de51eb8c91.png" alt="">
TPS图
<img src="https://static001.geekbang.org/resource/image/28/ba/28855bf2f93f744dd6854592b5796aba.png" alt=""><br>
基于上面四张图,我们可以看到:
1. 最大TPS超过了300。
1. 响应时间随着用户的增而增加在达到300TPS时响应时间在70ms左右。
### 业务5
Statistics
<img src="https://static001.geekbang.org/resource/image/90/cc/90c2464c14548607b3101a25a765decc.png" alt=""><br>
线程数:
<img src="https://static001.geekbang.org/resource/image/f2/d1/f291158a41f96e251b969da3068cf4d1.png" alt=""><br>
响应时间图:
<img src="https://static001.geekbang.org/resource/image/79/f5/79760efaeed680947747ab0393b292f5.png" alt="">
TPS图
<img src="https://static001.geekbang.org/resource/image/bf/58/bfd1e5b3b7dc2e7f50933ea2ff675d58.png" alt="">
基于上面四张图,我们可以看到:
1. 最大TPS在550左右。
1. 响应时间随着用户的增而增加在达到550TPS时响应时间在55ms左右。
### 业务6
Statistics
<img src="https://static001.geekbang.org/resource/image/be/59/be54540743d302757a0e935cee006f59.png" alt="">
线程数:
<img src="https://static001.geekbang.org/resource/image/b0/8f/b04fe8746c89e11c51f0dc7283508c8f.png" alt="">
响应时间图:
<img src="https://static001.geekbang.org/resource/image/1a/15/1a96bab4e7547a70dbb44780a5171a15.png" alt="">
TPS图
<img src="https://static001.geekbang.org/resource/image/70/6e/70243e0c8dbadf5b26129e0c1dad776e.png" alt="">
基于上面四张图,我们可以看到:
1. 最大TPS超过了2500。
1. 响应时间随着用户的增加而增加在达到2500TPS时响应时间在16ms左右。
有了上面这些单业务的容量结果,我们就可以做一个表格了:
<img src="https://static001.geekbang.org/resource/image/62/bc/6246f910d9112e9e4efd70b559a84ebc.png" alt=""><br>
还记得我们前面提到响应时间都不能大于100ms吧。通过测试结果我们可以看到业务1已经接近这个指标了也就是说这个业务如果在活动或促销期有可能出现峰值最大TPS超过承受值的情况超过了前面制定的响应时间指标。
有了这些基础数据之后,下面我们就可以设计容量场景了。
## 容量性能场景
我们希望得到的容量场景在本文的一开始就已经给出。下面我们通过设计线程来得到这个容量场景的结果。
你需要记住我们的重点:
1. 场景不断。
1. 控制比例。
我们这里只说一个容量性能场景,并且这个场景是峰值业务场景。如果在你的项目中,有特定的业务日,那就要根据业务日的业务比例,重新做一个针对性的场景。
在满足了最开始提到的业务比例之后,我们不断增加压力,得到如下结果。
Statistics
<img src="https://static001.geekbang.org/resource/image/ee/5e/eead380bafe36897ac3825c42e40255e.png" alt="">
线程数:
<img src="https://static001.geekbang.org/resource/image/14/17/14272cc783b99b70ad7a7312f59a3017.png" alt="">
响应时间图:
<img src="https://static001.geekbang.org/resource/image/59/a9/59fea1518fc8a7c390f9afcadc0437a9.png" alt="">
总TPS图
<img src="https://static001.geekbang.org/resource/image/91/b1/915e2c4152ed0a000f2d0849939730b1.png" alt="">
TPS细分图
<img src="https://static001.geekbang.org/resource/image/e9/1d/e9a3f7f16b982c4a3f411c509ec4781d.png" alt="">
从上面的结果可以看到业务4和业务5的响应时间随着业务的增加而增加这显然在容量上会影响整体的性能。
在具体的项目中,这就是我们要分析调优的后续方向。
还有一点请你注意,并不是说,看到了性能瓶颈就一定要解决,事实上,只要业务指标可控,不调优仍然可以上线。这一点也是很多做性能测试的人会纠结的地方,感觉看到这种有衰减趋势的,就一定要把它给调平了。其实这是没有必要的。我们做性能是为了让系统能支持业务,即使性能衰减已经出现,性能瓶颈也在了,只要线上业务没有超出容量场景的范围,那就仍然可以上线。
另外再说几句闲话,做技术的人容易钻进这样的牛角尖,觉得明显有问题,结果公司老板不支持去调优处理,显然是老板不重视性能测试,于是深感自己不得志,工作也无精打采的。这就没必要了,做性能不是为了炫技,应该为业务服务。
我们再说回来从总TPS图上看到在容量测试中我们仍然测试到了系统的上限。这是一个好事情让我们可以判断出线上的系统配置应该是什么样的。
在达到了系统上限时,我们来看一个业务的比例(请你注意,我是不赞同用表格来承载**分析**数据的,但是作为最终的结果,给老板看的时候,还是要尽量说得通俗易懂)。
如下所示:
<img src="https://static001.geekbang.org/resource/image/6f/7e/6f9a16076f49bbe50b05080ff32bf27e.png" alt="">
我们可以从上面的数据中看到业务目标TPS已经达到响应时间也没有超过指标。很好这个容量就完全满足业务需求了。
但是!
如果业务要扩展的话有两个业务将会先受到影响那就是业务4和业务5因为它们的测试TPS和最大TPS最为接近。这是在我们推算业务扩展之后再做架构分析时要重点考虑的内容。如果是在实际的项目中这里会标记一个业务扩展风险。
请你注意,根据架构,性能测试组需要根据当前的测试状态整理架构的关键配置给线上系统做为参考,并且每个项目都会不一样,所以并不是固定的内容。我想做运维的看到这些值可能会更为亲切。
在这里,我给一个之前项目中的示例(由于属于项目交付类文档,所以这里只截取部分技术片断),如下所示:
<img src="https://static001.geekbang.org/resource/image/4d/72/4da0eef402ea87ed100d7a0cbd548f72.png" alt="">
配置整理的范围包括架构中所有和性能相关的技术参数。如下所示:
<img src="https://static001.geekbang.org/resource/image/77/dd/77faa612395beb608598e00d2cd67fdd.png" alt="">
当然,这时我们也是要分析系统的资源使用率的。在本文为了避免混乱,所以我没有提及。在实际的项目中,我们还是要分析的哦。
说完了混合容量场景之后,我们回忆一下之前说过的两个重点,我的混合业务场景是不是没有断?是不是保持了业务比例?
下面我们就该说一下稳定性场景了。
## 稳定性性能场景
我在[第1篇文章](https://time.geekbang.org/column/article/178068)就提到过,稳定性场景的时间长度取决于系统上线后的运维周期。
在这个示例中,业务+运维部门联合给出了一个指标那就是系统要稳定运行一周支持2000万业务量。运维团队每周做全面系统的健康检查。当然谁没事也不用去重启系统只要检查系统是否还在健康运行即可。大部分时候运维是等着系统警告的。
那么针对前面给出的容量结果容量TPS能达到3800业务1到业务6的容量测试结果TPS总和。所以稳定性场景时间应该是20000000/3800 = 1.46小时。
下面是两小时的稳定性场景运行情况,我在这里只做一下大概的说明。
<img src="https://static001.geekbang.org/resource/image/91/13/91c74d10fd0d2aea83f8efb3263a9113.png" alt="">
Statistics
<img src="https://static001.geekbang.org/resource/image/4e/fa/4ee95d514972394c65fef32094bbdcfa.png" alt="">
线程数:
<img src="https://static001.geekbang.org/resource/image/38/ff/38e144260b35f7793de82f9cbb0a7bff.png" alt="">
响应时间图:
<img src="https://static001.geekbang.org/resource/image/e9/5d/e9011fa5b77fbe88a1bcd90fda21cd5d.png" alt="">
TPS细分图
<img src="https://static001.geekbang.org/resource/image/b1/a7/b14ba925ae99b95f22cb3e7144fd1ca7.png" alt="">
总TPS图
<img src="https://static001.geekbang.org/resource/image/9c/61/9c6b4159f90b5c2c091ee8fb9d4c1b61.png" alt="">
从上面几张图可以看出业务2和业务3对总TPS的动荡产生了影响但系统并没有报错。这种周期性的影响你可以去分析具体的原因由于本篇是场景篇所以这里不写分析过程直接给出原因这种影响是参数化数据周期性使用所导致的有些数据的关联记录数多有些数据的关联记录数少数据库中变化倒是不大但由于TPS过高表现出来得就比较明显了。
其他的业务都比较正常,也比较稳定,没有报错。
总体业务量达到27299712也达到了稳定性业务量级的要求。
有一点估计会有人提出疑问你这个稳定性的总体TPS看起来和容量测试场景中差不多呀有必要用容量测试中的最大TPS来跑稳定性吗
这里就涉及到另一个被误解的稳定性知识点了。有很多人在资料中看到稳定性测试应该用最大TPS的80%来跑。看到没有这似乎又是一个由28原则导致的惯性思维。
在这里我要澄清一下。在具体的项目实施过程中,完全没有必要遵守这些看似有道理,实则对具体项目没什么用的原则。
这个系统用最大TPS能跑下来业务一直很正常稳定性目标能达到为什么不能用最大TPS来跑呢本来稳定性场景就是为了知道会不会由于长时间处理业务而引发潜在瓶颈像内存泄露是个典型问题。至于用多大的TPS来运行又有什么关系只要系统在正常处理资源没有出现问题也没有报错那这个场景就是有效的目标也是能达到的。
所以说,这里的稳定性场景,完全合理。但是,你觉得这样就完了吗?当然没有,我们还有异常场景要做嘛。
## 异常性能场景
我之前有提到过,异常性能场景要看架构图,但是涉及到职业素养的问题,我这里只能画个示意图来说明此系统的架构,以此来实现逻辑的完整性。示意图如下所示:
<img src="https://static001.geekbang.org/resource/image/2f/a8/2f7595803a4f35fdf5c0b566c30a93a8.jpg" alt="">
这是一个完全按生产架构来的示意图在真实的测试过程中也是这样搭建的。在这里有六个业务区域包含基础架构区也有DMZ区。
其实在每个区域中,根据架构中用到的技术组件,异常测试都有细化的场景设计,而在这里,我给你展示一个全局架构层的场景,用来说明异常场景。
这里运行的场景是用容量场景中最大TPS的50%来做异常的压力背景。
是不是会有人问这里为什么只用50%了稳定性性能场景不是还用100%的压力背景嘛?
这里我就要再说一遍看目标异常性能场景的目标是为了判断所要执行的操作对系统产生的影响如果TPS不稳定怎么能看出来异常点当然是稳定无抖动的TPS是最容易做出异常动作产生的影响了。所以这里的50%是为了得到更为平稳的TPS曲线以便做出正确的判断。
下面我们就要看异常场景的设计了。这是一个大的异常场景。
我分别对各区域中的业务应用服务器、数据库服务器以及基础架构服务器做异常操作为了方便理解下文我直接用kill来说明其实在操作中有些不是直接kill像断电、断网卡的手段也都可以用取决于如何操作更为准确
下面是具体的操作步骤和时间记录。
第一部分业务应用服务器。停下如下区域的一半应用服务器查看TPS、响应时间及其他服务器压力。
1. kill 区域三17:02
1. kill 区域一17:15
1. kill 区域二17:20
1. kill 区域五17:25。
第二部分基础架构服务器。停下一半的基础架构主机查看TPS、响应时间及另外主机压力的恢复情况。
kill 一台基础架构主机中的某个服务的某个节点17:30。
第三部分数据库服务器。停下master数据库查看切换时间slave的压力及TPS的恢复情况。
1. reboot DB-2017:36。6分钟之后恢复
1. reboot DB-2618:07。1分钟左右恢复
1. reboot DB-218:20。2分钟之后恢复。
第四部分启动master数据库。
第五部分:启动被停的应用服务。
1. start 区域五18:28
1. start 区域三18:33
1. start 区域一18:38
1. start 区域二18:43。
第六部分:启动被停基础架构主机中的某个服务的某个节点。
start 基础架构主机中的某个服务的某个节点18:48。
根据上面的步骤这里我放出TPS图来做分析。
<img src="https://static001.geekbang.org/resource/image/2c/d8/2caa1b0dd37053c673fa0f4f86a935d8.png" alt="">
从上图中的TPS趋势可以看出停掉一半的区域三应用服务器后对TPS有明显的影响。为啥呢
我们来看一下细分的TPS图
<img src="https://static001.geekbang.org/resource/image/1e/56/1efca032f3dc2404d9087de1c51d1256.png" alt="">
从图上看并不是所有的TPS都在步骤1的时候受到了影响。受影响的只有业务2和业务3。显然只有业务2和业务3走到了这个区域。但是这仍然是一个BUG
理论上用一半的压力停了一半的服务器即便当前正在运行在被停掉的服务器上的session受到了影响那TPS也应该会恢复的但是TPS没恢复。所以先这里提个BUG。
另外停掉区域一、二、五的一半应用服务器影响不大TPS有些许下降但并没有报错这个结果还可以接受。
停掉基础架构服务器时TPS有下降但很快恢复了非常好。
在步骤6时记录的信息是在6分钟之后恢复的这个时间有点久了。我在这里拆开TPS细节来看一下。
<img src="https://static001.geekbang.org/resource/image/63/f7/63e152d94df1d51d49b8493170b95df7.png" alt="">
显然这段报错得比较多6分钟一个master库切换过去。这怎么能接受呢报BUG
另外步骤8中TPS显然下降到底了。还好时间并不长在2分钟后恢复。这个可以报BUG。为什么说是**可以**报呢因为这个时间并不算长。这里就有一个预期的问题。通常情况下我们做DB master的异常切换在这个架构中是期望在1分钟内完成切换的。在我这个场景中最快的数据库master切换是40s。
请你注意我看到有些厂商说数据库可以达到秒级切换这种说法未免过于空泛。如果把”不到1分钟“称为秒级的话那就欲盖弥彰了。我理解的秒级切换是一秒内而不是单位是秒就可以。
通常这种1秒内切换说的都只是数据库实例的前面一层有叫Plus的有叫Proxy的并且说的不是从出现异常到判断切换的过程。而是说从切换动作开始到结束。另外这个秒级切换也是有背景条件的。我们不要看广告要看实际的操作结果。
请注意,上面提到的容量场景和异常场景,都只是项目中的一个场景。其实在这个项目中,我还有很多其它的容量场景和异常场景。从场景设计上来说,这些场景都大同小异,但都需要在大量的调研分析之后才能设计得出来。
## 总结
在对基准场景、容量场景、稳定性场景和异常场景做了详细的,有逻辑的描述之后,相信你已经能体会到场景的博大精深了。
在本篇中,由于字数有限,我只对场景的具体执行过程中的关键点做了细致地描述。但是场景绝对不止这些哦,还有很多细节的内容。
同时,为了让理解更为清晰,这里我只描述了场景本身,场景包括的其他内容,比如说参数设计、监控设计等等,在本篇中都没有描述。
从这里,你大概能明白我说的场景对性能有多么重要了吧。
希望今天的这篇文章能给你在设计性能场景时提供参考。
## 思考题
看完了今天的文章你能说一下性能场景应该按什么样的逻辑设计吗以及在稳定性场景中为什么不用最大TPS的80%来做?
欢迎你在评论区写下你的思考,也欢迎把这篇文章分享给你的朋友或者同事,一起交流一下。

View File

@@ -0,0 +1,131 @@
<audio id="audio" title="14丨性能测试场景如何理解业务模型" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/f6/15/f67876b835fde08f9ddcf1efc7404515.mp3"></audio>
性能场景中的业务模型是性能测试工作中非常重要的一部分。而在我们真实的项目中,业务模型跟线上的业务模型不一样的情况实在是太多了。原因可能多种多样,这些原因大大降低了性能测试的价值。
有人说,就是因为这样才应该直接用生产流量的方式来做嘛,这样就不用管业务模型了,直接就有生产的业务模型了。没错,只要你能通过生产流量扩大回放的方式实现压力部分,确实可以不用考虑业务场景了。但这么做的前提也必须是你的生产流量来源是可以覆盖想要测试的业务场景的。
## 回放的逻辑
回放的逻辑是这样的。
<img src="https://static001.geekbang.org/resource/image/3a/ce/3a64cf7335ffbc5cec29900354e918ce.jpg" alt="">
如果你喜欢的话还可以在每一个业务产品和基础架构的层面做接口的回放甚至我们可以直接在数据库中回放SQL。而这些手段都是为了模拟生产的业务模型。
这是非常容易理解的逻辑吧。
这里要批驳一个观点,就是有些人觉得只有通过生产流量回放的方式,才是真实地 模拟了线上的流量。事实上,这个观点是偏颇的。前几天有一个性能测试工程师问我一个流量回放过程中遇到的问题,谈到为什么要用流量回放。他说他们领导觉得这个最新潮最直接最正确,但实际上他得到的那段业务流量根本不能完全覆盖想测试的场景,最后折腾了一个月也是无功而返。
我知道,现在有很多人在各种场合说,可以直接在生产环境中,通过业务统计动态统计出业务场景,并实时实现到性能平台中去。这当然是一个很好的路子,但这个路子需要完整的技术实现,并且在不同的企业中,这种方式难就难在创建业务模型的统计算法,此外还要有高层领导的支持,才能真正实现完整的逻辑。
所以在今天的文章中,我想写的是最朴素的逻辑。那就是从生产数据统计,怎么转化到具体的场景中的业务模型。明白了这个逻辑之后,不管你是用生产流量回放,还是用实时业务量统计,还是线下业务量统计,你会发现原理都是一样的。
这是一个真实的案例,我已经把所有的业务名都替换掉了,同时对业务量级也做了降级调整,但这并不影响描述获取业务场景的完整性。
原系统的量级如下图所示:
<img src="https://static001.geekbang.org/resource/image/c2/be/c25bb69556b8f7f409bc620ef42c37be.png" alt="">
这里我将降低10倍处理。
## 生产数据统计
首先我们从生产环境取出数据,粒度到秒级,取出所有业务的交易量数据。
业务量级按天统计的生成图如下:
<img src="https://static001.geekbang.org/resource/image/b1/43/b18ad11f4fb4ed38ff70aaf73d730843.png" alt="">
我为什么要取这一段时间的数据呢?答案很简单,因为这一段时间完整地体现了这个**业务系统的峰值数据**。
从这样的数据中取出业务量最高的一天最大的业务量是2000万左右。
注意,我这里说的是业务量最高的一天,并不是说我们的业务场景只从这一天产生,还有别的时间,可能业务量不多,但是业务比例会完全不同,这也是要取出来的场景,所以这个统计数据到业务模型的分析过程会比较细致。我们把这一天的逻辑说完后,你就会明白其他的场景获取方式。
接着,我再以小时为单位统计出业务量比例。如下图所示:
<img src="https://static001.geekbang.org/resource/image/48/3a/48a4a0d7b640ce14406f722999c6b63a.png" alt="">
从上图显然可以看出哪个小时的业务量最大那就是9点。
但是呢你不要忘记了在16点的时候明显蓝色表示的那个业务量是大于9点时的业务量的。这个也是要取出来的场景。
如果需要更细的数据,我们可以以分钟为单位看一下这个小时内的业务量分布。
<img src="https://static001.geekbang.org/resource/image/78/13/78c8b548205520deb627dd033de0cb13.png" alt="">
如果你的业务有必要从分钟或秒来看的话可以按分钟或秒来取场景比例。在我们今天的这个案例中取到小时就已经足够。因为我要的是业务模型而不是生产TPS量级。
另外既然说到了这里我再把生产TPS量级的统计说一下。有了上面的分钟统计比例就可以很容易统计出生产环境中每个业务的最大TPS了。这里得到的TPS将是最有效的测试是否通过的SLA指标。
下面我们再以小时为单位做出百分比图。
<img src="https://static001.geekbang.org/resource/image/8b/4e/8bf4cb8e7a949dd80eff31d9be03004e.png" alt="">
为什么要做百分比图呢因为这个比例才是我们在性能场景中设置的TPS比例是最直接有效的比例。
## 业务模型计算过程
针对这一天中的数据,我们将做出以下三个业务模型。
1. 通用业务场景模型。就是将这一天的所有业务数加在一起,再将各业务整天的交易量加在一起,计算各业务量的比例。
1. 9点钟的业务模型。将9点钟的业务比例直接拿出来用。
1. 16点的业务模型。将16点钟的业务比例直接拿出来用。
首先我们看一下通用业务模型。
<img src="https://static001.geekbang.org/resource/image/b0/74/b0aa820312cb9eed421ca3cded00dd74.jpg" alt="">
我们将上面的0%的业务全部删除,再计算一次百分比,得到测试场景中的业务比例。如下所示:
<img src="https://static001.geekbang.org/resource/image/e9/ce/e931d14ca74a7a5674687c1200306ace.png" alt="">
做为最基础的业务比例,这个可以覆盖大部分的业务时间了。
在通用的业务场景中如果业务团队有给出明确的TPS指标那就有依赖了。但是如果没有给的话也不要气馁。我们可以根据系统的运行时段计算平均值即可。
因为我们这个系统是24小时系统所以我用24小时来计算。得到如下值
$TPS1 = \frac{20000000}{24*3600} = 231$
也就是说通用场景中TPS不能低于231。
接着我们看下9点的业务模型。计算方法和上面一样最后得出比例。
<img src="https://static001.geekbang.org/resource/image/8f/fe/8f665c118fc36fe62167dcbcc8f8f2fe.png" alt="">
我们可以从小时图中看到9点的业务量总和有120万左右。为了方便这里我拿120万来计算。它的生产TPS就是1,200,000 / 3600 = 333。
$TPS2 = \frac{1200000}{3600} = 333$
显然这个模型下做场景时就不能低于333TPS。
最后看一下16点的业务场景。
<img src="https://static001.geekbang.org/resource/image/e3/26/e39e694cd9c0d77d613785dd4de9d426.png" alt="">
从小时图中我们可以看到16点的业务量总和有100万左右。为了方便这里我拿100万来计算。它的生产TPS就是
$TPS3 = \frac{1,000,000}{3600} = 277$
显然这个模型下做场景时就不能低于277TPS。
但是请注意像9点业务模型中的业务11占比达到30.25%而16点业务模型中只有8.69%。虽然TPS差不多但是业务比例差别大这两种业务模型下对系统资源的消耗会完全不一样。
最后我稍微说一下TPS的控制。
有了这个计算过程当我们把这些比例设计到场景中去的时候一定要注意这些TPS的比例在运行过程中不能发生大的变化。一旦压力发起后由于各业务的响应时间随着压力的增加发生的变化量不同就会导致运行过程中业务比例出现很大的偏差。
我们做性能测试工程师的都有过这样的经历。通常在LoadRunner里会用`pacing`来控制TPS而用JMeter的则会用`Constant Throughput Timer`来控制TPS。
## 总结
在这一篇中,我描述了业务模型的来源和计算过程。其实就是一些常规的求和平均计算,只要判断出哪一段业务是我们需要的就可以了。
另外我也强调了,不管用什么炫酷的手段来实现生产环境的流量模拟,最终的目标是实现和线上比较接近的业务模型。不是说一定用生产流量回放才是正确的,只有适合自己企业的手段才是最正确的。
## 问题
那么最后给你留两个问题为什么业务比例对性能场景如此重要以及如何在执行场景过程中控制TPS比例呢
欢迎你在评论区写下你的思考,也欢迎把这篇文章分享给你的朋友或者同事,一起交流一下。

View File

@@ -0,0 +1,330 @@
<audio id="audio" title="15丨性能测试场景如何进行监控设计" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/fe/a2/fe8b59bebf1f5605fdca7a9c598b52a2.mp3"></audio>
在性能测试中,我觉得监控是非常重要的环节。因为这是做性能分析的前提,走出这一步,才有后面的分析。
监控是性能分析承上启下的关键点。
设计监控是我们性能测试工程师必须要做的事情。当然了,仅仅设计监控是不够的,还要看懂监控数据才能分析。我们将在后面的篇幅一一拆解。
我觉得性能测试工程师也一定要自己去实现一遍监控的环节而不是直接用其他团队搭建的监控工具。你可以自己找个demo服务器做一遍这样才能真正理解后续要关注的点在哪里。
之前在一个项目上我跟团队成员说把监控一层层部署起来。有个小姑娘提出一个疑问“监控有什么要部署的吗不是用JConsole就好了吗”我说每个工具都有功能的局限性所以要多种工具配合在一起才能有完整的数据可分析。然后我又问她这个想法从哪来的。她说之前带她的一个测试经理说的对Java的应用只要用JConsole监控就好了。我不知道他们的沟通上下文但我理解如果不是这姑娘在断章取义那就是这个测试经理引导错误了。
监控平台还指望别人给搭好,点个链接就能出数据了,这显然不是一个技术人员该有的样子。
## 监控设计步骤
如果要让性能测试人员设计监控逻辑,要如何做呢?
首先,你要**分析系统的架构**。在知道架构中使用的组件之后,再针对每个组件进行监控。
其次,监控**要有层次,要有步骤**。有些人喜欢一上来就把方法执行时间、SQL执行时间给列出来直接干到代码层让人觉得触摸到了最顶级的技能。然而在我的工作中通常不这么做应该是**先全局,后定向定量分析**。
最后,通过分析全局、定向、分层的监控数据做分析,再根据分析的结果决定下一步要收集什么信息,然后找到完整的证据链。
这才是监控应该有的步骤,才能体现监控的价值。
## 监控技术图谱
这张图是我认为在一个性能测试中,该有的技术图谱。
<img src="https://static001.geekbang.org/resource/image/63/e7/63a249b3f653a895297bcce240a312e7.jpg" alt="">
从这个图中我们可以看到,除了压力工具之外,还有很多技术细节。通常在各种场合下,我都会说,这些都是我们要学习的范围,做性能分析的人,不一定能完全能掌握这些内容,那你所在的性能团队就应该有这样的能力。因为性能团队要推进瓶颈的定位解决,所以要有和其他团队正面沟通的能力。
下面我们就以具体的操作过程来说明设计的落地过程。
现在的流行框架比如说Spring Cloud中的熔断监控、限流服务、服务健康检查监控、链路监控、服务跟踪、聚合监控等等都是非常好的监控手段。比如说下面这样的架构图
<img src="https://static001.geekbang.org/resource/image/03/71/03afa01338826c768b87e50e075a5971.png" alt="">
这是比较常见的微服务技术架构。其中很多开源工具已经提供了监控的能力。在网上也能找到一些部署搭建的资料,好像不提微服务、全链路就不好意思见人了似的。
对技术的发展,我们要拥抱。但对思路的梳理更为重要,因为框架平台工具都是为了实现目标而存在的。
在本篇中,我们还是回归根本,说一下监控设计的思路,讲清楚性能测试中应该如何拆分监控的点。当你看完了之后,即使是面对不同的架构,也有监控部署的思路。
## 架构图
那么我们就来到开始的位置了。做性能监控之前,先画一个最简单的架构图,看一下架构中各有什么组件,各有什么服务,将这些列下来,再找对应的监控手段和方式,看哪种手段和方式在性能测试过程中成本最低,效率最高。
如果把性能归到**测试**的这个阶段,那就必须先考虑**测试**的具体情况。
有些企业因为有长期的积累,监控平台完整又稳定,那显然是最好的。如果是短期项目类的性能测试,又涉及到多方企业的,基本上不要想有完整成熟的监控平台这件事了。
但是不管怎么样,我们都需要拿到架构的全局监控数据。针对下面的这个不大的架构,我们来考虑下如何拆分。
<img src="https://static001.geekbang.org/resource/image/bd/c0/bd60bc8db374b2c163c504f85d893bc0.jpg" alt="">
需要监控的内容如下:
1. 操作系统
1. Nginx
1. Tomcat
1. Redis
1. MySQL
下面我就来细化下这个简单架构的监控设计。
## 监控设计
下图可以大概说明我对监控的整体设计理念。
<img src="https://static001.geekbang.org/resource/image/6e/48/6e1b8e6ab4153160791b962177a23b48.jpg" alt="">
我来说明一下:
1. 我们要对整个架构做分层。
1. 在每一个层级上列出要监控的计数器。
1. 寻找相应的监控工具,实现对这些计数器的监控。如果一个工具做不到,在定位过程中考虑补充工具。
1. 要做到对每层都不遗漏。
从大的分类上来看,我们识别出每个监控的节点和层级,再对应到架构中,如下图所示:
<img src="https://static001.geekbang.org/resource/image/76/c2/761c50279950fb2de47f06138b0490c2.jpg" alt="">
最适合的监控方式是什么样的呢?那就是成本最低,监控范围最大,效率最快。而是否持久就不再是考虑的重点了,因为项目结束了,监控工具可能也被拆了。
在企业中,我们也是首先考虑快速的监控实现。但是,还要一点要考虑,就是监控的持久有效性,能一直用下去。所以,在快速实现了之后,在必要时,会做一些二次开发,定制监控。
对了,这里再提一句,我不建议一开始就把代码级的监控给加进来。不光是因为它消耗资源,更重要的是,真的没有太大的必要。像方法的执行时间这类监控,如果没有定位到它们有问题,我们为什么要去看呢?当我们有了证据链的时候,是不是更一针见血呢?
所以最重要的是,我想看到的数据,到底能不能看得到。
对于上述的每个组件,我都建议用下面这样的监控思路。**敲黑板!下图是重点!**
<img src="https://static001.geekbang.org/resource/image/a5/ea/a583b52fc908dbcc239be72cafd6deea.jpg" alt="">
有人可能会想说:就这几个字还值当画个图吗?我觉得非常有必要。因为**全局到定向**的思路帮我解决了很多的问题。
## 全局监控设计
那么什么是全局监控呢?
### OS层CentOS为例
就拿OS来说吧我们一般进到系统中看的就是CPU、I/O、内存、网络的使用率这是很常规的计数器。在很多人看来这些计数器是可以反应出一个系统的全局健康状态的。
先不管通过这些计数器得到的结论是不是对的。我们首先要知道的是,要有这样全局监控的潜意识,之所以说潜意识,是因为很多人不知道为什么看这些,但还是这样看了。
那么实际上做一个OS的全局监控需要看多少个计数器呢我们看下架构图。
<img src="https://static001.geekbang.org/resource/image/8b/08/8b78b6c01b8fe3698c1406612ff4d208.png" alt="">
因为新版内核没有给更细的内核架构图所以我用2.6.26版本的Linux内核架构图来说明思路。
给这张图的目的就是建议先看架构图再考虑要监控的大分类有多少。从上图中我们可以看到有这么几类system、processing、memory、storage、networking等。
这里画出一个思维导图,给出我的经验计数器。
<img src="https://static001.geekbang.org/resource/image/7b/ad/7bbd6e072108d4f694af7b59009c42ad.jpg" alt="">
针对OS我通常看上图中红色计数器的部分这是OS查看的**第一层**。有第一层就有第二层,所以才需要定向的监控。后面我们再说定向监控的思路。
### DB层MySQL为例
我们再说DB层以MySQL为例。和上面的理念一样我们也要看架构图。
<img src="https://static001.geekbang.org/resource/image/e0/7e/e091d798d1031ccedeeff25058ed047e.png" alt="">
此图来自于MySQL官方各大技术网站均有展示。
接着我们看下全局监控的分类,如下图所示:
<img src="https://static001.geekbang.org/resource/image/e7/13/e740f7907d2e882e1b6d7d1d91676613.jpg" alt="">
同样这也是MySQL全局监控的第一层。
这个内容的整理并不具有什么技术性。稍微了解一下Linux和MySQL的架构就可以整理出来。我们依此类推按照这个思路就可以把其他的组件都整理出第一层监控组件。
有了全局监控,接着就是定向监控了。这是寻找**证据链**的关键一节。
## 定向监控
有了OS层的全局监控计数器我们首先要学会的就是判断这些计数器说明了什么问题。我在第三模块中写监控分析工具会详细说明这部分。
这里呢,我先把定向监控细化地解释一下,把这个思路给你讲得明明白白,通通透透。
### OS层之定向监控细化1
当你看到CPU消耗得多那么你就得按照下面这张图细化思路从左向右看
<img src="https://static001.geekbang.org/resource/image/33/06/33745b822203cfdd42c1f929b7b6fe06.jpg" alt="">
列出流程图来就是如下所示:
```
st=&gt;start: 开始
e=&gt;end: 结束
op1=&gt;operation: 通过si CPU找到对应的软中断号及中断设备
op2=&gt;operation: 再找到软中断对应的模块
op3=&gt;operation: 再到模块对应的实现原理
op4=&gt;operation: 给出解决方案
st-&gt;op1-&gt;op2-&gt;op3-&gt;op4-&gt;e
```
### OS层之定向监控细化2
当你看到OS全局监控图中的Network中的Total总流量比较大时就要有这样的分析思路从右向左看
<img src="https://static001.geekbang.org/resource/image/c8/d1/c87e47408fb3b317b28c94eb049d73d1.jpg" alt="">
列出流程图来就是如下所示:
```
st=&gt;start: 开始
e=&gt;end: 结束
op1=&gt;operation: 网络总流量
op2=&gt;operation: 分析性能场景中的业务流量
op3=&gt;operation: 分析网络带宽
op4=&gt;operation: 分析网络队列
op5=&gt;operation: 解决方案
st-&gt;op1-&gt;op2-&gt;op3-&gt;op4-&gt;op5-&gt;e
```
依此类推就可以列出更多OS层的定向监控分析的思路。
### DB层之定向监控细化1
同OS层的定向监控细化思路一样在DB层中要想找到完整的链路那么在MySQL中也必须把逻辑想明白。
当你发现查询和排序的报表有问题时比如说下面这样数据来自于MySQL Report
```
__ SELECT and Sort _____________________________________________________
Scan 7.88M 2.0/s %SELECT: 38.04
Range 237.84k 0.1/s 1.15
Full join 5.97M 1.5/s 28.81
Range check 913.25k 0.2/s 4.41
Full rng join 18.47k 0.0/s 0.09
Sort scan 737.86k 0.2/s
Sort range 56.13k 0.0/s
Sort mrg pass 282.65k 0.1/s
```
居然每秒就能有2次全表扫描那该怎么办呢定向细化如下所示
<img src="https://static001.geekbang.org/resource/image/9d/bb/9d36a7ce7d000006f9d35c2acbd363bb.jpg" alt="">
相信这样常规的动作,你肯定能掌握得了。
那么来看下一个。
### DB层之定向监控细化2
当你看到锁数据的时候,如下所示:
```
__ InnoDB Lock _________________________________________________________
Waits 227829 0.1/s
Current 1
Time acquiring
Total 171855224 ms
Average 754 ms
Max 6143 ms
```
当前等待并不多只有1。但是你看下面的平均时间为754ms这还能不能愉快地玩耍了
下面我也同样列出定向监控细化的思路。
<img src="https://static001.geekbang.org/resource/image/e1/e3/e1ab579bfc31cbf95d78a711add0e7e3.jpg" alt="">
分析产生锁的SQL看SQL的Profiling信息再根据信息找下一步原因最终给出解决方案。
有了上面的**全局—定向**监控思路,并且将每个组件的第一层的计数器一一列出。这是我们监控分析的第一步。
至于定向监控部分,我不建议一开始就列,主要原因有三个:
1. 耗费太多时间;
1. 列出来也可能半辈子也用不上;
1. 照搬列出来的定向监控逻辑,有可能误导你对实时数据的判断。
所以最好的定向监控就是在实际的性能执行过程中,根据实际的场景画出来。这帮助我在工作中无往不利,理清了很多一开始根本想不到的问题。
## 监控工具
有了思路,工具都不是事儿。
针对上面我们画的架构图,我大概列出相应的监控工具及优缺点。这里列得并不详尽,只供借鉴思路使用。
<img src="https://static001.geekbang.org/resource/image/97/2c/974b742e65ac430024718d5774d8702c.jpg" alt="">
如果要选择的话肯定是用Prometheus + Exporter的思路会好一点。于是我们这样实现全局的监控。
OS
<img src="https://static001.geekbang.org/resource/image/18/d2/18ea14f2d179662d01662f81d99ab4d2.png" alt="">
DB
<img src="https://static001.geekbang.org/resource/image/67/74/67e8c7f811895efe028e7d6fed18c774.png" alt="">
Nginx
<img src="https://static001.geekbang.org/resource/image/fa/1c/fa92d16c23e9eb9161f2590fb824ab1c.png" alt="">
Redis
<img src="https://static001.geekbang.org/resource/image/29/cf/299272c8d01d3a68c4c6be025ee1e2cf.png" alt="">
上面图看腻了,你能换个吗?客官别着急,现在就换。
Tomcat
<img src="https://static001.geekbang.org/resource/image/4c/8a/4cac38e9ce84c84597cc072cf994198a.png" alt="">
好了,有了这些监控工具,基本上对每个组件的全局监控就解决了。
这时可能会有人说你这个架构也太不新潮了。现在都玩Kubernetes、Docker、Spring Cloud、微架构啥的了。
那同样,我们还是要列出有哪些监控的组件。
1. Node就是OS。
1. Cluster
1. Pod
1. 微服务链路。
然后再实现相应的全局监控。我们可以在Kubernetes+Docker下可以看到这样的部分全局监控数据。
DashBoard
<img src="https://static001.geekbang.org/resource/image/8a/b3/8ab5823218784fc00b7669ec0d5546b3.png" alt="">
Cluster
<img src="https://static001.geekbang.org/resource/image/3c/40/3c0664fc9b8e42cfea069ba8cdee3140.png" alt="">
Pod
<img src="https://static001.geekbang.org/resource/image/85/c8/858c2e017c33a58c255cf0e5d59d31c8.png" alt="">
微服务链路:
<img src="https://static001.geekbang.org/resource/image/e9/dc/e90a83863cfb415213915f4b697e8edc.png" alt="">
那么具体的定向监控细化呢?在你的具体场景中,照样可以通过上面的思路标识出来。
有人说,那我还有什么什么组件,相信你通看全篇,已经学会思路了,那就自己动手吧。
我想说的是,不管你的架构有多么复杂,组件有多少,这样的监控逻辑都是一定要有的。适合的工具要用,并且尽量多用,但工具还远远替代不了分析的思维逻辑。没有判断的能力,再强悍的工具也只是个花架子。
PS如没有注明引用本专栏所有的截图都是在我搭建的环境中截来的所以不存在在其他地方看到相同图的可能性。
## 总结
在本篇中,我描述了监控设计的思维逻辑。对架构中的组件进行了分析之后,通过**全局—定向**的思路列出要看的计数器,再通过相应的监控工具去实现,拿到要分析的数据。
这就完成了要做的监控设计和具体实施。
至于你是用什么工具去实现的,这并不重要,因为拿到监控数据,可供分析证据链最重要。
## 思考题
看完了今天的文章,你不妨说下为什么要先有全局监控,再有定向监控?以及我为什么不建议一开始就上代码级的监控工具呢?
欢迎你在评论区写下你的思考,也欢迎把这篇文章分享给你的朋友或者同事,一起交流一下。