1. 程式人生 > >深入淺出開源效能測試工具Locust(使用篇)

深入淺出開源效能測試工具Locust(使用篇)

《【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提供的非阻塞IOcoroutine來實現網路層的併發請求,因此即使是單臺壓力機也能產生數千併發請求數;再加上對分散式執行的支援,理論上來說,Locust

能在使用較少壓力機的前提下支援極高併發數的測試。

指令碼編寫

編寫Locust指令碼,是使用Locust的第一步,也是最為重要的一步。

簡單示例

先來看一個最簡單的示例。

12345678910111213141516171819202122 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的測試指令碼中,所有業務測試場景都是在LocustTaskSet兩個類的繼承子類中進行描述的。

那如何理解LocustTaskSet這兩個類呢?

簡單地說,Locust類就好比是一群蝗蟲,而每一隻蝗蟲就是一個類的例項。相應的,TaskSet類就好比是蝗蟲的大腦,控制著蝗蟲的具體行為,即實際業務場景測試對應的任務集。

這個比喻可能不是很準確,接下來,我將分別對LocustTaskSet兩個類進行詳細介紹。

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例項)的執行邏輯都會遵循如下規律:

  1. 先執行WebsiteTasks中的on_start(只執行一次),作為初始化;
  2. WebsiteTasks中隨機挑選(如果定義了任務間的權重關係,那麼就是按照權重關係隨機挑選)一個任務執行;
  3. 根據Locust類min_waitmax_wait定義的間隔時間範圍(如果TaskSet類中也定義了min_wait或者max_wait,以TaskSet中的優先),在時間範圍中隨機取一個值,休眠等待;
  4. 重複2~3步驟,直至測試任務終止。

class TaskSet

再說下TaskSet類

效能測試工具要模擬使用者的業務操作,就需要通過指令碼模擬使用者的行為。在前面的比喻中說到,TaskSet類好比蝗蟲的大腦,控制著蝗蟲的具體行為。

具體地,TaskSet類實現了虛擬使用者所執行任務的排程演算法,包括規劃任務執行順序(schedule_task)、挑選下一個任務(execute_next_task)、執行任務(execute_task)、休眠等待(wait)、中斷控制(interrupt)等等。在此基礎上,我們就可以在TaskSet子類中採用非常簡潔的方式來描述虛擬使用者的業務測試場景,對虛擬使用者的所有行為(任務)進行組織和描述,並可以對不同任務的權重進行配置。

TaskSet子類中定義任務資訊時,可以採取兩種方式,@task裝飾器tasks屬性

採用@task裝飾器定義任務資訊時,描述形式如下:

12345678910 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屬性定義任務資訊時,描述形式如下:

1234567891011 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

12345678910 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')
1234567891011 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,因此後續所有任務執行過程中就都具有登入態了。

指令碼增強

掌握了HttpLocustTaskSet,我們就基本具備了編寫測試指令碼的能力。此時再回過頭來看前面的案例,相信大家都能很好的理解了。

然而,當面對較複雜的測試場景,可能有的同學還是會感覺無從下手;例如,很多時候指令碼需要做關聯或引數化處理,這些在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的listqueue資料結構即可!具體做法是,在WebsiteUser定義一個數據集,然後所有虛擬使用者在WebsiteTasks中就可以共享該資料集了。如果不要求資料唯一性,資料集選擇list資料結構,從頭到尾迴圈遍歷即可;如果要求資料唯一性,資料集選擇queue資料結構,取資料時進行queue.get()操作即可,並且這也不會迴圈取資料;至於涉及到需要迴圈取資料的情況,那也簡單,每次取完資料後再將資料插入到隊尾即可,queue.put_nowait(data)

最後再說下檢查點。該功能在LoadRunner中通常是使用web_reg_find這類註冊函式進行檢查的。在Locust指令碼中,處理就更方便了,只需要對響應的內容關鍵字進行assert xxx in response操作即可。

