性能測試腳本的編寫和調試
性能測試腳本的編寫和調試
摘要: 性能測試是一個入門簡單,但是精通難,很依賴實踐經驗的技術活。如何編寫壓測腳本只是小術,而如何快速找到問題的原因,壓出瓶頸卻是大有學問。這次,雲享團的專家從“術”入手,對一個自己臨時寫的的一個網站進行壓測,希望能幫大家更好理解性能測試產品,特別是腳本編寫的部分。
性能測試是一個入門簡單,但是精通難,很依賴實踐經驗的技術活。如何編寫壓測腳本只是小術,而如何快速找到問題的原因,壓出瓶頸卻是大有學問。不過本文先從術入手,先對一個自己臨時寫的的一個網站進行壓測,希望能幫大家更好理解性能測試產品,特別是腳本編寫的部分。
開始壓測第一件事情絕對不是直接動手就寫壓測腳本。一個規范的性能測試需要包括需求調研、測試准備、執行壓測、生成壓測結果並做匯總幾個部分。這些步驟都有其存在的意義,保證我們壓測不會跑偏,這里針對具體的case我們分析下(注:本文涉及的機器會在本文發布前釋放,相關請求地址不再可用,大家就不要壓文中的地址了)。
壓測之前
需求調研
這一步我們需要先知道自己要壓的系統的情況。需要根據實際的項目情況進行需求調研。
項目背景
這是一個很簡單的測試系統,功能上涉及的主要是主頁瀏覽、一個登錄功能和一個登錄后的一個簡易下單操作。
項目目標
這次我主要是希望壓出這個網站里的首頁(靜態頁面)、登錄、下單3個頁面能承載的最大TPS,我會使用不同的並發去壓,只為了尋找處理能力的上限。如果是實際的場景里,大家很可能是被問的是,xx個用戶能不能頂的住。這時候可以通過來估算。算出並發數后,根據這些並發數壓測后的響應時間、成功率等指標是否達到預期來判斷軟件是否滿足要求。
軟件架構
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參數,沒有調過其他參數。
測試內容
本例子先只涉及單交易負載測試,基准測試、混合場景下的測試先暫時不考慮。需要壓測的頁面為:
測試策略
我們會先調試通過后,先用1-5個並發保證壓測能跑起來,然后逐漸調整並發用戶數,每次調整后停留至少30秒觀察服務器的負載和數據庫的負載,以及諸如TPS、響應時間的性能指標。在觀察到服務器的TPS達到瓶頸或者負載達到上限后停止壓測,認為服務的處理能力已經達到。整個壓力過程中的並發數是人為根據當時的情況動態調整的。這里不要起來就是幾千一萬的壓力去壓,否則一般情況下,除了把服務器壓掛掉外別的什么都說明不了。
關於監控模型,我們配置ECS、RDS為監控對象。不過因為性能測試的監控數據有延遲,ECS為1分鍾,RDS為5分鍾,所以在壓測的過程中,會登錄到ECS上,使用TOP命令來觀察更加實時的ECS負載,並登錄到RDS的DMS上使用實時性能功能觀察RDS的負載。
首頁
首頁是一個簡單的靜態頁面,這里主要是展示一下如何使用性能測試產品提供的腳本錄制工具的使用方法。
腳本分析
產生的腳本為(第一次建議先只看注釋不看代碼,就是#之后的)
-
-
-
-
-
from util import PTS
-
from HTTPClient import NVPair
-
from HTTPClient import Cookie
-
from HTTPClient import HTTPRequest
-
from HTTPClient import CookieModule
-
-
-
-
-
-
PTS.HttpUtilities.setUseContentEncoding(True)
-
PTS.HttpUtilities.setUseTransferEncoding(True)
-
-
-
-
-
-
-
# 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)
收尾。
腳本調試
在保存按鈕邊上有個調試按鈕,點擊后可以看到調試的結果。
在腳本調試的過程中,每個請求的內容,響應內容一目了然。執行日志里還有提供壓測的過程中的日志。如果中間有自己打印了一些日志,也可以在這里看到。關於日志打印的功能后面也會實踐里提到。
壓測過程
保存了腳本后,去創建一個壓測場景:
把這個場景運行起來。看到並發很低,從性能測試產品上可以看到性能參數圖:
同時對比一下ECS的負載指標:
看到ECS的CPU根本沒用掉。通過TOP命令看到的CPU、內存的使用情況也是如此。同時我還用iftop -i eth1
看了下公網網卡的流量情況,和監控上看到的一樣,公網帶寬被打滿了
結果總結
從壓測結果可以看到,瓶頸在公網帶寬上。因為ECS購買的公網帶寬比較小,而首頁的靜態文件比較大(圖片比較大),可以考慮在夠用的情況下減少圖片的分辨率減少圖片的大小。另外可以做到動靜分離,一些靜態文件就放到對象存儲OSS上面,再配合CDN就完美了。壓測的時候也就不需要在壓測這些已經放在OSS/CDN的文件。
登錄功能
同樣的登錄功能也是腳本錄制出來的。這里就不重復說明。因為后面的登錄后下單的這個例子包括登錄的所有功能點,這里登錄就先跳過。
下單功能
下單是本次測試的最復雜的一個模塊。首先,下單前需要登錄,但是我們這次只是為了測試下單的工單,所有所有的請求,我們希望只登錄一次(實際上如果是用了多台施壓機,腳本里寫一次登錄,實際上是每台機器一次,一共登錄會被執行多次)。根據前面講的腳本的組成邏輯,我們需要把登錄寫在def __init__(self)
里。除了登錄,我們還希望測試每次下單購買的是不同的商品和數量,這時候需要用到參數文件。另外因為我們這次是希望壓測下單的過程中ECS和數據庫的壓力,對於之前的瓶頸公網帶寬,我們假設已經通過動靜分離解決了,所以在這里壓測腳本里我們不涉及靜態資源,走內網壓測。還有我們希望在這個例子里對腳本代碼做一次調試,所以需要做一些日志打印。