性能测试是一个入门简单,但是精通难,很依赖实践经验的技术活。如何编写压测脚本只是小术,而如何快速找到问题的原因,压出瓶颈却是大有学问。不过本文先从术入手,先对一个自己临时写的的一个网站进行压测,希望能帮大家更好理解性能测试产品,特别是脚本编写的部分。

开始压测第一件事情绝对不是直接动手就写压测脚本。一个规范的性能测试需要包括需求调研、测试准备、执行压测、生成压测结果并做汇总几个部分。这些步骤都有其存在的意义,保证我们压测不会跑偏,这里针对具体的case我们分析下(注:本文涉及的机器会在本文发布前释放,相关请求地址不再可用,大家就不要压文中的地址了)。

压测之前

需求调研

这一步我们需要先知道自己要压的系统的情况。需要根据实际的项目情况进行需求调研。

项目背景

这是一个很简单的测试系统,功能上涉及的主要是主页浏览、一个登录功能和一个登录后的一个简易下单操作。

项目目标

这次我主要是希望压出这个网站里的首页(静态页面)、登录、下单3个页面能承载的最大TPS,我会使用不同的并发去压,只为了寻找处理能力的上限。如果是实际的场景里,大家很可能是被问的是,xx个用户能不能顶的住。这时候可以通过这里来估算。算出并发数后,根据这些并发数压测后的响应时间、成功率等指标是否达到预期来判断软件是否满足要求。

项目范围

这个网站搭建在我刚购买的机器(公网:120.55.240.49/内网:10.47.121.62)上。上面搭建了个Tomcat,跑了个通过war包打出的简单java web应用。本次压测主要涉及主页(http://120.55.240.49:8080/demo/ )、登录页面(http://120.55.240.49:8080/demo/login.jsp )和购买页面(http://120.55.240.49:8080/demo/buy.jsp )。其中购买页面是需要登录成功后才能下单的,否则会302回登录界面。

软件架构

ECS上安装Tomcat,部署的一个简单Java应用。其中登录需要用账号密码去查询数据库的用户表,目前表里就初始化了一个admin/123作为登录账号。购买页面的下单操作也会往数据库里写一条记录。这里只用了一台ECS,没有使用负载均衡。总体而言,是一个简单的一台ECS+一个RDS的应用。

这次压测没有分生产系统和测试环境。不过在实际场景里,需要注明生产环境和测试的环境的区别,并在压测的过程中加以注意。

当前系统里只有少量几条测试数据,所以数据库查询的话,理论上不会有数据库慢查询(实际上这次也就压测涉及的数据库查询只有登录的时候会查用户表,而用户表目前只有一条记录)。而关于写入,目前没有在表上做索引。实际工作中,不仅需要考虑到系统的当前数据量,还需要顾及未来2-3年的数据量情况,以免以后数据量增加的时候负载跟不上。

硬件准备是否充分。这里可以先评估的是峰值的网络带宽。CPU、内存主要是需要根据压测的结果进行评估,但是带宽可以根据预先估算的TPS乘以每个请求涉及的文件的大小来估算。我这里是压瓶颈,回头看下瓶颈是不是在带宽上。

性能指标

主要涉及网站预期的性能指标,比如TPS、响应时间、成功率、压测过程中的涉及的ECS/RDS的负载。我这里就想看看它能“走多远”,先不设置TPS的指标。但是响应时间,我希望首页、登录、下单的响应时间能在2秒内,请求成功率在99.9%,在压测的过程中ECS的各项指标低于80%,数据库的资源利用率低于60%。如前面提到的,设置指标的时候最好能考虑到未来2-3年的情况,至少要考虑到近期的峰值(比如接下来是否会有大促)的性能要求。

业务描述

涉及主页、登录、下单3个页面。
主页包括1个html1个css1个图片。
登录页面通过post请求提交。如果账号密码错会302回到登录页面。如果是登录成功,会跳到成功页面提示处理成功。为联机操作。
下单页面也一样通过post请求提交当前购买的商品和数量。服务器会判断当前session里的用户信息,如果取不到判断为没登录状态,会302跳到登录界面。下单的逻辑很简单,没有对库存做校验,只是增加一条记录。也是联机操作。
本系统不涉及跑批作业,也不涉及其他外部系统。

业务描述

我也没编出来: ) 不过大家实际使用中,需要注意用户的行为方式,比如服务对象,他们的使用均值、高峰如何,一般都是如何使用系统的。这对于脚本编写逻辑和压测的目标的设置有非常重要的参考意义。