針對如上各種指令碼增強的場景,我也通過程式碼示例分別進行了演示。但考慮到文章中插入太多程式碼會影響到閱讀,因此將程式碼示例部分剝離了出來,如有需要請點選檢視《深入淺出開源效能測試工具Locust(指令碼增強)》

Locust執行模式

在開始執行Locust指令碼之前,我們先來看下Locust支援的執行模式。

執行Locust時,通常會使用到兩種執行模式:單程序執行和多程序分散式執行。

單程序執行模式的意思是,Locust所有的虛擬併發使用者均執行在單個Python程序中,具體從使用形式上,又分為no_webweb兩種形式。該種模式由於單程序的原因,並不能完全發揮壓力機所有處理器的能力,因此主要用於除錯指令碼和小併發壓測的情況。

當併發壓力要求較高時,就需要用到Locust的多程序分散式執行模式。從字面意思上看,大家可能第一反應就是多臺壓力機同時執行,每臺壓力機分擔負載一部分的壓力生成。的確,Locust支援任意多臺壓力機(一主多從)的分散式執行模式,但這裡說到的多程序分散式執行模式還有另外一種情況,就是在同一臺壓力機上開啟多個slave的情況。這是因為當前階段大多數計算機的CPU都是多處理器(multiple processor cores),單程序執行模式下只能用到一個處理器的能力,而通過在一臺壓力機上執行多個slave,就能呼叫多個處理器的能力了。比較好的做法是,如果一臺壓力機有N個處理器核心,那麼就在這臺壓力機上啟動一個masterNslave。當然,我們也可以啟動N的倍數個slave,但是根據我的試驗資料,效果跟N個差不多,因此只需要啟動Nslave即可。

指令碼除錯

Locust指令碼編寫完畢後,通常不會那麼順利,在正式開始效能測試之前還需要先除錯執行下。

不過,Locust指令碼雖然為Python指令碼,但卻很難直接當做Python指令碼執行起來,為什麼呢?這主要還是因為Locust指令碼中引用了HttpLocustTaskSet這兩個類,如果要想直接對其進行呼叫測試,會發現編寫啟動指令碼是一個比較困難的事情。因為這個原因,剛接觸Locust的同學可能就會覺得Locust指令碼不好除錯。

但這個問題也能克服,那就是藉助Locust的單程序no_web執行模式。

Locust的單程序no_web執行模式中,我們可以通過--no_web引數,指定併發數(-c)和總執行次數(-n),直接在Terminal中執行指令碼。

在此基礎上,當我們想要除錯Locust指令碼時,就可以在指令碼中需要除錯的地方通過print列印日誌,然後將併發數和總執行次數都指定為1,執行形式如下所示。

1 $ locust -f locustfile.py --no_web -c 1 -n 1

通過這種方式,我們就能很方便地對Locust指令碼進行除錯了。

執行測試

Locust指令碼除錯通過後,就算是完成了所有準備工作,可以開始進行壓力測試了。

Locust是通過在Terminal中執行命令進行啟動的,通用的引數有如下兩個:

  • -H, --host:被測系統的host,若在Terminal中不進行指定,就需要在Locust子類中通過host引數進行指定;
  • -f, --locustfile:指定執行的Locust指令碼檔案;

除了這兩個通用的引數,我們還需要根據實際測試場景,選擇不同的Locust執行模式,而模式的指定也是通過其它引數來進行控制的。

單程序執行

no_web

如果採用no_web形式,則需使用--no-web引數,並會用到如下幾個引數。

  • -c, --clients:指定併發使用者數;
  • -n, --num-request:指定總執行測試;
  • -r, --hatch-rate:指定併發加壓速率,預設值位1。
