[效能測試] locust學習-基礎篇

波小藝發表於2022-04-01

在本文中,我將介紹一個名為Locust的效能測試工具。我將從Locust的功能特性出發,結合例項對Locust的使用方法進行介紹。

概述

Locust主要有以下的功能特性:

  • 在Locust測試框架中,測試場景是採用純Python指令碼進行描述的。不需要笨重的UI和臃腫的XML

  • 對於最常見的http(s)協議的系統,Locust採用Python的requests作為客戶端,使得指令碼編寫大大簡化。除了http(s)協議的系統之外,Locust還支援測試其他系統或協議,只需要我們為測試的內容編寫一個客戶端就可以了。

  • 在模擬併發方面,Locust是基於事件驅動,使用gevent提供的非阻塞IO和coroutine來實現網路層的併發請求,使得單個程式處理千個併發使用者。再加上Locust支援分散式,使得支援數十萬併發使用者不是夢。

  • Locust有一個簡單幹淨的Web介面,可以實時顯示測試進度。在測試執行期間,可以隨時更改負載。它還可以在沒有UI的情況下執行,便於用於CI/CD測試。

我們都知道服務端效能測試工具最核心的部分是壓力發生器,而壓力發生器的核心要點有兩個:一是真實模擬使用者操作,二是模擬有效併發。

  • 相比 LoadRunner、Jmeter 這種壓測工具(通過執行緒對應一個使用者/併發的方式產生負載)而言,Locust能夠以比較低的成本產生負載(LoadRunner 一個 Vuser 佔用記憶體數M甚至數十MB,而 Jmeter 最高併發數受限於 JVM 大小)。
  • 支援BDD(行為驅動開發)編寫任務以及執行任務,能夠更好地模擬使用者真實的操作流程。

指令碼結構介紹

下面通過一個簡單的案例學習一下locust的基本使用:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time:2022/3/26 9:38 上午
# @Author:boyizhang
from locust import TaskSet, HttpUser, task, run_single_user


class BaiduTaskSet(TaskSet):
    """
    任務集
    """

    @task
    def search_by_key(self):
        self.client.get('/')

class BaiduUser(HttpUser):
    """
    - 會產生併發使用者例項
    - 產生的使用者例項會依據規則去執行任務集

    """

    # 定義的任務集
    tasks = [BaiduTaskSet,]
    host = 'http://www.baidu.com'


if __name__ == '__main__':
    # debug:除錯任務是否可以跑通
    run_single_user(BaiduUser)

從指令碼中可以看出,指令碼主要包含兩個類:BaiduTaskSet與BaiduUser,BaiduTaskSet繼承TaskSet,BaiduUser繼承HttpUser(HttpUser繼承User)。

BaiduTaskSet是定義使用者執行的任務細節,而BaiduUser(User)則是負責生成使用者例項去執行這些任務。

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

HttpUser(User)

User類中,具有一個client屬性,它對應著虛擬使用者作為客戶端所具備的請求能力。

  • 通常情況下,我們不會直接使用User類,因為其client屬性沒有繫結任何方法。
  • 在使用User類時,需要先繼承User類,然後在繼承子類中的client屬性中繫結客戶端的實現類。

對於常見的HTTP(S)協議,我們可以繼承HttpUser類。HttpUser 是最常用的使用者類。它新增了一個client屬性,用於發出 HTTP 請求。

  • client屬性繫結了HttpSession類,而HttpSession又繼承自requests.Session。因此在測試HTTP(S)的Locust指令碼中,我們可以通過client屬性來使用Python requests庫的所有方法,呼叫方式也與requests完全一致。
  • 由於requests.Session的使用,因此client的方法呼叫之間就自動具有了狀態記憶的功能。常見的場景就是,在登入系統後可以維持登入狀態的Session,從而後續HTTP請求操作都能帶上登入態。

而對於HTTP(S)以外的協議,我們同樣可以使用Locust進行測試,

  • 雖然Locust 僅內建了對 HTTP/HTTPS 的支援,但它可以擴充套件到測試幾乎任何系統。只需要基於User類實現client即可。
  • 我們可以使用locust-plugins,這個是第三方維護的庫,支援Kafkamqttwebdriver等測試。

TaskSet

介紹

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

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

  • 採用@task裝飾器
from locust import TaskSet, task, constant

class MyTaskSet(TaskSet):
    def on_start(self):
        """
        使用者開始執行此任務集時觸發
        :return:
        """

        print("task is running")
    def on_stop(self):
        """
        使用者停止執行此任務集時觸發
        :return:
        """

        print(("task is stopped"))
    @task(2)
    def task1(self):
        print("User instance (%r) executing my_task1" % self)
    @task
    def task2(self):
        print("User instance (%r) executing my_task2" % self)   
  • 採用tasks屬性

可以使用list,也可以使用dict。如果使用list,則權重為1:1

from locust import User, task, constant

