1. 概述
該方案寫作目的在于描述一個基于Locust實作的壓力測驗,文中詳細地描述了如何利用locustfile.py檔案定義期望達成的測驗用例,并利用Locust對目標站點進行并發壓力測驗,
特別說明:
本檔案所使用的 Locust 環境一鍵安裝自 Rainbond 開源應用商店中的 Locust 應用,版本為0.14.4,更高版本的特性和語法,煩請參見 Locust 官方檔案,
關于Locust這個壓力測驗工具,其官網與檔案,請關注如下鏈接:
Locust官方網站地址
Locust官方檔案
如果不想閱讀英文檔案,那么強烈建議先讀如下鏈接中的內容:
分布式壓力測驗工具Locust在Rainbond上的部署
接下來,我將重點講解不同場景下的 locustfile.py 的寫作方法,
首先,我們聊一聊什么是 locustfile.py,
2. 什么是locustfile.py
Locust通過識別 /locustfile.py 來獲悉壓力測驗任務的細節,這個檔案的路徑當前是默認值,
如果你利用Rainbond應用市場一鍵安裝部署了 Locust 集群,那么你可以在 locust_master 組件的 環境配置 > 組態檔設定 中找到已掛載的該檔案,如有必要,只需要修改里面的內容,然后更新整個應用(包括 locust_master 和 locust_slave集群),
locustfile.py 是一個標準的 python 腳本檔案,通過官方指定的方式,你可以在這個檔案里定義特定的類 ,通過類實體化之后,就會“范訓”出符合定義的實體,模仿用戶對被測驗的目標站點“群起而攻之”,就像蝗蟲(Locust)一樣,這就是 Locust 得名的由來,
3. 寫作的要領
locustfile.py檔案的寫作,核心是規定了兩個類(class),它們分別繼承由locust匯入的 HttpLocust 、TaskSet 兩個超類,
-
繼承自 HttpLocust 的類,是 Locust 呼叫的入口,
-
繼承自 TaskSet 的類,用于定義虛擬用戶要模擬的任務,
簡要的寫作方式如下面的代碼所示:
from locust import HttpLocust, TaskSet, between, task
class MyTaskSet(TaskSet):
@task(1)
def task1(self):
do task1
@task(2)
def task2(self):
do task2
@task(3)
def task3(self):
do task3
@task(4)
def task4(self):
do task4
class Mytest(HttpLocust):
task_set = MyTaskSet
wait_time = between(5.0, 10.0)
在這個 locustfile.py 中,我依次做了這些事:
- 由模塊 locust 匯入了類:HttpLocust, TaskSet, between, task,
- 定義了一個類,名為 MyTaskSet,該類繼承自 TaskSet,具備 TaskSet 所有的方法和屬性,這個類中,后續定義的所有新的方法,都可以視作壓力測驗要執行的任務,
- 依次定義了task1 - task4 四個新的方法,如何讓 Locust 知道這些方法是要執行的任務呢?關鍵在于裝飾器
@task,裝飾器包裝了新定義的方法,告知 Locust 這個方法是一個要執行的任務,圓括號中的數字,用來表示任務執行的權重,即一個虛擬的用戶,會執行被包裝的方法(即任務)指定的次數,需要指出的是,執行的順序是隨機的,如何定義執行順序將在后文講解, - 定義了一個類,名為 Mytest,該類繼承自 HttpLocust,具備 HttpLocust 所有的方法和屬性,這個類通過屬性
task_set = MyTaskSet定義Locust要執行指定的任務類(就是上一個自定義的類),通過wait_time = between(5.0, 10.0)來定義虛擬用戶執行每個任務間隔的時間,當前的寫法,是指定間隔時間為5s至10s中的隨機值,
一個 locustfile.py 的基本框架即是如此,當然還可以有其它的方式來定義這個檔案,但是我認為這種寫作方式已經足夠,而且明了,
關于 wait_time ,這是一個很必要的屬性,Locust 的開發者認為,真正的用戶行為,并不會像機器人一樣迅速而接連不斷地執行所有任務,大家都會有這樣的體驗,訪問到一個頁面后,會東瞧瞧,西看看,或者干脆發會兒呆,心滿意足之后,再點擊下個頁面進行下一個流程,所以在這里會定義 wait_time 來明確兩個任務間隔時間,
至此,我們基本可以明確,真正的難點在于如何定義任務類,至于 Mytest 這個類,大多數情況下復制上面的代碼,就已經足夠了,
4. 典型的場景
下面我們來聊一聊,在以下幾個典型的場景下,該如何定義任務類:
- get請求
- post請求
- 獲取回應
- 猴子測驗
- 流程測驗
4.1 get請求
這是一種最簡單的場景,一般情況下,我們會通過 get 方法,來請求站點的靜態頁面資源,比如 /index.html 這樣的路徑,
簡要代碼如下:
class MyGet(TaskSet):
@task
def index(self):
self.client.get("/index.html")
這就定義了一個簡單的壓測目標站點 “/index.html” 的get請求,
4.2 post請求
post請求一般用于帶著引數訪問站點的指定介面,來實作一些特定的功能,比如登陸行為,
簡要代碼如下:
class MyPost(TaskSet):
@task
def login(self):
self.client.post("/login", {"username":"admin", "password":"mypassword"})
這就定義了一個post請求,用來攜帶著用戶名密碼進行一次登陸,
關于 client ,官方的介紹稱之為 Locust 實體化程序中,類 HttpSession 生成的一個實體,這個實體支持保存 Cookies,以實作保持 Http 請求之間的 session,個人認為,沒必要細究,首先了解如何建立請求,其次需要了解在發起請求之后,如何獲取回應(response)的詳情,做法在下個小節中講述,
4.3 獲取回應
在一些請求完成后,其回應(response)的內容往往非常關鍵,在這里最重要的有兩點:
- 回傳的狀態碼,這直接標明這次請求大致的結果,默認 2xx、3xx這樣的狀態碼表示請求成功;4xx、5xx反之,但是憑借狀態碼不一定能夠完全判定請求的介面是否真的按照預想的情況作業,詳細的內容請見后文斷言一節,在這里,我們需要知道狀態碼是 Locust 判定請求是否成功的默認條件,
- 回傳的內容,請求介面回傳的內容(一般情況下是個Json),有的時候攜帶了非常重要的資訊,比如后文要描述的對某個 CRM 系統的壓力測驗實體中,我們需要通過登陸請求后回傳內容中的
authKey來定義后續請求,以實作登陸態,
Locust中的所有請求,都可以通過下面的方法獲得狀態碼和回傳的內容:
獲取回傳狀態碼:
response = self.client.get("/about")
print("Response status code:", response.status_code)
即實體 response.status_code 回傳了這次請求的狀態碼,
獲取回傳的內容:
response = self.client.get("/about")
print("Response content:", response.text) #回傳字串
print("Response content:", response.json()) #回傳Json,可以作為字典處理
4.4 猴子測驗
假設一只“猴子”作為用戶,它不會按照正常的業務流程去使用業務系統,而是瞎搞一氣,還沒注冊就登陸,沒有選擇商品就要付錢,這可以幫助我們發現更多在正常業務流程之外的BUG,
我們已經了解到如何定義一些普通的請求方式,來請求我們所有已知的介面,那么我們只需要把這些任務統統放進我們的 TaskSet 類中,隨機定義任務被呼叫的權重即可形成一個猴子測驗的任務設定,最終,我們會發現這個猴子測驗使用的 locustfile.py 和我在 [寫作的要領](#3. 寫作的要領) 一節中展示的模版檔案大同小異,故此這里就不再贅述了,
4.5 流程測驗
在這里我想表達如何進行一次對某一業務流程的順序測驗,和之前的猴子測驗不同,我會在這個測驗中規劃一個正確的流程,如果這個流程可以走得通,并承受住大并發壓力的考驗,那么就可以認為業務系統通過了測驗,
這份 locustfile.py 和之前的模版會有一些不同,主要在于我們想要定義順序執行,而非隨機呼叫的任務,所以我們生成的任務類不再繼承自 TaskSet,而是繼承自 TaskSequence ,后者是前者的子類,但是新增了順序呼叫的方法,搭配新的裝飾器 @seq_task ,這樣我們就可以定義所有任務的執行順序了,
流程測驗是這個方案的重點,所以我專門找了一個CRM系統,來作為測驗的受體,然后對 登陸 —— 獲取用戶串列 —— 新增用戶 —— 洗掉用戶 —— 登出 這一流程做壓力測驗,
特意指出,在這里我不僅使用了類似 self.client.get()、self.client.post() 等相對簡單明確的方法,還使用了更基礎的 self.client.request()方法,這個方法可以通過傳遞引數來實作和 post 以及 get 一樣的效果,了解更多請看看這里,
來看看代碼:
from locust import HttpLocust, TaskSequence, between, task, seq_task
import random, string
class MyTaskSet(TaskSequence):
login_header = {}
del_params = {}
@seq_task(1)
def login(self):
to_login = self.client.post("/index.php/admin/base/login", {"username":"184xxxxxx66", "password":"mypassword"})
self.login_header['authKey'] = to_login.json()["data"]["authKey"]
self.login_header['sessionId'] = to_login.json()["data"]["sessionId"]
self.login_header['Cookie'] = 'PHPSESSID=' + to_login.json()["data"]["sessionId"]
print(to_login.json())
@seq_task(2)
def get_customer(self):
to_get = self.client.request(method="post", url="/index.php/crm/customer/index", params={"page":1, "limit":15}, headers=self.login_header)
print(to_get.json())
@seq_task(3)
def add_customer(self):
self.add_params={"level":"A(重點客戶)", "industry":"金融業", "source":"促銷活動", "deal_status":"未成交", "telephone":"13555555555"}
self.add_params['name'] = ''.join(random.sample(string.ascii_letters + string.digits, 8)) #隨機生成客戶名
to_add = self.client.request(method="post", url="/index.php/crm/customer/save", params=self.add_params, headers=self.login_header)
self.del_params["id[0]"] = to_add.json()['data']['customer_id']
print(to_add.json())
@seq_task(4)
def del_customer(self):
to_del = self.client.request(method="post", url="/index.php/crm/customer/delete", params=self.del_params, headers=self.login_header)
print(to_del.json())
@seq_task(5)
@task(3)
def index(self):
self.client.get("/index.php/admin/system/index")
@seq_task(6)
def logout(self):
self.client.post("/index.php/admin/base/logout")
class Mytest(HttpLocust):
task_set = MyTaskSet
wait_time = between(5.0, 10.0)
接下來講解下和最開頭的模版不一樣的地方:
- 新匯入了
TaskSequence、seq_task,前者是TaskSet的替代者,用于實作順序呼叫任務,后者是新的裝飾器,來定義任務的呼叫順序, - 新匯入
random, string模塊,來為新增的用戶隨機生成用戶名,CRM系統規定用戶名不可以相同, - 任務類
MyTaskSet繼承自TaskSequence, - 通過
@seq_task()來裝飾任務,圓括號中的數字代表執行順序, - 為所有任務實體定義新的屬性
login_header、del_params,初始值均為空的字典,在任務執行程序中,會將保持登陸態所需要的header資訊保存進去以供其他行為呼叫、保存新增用戶的特殊ID,以供洗掉操作使用, - 登陸程序完成后,收集請求的回應內容(Json格式),經過操作,更新實體的
login_header屬性,將保持登陸態所使用的authKeyCookiesessionId保存起來, - 列印每個任務的回應內容,這樣可以在后臺日志中清楚地看到每個任務的執行情況,這對于發現BUG非常有用,
- 有關如何知悉新增用戶或者洗掉用戶流程中所傳遞的引數,將在下一節講解,
整個流程測驗任務的重點,在于明確地規劃好整個測驗的任務流程,在執行流程測驗的時候,一定要保證業務是按照我們預先設計好的流程進行,這樣才可以發現在大并發壓力下,我們的業務系統是否正常表現,
對于這個例子而言,登陸后的操作如何保持登陸態?新建用戶要傳遞哪些引數?洗掉用戶的憑據是什么?這三個問題是保障整個流程順利進行的重點,而這三個問題的答案都在于請求時傳遞哪些引數,
根據官方檔案介紹,client實體具備了保存Cookie與Session的功能,但是并非所有的業務系統保持登陸態都依賴于這兩個引數,比如例子中的 CRM 系統,保持登陸態還需要獲取引數 authKey,所以,無論如何,都需要自行定義保持登陸態的操作,
5. 請求的引數
這里所說的引數,是一個廣義的概念,實際上包括了 params、header ,甚至在某些情況下還需要 data 、auth等等資料,
有很多的時候,被測驗的業務系統,并不是我們自己設計的,我們并不知道這些 “引數” 究竟包含什么,在這一節,我想要闡述一個方法,可以通過瀏覽器來幫助我們獲取這些“引數”,
對于 B/S 架構程式而言,我們通過瀏覽器進行的所有操作,都是有跡可循的,打開瀏覽器的 檢查 (一般情況下,默認快捷鍵 F12),選擇 Network 并進行操作,就可以獲悉我們通過瀏覽器到底做了什么,
下面是一個例子,模擬了CRM系統在登陸時的行為:
![[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-dg57P2iM-1638890209365)(upload://wSoHI9QpcFHV4SlH36RemZzJ2by.jpeg)]](https://img.uj5u.com/2021/12/09/286274090759562.png)
-
通過
Request URL獲取所有的路由資訊,這個URL由業務系統的域名和介面路徑組成,域名在 Locust的WEB-UI中寫入,介面路徑作為引數 url 傳遞給任務中的請求, -
通過
Request Method我們可以知道這是一個 POST 請求, -
最下面,是通過
params傳遞登陸引數,這里定義的是用戶名和密碼,
結合下實際的請求,這會更利于理解:
self.client.post(url="/index.php/admin/base/login", params={"username":"184xxxxxx66", "password":"xxxxxxxx"})
實際上引數名可以省略,用以上引數,就可以獲取執行請求的全部引數了,
那么請求成功后,會得到什么回傳呢?
![[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-dkjs8U5w-1638890209367)(upload://iEgwqnM2dtd1TWyBsFPdE4U98yF.jpeg)]](https://img.uj5u.com/2021/12/09/286274090759563.png)
切換到 Preview 或者 Response 頁面,可以得到介面的回傳,建議看 Preview ,
- 在這里,我們最想要得到的是
authKey和sessionId這兩個值,結合與sessionId相關的Cookie就可以獲得保持登陸態的所有引數,
接下來,我們來執行列示用戶的操作,訪問用戶管理頁面,選擇用戶,可以得到 getField這個介面的請求資訊,這里要重點關注 Request Header 部分,因為我們需要從這里獲悉如何保持登陸態,
![[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-DyPOwzyi-1638890209367)(upload://xJRT9WREyRsIiERzBQVeir0zfAn.jpeg)]](https://img.uj5u.com/2021/12/09/286274090759564.png)
看到這里,我們就明確了,為什么在登陸時會回傳 authKey 和 sessionId ,其實是在請求頭 (Request Header)加入這些資訊來保持登陸態,
通過分析瀏覽器中的請求資訊,我們就可以知道特定的流程中瀏覽器為我們做了哪些事,接著就可以去定義測驗任務了,
6. 斷言
前文已經說過,Locust 默認通過請求回傳的狀態碼來判斷這次請求是否成功,但是這還不夠,我在測驗CRM的時候,發現了一個問題,
我沒有執行登陸操作,直接去列示了所有用戶,這個操作回傳給我如下回復:
>>> to_get = l.client.request(method="post", url="/index.php/crm/customer/index", params={"page":1, "limit":15})
>>> to_get.status_code
200
>>> to_get.json()
{'code': 101, 'error': '請先登錄'}
狀態碼回傳200,這將導致Locust 認為該請求執行成功,
CRM的處理也是正確的,我沒有登陸,卻請求了這個埠,CRM非常明確地處理了這個問題,告訴我“請先登錄”,
但是從我自定義的業務流程上講,這是個錯誤,我并沒有獲取我預期的所有用戶的串列,我希望在Locust的結果分析中,將這種情況視作失敗的請求,所以,我需要一個斷言,
斷言就是分析請求的回傳,即使狀態碼回傳2xx、3xx,也可以根據回傳內容將任務判定為失敗狀態,反之亦然,
Locust 中斷言的實作,是在請求中設定 catch_response=True 引數來抓獲回傳,再進行判斷,
我們來優化CRM測驗任務中有關get_customer方法的代碼:
@task
def get_customer(self):
with self.client.request(method="post", url="/index.php/crm/customer/index", params={"page":1, "limit":15}, catch_response=True) as to_get:
if to_get.json() == {'code': 101, 'error': '請先登錄'}:
to_get.failure("它告訴我要先登陸")
執行壓力測驗后,我的目的達到了:
![[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-CHJP0PHp-1638890209367)(upload://s9Cdgi0LHdo3Qv2nYnK2nYaG8mT.jpeg)]](https://img.uj5u.com/2021/12/09/286274090759565.png)
結果分析也拋出了我已經設定好的訊息:
![[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-CaEUlFEc-1638890209367)(upload://t5Nb3z2hkMTx5LdXI9478DiIpgv.jpeg)]](https://img.uj5u.com/2021/12/09/286274090759566.png)
7. 壓力
使用Locust的一大好處就是它可以模擬很大的壓力,這里我不去描述它在實作原理上有何優勢,而是想向大家介紹它的分布式特點,Locust支持主從集群模式,主節點(locust_master)負責壓力測驗任務的調度,從節點(locust_slave)負責具體的用戶模擬和測驗的執行,其中,從節點支持分布式部署,也就是說,如果需要,就可以使用Rainbond的橫向伸縮功能擴展出很多 locust_slave 的實體,能夠模擬的壓力也就隨之增大,
這一節,我們來描述在WEB-UI界面中如何定義壓力,
![[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-ya43gTiu-1638890209368)(upload://7kdcl6azfwXU3Biqz8OnT47SA6K.jpeg)]](https://img.uj5u.com/2021/12/09/286274090759567.png)
訪問 locust_master 服務組件的 8089 埠,會進入到WEB-UI界面,并開始規劃一個新的壓力并發,
-
第一個值規定了本次測驗的最大模擬用戶數量,即并發,
-
第二個值規定每秒“范訓”的用戶數量,如圖中的配置,開始測驗后,Locust將在十秒鐘內啟動100個用戶,
-
測驗站點的域名,這里有一點要注意,就是一定要帶協議頭,即 “http://”,
那么,Locust能提供多大壓力,是否有個衡量呢?我執行的一個測驗顯示,3個slave的 Locust可以輕松提供5000并發,
![[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-toePYJHI-1638890209368)(upload://fSScaC6JxoKKQg30nFrMYUZz5EI.jpeg)]](https://img.uj5u.com/2021/12/09/286274090759568.png)
而被壓測的CRM已經超出了能接受的壓力極限,開始出現大量的錯誤,使用的記憶體急劇飆升到了90%以上,Locust 得出的 Failures 頁面報告了錯誤產生的原因,
![[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-E5edSUSr-1638890209368)(upload://5AsLPdoEGgOMUJJyYV4zR95rmj9.jpeg)]](https://img.uj5u.com/2021/12/09/286274090759569.png)
8. 結果分析
借助Locust提供的WEB-UI界面,我們可以非常方便地分析壓力測驗結果,
![[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-GDcZPZpo-1638890209368)(upload://bfRy1qFxmpFO66z0ad61xYYbQqe.jpeg)]](https://img.uj5u.com/2021/12/09/2862740907595610.png)
Statistics頁面將向我們展示所有被壓測介面的匯總報告,結果包括:
請求總數、失敗次數、中位數回應時間、90%請求回應時間、平均回應時間、最小回應時間、最大回應時間、請求的平均大小、當前吞吐率、當前錯誤率,
![[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-8Pr3RIga-1638890209369)(upload://aQd8G56I5M6SY7VLlluv77xnghM.jpeg)]](https://img.uj5u.com/2021/12/09/2862740907595611.png)
Charts頁面將主要結果繪制成為隨時間變化的圖表,能夠在趨勢上給予用戶指引,
除了這些之外,還有幾項值得關注的值會在最上面一排全域展示,包括當前請求的主機域名、當前產生的并發用戶數量、slave節點數量、當前所有請求介面的總吞吐率、錯誤率,以及停止測驗的按鈕,
其它的幾個頁面會提供請求失敗的介面及失敗原因(Failures)、測驗中意外的錯誤以及錯誤原因(Expections)、csv格式的測驗資料下載地址(Download Data)、 所有slave實體的資訊(Slaves),
所有的資料都基于圖形展示,十分方便,
9. 寫在最后
Locust 是一個相當不錯的壓力測驗工具,可自由定制的東西很多,我寫在這個方案中的種種用法還不足以囊括它的所有特性,但是 Good Enough is Best ,這個方案已經可以應付絕大多數場景了,
Rainbond 是一個開源的云原生應用管理平臺,使用簡單,不需要懂容器和Kubernetes,支持管理多個Kubernetes集群,提供企業級應用的全生命周期管理,功能包括應用開發環境、應用市場、微服務架構、應用持續交付、應用運維、應用級多云管理等,

轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/377047.html
標籤:其他
上一篇:【微信開發者工具】at-rule or selector expected
下一篇:軟體測驗之功能測驗
