效能測試: 編寫一個 Locust 檔案

Yuanjie發表於2017-06-02

Locust檔案就是一般的Python檔案。唯一的需求就是它至少需要一個繼承於Locust的類.

Locust類

Locust類代表一個使用者(如果願意,也可以是一個準備出動的蝗蟲)。Locust會為每一個模擬使用者生成一個locust類例項。同時會有一些locust類屬性被定義。

task_set屬性

task_set屬性是指向一個定義使用者行為的TaskSet類,下面會有詳細的介紹。

min_waitmax_wait屬性

除了task_set屬性,另外一個經常被使用的就是min_waitmax_wait屬性。是用於各自以毫秒為單位的最小值和最大值,一個模擬使用者將會在每個任務執行時的等待執行的時間間隔。min_waitmax_wait預設設定為1000,如果不宣告的話,Locust會預設在每個任務間等待1秒

參考下面的程式碼,每個使用者將會在每個任務間等待5至15秒:

from locust import Locust, TaskSet, task_set

class MyTaskSet(TaskSet):
    @task
    def my_task(self):
        print "executing my_task"

class MyLocust(Locust):
    task_set = MyTaskSet
    min_wait = 5000
    max_wait = 15000複製程式碼

min_waitmax_wait屬性可以用於重寫TaskSet類。

weight屬性

你可以通過同一個檔案來執行兩個locust,就像這樣:

locust -f locust_file.py WebUserLocust MobileUserLocust複製程式碼

如果你更傾向於用這種方法來執行,便可以在這些類中嘗試weight屬性。比如,就像這樣來定義web使用者比Mobile使用者多3倍

class WebUserLocust(Locust):
    weight = 3
    ...

class MobileUserLocust(Locust):
    weight = 1
    ...複製程式碼

host屬性

host屬性是到要載入目標URL的字首(如:"google.com")。通常情況下,當Locust被啟動時,在命令列中是需要通過--host來指定的。如果host屬性在locustfile檔案中被宣告,則在命令列中則不需要使用--host屬性來再次宣告。

TaskSet

如果Locust類代表一隻準備出動的蝗蟲,那麼你可以說TaskSet類代表蝗蟲的大腦。每一個Locust類中必須要包含一個指向TaskSettask_set屬性設定。

TaskSet就像它的名字一樣,是一個任務集合。這些任務是常規的Python呼叫,如果我們壓力測試一個拍賣網站,便可以做這些操作載入啟動頁面搜尋一些產品競標

當一個壓力測試被啟動時,每一個準備的Locust類例項將會開始執行它們的TaskSet。接下來是每一個TaskSet找到它的task並呼叫它。它將在min_waitmax_wait屬性值之間隨機等待幾毫秒(除非min_waitmax_wait被定義在TaskSet中,在這種情況下將會使用TaskSet設定的值)。然後,它將會找到一個新task並呼叫,再次等待,一直這樣持續下去。

宣告task

對於TaskSet來說,典型的宣告task的方法是直接使用task

參考這個例子:

from locust import Locust, TaskSet, task
class MyTaskSet(TaskSet):
    @task
    def my_task(self):
        print "Locust instance (%r) executing my_task" % (self.locust)

class MyLocust(Locust):
    task_set = MyTaskSet複製程式碼

@task 將會獲取一個可選的權重引數,用於說明任務執行的比率。在下面的例子中 task2 將會比 task1 執行的次數多兩倍:

from locust import Locust, TaskSet, task
class MyTaskSet(TaskSet):
    min_wait = 5000
    max_wait = 15000

    @task(3)
    def task1(self):
        pass

    @task(6)
    def task2(self):
        password

class MyLocust(Locust):
    task_set = MyTaskSet複製程式碼

task屬性

使用@task操作符來宣告task是一種便捷的方法,並且經常是最好的方式。然而,也可以定義TaskSet中的task通過設定tasks屬性(使用操作符@task比tasks屬性更流行)。

tasks 屬性不是python列表的呼叫就是一個字典。tasks是python呼叫接收執行task的TaskSet類例項引數。下面是一個極其簡單的示例(不會影響任何測試):

from locust import Locust, TaskSet