class MyTaskSet(TaskSet):

    def on_start(self):
        """
        使用者開始執行此任務集時觸發
        :return:
        """

        print("task is running")
    def on_stop(self):
        """
        使用者停止執行此任務集時觸發
        :return:
        """

        print(("task is stopped"))
        
    def task1(self):
        print("User instance (%r) executing my_task1" % self)
    def task2(self):
        print("User instance (%r) executing my_task2" % self)   
    tasks = {task1:2, task2:1}
    # 如果是列表的形式,那執行任務的許可權均為1:1
    # tasks = [task1, task2]

在如上兩種定義任務資訊的方式中,均設定了權重屬性,即執行task1的頻率是task2的兩倍。若不指定執行任務的權重,則相當於比例為1:1。

on_start()on_stop()方法,分別重寫父類的TaskSet的on_start()on_stop()。分別在使用者開始和停止執行此任務集時觸發。

TaskSet 巢狀-真實模擬使用者場景

TaskSet 類的任務可以是其他 TaskSet 類,允許它們巢狀任意數量的級別。這使我們能夠以更真實的方式定義模擬使用者的行為。

class NestTaskSet(TaskSet):
    @task(3)
    def get_index_page(self):
        print("get_Index_page")
    @task(7)
    class get_forum_page(TaskSet):
        @task(3)
        def get_view_detail(self):
            print('get_view_detail')
        @task(1)
        def create_forum(self):
            print('create_forum')
        @task(1)
        def stop(self):
            print('exit forum page')
            self.interrupt()


    @task(1)
    def get_info(self):
        print('get info')
from locust import HttpUser, TaskSet, task, between

class ForumThread(TaskSet):
    pass

class ForumPage(TaskSet):
    # wait_time can be overridden for individual TaskSets
    wait_time = between(10300)
    
    # TaskSets can be nested multiple levels
    tasks = {
        ForumThread:3
    }
    
    @task(3)
    def forum_index(self):
        pass
    
    @task(1)
    def stop(self):
        self.interrupt()

class AboutPage(TaskSet):
    pass

class WebsiteUser(HttpUser):
    wait_time = between(515)
    
    # We can specify sub TaskSets using the tasks dict
    tasks = {
        ForumPage: 20,
        AboutPage: 10,
    }
    
    # We can use the @task decorator as well as the  
    # tasks dict in the same Locust/TaskSet
    @task(10)
    def index(self):
        pass

關於 TaskSet 需要特別注意的是,它們永遠不會停止執行其任務,需要手動呼叫該TaskSet.interrupt()方法來停止執行。

在上面的案例一中,如果沒有stop方法,那麼一旦使用者進入了get_forum_page之後,就無法從此類中跳出來了,只會執行get_forum_page下的task。

指令碼編寫

案例1:

百度搜尋流量比較大,現在想針對百度的搜尋介面進行壓測,如何寫壓測指令碼呢?

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time:2022/3/27 5:15 下午
# @Author:boyizhang
import random

from locust import TaskSet, task, FastHttpUser, HttpUser,run_single_user
from locust.clients import ResponseContextManager
from locust.runners import logger



class BaiduTask(TaskSet):
    @task
    def search_by_baidu(self):

        wd = random.choice(self.user.share_data)
        path = f"/s?wd={wd}"

        with self.client.get(path,catch_response=Trueas res:
        # 如果想同一介面不同引數放在同一組,可用下面這種方式
        # with self.client.get(path,catch_response=True,name="/s?wd=[wd]") as res:
            res: ResponseContextManager
            # 如果不滿足,則標記為failure
            if res.status_code != 200:
                res.failure(res.text)


    def on_start(self):
        logger.info('hello')
    def on_stop(self):
        logger.info('goodbye')

class Baidu(HttpUser):
    host = 'https://www.baidu.com'

    tasks = [BaiduTask,]
    share_data = ['波小藝','boxiaoyi','效能測試','locust']

if __name__ == '__main__':
    run_single_user(Baidu)

在案例當中,通過在HttpUser的子類中定義一個列表share_data,在執行任務集時,可以隨機選取列表share_data中的一個元素作為介面入參。

指令碼執行

揭開了Locust的第一層神祕的面紗後:指令碼結構介紹,下面繼續結合案例講下Locust的執行。 負載測試啟動時,會按照使用者定義的Number of users以及Spawn rate生成使用者例項。

  • 使用者例項執行指定的TaskSet
  • 使用者例項將選中TaskSet 的任務之一去執行
  • 執行完畢之後執行緒使使用者處於休眠並持續指定時間(使用者定義的 wait_time )
  • 休眠結束之後,再從 TaskSet 的任務中選擇一個新任務執行
  • 再次等待,依此類推。

以上就是Locust大致的執行流程。

執行方式

命令列執行

可以通過locust -h檢視Locust的命令列引數。也可以通過檢視:Locust命令列引數解析 獲取具體用法。

$ locust -f example.py --headless --users 10 --spawn-rate 1 -H http://www.boxiaoyi.com -t 300s

  • -f: 指定執行的Locust指令碼
  • --headless:禁用 Web 介面(使用終端)),並立即開始測試。使用 -u 和 -t 控制使用者數和執行時間
  • -u/--users:併發 Locust 使用者的峰值數量。主要--headless--autostart 一起使用。可以在測試期間通過鍵盤輸入 w、W(生成 1、10 個使用者)和 s、S(停止 1、10 個使用者)來更改
  • -r/--spawn-rate:以(每秒使用者數)生成使用者的速率。主要與-–headless-–autostart 一起使用
  • -t/--run_time:在指定的時間後停止,例如(300s、20m、3h、1h30m 等)。僅與 --headless--autostart 一起使用。預設永遠執行。
  • --autostart: 立即開始測試(不禁用 Web UI)。使用 -u 和 -t 控制使用者數和執行時間。可同時使用終端以及web ui頁面觀察

