性能測試工具Locust源碼淺析


原文地址: http://testqa.cn/article/detail/235

近期由於有項目需要做性能評測,於是半道出家的我便從選擇性能測試工具,開始了我的性能之旅。

爲什麼要做工具評測

作爲性能測試的老司機們而言,要麼對各大性能測試工具的特性都瞭然於心了,要麼已經使用“慣”了手頭上的工具;他們是不會沒事做個性能評測的,只有新手們纔會認認真真的、按部就班的從第一步走起。

而對於性能測試而言,首要的任務自然是選擇工具了。所以就有了性能測試工具評測這一趴!

爲什麼要解析Locust源碼

由於Python是我的主語言,所以在選擇性能工具評測的時候,自然是會多“關照”下Locust了。因爲對評測的結果不是很滿意,所以就乘着興致順便看了下源碼。而本文就是對Locust源碼解析的簡述。

Locust的執行流程

首先,來看下Locust的執行命令如下:

locust -f locustfile.py --host http://www.testqa.cn

那麼執行了這一條語句後,Locust究竟在後臺做了哪些事情呢?請看下面的程序執行流程。

|--Python進程
    |--主線程
        |--參數解析(-f、--host等)
        |--性能場景腳本(-f參數後的文件名)加載、分析VUser數量
        |--協程1(local、master、slave)
            |--計算各VUser的併發數佔比(按VUser的權重)
            |--生成VUser啓動列表
            |--啓動VUser協程組
            |        |--子協程1(對應一個VUser)
            |        |--...
            |        |--子協程N
            |               |--獲取VUser任務集
            |               |--循環執行任務(順序、按權重)
            |               |       |--嵌套執行子任務
            |               |--執行指定時間後停止(需設定)
            |
            |--協程組阻塞等待

接下來,我們一個個的來過一下。首先啓動進程和主線程這個不用講,所有的程序都是一樣的。然後再是參數解析,這個也是大多數程序都會提供的常規邏輯。

在解析-f參數成功之後(沒有指定-f參數則不會啓動成功),會去自動的導入該腳本模塊;再通過python的自省能力來檢查腳本中的VUser類,主要檢查繼承自Locust且帶有task_set屬性的子類;一個子類相當於一個VUser。通過-l參數則可以直接列出腳本中所有的VUser名稱且不會執行腳本。

當VUser類都檢查完畢之後,會把這些VUser類收集到一個列表中去;之後就會根據指定的啓動模式(local、no-web、master、slave)來啓動一個協程,並且會把VUser列表和解析後的命令行參數內容都作爲參數傳遞過去。

在該協程中會先計算各VUser的權重,這會影響VUser被執行的次數。具體的實現代碼如下:

    def weight_locusts(self, amount, stop_timeout = None):
        """
        Distributes the amount of locusts for each WebLocust-class according to it's weight
        returns a list "bucket" with the weighted locusts
        """
        bucket = []
        weight_sum = sum((locust.weight for locust in self.locust_classes if locust.task_set))
        for locust in self.locust_classes:
            ... # 部分代碼省略
            # create locusts depending on weight
            percent = locust.weight / float(weight_sum)
            num_locusts = int(round(amount * percent))
            bucket.extend([locust for x in xrange(0, num_locusts)])
        return bucket

代碼的主要實現邏輯是,先把所有的權重數都加起來求總和,再計算每個VUser的權重百分比;接着用總的VUser數乘以這個百分比後取整,就得到了該VUser需要啓動數量,最後把指定數量的VUser都填充到隊列中再返回。舉個例子:

VUser1.weight = 1
VUser2.weight = 2
VUser3.weight = 3
假設現在需要啓動12個VUser,那麼各VUser的數量是:
VUser1  => 1/(1+2+3)*12=2
VUser2  => 2/(1+2+3)*12=4
VUser3  => 3/(1+2+3)*12=6
經過這個函數處理之後,會得到一個如下的列表:
[VUser1, VUser1, VUser2, VUser2, VUser2, VUser2, VUser3, VUser3, VUser3, VUser3, VUser3, VUser3]

拿到這個VUser啓動列表之後,會依次隨機pop一個VUser類,然後新起一個協程來實例化它,實例之後調用它的run方法開始執行該VUser的任務內容,直到所有VUser都實例化完成。

from gevent.pool import Group
...
self.locusts = Group()
...
locust = bucket.pop(random.randint(0, len(bucket)-1))
occurence_count[locust.__name__] += 1
def start_locust(_):
    try:
        locust().run(runner=self)
    except GreenletExit:
        pass
new_locust = self.locusts.spawn(start_locust, locust)
...
if wait:
    self.locusts.join()

實例化VUser的協程會在一個協程組內,該協程組會根據外部參數確定是否阻塞主線程。

VUser的執行流程

上面介紹了Locust從啓動後,開始執行性能測試的整體流程。而在這個整體流程內其實還包含另外一個子流程,就是VUser執行任務的流程。在介紹具體的流程之前,可以先看下Locust的腳本文件樣例:

from locust import HttpLocust, TaskSet, task

class WebsiteTasks(TaskSet):
    @task
    def index(self):
        self.client.get("/")

class WebsiteUser(HttpLocust):
    task_set = WebsiteTasks
    host = "https://www.baidu.com"
    min_wait = 5000
    max_wait = 15000