def my_task(l):
    pass

class MyTaskSet(TaskSet):
    tasks = [my_task]

class MyLocust(Locust):
    task_set = MyTaskSet複製程式碼

如果task屬性被定義在列表中,每次任務被執行時,將會隨機tasks 屬性中選擇。如果 tasks 是一個帶有關健字和數值呼叫的字典,被執行的任務將會被隨機選擇以數字的比率來執行。就像下面的這樣:

{my_task: 3, another_task:1}複製程式碼

my_task 將會比 another_task 多執行三倍。

TaskSet可以巢狀

TaskSet有一個重要的屬性就是可以被巢狀,由於真實的網站是有一定的業務層級結構的,並帶有一些子模組。巢狀的TaskSet將會幫助我們來定義更加真實的使用者行為。比如,我們可以定義TaskSet像下面的結構

  • Main user behaviour
    • Index page
    • Forum page
      • Read thread
        • Reply
      • New thread
      • View next page
    • Browser categories
      • Watch movies
      • Filter movies
    • About page

巢狀TaskSet的方法就像使用task屬性來說明task一樣,但代替參考Python函式,你可以參考下面的TaskSet:

class ForumPage(TaskSet):
    @task(20)
    def read_thread(self):
        pass

    @task(1)
    def new_thread(self):
        pass

    @task(5)
    def stop(self):
        self.interrupt()

class UserBehaviour(TaskSet):
    tasks = {ForumPage:10}

    @task
    def index(self):
        pass複製程式碼

在上面的示例中,當UserBehaviour的TaskSet執行時,ForumPage會被選中來執行,接下來ForumPage的TaskSet將會開始執行。ForumPage的TaskSet會找到它的tasks並執行它,再等待,一直這樣持續下去。

針對上面的例子中有一個重要的事情要注意,就是在ForumPage頁面中的Stop方法中呼叫self.interrupt()。這個做的事情是停止執行ForumPage任務並在UserBehaviour例項中繼續執行。如果在ForumPage中,我們沒有呼叫interrupt()方法,除非被呼叫否則Locust不會呼叫ForumPage任務。但通過interrupt函式 ,我們可以結合weight任務來定義模擬使用者離開Forum.

也可以在類內部宣告巢狀TaskSet,通過使用@task操作符,像宣告正常的task一樣:

class MyTaskSet(TaskSet):
    @task
    class SubTaskSet(TaskSet):
        @task
        def my_task(self):
            pass複製程式碼

on_start函式

TaskSet可以選擇宣告on_start函式。如果這樣的話,當模擬使用者開始執行TaskSet類時,函式被呼叫。

關聯Locust例項,或父TaskSet例項

TaskSet例項有locust屬性來指向它的Locust例項,屬性parent用來指向它的父TaskSet(它會指向Locsut例項,在基類TaskSet中)。

HTTP請求

到現在為止,我們僅覆蓋了一個Locsut使用者的部分任務計劃。為了真實的壓力測試一個系統時,我們需要生成HTTP請求。為了幫助我們實現這個功能,可以使用HttpLocust類。當使用這個類時,每一個例項將會獲得一個用於生成Http請求的HttpSession例項的client屬性。

class HttpLocust複製程式碼

表示一個用於壓力測試的孵化和攻擊系統的HTTP 使用者

這個使用者的行為通過task_set屬性來定義,直接指向TaskSet類。

這個類建立一個client屬性,在初始化時,HTTP客戶端支援為每一個使用者在請求間儲存session。

   client=None複製程式碼

HttpSession例項在Locust初始化時被建立。client支援cookies,同時在請求間會儲存session。

當從HttpLocust類繼承時,我們可以使用client屬性來對伺服器生成HTTP請求。下面是一個locust檔案示例用於在一個網站的兩個URL //about/

from locust import HttpLocust, TaskSet, task

class MyTaskSet(TaskSet):
    @task(2)
    def index(self):
        self.client.get('/')

    @task(1)
    def about(self):
        self.client.get('/about/')
class MyLocust(HttpLocust):
    task_set = MyTaskSet
    min_wait = 5000
    max_wait = 15000複製程式碼