由於命令列執行的支援,加上引數的支援,可以進行整合到CI/CD的流程當中,不過有一點需要注意的是,需要指定--run_time,否則將無法自動退出該流程。

web ui介面執行

$ locust -f example.py

啟動 Locust 後,開啟瀏覽器並將其指向 http://localhost:8089。會展示以下頁面: 點選start swarming,即可開始負載測試。

執行策略

單機執行

單機執行,即執行的時候對應一個Locust程式。可參考上面的案例

分散式執行

執行 Locust 的單個程式可以模擬相當高的吞吐量。對於一個簡單的測試計劃,它應該能夠每秒發出數百個請求,如果使用FastHttpUser則數千個。但是如果你的測試計劃很複雜或者你想執行更多的負載,你就需要擴充套件到多個程式,甚至可能是多臺機器。

我們可以使用--master標誌Master啟動一個Locust例項,並使用--worker標誌Worker啟動多個工作例項。

  • 如果worker程式與master程式在同一臺機器上,建議worker的數量不要超過機器的CPU核數。一旦超過,發壓效果可能不增反減。
  • 如果worker程式與master程式不在同一臺機器上,可以使用--master-host將它們指向執行master程式的機器的IP/主機名。
  • 在Locust在執行分散式時,master和worker機器例項上一定要有locusfile的副本。
  • master例項執行Locust的Web介面,並告訴workers何時產生/停止使用者。worker執行使用者並將統計資料傳送回master例項。master例項本身不執行任何使用者。

注意點

  • 因為Python不能完全利用每個程式一個以上的核心(參見GIL),所以通常應該在Worker機器上為每個處理器核心執行一個Worker例項,以便利用它們的所有計算能力。
  • 對於每個Worker例項可以執行的使用者數量幾乎沒有限制。只要使用者的總請求率/RPS不太高,Locust/gevent就可以在每個程式中執行數千甚至數萬個使用者。
  • 如果Locust即將耗盡CPU資源,它將記錄一個警告。

如何使用分散式?

  • 開啟Master例項:
locust -f my_locustfile.py --master
  • 然後在每個Worker上(xxx為master例項的IP,或者如果您的Worker與主計算機在同一臺計算機上,則完全省略該引數):
locust -f my_locustfile.py --worker --master-host=xxx

其他引數:

  • --master:將 locust 設定為 master 模式。Web 介面將在此節點上執行。
  • --worker:將蝗蟲設定為worker模式。
  • --master-host=X.X.X.X:可選擇與--worker設定master節點的主機名/IP 一起使用(預設為 127.0.0.1)
  • --master-port=5557:可選地與--worker設定master節點的埠號一起使用(預設為 5557)。
  • --master-bind-host=X.X.X.X:可選地與--master. 確定master節點將繫結到的網路介面。預設為 *(所有可用介面)。
  • --master-bind-port=5557:可選地與--master. 確定master節點將偵聽的網路埠。預設為 5557。
  • --expect-workers=X:在使用 啟動主節點時使用--headless。然後,主節點將等待 X 個worker節點連線,然後再開始測試。

使用docker執行分散式

version: '3'

services:
  master:
    image: locustio/locust
    ports:
      - 8089:8089
      - 5557:5557
    volumes:
      - ./:/myexample
    command: -f /myexample/locustfile.py WebsiteUser --master -H http://www.baidu.com

  worker:
    image: locustio/locust
    links:
      - master
    volumes:
      - ./:/myexample
    command: -f /myexample/locustfile.py WebsiteUser --worker --master-port=5557

啟動

$ docker-compose -d -f myexample/run_locust_by_docker.yml up --scale worker=3

結果分析

Locust在執行測試的過程中,我們可以在web介面中實時地看到結果執行情況。主要展示了以下指標:併發數、RPS、失敗率、響應時間 latency,另外還展示了部分指標的趨勢圖,如案例1-圖3。

執行案例1locust -f locustfile.py,通過Web頁面,可以看到以下結果:

案例1-圖1
案例1-圖1
案例1-圖2
案例1-圖2
案例1-圖3
案例1-圖3

相關文章