测试准备

测试准备主要包括测试环境的配置、测试内容的梳理和测试策略的设定。

测试环境

本例子没有分测试环境/线上环境,直接就开始压了。真实的压测例子里,需要记录生产环境和测试环境在系统架构图、部署图、硬件配置、软件环境,并分析其差异。这里我就例子里的被压系统做下记录:
系统架构图用的是serverlet+jdbc直接连mysql,没有连接池,或者诸如ssh、Ibatis等常用框架。因为太简单这里就不画图了。
部署情况为一台ECS上安装了tomcat 8,然后直接拷上war包完事。mysql的数据用的是之前调试代码里就创好的表,没有走平时的上线流程之类的。
硬件配置为:
ECS的配置为华东1区域的2核4G I/O优化实例,使用操作系统为CentOS 6.8 64位。公网带宽购买时设置为5Mbps(峰值)。
RDS的配置为1核2G通用型MySQL 5.6。能达到的最大IOPS为1000,最大连接数为600。
软件环境上为Java 8,Tomcat没有调JVM参数,没有调过其他参数。

测试内容

本例子先只涉及单交易负载测试,基准测试、混合场景下的测试先暂时不考虑。需要压测的页面为:

模块

涉及的请求

参数

前提条件

首页

http://120.55.240.49:8080/demo/



登录

http://120.55.240.49:8080/demo/login.jsp

userName=admin&password=123


下单

http://120.55.240.49:8080/demo/buy.jsp

goods=g02&count=1

登录

测试策略

我们会先调试通过后,先用1-5个并发保证压测能跑起来,然后逐渐调整并发用户数,每次调整后停留至少30秒观察服务器的负载和数据库的负载,以及诸如TPS、响应时间的性能指标。在观察到服务器的TPS达到瓶颈或者负载达到上限后停止压测,认为服务的处理能力已经达到。整个压力过程中的并发数是人为根据当时的情况动态调整的。这里不要起来就是几千一万的压力去压,否则一般情况下,除了把服务器压挂掉外别的什么都说明不了。
关于监控模型,我们配置ECS、RDS为监控对象。不过因为性能测试的监控数据有延迟,ECS为1分钟,RDS为5分钟,所以在压测的过程中,会登录到ECS上,使用TOP命令来观察更加实时的ECS负载,并登录到RDS的DMS上使用实时性能功能观察RDS的负载。

首页

首页是一个简单的静态页面,这里主要是展示一下如何使用性能测试产品提供的脚本录制工具的使用方法。

lua脚本 面试提 写脚本测试_lua脚本 面试提

脚本分析