這是一個最簡答的Locust的性能腳本文件,其中WebsiteUser就代表了VUser,它具備成爲VUser的2個充要條件:Locust的子類、具有task_set屬性且爲真。(HttpLocust是Locust的子類)

task_set就是該VUser要執行的請求任務集合,這個集合裏面可以有1或N個任務,還可以包含子任務集;子任務集還可以包含任務和子子任務集,所以任務集是可以嵌套的。

而VUser在實例化之後,通過調用run方法就會開始執行真正的請求任務。整體的示意流程如下:

|--VUser
    |--思考時間(默認1秒)
    |--host
    |--client(可自定義)
    |--任務集
        |--子任務集
        |--普通任務
            |--request(requests.session)
            |   |--get
                |--post
            |--check
            |--result(response.success)

首先會存儲相關的執行屬性,比如:思考時間,host等。這裏需要注意的是,Locust默認會把思考時間設置爲1秒,所以如果你不期望有思考時間,那麼你最好顯式的把min_wait和max_wait都設置爲0。

與此同時還會實例化真正的請求客戶端,以便於在後面直接可以用來發送請求,而默認Locust發送請求的客戶端其實就是requests。具體源碼如下:

import requests
...
class HttpSession(requests.Session):
...
class HttpLocust(Locust):
    client = None
    def __init__(self):
        super(HttpLocust, self).__init__()
        if self.host is None:
            raise LocustError("You must specify the base host. Either in the host attribute in the Locust class, or on the command line using the --host option.")
        
        self.client = HttpSession(base_url=self.host)   # 實例化發送請求的client

這些準備工作都完成之後,就開始實例化task_set變量對應的類,也就是樣例代碼中的WebsiteTasks類(TaskSet的子類);接着會調用TaskSet實例的run方法來執行所有的任務。

在TaskSet.run方法內,會先檢查是否有on_start方法,如果有會執行它;然後會進入一個while死循環,循環內每次會獲取一個要執行的任務並執行完成,直到執行時間結束或者主動中斷。

在獲取執行任務的邏輯中會分2種情況:一種是隨機,另一種是按順序。這主要取決於你在標註任務方法時,使用的是@task裝飾器,還是@seq_task裝飾器。除此之外,task也是有權重的概念,通常權重越高的task被執行的概率就越高。

...
    for item in six.itervalues(classDict):
        if hasattr(item, "locust_task_weight"):
            for i in xrange(0, item.locust_task_weight):
                new_tasks.append(item)
    classDict["tasks"] = new_tasks  
...
def get_next_task(self):
    return random.choice(self.tasks)

這個是隨機獲取任務的實現片段,首先在生成tasks列表的時候,會根據任務的locust_task_weight屬性值來添加同等數量的任務;之後在獲取任務的時候,直接使用隨機函數從tasks列表中獲取即可。因爲權重越高在tasks列表中出現的次數就越多,所以被隨機選到的概率就越高。

class TaskSequence(TaskSet):
    def __init__(self, parent):
        super(TaskSequence, self).__init__(parent)
        self._index = 0
        self.tasks.sort(key=lambda t: t.locust_task_order if hasattr(t, 'locust_task_order') else 1)

    def get_next_task(self):
        task = self.tasks[self._index]
        self._index = (self._index + 1) % len(self.tasks)
        return task

這個是順序獲取任務的實現,首先在實例化時就根據locust_task_order屬性對任務進行排序,之後獲取任務的get_next_task方法內會按照索引依次獲取任務,並且支持無限循環的獲取方式。

最後則是執行具體任務的邏輯,任務分3種:TaskSet實例的成員方法;子任務集;普通函數。根據任務類型的不同,會執行相應的調用操作:

  • 如果是TaskSet成員方法,會直接調用
  • 如果是子任務集,會遞歸調用子任務集的run方法
  • 如果是普通函數,會直接調用並把Locust實例作爲第一參數
    def execute_task(self, task, *args, **kwargs):
        # check if the function is a method bound to the current locust, and if so, don't pass self as first argument
        if hasattr(task, "__self__") and task.__self__ == self:
            # task is a bound method on self
            task(*args, **kwargs)
        elif hasattr(task, "tasks") and issubclass(task, TaskSet):
            # task is another (nested) TaskSet class
            task(self).run(*args, **kwargs)
        else:
            # task is a function
            task(self, *args, **kwargs)

需要注意的是:TaskSet中的子任務集是通過tasks成員變量來獲取的,不同於VUser中使用task_set成員變量。

小結

分析到這裏其實會發現Locust的邏輯還是蠻清晰的,這些主要邏輯只包含在2個文件中。而通過源碼分析也解答了我的一個疑惑,就是雖然各VUser之間是併發執行的,但是VUser內的請求確實順序執行的。

而這與瀏覽器行爲是有所差異的,現代瀏覽器通常可以支持同時6-8個併發請求。正是因爲想解開這個迷惑,所以纔有查看Locust代碼的想法;顯然它和Jmeter是一樣的,VUser內的請求是順序的。

PS:除了源碼分析,還對Locust進行了性能評測及優化實驗,感覺興趣的同學可以關注公衆號,後期會分享相關文章哦!!

獲取更多關於Python和自動化測試的文章,請掃描如下二維碼!
關注二維碼

新書推薦

Python Web自動化測試設計與實現

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章