使用上面的Locust類,每一個模擬使用者將間隔5-15秒內請求,並且/將會比/about/請求數量多2倍

細心的讀者會發現有一些奇怪,我們使用self.client關聯HttpSession例項,而不是TaskSet,也不是self.locust.client。我們可以這樣做,是因為TaskSet類有一個屬性呼叫client簡單的返回self.locust.client

使用HTTP client

每一個HttpLocust例項在client屬性中有一個HttpSession例項。HttpSession類實際上是requests.Session的子類,可使用get post put delete head patchoptions方法來生成HTTP請求,用於Locust的資料統計。HttpSession例項在請求間維護cookies,因此可用於登入網站並儲存session在請求之間。client可以通過Locust例項的TaskSet例項來關聯,因此很容易獲取client並在任務中生成HTTP請求。

下面是一個生成GET請求到 /about 路徑的示例(在這裡,我們可以假設 self 是一個TaskSetHttpLocust 類的例項):

response = self.client.get("/about")
print "Response staus code:", response.status_code
print "Response content:", response.content複製程式碼

下面是一個生成POST請求的示例:

response = self.client.post("/login", {"username": "testuser", "password": "password"})複製程式碼

安全模式

HTTP client被配製執行在safe_mode。這樣做是任何請求在連線超時、錯誤、相似失敗時將不會丟擲異常,而是返回一個空的假Response物件。請求將會在Locust統計中算做一次失敗。返回假Response內容屬性將會被設定為None,並且它的status_code將會是0.

手動設定請求是成功或失敗

預設情況下,請求被標記為失敗除非在返回狀態碼是OK(2XX)。大部分時間內,這個預設就是你所需要的。然而,比如在測試一個URL節點,你期待返回狀態碼為404,或者測試一個即使錯誤發生也會返回200的系統,因此,需要手工控制locust來判斷是成功還是失敗。

一個可以生成失敗請求,即使當響應程式碼是OK,通過使用catch_response引數和with語法:

with client.get("/", catch_response = True) as response:
    if response.content != "Success":
        response.failure("Got wrong response")複製程式碼

就像一個可以使用響應為OK的請求當做失敗來處理,一個方法就是可以使用catch_response引數和with語法來讓請求HTTP錯誤時,仍然統計資料為成功:

with client.get("/does_not_exist/", catch_response = True) as response:
    if response.status_code = 404:
        response.success()複製程式碼

使用動態引數來分組URL請求

針對網站,有一個常用的功能是獲取URL中包含一些動態引數的頁面資料。通常情況下,在Locust統計中,使用動態分組在URL中是很有意義的。通過name引數來給HttpSession傳遞不同的請求方法。

比如:

# Statistics for these requests will be grouped under: /blog/?id=[id]
for i in range(10):
  client.get("/blog?id=%i" % i, name = "/blog?id=[id]")複製程式碼

常用庫

通常,大家想分享多個locust檔案用於分享常用的庫。在這種情況下,定義專案根目錄用於呼叫Locsut是很重要的,建議將所有的locust檔案有些話在專案的根目錄中。

一個平鋪的結構像下面這樣:

  • 專案根目錄
    • commonlib_conf.py
    • commonlib_auth.py
    • locustfile_web_app.py
    • locsutfile_api.py
    • locustfile_ecommerce.py

locust檔案可以呼叫常用的庫通過使用import commonlib_auth.然而,這種方法不會從locust檔案中,清晰分辨出常用庫。

子資料夾可以有一個清晰的方法(檢視下面的示例),但是locust僅會有執行locsut檔案的位置引用相關的模組。如果你想從你的根目錄匯入(如,你執行locust命令的位置),確保在任何locust檔案中新增常用庫前有程式碼sys.path.append(os.getcwd()),會生成匯入根目錄(如,當前工作目錄)。

  • project root
    • __init__.py
    • common/
      • __init__.py
      • config.py
      • auth.py
    • locustfiles/
      • __init__.py
      • web_app.py
      • api.py
      • ecommerce.py

使用上面的專案結構,你的locust檔案可以通過下面程式碼匯入常用的庫:

sys.path.append(os.getcwd())
import common.auth複製程式碼

相關文章