产生的脚本为(第一次建议先只看注释不看代码,就是#之后的)

#! /usr/bin/env python   
# -*- coding: utf-8 -*-
# PTS Script record tool v0.2.6.4
# PTS脚本SDK:框架API、常用HTTP请求/响应处理API
from util import PTS
from HTTPClient import NVPair
from HTTPClient import Cookie
from HTTPClient import HTTPRequest
from HTTPClient import CookieModule
# 脚本初始化段,可以设置压测引擎的常用HTTP属性
#PTS.HttpUtilities.setKeepAlive(False)
#PTS.HttpUtilities.setUrlEncoding('GBK')
#PTS.HttpUtilities.setFollowRedirects(False)
#PTS.HttpUtilities.setUseCookieModule(False)
PTS.HttpUtilities.setUseContentEncoding(True)
PTS.HttpUtilities.setUseTransferEncoding(True)

## 如想通过ECS内网IP进行压测,必须在下方“innerIp”备注行中输入ECS内网IP,如有多个请以英文逗号分隔,例如:127.0.0.1,127.0.0.2
# innerIp:

## 脚本执行单元类,每个VU/压测线程会创建一个TestRunner实例对象
class TestRunner:
    # TestRunner对象的初始化方法,每个线程在创建TestRunner后执行一次该方法
    def __init__(self):
        self.threadContext = PTS.Context.getThreadContext()
        self.init_cookies = CookieModule.listAllCookies(self.threadContext)
    # 主体压测方法,每个线程在测试生命周期内会循环调用该方法
    def __call__(self):
        PTS.Data.delayReports = 1
        for c in self.init_cookies:
            CookieModule.addCookie(c, self.threadContext)
    # 在call里调用事物1的函数
        statusCode = self.action1()
        PTS.Framework.setExtraData(statusCode)                
        PTS.Data.report()
        PTS.Data.delayReports = 0
    # TestRunner销毁方法,每个线程循环执行完成后执行一次该方法
    def __del__(self):
        for c in self.init_cookies:
            CookieModule.addCookie(c, self.threadContext)
    # 定义请求函数

    ## action1
    def action1(self):
        statusCode = [0L, 0L, 0L, 0L]        

        headers = [ NVPair('Accept', '*/*'), NVPair('Upgrade-Insecure-Requests', '1'), NVPair('X-DevTools-Emulate-Network-Conditions-Client-Id', '4c145a4a-8df7-4d40-9906-592a0a1ea620'), NVPair('Accept-Encoding', 'gzip, deflate, sdch'), NVPair('Accept-Language', 'zh-CN,zh;q=0.8'), NVPair('User-Agent', 'PTS-HTTP-CLIENT'), ]
        result = HTTPRequest().GET('http://120.55.240.49:8080/demo/', None, headers)
        PTS.Framework.addHttpCode(result.getStatusCode(), statusCode)

        headers = [ NVPair('Accept', '*/*'), NVPair('X-DevTools-Emulate-Network-Conditions-Client-Id', '4c145a4a-8df7-4d40-9906-592a0a1ea620'), NVPair('Referer', 'http://120.55.240.49:8080/demo/'), NVPair('Accept-Encoding', 'gzip, deflate, sdch'), NVPair('Accept-Language', 'zh-CN,zh;q=0.8'), NVPair('User-Agent', 'PTS-HTTP-CLIENT'), ]
        result = HTTPRequest().GET('http://120.55.240.49:8080/demo/css/demo.css', None, headers)
        PTS.Framework.addHttpCode(result.getStatusCode(), statusCode)

        headers = [ NVPair('Accept', '*/*'), NVPair('X-DevTools-Emulate-Network-Conditions-Client-Id', '4c145a4a-8df7-4d40-9906-592a0a1ea620'), NVPair('Referer', 'http://120.55.240.49:8080/demo/'), NVPair('Accept-Encoding', 'gzip, deflate, sdch'), NVPair('Accept-Language', 'zh-CN,zh;q=0.8'), NVPair('User-Agent', 'PTS-HTTP-CLIENT'), ]
        result = HTTPRequest().GET('http://120.55.240.49:8080/demo/hello-world.png', None, headers)
        PTS.Framework.addHttpCode(result.getStatusCode(), statusCode)

        ## statusCode[0]代表http code < 300 个数,    statusCode[1] 代表 300<=http code<400 个数
        # statusCode[2]代表400<=http code<500个数,  statusCode[3] 代表 http code >=500个数
        # 如果http code 300 到 400 之间是正常的
        # 那么判断事务失败,请将statusCode[1:3] 改为   statusCode[2:3] 即可
        if(sum(statusCode[1:3]) > 0):
            PTS.Data.forCurrentTest.success = False
            PTS.Logger.error(u'事务请求中http 返回状态大于300,请检查请求是否正确!')

        return statusCode

# 调用施压引擎施压。第一个参数是事务名,可以为中文;第二个参数是执行事务方法的方法名;第三个统一写TestRunner
PTS.Framework.instrumentMethod(u'action1', 'action1', TestRunner)

可以看到函数里需要注意的是def __init__(self)做初始化,这里暂时不涉及,后面会提到。初始化后压测服务会多次调用def __call__(self)。最后调用一次__del__(self)收尾。

脚本调试

在保存按钮边上有个调试按钮,点击后可以看到调试的结果。

lua脚本 面试提 写脚本测试_java_02

lua脚本 面试提 写脚本测试_数据库_03


在脚本调试的过程中,每个请求的内容,响应内容一目了然。执行日志里还有提供压测的过程中的日志。如果中间有自己打印了一些日志,也可以在这里看到。关于日志打印的功能后面也会实践里提到。

压测过程

保存了脚本后,去创建一个压测场景:

lua脚本 面试提 写脚本测试_压测_04


把这个场景运行起来。看到并发很低,从性能测试产品上可以看到性能参数图:

lua脚本 面试提 写脚本测试_数据库_05


lua脚本 面试提 写脚本测试_运维_06


lua脚本 面试提 写脚本测试_java_07


lua脚本 面试提 写脚本测试_运维_08


同时对比一下ECS的负载指标:

lua脚本 面试提 写脚本测试_压测_09


lua脚本 面试提 写脚本测试_java_10


lua脚本 面试提 写脚本测试_java_11


看到ECS的CPU根本没用掉。通过TOP命令看到的CPU、内存的使用情况也是如此。同时我还用iftop -i eth1看了下公网网卡的流量情况,和监控上看到的一样,公网带宽被打满了

lua脚本 面试提 写脚本测试_java_12

结果总结

从压测结果可以看到,瓶颈在公网带宽上。因为ECS购买的公网带宽比较小,而首页的静态文件比较大(图片比较大),可以考虑在够用的情况下减少图片的分辨率减少图片的大小。另外可以做到动静分离,一些静态文件就放到对象存储OSS上面,再配合CDN就完美了。压测的时候也就不需要在压测这些已经放在OSS/CDN的文件。

登录功能

同样的登录功能也是脚本录制出来的。这里就不重复说明。因为后面的登录后下单的这个例子包括登录的所有功能点,这里登录就先跳过。

下单功能

下单是本次测试的最复杂的一个模块。首先,下单前需要登录,但是我们这次只是为了测试下单的工单,所有所有的请求,我们希望只登录一次(实际上如果是用了多台施压机,脚本里写一次登录,实际上是每台机器一次,一共登录会被执行多次)。根据前面讲的脚本的组成逻辑,我们需要把登录写在def __init__(self)里。除了登录,我们还希望测试每次下单购买的是不同的商品和数量,这时候需要用到参数文件。另外因为我们这次是希望压测下单的过程中ECS和数据库的压力,对于之前的瓶颈公网带宽,我们假设已经通过动静分离解决了,所以在这里压测脚本里我们不涉及静态资源,走内网压测。还有我们希望在这个例子里对脚本代码做一次调试,所以需要做一些日志打印。

脚本录制

登录功能可以在录制的时候直接录制好:

先双击上面的初始化,然后录制登录功能。

lua脚本 面试提 写脚本测试_lua脚本 面试提_13


登录录制好了后,双击事物然后开始录制下单页面。

lua脚本 面试提 写脚本测试_数据库_14


最后得到的压测脚本如下:

#! /usr/bin/env python   
# -*- coding: utf-8 -*-
# PTS Script Version 1.0
# PTS脚本SDK:框架API、常用HTTP请求/响应处理API
from util import PTS
from HTTPClient import NVPair
from HTTPClient import Cookie
from HTTPClient import HTTPRequest
from HTTPClient import CookieModule
# 脚本初始化段,可以设置压测引擎的常用HTTP属性
#PTS.HttpUtilities.setKeepAlive(False)
#PTS.HttpUtilities.setUrlEncoding('GBK')
#PTS.HttpUtilities.setFollowRedirects(False)
#PTS.HttpUtilities.setUseCookieModule(False)
PTS.HttpUtilities.setUseContentEncoding(True)
PTS.HttpUtilities.setUseTransferEncoding(True)
# 脚本执行单元类,每个VU/压测线程会创建一个TestRunner实例对象
class TestRunner:
    # TestRunner对象的初始化方法,每个线程在创建TestRunner后执行一次该方法
    def __init__(self):
        self.threadContext = PTS.Context.getThreadContext()
        self.init1()
        self.init_cookies = CookieModule.listAllCookies(self.threadContext)
    # 主体压测方法,每个线程在测试生命周期内会循环调用该方法
    def __call__(self):
        PTS.Data.delayReports = 1
        for c in self.init_cookies:
            CookieModule.addCookie(c, self.threadContext)
        statusCode = self.action1()
        PTS.Framework.setExtraData(statusCode)                
        PTS.Data.report()
        PTS.Data.delayReports = 0
    # TestRunner销毁方法,每个线程循环执行完成后执行一次该方法
    def __del__(self):
        for c in self.init_cookies:
            CookieModule.addCookie(c, self.threadContext)
    # 定义请求函数

    ## init1
    def init1(self):

        headers = [ NVPair('Accept', '*/*'), NVPair('Content-Type', 'application/x-www-form-urlencoded'), NVPair('Content-Length', '27'), NVPair('Host', '120.55.240.49:8080'), NVPair('Accept-Language', 'zh-cn,zh;q=0.8,en-us;q=0.5,en;q=0.3'), NVPair('Accept-Encoding', 'gzip, deflate'), NVPair('Referer', 'http://120.55.240.49:8080/demo/login.jsp'), NVPair('Connection', 'keep-alive'), NVPair('User-Agent', 'PTS-HTTP-CLIENT'), ]
        result = HTTPRequest().POST('http://120.55.240.49:8080/demo/Login.do', '''userName=admin&password=123''', headers)        

        headers = [ NVPair('Accept', '*/*'), NVPair('Host', '120.55.240.49:8080'), NVPair('Accept-Language', 'zh-cn,zh;q=0.8,en-us;q=0.5,en;q=0.3'), NVPair('Accept-Encoding', 'gzip, deflate'), NVPair('Referer', 'http://120.55.240.49:8080/demo/login.jsp'), NVPair('Connection', 'keep-alive'), NVPair('User-Agent', 'PTS-HTTP-CLIENT'), ]
        result = HTTPRequest().GET('http://120.55.240.49:8080/demo/afterLogin.jsp', None, headers)                

    ## action1
    def action1(self):
        statusCode = [0L, 0L, 0L, 0L]        

        headers = [ NVPair('Accept', '*/*'), NVPair('Content-Type', 'application/x-www-form-urlencoded'), NVPair('Content-Length', '17'), NVPair('Host', '120.55.240.49:8080'), NVPair('Accept-Language', 'zh-cn,zh;q=0.8,en-us;q=0.5,en;q=0.3'), NVPair('Accept-Encoding', 'gzip, deflate'), NVPair('Referer', 'http://120.55.240.49:8080/demo/buy.jsp'), NVPair('Connection', 'keep-alive'), NVPair('User-Agent', 'PTS-HTTP-CLIENT'), ]
        result = HTTPRequest().POST('http://120.55.240.49:8080/demo/Buy.do', '''goods=g02&count=3''', headers)
        PTS.Framework.addHttpCode(result.getStatusCode(), statusCode)

        ## statusCode[0]代表http code < 300 个数,    statusCode[1] 代表 300<=http code<400 个数
        # statusCode[2]代表400<=http code<500个数,  statusCode[3] 代表 http code >=500个数
        # 如果http code 300 到 400 之间是正常的
        # 那么判断事务失败,请将statusCode[1:3] 改为   statusCode[2:3] 即可
        if(sum(statusCode[1:3]) > 0):
            PTS.Data.forCurrentTest.success = False
            PTS.Logger.error(u'事务请求中http 返回状态大于300,请检查请求是否正确!')

        return statusCode

# 编织压测事务
PTS.Framework.instrumentMethod(u'action1', 'action1', TestRunner)

分析下这个脚本,特别注意__init__(self)里调用了self.init1(),做了登录,然后做了cookies的设置。其他地方同前一个脚本基本一样。

调试一下,结果还可以。

lua脚本 面试提 写脚本测试_压测_15

检查点

检查点用于检查请求的返回内容是否符合预期。有的时候,我们针对失败不会直接报3xx甚至是4xx或者5xx,而是返回200,但是响应的内容里提示报错信息。只有返回的内容是200而且内容是success!我们才认为这个下单操作是成功的。于是我们修改action1,增加检查点的功能,修改为

## action1
    def action1(self):
        statusCode = [0L, 0L, 0L, 0L]        

        headers = [ NVPair('Accept', '*/*'), NVPair('Content-Type', 'application/x-www-form-urlencoded'), NVPair('Content-Length', '17'), NVPair('Host', '120.55.240.49:8080'), NVPair('Accept-Language', 'zh-cn,zh;q=0.8,en-us;q=0.5,en;q=0.3'), NVPair('Accept-Encoding', 'gzip, deflate'), NVPair('Referer', 'http://120.55.240.49:8080/demo/buy.jsp'), NVPair('Connection', 'keep-alive'), NVPair('User-Agent', 'PTS-HTTP-CLIENT'), ]
        result = HTTPRequest().POST('http://120.55.240.49:8080/demo/Buy.do', '''goods=g02&count=3''', headers)
        PTS.Framework.addHttpCode(result.getStatusCode(), statusCode)

        ## statusCode[0]代表http code < 300 个数,    statusCode[1] 代表 300<=http code<400 个数
        # statusCode[2]代表400<=http code<500个数,  statusCode[3] 代表 http code >=500个数
        # 如果http code 300 到 400 之间是正常的
        # 那么判断事务失败,请将statusCode[1:3] 改为   statusCode[2:3] 即可

        if(sum(statusCode[1:3]) > 0):
            PTS.Data.forCurrentTest.success = False
            PTS.Logger.error(u'事务请求中http 返回状态大于300,请检查请求是否正确!')
        checkPoint = u'success!'    
        if(not PTS.HttpUtilities.checkResponse(200, checkPoint)):
            PTS.Logger.error(u'检查点:"' + checkPoint + u'"校验失败')
            PTS.Data.forCurrentTest.success = False
        return statusCode

内网压测

我们修改脚本界面,把所有的公网ip换成内网ip。以前版本的PTS还是需要设置#innerIp:,但是现在看来不设置也是没有问题了。
另外在压测脚本上,需要把压测模式选择为内网压测。

参数文件

如果我们先用模板模式写一个简单的带参数的压测脚本,然后切换到脚本模式,可以发现和没有参数化比,主要改了
1 参数化相关引用

from com.aliyun.pts import DsvReader
from com.aliyun.pts import ParamManager

params = ParamManager.getInstance()
params.addProvider(DsvReader(u"xx.csv"))

2 __call__(self)里调用 params.nextRecord(u'xx.csv')使得参数文件进入下一行
3 用params.getParamValue(u'xx.csv:uid')等对参数进行替换

我们写了个order.csv文件,内容如下:

lua脚本 面试提 写脚本测试_lua脚本 面试提_16

然后针对前面提到的3处修改点,修改了我们的脚本,这样每次请求都会到参数文件里获取不同的参数来发请求l。再把参数文件上传上来,最后调试截图:

lua脚本 面试提 写脚本测试_数据库_17


可以看到

日志打印

为了验证日志打印功能,我们可以在脚本里打印一些日志。最后算上前面提到的参数等功能,最后的完整脚本为:

#! /usr/bin/env python   
# -*- coding: utf-8 -*-
# PTS Script Version 1.0
# PTS脚本SDK:框架API、常用HTTP请求/响应处理API
from util import PTS
from HTTPClient import NVPair
from HTTPClient import Cookie
from HTTPClient import HTTPRequest
from HTTPClient import CookieModule

from com.aliyun.pts import DsvReader
from com.aliyun.pts import ParamManager

params = ParamManager.getInstance()
params.addProvider(DsvReader(u"order.csv"))
# 脚本初始化段,可以设置压测引擎的常用HTTP属性
#PTS.HttpUtilities.setKeepAlive(False)
#PTS.HttpUtilities.setUrlEncoding('GBK')
#PTS.HttpUtilities.setFollowRedirects(False)
#PTS.HttpUtilities.setUseCookieModule(False)
PTS.HttpUtilities.setUseContentEncoding(True)
PTS.HttpUtilities.setUseTransferEncoding(True)
# 脚本执行单元类,每个VU/压测线程会创建一个TestRunner实例对象
class TestRunner:
    # TestRunner对象的初始化方法,每个线程在创建TestRunner后执行一次该方法
    def __init__(self):
        self.threadContext = PTS.Context.getThreadContext()
        self.init1()
        self.init_cookies = CookieModule.listAllCookies(self.threadContext)
    # 主体压测方法,每个线程在测试生命周期内会循环调用该方法
    def __call__(self):
        PTS.Data.delayReports = 1
        params.nextRecord(u'order.csv')
        for c in self.init_cookies:
            CookieModule.addCookie(c, self.threadContext)
        statusCode = self.action1()
        PTS.Framework.setExtraData(statusCode)                
        PTS.Data.report()
        PTS.Data.delayReports = 0
    # TestRunner销毁方法,每个线程循环执行完成后执行一次该方法
    def __del__(self):
        for c in self.init_cookies:
            CookieModule.addCookie(c, self.threadContext)
    # 定义请求函数

    ## init1
    def init1(self):

        headers = [ NVPair('Accept', '*/*'), NVPair('Content-Type', 'application/x-www-form-urlencoded'), NVPair('Content-Length', '27'), NVPair('Host', '10.47.121.62:8080'), NVPair('Accept-Language', 'zh-cn,zh;q=0.8,en-us;q=0.5,en;q=0.3'), NVPair('Accept-Encoding', 'gzip, deflate'), NVPair('Referer', 'http://10.47.121.62:8080/demo/login.jsp'), NVPair('Connection', 'keep-alive'), NVPair('User-Agent', 'PTS-HTTP-CLIENT'), ]
        result = HTTPRequest().POST('http://10.47.121.62:8080/demo/Login.do', '''userName=admin&password=123''', headers)        

        headers = [ NVPair('Accept', '*/*'), NVPair('Host', '10.47.121.62:8080'), NVPair('Accept-Language', 'zh-cn,zh;q=0.8,en-us;q=0.5,en;q=0.3'), NVPair('Accept-Encoding', 'gzip, deflate'), NVPair('Referer', 'http://10.47.121.62:8080/demo/login.jsp'), NVPair('Connection', 'keep-alive'), NVPair('User-Agent', 'PTS-HTTP-CLIENT'), ]
        result = HTTPRequest().GET('http://10.47.121.62:8080/demo/afterLogin.jsp', None, headers)                

    ## action1
    def action1(self):
        statusCode = [0L, 0L, 0L, 0L]        

        headers = [ NVPair('Accept', '*/*'), NVPair('Content-Type', 'application/x-www-form-urlencoded'), NVPair('Content-Length', '17'), NVPair('Host', '10.47.121.62:8080'), NVPair('Accept-Language', 'zh-cn,zh;q=0.8,en-us;q=0.5,en;q=0.3'), NVPair('Accept-Encoding', 'gzip, deflate'), NVPair('Referer', 'http://10.47.121.62:8080/demo/buy.jsp'), NVPair('Connection', 'keep-alive'), NVPair('User-Agent', 'PTS-HTTP-CLIENT'), ]
        result = HTTPRequest().POST('http://10.47.121.62:8080/demo/Buy.do', 'goods=' + params.getParamValue(u'order.csv:goods') + u'&count=' + params.getParamValue(u'order.csv:count') + u'', headers)
        PTS.Framework.addHttpCode(result.getStatusCode(), statusCode)

        ## statusCode[0]代表http code < 300 个数,    statusCode[1] 代表 300<=http code<400 个数
        # statusCode[2]代表400<=http code<500个数,  statusCode[3] 代表 http code >=500个数
        # 如果http code 300 到 400 之间是正常的
        # 那么判断事务失败,请将statusCode[1:3] 改为   statusCode[2:3] 即可
        # 打印WARN级别的日志
        PTS.Logger.warn(str(result.getText()))
        if(sum(statusCode[1:3]) > 0):
            PTS.Data.forCurrentTest.success = False
            PTS.Logger.error(u'事务请求中http 返回状态大于300,请检查请求是否正确!')
        checkPoint = u'success!'    
        if(not PTS.HttpUtilities.checkResponse(200, checkPoint)):
            PTS.Logger.error(u'检查点:"' + checkPoint + u'"校验失败')
            PTS.Data.forCurrentTest.success = False
        return statusCode    
  

# 编织压测事务
PTS.Framework.instrumentMethod(u'action1', 'action1', TestRunner)

大家可以对比一下这个版本和上个版本的区别,就可以很清楚的知道这两个功能的用法。

lua脚本 面试提 写脚本测试_压测_18

压测过程

还是和以前一样的压,并发先创建一个场景,这次设置施压机为3台,这样可以3台3台地增加并发数。然后从3台开始,再改成30,再到300,最后到600停住,看到延迟已经超过预期了。需要注意的是,一开始是因为响应时间超过预期才停止增加并发数的,但网站不能服务是600并发保持了约四分钟后发生的。

先截个压测场景的图,因为是内网压测,注意施压机所在集群和ECS要一样。

lua脚本 面试提 写脚本测试_压测_19


然后看业务指标。首先TPS基本没变化。到后来服务运行了一段时间后挂了,tps就掉到0了。

lua脚本 面试提 写脚本测试_压测_20


随着并发的增加,响应时间增加了。但是因为后来服务不可用后响应时间变得太长了导致前面的响应增加看不出来了。

lua脚本 面试提 写脚本测试_数据库_21


我用另外一次在出现问题就停手的压测记录截图来看大家会清楚一些,这里的响应时间的几个波动分别是3->30->300->600带来的。

lua脚本 面试提 写脚本测试_运维_22


并发是是3->30->300->600

lua脚本 面试提 写脚本测试_数据库_23


接下来是ECS的负载

lua脚本 面试提 写脚本测试_运维_24


lua脚本 面试提 写脚本测试_数据库_25


lua脚本 面试提 写脚本测试_压测_26


然后是RDS的

lua脚本 面试提 写脚本测试_数据库_27


lua脚本 面试提 写脚本测试_压测_28


lua脚本 面试提 写脚本测试_数据库_29


lua脚本 面试提 写脚本测试_运维_30


lua脚本 面试提 写脚本测试_lua脚本 面试提_31

因为RDS的监控是5分钟1次,我压测的时间不长截出来图并不好看。不过可以看到,数据库的负载不高

结果总结

这次是比较典型的把服务器压挂了的例子,我们登录到服务器上,用TOP命令看到CPU已经满了。

lua脚本 面试提 写脚本测试_lua脚本 面试提_32


把11941这个PID用jstack 11941 > ~/11941.dump抓个现场,打开看下。里面茫茫多的

lua脚本 面试提 写脚本测试_数据库_33


虽然从Mysql上不论是监控还是实例诊断报告上看都很正常,但是从这里可以判断是连接数据库写入订单数据的步骤出现问题。出现问题的是通过Buy里使用synchronized申请com.mysql.jdbc.JDBC4Connection对象锁未成功。到这里再联系前面的数据库连接数一共就2个(其中有1个还是DMS用掉的,其实只用掉了一个),终于枉然大悟:之前做demo的时候图方便,数据库的连接是使用单实例模式做的,全部的请求用的是同一个数据库连接。回头可以考虑用数据库连接池配一个应该可以提高应用的性能。