123456789101112131415161718192021222324252627282930313233343536373839 $ locust -H http://debugtalk.com -f demo.py --no-web -c1 -n2[2017-02-21 21:27:26,522] Leos-MacBook-Air.local/INFO/locust.main: Starting Locust 0.8a2[2017-02-21 21:27:26,523] Leos-MacBook-Air.local/INFO/locust.runners: Hatching and swarming 1 clients at the rate 1 clients/s... Name # reqs # fails Avg Min Max | Median req/s---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Total 0 0(0.00%) 0.00[2017-02-21 21:27:27,526] Leos-MacBook-Air.local/INFO/locust.runners: All locusts hatched: WebsiteUser: 1[2017-02-21 21:27:27,527] Leos-MacBook-Air.local/INFO/locust.runners: Resetting stats Name # reqs # fails Avg Min Max | Median req/s-------------------------------------------------------------------------------------------------------------------------------------- GET /about/ 0 0(0.00%) 0 0 0 | 0 0.00-------------------------------------------------------------------------------------------------------------------------------------- Total 0 0(0.00%) 0.00 Name # reqs # fails Avg Min Max | Median req/s-------------------------------------------------------------------------------------------------------------------------------------- GET /about/ 1 0(0.00%) 17 17 17 | 17 0.00-------------------------------------------------------------------------------------------------------------------------------------- Total 1 0(0.00%) 0.00[2017-02-21 21:27:32,420] Leos-MacBook-Air.local/INFO/locust.runners: All locusts dead[2017-02-21 21:27:32,421] Leos-MacBook-Air.local/INFO/locust.main: Shutting down (exit code 0), bye. Name # reqs # fails Avg Min Max | Median req/s-------------------------------------------------------------------------------------------------------------------------------------- GET / 1 0(0.00%) 20 20 20 | 20 0.00 GET /about/ 1 0(0.00%) 17 17 17 | 17 0.00-------------------------------------------------------------------------------------------------------------------------------------- Total 2 0(0.00%) 0.00Percentage of the requests completed within given times Name # reqs 50% 66% 75% 80% 90% 95% 98% 99% 100%-------------------------------------------------------------------------------------------------------------------------------------- GET / 1 20 20 20 20 20 20 20 20 20 GET /about/ 1 17 17 17 17 17 17 17 17 17--------------------------------------------------------------------------------------------------------------------------------------

web

如果採用web形式,,則通常情況下無需指定其它額外引數,Locust預設採用8089埠啟動web;如果要使用其它埠,就可以使用如下引數進行指定。

  • -P, --port:指定web埠,預設為8089.
123 $ locust -H http://debugtalk.com -f demo.py[2017-02-21 21:31:26,334] Leos-MacBook-Air.local/INFO/locust.main: Starting web monitor at *:8089[2017-02-21 21:31:26,334] Leos-MacBook-Air.local/INFO/locust.main: Starting Locust 0.8a2

此時,Locust並沒有開始執行測試,還需要在Web頁面中配置引數後進行啟動。

如果Locust執行在本機,在瀏覽器中訪問http://localhost:8089即可進入Locust的Web管理頁面;如果Locust執行在其它機器上,那麼在瀏覽器中訪問http://locust_machine_ip:8089即可。

Locust的Web管理頁面中,需要配置的引數只有兩個:

  • Number of users to simulate: 設定併發使用者數,對應中no_web模式的-c, --clients引數;
  • Hatch rate (users spawned/second): 啟動虛擬使用者的速率,對應著no_web模式的-r, --hatch-rate引數。

引數配置完畢後,點選【Start swarming】即可開始測試。

多程序分散式執行

不管是單機多程序,還是多機負載模式,執行方式都是一樣的,都是先執行一個master,再啟動多個slave

啟動master時,需要使用--master引數;同樣的,如果要使用8089以外的埠,還需要使用-P, --port引數。

123 $ locust -H http://debugtalk.com -f demo.py --master --port=8088[2017-02-21 22:59:57,308] Leos-MacBook-Air.local/INFO/locust.main: Starting web monitor at *:8088[2017-02-21 22:59:57,310] Leos-MacBook-Air.local/INFO/locust.main: Starting Locust 0.8a2

master啟動後,還需要啟動slave才能執行測試任務。

