閱讀683 返回首頁    go 阿裏雲 go 技術社區[雲棲]


性能測試腳本的編寫和調試【雲享團】

_
性能測試是一個入門簡單,但是精通難,很依賴實踐經驗的技術活。如何編寫壓測腳本隻是小術,而如何快速找到問題的原因,壓出瓶頸卻是大有學問。不過本文先從術入手,先對一個自己臨時寫的的一個網站進行壓測,希望能幫大家更好理解性能測試產品,特別是腳本編寫的部分。
開始壓測第一件事情絕對不是直接動手就寫壓測腳本。一個規範的性能測試需要包括需求調研、測試準備、執行壓測、生成壓測結果並做匯總幾個部分。這些步驟都有其存在的意義,保證我們壓測不會跑偏,這裏針對具體的case我們分析下(注:本文涉及的機器會在本文發布前釋放,相關請求地址不再可用,大家就不要壓文中的地址了)。

壓測之前

需求調研

這一步我們需要先知道自己要壓的係統的情況。需要根據實際的項目情況進行需求調研。

項目背景

這是一個很簡單的測試係統,功能上涉及的主要是主頁瀏覽、一個登錄功能和一個登錄後的一個簡易下單操作。

項目目標

這次我主要是希望壓出這個網站裏的首頁(靜態頁麵)、登錄、下單3個頁麵能承載的最大TPS,我會使用不同的並發去壓,隻為了尋找處理能力的上限。如果是實際的場景裏,大家很可能是被問的是,xx個用戶能不能頂的住。這時候可以通過這裏來估算。算出並發數後,根據這些並發數壓測後的響應時間、成功率等指標是否達到預期來判斷軟件是否滿足要求。

項目範圍

