在《【LocustPlus序】漫談服務端性能測試》中,我對服務端性能測試的基礎概念和性能測試工具的基本原理進行了介紹,並且重點推薦了Locust
這一款開源性能測試工具。然而,當前在網絡上針對Locust
的教程極少,不管是中文還是英文,基本都是介紹安裝方法和簡單的測試案例演示,但對於較複雜測試場景的案例演示卻基本沒有,因此很多測試人員都感覺難以將Locust
應用到實際的性能測試工作當中。
經過一段時間的摸索,包括通讀Locust
官方文檔和項目源碼,並且在多個性能測試項目中對Locust
進行應用實踐,事實證明,Locust
完全能滿足日常的性能測試需求,LoadRunner
能實現的功能Locust
也基本都能實現。
本文將從Locust
的功能特性出發,結合實例對Locust
的使用方法進行介紹。考慮到大衆普遍對LoadRunner
比較熟悉,在講解Locust
時也會採用LoadRunner
的一些概念進行類比。
概述
先從Locust
的名字說起。Locust
的原意是蝗蟲,原作者之所以選擇這個名字,估計也是聽過這麼一句俗語,“蝗蟲過境,寸草不生”。我在網上找了張圖片,大家可以感受下。
而Locust
工具生成的併發請求就跟一大羣蝗蟲一般,對我們的被測系統發起×××,以此檢測系統在高併發壓力下是否能正常運轉。
在《【LocustPlus序】漫談服務端性能測試》中說過,服務端性能測試工具最核心的部分是壓力發生器,而壓力發生器的核心要點有兩個,一是真實模擬用戶操作,二是模擬有效併發。
在Locust
測試框架中,測試場景是採用純Python腳本進行描述的。對於最常見的HTTP(S)
協議的系統,Locust
採用Python的requests
庫作爲客戶端,使得腳本編寫大大簡化,富有表現力的同時且極具美感。而對於其它協議類型的系統,Locust
也提供了接口,只要我們能採用Python編寫對應的請求客戶端,就能方便地採用Locust
實現壓力測試。從這個角度來說,Locust
可以用於壓測任意類型的系統。
在模擬有效併發方面,Locust
的優勢在於其摒棄了進程和線程,完全基於事件驅動,使用gevent
提供的非阻塞IO
和coroutine
來實現網絡層的併發請求,因此即使是單臺壓力機也能產生數千併發請求數;再加上對分佈式運行的支持,理論上來說,Locust
能在使用較少壓力機的前提下支持極高併發數的測試。
腳本編寫
編寫Locust
腳本,是使用Locust
的第一步,也是最爲重要的一步。
簡單示例
先來看一個最簡單的示例。
from locust import HttpLocust, TaskSet, taskclass WebsiteTasks(TaskSet): def on_start(self): self.client.post("/login", { "username": "test", "password": "123456" }) @task(2) def index(self): self.client.get("/") @task(1) def about(self): self.client.get("/about/")class WebsiteUser(HttpLocust): task_set = WebsiteTasks host = "http://debugtalk.com" min_wait = 1000 max_wait = 5000
在這個示例中,定義了針對http://debugtalk.com
網站的測試場景:先模擬用戶登錄系統,然後隨機地訪問首頁(/
)和關於頁面(/about/
),請求比例爲2:1
;並且,在測試過程中,兩次請求的間隔時間爲1~5
秒間的隨機值。
那麼,如上Python腳本是如何表達出以上測試場景的呢?
從腳本中可以看出,腳本主要包含兩個類,一個是WebsiteUser
(繼承自HttpLocust
,而HttpLocust
繼承自Locust
),另一個是WebsiteTasks
(繼承自TaskSet
)。事實上,在Locust
的測試腳本中,所有業務測試場景都是在Locust
和TaskSet
兩個類的繼承子類中進行描述的。
那如何理解Locust
和TaskSet
這兩個類呢?
簡單地說,Locust類
就好比是一羣蝗蟲,而每一隻蝗蟲就是一個類的實例。相應的,TaskSet類
就好比是蝗蟲的大腦,控制着蝗蟲的具體行爲,即實際業務場景測試對應的任務集。
這個比喻可能不是很準確,接下來,我將分別對Locust
和TaskSet
兩個類進行詳細介紹。
class HttpLocust(Locust)
在Locust類
中,具有一個client
屬性,它對應着虛擬用戶作爲客戶端所具備的請求能力,也就是我們常說的請求方法。通常情況下,我們不會直接使用Locust
類,因爲其client
屬性沒有綁定任何方法。因此在使用Locust
時,需要先繼承Locust類
,然後在繼承子類中的client
屬性中綁定客戶端的實現類。
對於常見的HTTP(S)
協議,Locust
已經實現了HttpLocust
類,其client
屬性綁定了HttpSession
類,而HttpSession
又繼承自requests.Session
。因此在測試HTTP(S)
的Locust腳本
中,我們可以通過client
屬性來使用Python requests
庫的所有方法,包括GET/POST/HEAD/PUT/DELETE/PATCH
等,調用方式也與requests
完全一致。另外,由於requests.Session
的使用,因此client
的方法調用之間就自動具有了狀態記憶的功能。常見的場景就是,在登錄系統後可以維持登錄狀態的Session
,從而後續HTTP請求操作都能帶上登錄態。
而對於HTTP(S)
以外的協議,我們同樣可以使用Locust
進行測試,只是需要我們自行實現客戶端。在客戶端的具體實現上,可通過註冊事件的方式,在請求成功時觸發events.request_success
,在請求失敗時觸發events.request_failure
即可。然後創建一個繼承自Locust類
的類,對其設置一個client
屬性並與我們實現的客戶端進行綁定。後續,我們就可以像使用HttpLocust類
一樣,測試其它協議類型的系統。
原理就是這樣簡單!
在Locust類
中,除了client
屬性,還有幾個屬性需要關注下:
task_set
: 指向一個TaskSet
類,TaskSet
類定義了用戶的任務信息,該屬性爲必填;max_wait/min_wait
: 每個用戶執行兩個任務間隔時間的上下限(毫秒),具體數值在上下限中隨機取值,若不指定則默認間隔時間固定爲1秒;host
:被測系統的host,當在終端中啓動locust
時沒有指定--host
參數時纔會用到;weight
:同時運行多個Locust類
時會用到,用於控制不同類型任務的執行權重。
測試開始後,每個虛擬用戶(Locust實例
)的運行邏輯都會遵循如下規律:
先執行
WebsiteTasks
中的on_start
(只執行一次),作爲初始化;從
WebsiteTasks
中隨機挑選(如果定義了任務間的權重關係,那麼就是按照權重關係隨機挑選)一個任務執行;根據
Locust類
中min_wait
和max_wait
定義的間隔時間範圍(如果TaskSet類
中也定義了min_wait
或者max_wait
,以TaskSet
中的優先),在時間範圍中隨機取一個值,休眠等待;重複
2~3
步驟,直至測試任務終止。
class TaskSet
再說下TaskSet類
。
性能測試工具要模擬用戶的業務操作,就需要通過腳本模擬用戶的行爲。在前面的比喻中說到,TaskSet類
好比蝗蟲的大腦,控制着蝗蟲的具體行爲。
具體地,TaskSet類
實現了虛擬用戶所執行任務的調度算法,包括規劃任務執行順序(schedule_task
)、挑選下一個任務(execute_next_task
)、執行任務(execute_task
)、休眠等待(wait
)、中斷控制(interrupt
)等等。在此基礎上,我們就可以在TaskSet
子類中採用非常簡潔的方式來描述虛擬用戶的業務測試場景,對虛擬用戶的所有行爲(任務)進行組織和描述,並可以對不同任務的權重進行配置。
在TaskSet
子類中定義任務信息時,可以採取兩種方式,@task裝飾器
和tasks屬性
。
採用@task裝飾器
定義任務信息時,描述形式如下:
from locust import TaskSet, taskclass UserBehavior(TaskSet): @task(1) def test_job1(self): self.client.get('/job1') @task(2) def test_job2(self): self.client.get('/job2')
採用tasks屬性
定義任務信息時,描述形式如下:
from locust import TaskSetdef test_job1(obj): obj.client.get('/job1')def test_job2(obj): obj.client.get('/job2')class UserBehavior(TaskSet): tasks = {test_job1:1, test_job2:2} # tasks = [(test_job1,1), (test_job1,2)] # 兩種方式等價
在如上兩種定義任務信息的方式中,均設置了權重屬性,即執行test_job2
的頻率是test_job1
的兩倍。
若不指定執行任務的權重,則相當於比例爲1:1
。
from locust import TaskSet, taskclass UserBehavior(TaskSet): @task def test_job1(self): self.client.get('/job1') @task def test_job2(self): self.client.get('/job2')
from locust import TaskSetdef test_job1(obj): obj.client.get('/job1')def test_job2(obj): obj.client.get('/job2')class UserBehavior(TaskSet): tasks = [test_job1, test_job2] # tasks = {test_job1:1, test_job2:1} # 兩種方式等價
在TaskSet
子類中除了定義任務信息,還有一個是經常用到的,那就是on_start
函數。這個和LoadRunner
中的vuser_init
功能相同,在正式執行測試前執行一次,主要用於完成一些初始化的工作。例如,當測試某個搜索功能,而該搜索功能又要求必須爲登錄態的時候,就可以先在on_start
中進行登錄操作;前面也提到,HttpLocust
使用到了requests.Session
,因此後續所有任務執行過程中就都具有登錄態了。
腳本增強
掌握了HttpLocust
和TaskSet
,我們就基本具備了編寫測試腳本的能力。此時再回過頭來看前面的案例,相信大家都能很好的理解了。
然而,當面對較複雜的測試場景,可能有的同學還是會感覺無從下手;例如,很多時候腳本需要做關聯或參數化處理,這些在LoadRunner
中集成的功能,換到Locust
中就不知道怎麼實現了。可能也是這方面的原因,造成很多測試人員都感覺難以將Locust應用到實際的性能測試工作當中。
其實這也跟Locust
的目標定位有關,Locust
的定位就是small and very hackable
。但是小巧並不意味着功能弱,我們完全可以通過Python腳本本身來實現各種各樣的功能,如果大家有疑問,我們不妨逐項分解來看。
在LoadRunner
這款功能全面應用廣泛的商業性能測試工具中,腳本增強無非就涉及到四個方面:
關聯
參數化
檢查點
集合點
先說關聯這一項。在某些請求中,需要攜帶之前從Server端返回的參數,因此在構造請求時需要先從之前請求的Response中提取出所需的參數,常見場景就是session_id
。針對這種情況,LoadRunner
雖然可能通過錄制腳本進行自動關聯,但是效果並不理想,在實際測試過程中也基本都是靠測試人員手動的來進行關聯處理。
在LoadRunner
中手動進行關聯處理時,主要是通過使用註冊型函數,例如web_reg_save_param
,對前一個請求的響應結果進行解析,根據左右邊界或其它特徵定位到參數值並將其保存到參數變量,然後在後續請求中使用該參數。採用同樣的思想,我們在Locust
腳本中也完全可以實現同樣的功能,畢竟只是Python腳本,通過官方庫函數re.search
就能實現所有需求。甚至針對html頁面,我們也可以採用lxml
庫,通過etree.HTML(html).xpath
來更優雅地實現元素定位。
然後再來看參數化這一項。這一項極其普遍,主要是用在測試數據方面。但通過歸納,發現其實也可以概括爲三種類型。
循環取數據,數據可重複使用:e.g. 模擬3用戶併發請求網頁,總共有100個URL地址,每個虛擬用戶都會依次循環加載這100個URL地址;
保證併發測試數據唯一性,不循環取數據:e.g. 模擬3用戶併發註冊賬號,總共有90個賬號,要求註冊賬號不重複,註冊完畢後結束測試;
保證併發測試數據唯一性,循環取數據:模擬3用戶併發登錄賬號,總共有90個賬號,要求併發登錄賬號不相同,但數據可循環使用。
通過以上歸納,可以確信地說,以上三種類型基本上可以覆蓋我們日常性能測試工作中的所有參數化場景。
在LoadRunner
中是有一個集成的參數化模塊,可以直接配置參數化策略。那在Locust
要怎樣實現該需求呢?
答案依舊很簡單,使用Python的list
和queue
數據結構即可!具體做法是,在WebsiteUser
定義一個數據集,然後所有虛擬用戶在WebsiteTasks
中就可以共享該數據集了。如果不要求數據唯一性,數據集選擇list
數據結構,從頭到尾循環遍歷即可;如果要求數據唯一性,數據集選擇queue
數據結構,取數據時進行queue.get()
操作即可,並且這也不會循環取數據;至於涉及到需要循環取數據的情況,那也簡單,每次取完數據後再將數據插入到隊尾即可,queue.put_nowait(data)
。
最後再說下檢查點。該功能在LoadRunner
中通常是使用web_reg_find
這類註冊函數進行檢查的。在Locust
腳本中,處理就更方便了,只需要對響應的內容關鍵字進行assert xxx in response
操作即可。
針對如上各種腳本增強的場景,我也通過代碼示例分別進行了演示。但考慮到文章中插入太多代碼會影響到閱讀,因此將代碼示例部分剝離了出來,如有需要請點擊查看《深入淺出開源性能測試工具Locust(腳本增強)》。