啟動slave時需要使用--slave引數;在slave中,就不需要再指定埠了。

1234 $ locust -H http://debugtalk.com -f demo.py --slave[2017-02-21 23:07:58,696] Leos-MacBook-Air.local/INFO/locust.main: Starting Locust 0.8a2[2017-02-21 23:07:58,696] Leos-MacBook-Air.local/INFO/locust.runners: Client 'Leos-MacBook-Air.local_980ab0eec2bca517d03feb60c31d6a3a' reported as ready. Currently 2 clients ready to swarm.

如果slavemaster不在同一臺機器上,還需要通過--master-host引數再指定master的IP地址。

1234 $ locust -H http://debugtalk.com -f demo.py --slave --master-host=<locust_machine_ip>[2017-02-21 23:07:58,696] Leos-MacBook-Air.local/INFO/locust.main: Starting Locust 0.8a2[2017-02-21 23:07:58,696] Leos-MacBook-Air.local/INFO/locust.runners: Client 'Leos-MacBook-Air.local_980ab0eec2bca517d03feb60c31d6a3a' reported as ready. Currently 2 clients ready to swarm.

masterslave都啟動完畢後,就可以在瀏覽器中通過http://locust_machine_ip:8089進入Locust的Web管理頁面了。使用方式跟單程序web形式完全相同,只是此時是通過多程序負載來生成併發壓力,在web管理介面中也能看到實際的slave數量。

測試結果展示

Locust在執行測試的過程中,我們可以在web介面中實時地看到結果執行情況。

相比於LoadRunnerLocust的結果展示十分簡單,主要就四個指標:併發數RPS響應時間異常率。但對於大多數場景來說,這幾個指標已經足夠了。

在上圖中,RPS平均響應時間這兩個指標顯示的值都是根據最近2秒請求響應資料計算得到的統計值,我們也可以理解為瞬時值。

如果想看效能指標資料的走勢,就可以在Charts欄檢視。在這裡,可以檢視到RPS平均響應時間在整個執行過程中的波動情況。這個功能之前在Locust中一直是缺失的,直到最近,這個坑才被我之前在阿里移動的同事(網路IDmyzhan)給填上了。當前該功能已經合併到Locust了,更新到最新版即可使用。

除了以上資料,Locust還提供了整個執行過程資料的百分比統計值,例如我們常用的90%響應時間響應時間中位值,該資料可以通過Download response time distribution CSV獲得,資料展示效果如下所示。

總結

通過前面對Locust全方位的講解,相信大家對Locust的功能特性已經非常熟悉了,在實際專案中將Locust作為生產力工具應該也沒啥問題了。

不過,任何一款工具都不是完美的,必定都會存在一些不足之處。但是好在Locust具有極強的可定製型,當我們遇到一些特有的需求時,可以在Locust上很方便地實現擴充套件。

還是前面提到的那位技術大牛(myzhan),他為了擺脫CPythonGILgevent的 monkey_patch(),將Locustslave端採用golang進行了重寫,採用goroutine取代了gevent。經過測試,相較於原生的Python實現,他的這套golang實現具有5~10倍以上的效能提升。當前,他已經將該實現開源,專案名稱為myzhan/boomer,如果大家感興趣,可以閱讀他的部落格文章進一步瞭解,《用 golang 來編寫壓測工具》

如果我們也想在Locust的基礎上進行二次開發,那要怎麼開始呢?

毫無疑問,閱讀Locust的專案原始碼是必不可少的第一步。可能對於很多人來說,閱讀開源專案原始碼是一件十分困難的事情,不知道如何著手,在知乎上也看到好多關於如何閱讀開源專案原始碼的提問。事實上,Locust專案的程式碼結構清晰,核心程式碼量也比較少,十分適合閱讀學習。哪怕只是想體驗下閱讀開源專案原始碼,或者說想提升下自己的Python技能,Locust也是個不錯的選擇。


轉載自 http://debugtalk.com/post/head-first-locust-user-guide/