這個網站搭建在我剛購買的機器(公網:120.55.240.49/內網:10.47.121.62)上。上麵搭建了個Tomcat,跑了個通過war包打出的簡單java web應用。本次壓測主要涉及主頁(https://120.55.240.49:8080/demo/ )、登錄頁麵(https://120.55.240.49:8080/demo/login.jsp )和購買頁麵(https://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參數,沒有調過其他參數。

測試內容

本例子先隻涉及單交易負載測試,基準測試、混合場景下的測試先暫時不考慮。需要壓測的頁麵為:

模塊 涉及的請求 參數 前提條件
首頁 https://120.55.240.49:8080/demo/
登錄 https://120.55.240.49:8080/demo/login.jsp userName=admin&password=123
下單 https://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的負載。

首頁

首頁是一個簡單的靜態頁麵,這裏主要是展示一下如何使用性能測試產品提供的腳本錄製工具的使用方法。
a1

腳本分析

產生的腳本為(第一次建議先隻看注釋不看代碼,就是#之後的)

#! /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('https://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', 'https://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('https://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', 'https://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('https://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)收尾。

腳本調試

在保存按鈕邊上有個調試按鈕,點擊後可以看到調試的結果。

a1

a2
在腳本調試的過程中,每個請求的內容,響應內容一目了然。執行日誌裏還有提供壓測的過程中的日誌。如果中間有自己打印了一些日誌,也可以在這裏看到。關於日誌打印的功能後麵也會實踐裏提到。

壓測過程

保存了腳本後,去創建一個壓測場景:

a1
把這個場景運行起來。看到並發很低,從性能測試產品上可以看到性能參數圖:
b2
b3
b4
b5
同時對比一下ECS的負載指標:
c1
c2
c3
看到ECS的CPU根本沒用掉。通過TOP命令看到的CPU、內存的使用情況也是如此。同時我還用iftop -i eth1看了下公網網卡的流量情況,和監控上看到的一樣,公網帶寬被打滿了
aa

結果總結

從壓測結果可以看到,瓶頸在公網帶寬上。因為ECS購買的公網帶寬比較小,而首頁的靜態文件比較大(圖片比較大),可以考慮在夠用的情況下減少圖片的分辨率減少圖片的大小。另外可以做到動靜分離,一些靜態文件就放到對象存儲OSS上麵,再配合CDN就完美了。壓測的時候也就不需要在壓測這些已經放在OSS/CDN的文件。

登錄功能

同樣的登錄功能也是腳本錄製出來的。這裏就不重複說明。因為後麵的登錄後下單的這個例子包括登錄的所有功能點,這裏登錄就先跳過。

下單功能

下單是本次測試的最複雜的一個模塊。首先,下單前需要登錄,但是我們這次隻是為了測試下單的工單,所有所有的請求,我們希望隻登錄一次(實際上如果是用了多台施壓機,腳本裏寫一次登錄,實際上是每台機器一次,一共登錄會被執行多次)。根據前麵講的腳本的組成邏輯,我們需要把登錄寫在def __init__(self)裏。除了登錄,我們還希望測試每次下單購買的是不同的商品和數量,這時候需要用到參數文件。另外因為我們這次是希望壓測下單的過程中ECS和數據庫的壓力,對於之前的瓶頸公網帶寬,我們假設已經通過動靜分離解決了,所以在這裏壓測腳本裏我們不涉及靜態資源,走內網壓測。還有我們希望在這個例子裏對腳本代碼做一次調試,所以需要做一些日誌打印。

腳本錄製

登錄功能可以在錄製的時候直接錄製好:
先雙擊上麵的初始化,然後錄製登錄功能。
b1
登錄錄製好了後,雙擊事物然後開始錄製下單頁麵。
b2
最後得到的壓測腳本如下:

#! /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', 'https://120.55.240.49:8080/demo/login.jsp'), NVPair('Connection', 'keep-alive'), NVPair('User-Agent', 'PTS-HTTP-CLIENT'), ]
        result = HTTPRequest().POST('https://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', 'https://120.55.240.49:8080/demo/login.jsp'), NVPair('Connection', 'keep-alive'), NVPair('User-Agent', 'PTS-HTTP-CLIENT'), ]
        result = HTTPRequest().GET('https://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', 'https://120.55.240.49:8080/demo/buy.jsp'), NVPair('Connection', 'keep-alive'), NVPair('User-Agent', 'PTS-HTTP-CLIENT'), ]
        result = HTTPRequest().POST('https://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的設置。其他地方同前一個腳本基本一樣。

調試一下,結果還可以。
e1

檢查點

檢查點用於檢查請求的返回內容是否符合預期。有的時候,我們針對失敗不會直接報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', 'https://120.55.240.49:8080/demo/buy.jsp'), NVPair('Connection', 'keep-alive'), NVPair('User-Agent', 'PTS-HTTP-CLIENT'), ]
        result = HTTPRequest().POST('https://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文件,內容如下:
order

然後針對前麵提到的3處修改點,修改了我們的腳本,這樣每次請求都會到參數文件裏獲取不同的參數來發請求l。再把參數文件上傳上來,最後調試截圖:
order2
可以看到

日誌打印

為了驗證日誌打印功能,我們可以在腳本裏打印一些日誌。最後算上前麵提到的參數等功能,最後的完整腳本為:

#! /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', 'https://10.47.121.62:8080/demo/login.jsp'), NVPair('Connection', 'keep-alive'), NVPair('User-Agent', 'PTS-HTTP-CLIENT'), ]
        result = HTTPRequest().POST('https://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', 'https://10.47.121.62:8080/demo/login.jsp'), NVPair('Connection', 'keep-alive'), NVPair('User-Agent', 'PTS-HTTP-CLIENT'), ]
        result = HTTPRequest().GET('https://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', 'https://10.47.121.62:8080/demo/buy.jsp'), NVPair('Connection', 'keep-alive'), NVPair('User-Agent', 'PTS-HTTP-CLIENT'), ]
        result = HTTPRequest().POST('https://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)

大家可以對比一下這個版本和上個版本的區別,就可以很清楚的知道這兩個功能的用法。
order3

壓測過程

還是和以前一樣的壓,並發先創建一個場景,這次設置施壓機為3台,這樣可以3台3台地增加並發數。然後從3台開始,再改成30,再到300,最後到600停住,看到延遲已經超過預期了。需要注意的是,一開始是因為響應時間超過預期才停止增加並發數的,但網站不能服務是600並發保持了約四分鍾後發生的。
先截個壓測場景的圖,因為是內網壓測,注意施壓機所在集群和ECS要一樣。
o1
然後看業務指標。首先TPS基本沒變化。到後來服務運行了一段時間後掛了,tps就掉到0了。
o1
隨著並發的增加,響應時間增加了。但是因為後來服務不可用後響應時間變得太長了導致前麵的響應增加看不出來了。
o2
我用另外一次在出現問題就停手的壓測記錄截圖來看大家會清楚一些,這裏的響應時間的幾個波動分別是3->30->300->600帶來的。
o3
並發是是3->30->300->600
o3
接下來是ECS的負載
p1
p2
p3
然後是RDS的
q5
q1
q2
q3
q4

因為RDS的監控是5分鍾1次,我壓測的時間不長截出來圖並不好看。不過可以看到,數據庫的負載不高

結果總結

這次是比較典型的把服務器壓掛了的例子,我們登錄到服務器上,用TOP命令看到CPU已經滿了。
s1
把11941這個PID用jstack 11941 > ~/11941.dump抓個現場,打開看下。裏麵茫茫多的
s2
雖然從Mysql上不論是監控還是實例診斷報告上看都很正常,但是從這裏可以判斷是連接數據庫寫入訂單數據的步驟出現問題。出現問題的是通過Buy裏使用synchronized申請com.mysql.jdbc.JDBC4Connection對象鎖未成功。到這裏再聯係前麵的數據庫連接數一共就2個(其中有1個還是DMS用掉的,其實隻用掉了一個),終於枉然大悟:之前做demo的時候圖方便,數據庫的連接是使用單實例模式做的,全部的請求用的是同一個數據庫連接。回頭可以考慮用數據庫連接池配一個應該可以提高應用的性能。

最後更新:2017-04-01 17:13:51

  上一篇:go 分片集群Mongos到Shard請求管理
  下一篇:go PostgreSQL 10.0 preview 功能增強 - 動態視圖pg_stat_activity新增數據庫管理進程信息