6000字Locust入門詳解

2022-06-20 06:06:33

一、Locust 效能測試

同步Notion :

問題引言:

  1. 主流效能工具對比
  2. 為什麼要用locust進行效能測試
  3. 如何對http介面進行效能測試
  4. 如何對websocket介面進行效能測試
  5. locust效能測試報告分析
  6. locust 核心部件瞭解
  7. locust 主要用法詳解

(一). 效能測試工具

主流效能測試工具對比

  • loadrunner : 收費 昂貴
  • jmeter: 開源(二次開發) 、基於java、多執行緒 、使用gui設計用例 ,xml儲存 、錄製工具

loadRunner vs Jmeter

loadrunner筆記:

LoadRunner Jmeter
非開源工具 開源工具
C 語言 100% java 語言
檢查點(web_reg_find) 斷言
關聯(web_reg_save_param) 關聯(前置/後置處理器正則提取器)
支援IP欺騙 不支援IP 欺騙
測試結果分析圖表功能強大(資料採集) 測試結果分析圖表功能相對較弱,需依賴擴充套件外掛
重量級 輕量級
安裝複雜 安裝簡單
跨平臺
根據不同負載生成不同數量並行使用者 當前一個執行緒組只能生成一個
效能 支援web端功能測試
廣泛支援業界各種標準協定、多種平臺開發指令碼 元件

認識Locust

定義

Locust是一款易於使用的分散式負載測試工具,完全基於事件,即一個locust節點也可以在一個程序中支援數千並行使用者,不使用回撥,通過gevent使用輕量級過程(即在自己的程序內執行)。

locust: 開源 、基於python ,非多執行緒(協程)、「用例即程式碼」 ; 無錄製工具、

  • python的一個庫 ,需要python3.6 及以上環境支援
  • 可用做效能測試
  • 基於事件,用協程 進行效能測試
  • 支援 圖形 、no-gui、 分散式等多種執行方式

為什麼選擇locust

  • 基於協程 ,低成本實現更多並行

  • 指令碼增強(「測試即程式碼」)

  • 使用了requests傳送http請求

  • 支援分散式

  • 使用Flask 提供WebUI

  • 有第三方外掛、 易於擴充套件

(二) locust 基本用法

約定大於設定

1.安裝locust

pip install  locust
locust -v 

2.編寫用例

test_xxx (一般測試框架約定)

dockerfile (docker約定)

locustfile.py (locust約定)

# locustfile.py

eg: 入門範例

from locust import HttpUser, task, between


# User  ?
# function  包裝task
class MyHttpUser(HttpUser):
    wait_time = between(1, 2)  # 執行任務 等待時長   檢查點 思考時間

    @task
    def index_page(self):
        self.client.get("https://baidu.com/123")
        self.client.get("https://baidu.com/456")

    pass

總結三步:

  1. 建立locust.HttpUser 之類
  2. 為待測試用例新增@locust.task 裝飾器
  3. 使用self.client 傳送請求
  4. 指定 wait_time 屬性

3. 啟動測試

Locust官方檔案(API)解讀(全)

GUI 模式啟動 locust

啟動locust

​ 存取:http://[::1]:8089/

指標詳解:

- Number of users  模擬使用者數
- Spawn rate  : 生產數 (每秒)、   =>jmeter : Ramp-Up Period (in seconds)
- Host (e.g. http://www.example.com)  => 取決指令碼中 絕對地址
- ![](https://s3.bmp.ovh/imgs/2022/06/14/22e82961a5609f42.png)
  • WebUI 模組說明:
    • New test:點選該按鈕可對模擬的總虛擬使用者數和每秒啟動的虛擬使用者數進行編輯;
    • Statistics:類似於jmeter中Listen的聚合報告;
    • Charts:測試結果變化趨勢的曲線展示圖,分別為每秒完成的請求數(RPS)、響應時間、不同時間的虛擬使用者數;
    • Failures:失敗請求的展示介面;
    • Exceptions:異常請求的展示介面;
    • Download Data:測試資料下載模組, 提供三種型別的CSV格式的下載,分別是:Statistics、responsetime、exceptions;

命令列模式啟動 locust

locust -f locustfile.py --headless -u 500 -r 10  --host 123  -t 1h5m

​ 框架是通過命令locust執行的,常用引數有:

  • -H:指定測試的主機地址(注:會覆蓋Locust類指定的主機地址)
  • -f:指定測試指令碼地址(注:指令碼中必須包含一個Locust的衍生類)
  • --no-web:不啟動web網頁,而是直接開始執行測試,需提供屬性-c和-r
  • -u:並行的使用者數,與--no-web一起使用
  • -r:每秒啟動的使用者數,與--no-web一起使用
  • -t:執行時間(單位:秒),與--no-web一起使用
  • -L:紀錄檔級別,預設為INFO
    偵錯命令:locust -f **.py --no-web -u 1 -t 1
    執行命令:locust -f **.py

4. locust概念

  1. 父類別是個User ?

    表示要生成進行負載測試的系統的 HTTP「使用者」。

    • 效能測試 模擬真實使用者
    • 每個user相當於一個協程連結 ,進行相關係統互動操作
  2. 為什麼方法,要包裝為task

    • task 表示使用者要進行的操作

      • 存取首頁 → 登入 → 增、刪改查 → homPage
    • TaskSet : 定義使用者將執行的一組任務的類。測試任務開始後,每個Locust使用者會從TaskSet中隨機挑選 一個任務執行

    • 具體的內容: 方法的程式碼

       class MyHttpUser(HttpUser):   #使用者
          # wait_time = lambda self: random.expovariate(1)*1000
          wait_time = between(1, 2)  # 執行任務 等待時長   檢查點 思考時間
      
          @task
          def index_page(self):   # 使用者執行操作
              self.client.get("https://baidu.com/123")  #服務錯誤、網路錯誤
              self.client.get("https://baidu.com/456")
              # 斷言 、 業務錯誤
      

(三) locust 自定義壓測協定 websocket

locust 並非 http 介面測試工具 , 只是內建了 「HttpUser」 範例 ,可以測試任何協定: websocket 、socket 、mqtt (webAPP、Hybrid、Html5 、桌面瀏覽器) 、rpc

什麼是websocket協定 ?

WebSocket是一種在單個TCP連線上進行全雙工通訊的協定。 WebSocket使得使用者端和伺服器之間的資料交換變得更加簡單,允許伺服器端主動向使用者端推播資料。在WebSocket API中,瀏覽器和伺服器只需要完成一次握手,兩者之間就直接可以建立永續性的連線,並進行雙向資料傳輸。ws長連線、 和http有本質不同 ;

使用者端發起,伺服器可能接受或拒絕切換到新協定。使用者端可使用常用的協定(如HTTP / 1.1)發起請求,請求說明需要切換到HTTP / 2或甚至到WebSocket

一句話概述:協定「使用HTTP進行握手, 後使用TCP進行對談」 的全雙工協定

選擇websocket 使用者端

目前pypi , 2個ws庫

  • websockets: 提供client , 提供了server ; 使用async 語法
  • websocket_client: 僅提供client 、使用(非async)同步語法
  1. 安裝 pip install websocket_client

  2. 使用

    import asyncio
    import websockets
    
    async def hello():
        async with websockets.connect('ws://localhost:8765') as websocket:
            name = input("What's your name? ")
    
            await websocket.send(name)
            print(f" send:>>> {name}")
    
            greeting = await websocket.recv()
            print(f" recv: <<< {greeting}")
    
    asyncio.get_event_loop().run_until_complete(hello())
    

    建立WebSocketUser

    1. 建立專用使用者端連結

    2. 設計統計結果

    3. 設定成功、失敗條件

      pyfile:

    TimeoutError(10060, '[WinError 10060] 由於連線方在一段時間後沒有正確答覆或連線的主機沒有反應,連線嘗試失敗。')
    1.請求伺服器的反爬機制導致的,請求不被接受的 ; ip限制
    → 網頁存取驗證 -
    2. http的連線數超過最大限制。
    headers的Connection引數預設為keep-alive,之前所有請求的連結都一直存在,佔用了後續的連結請求
    3. 網站伺服器太差 → 修改中介軟體設定(SLB、nginx 、ApiGateway 熔斷、限流 ~
    →timeout 超時時間
    ........... 9大策略和6大指標 、
    ........... 硬體(計算機體系機構)、作業系統(OS\JVM)、檔案系統、網路通訊、資料庫系統、中介軟體(transaction、message、app)、應用程式本身

本地:netsh winsock reset → 重啟

​ 初步結果:

​ 1. RPS xxxx 左右

​ 2. 最大使用者數 xxx

​ > 表示:當前允許最大使用者數請求,但是無法全部返回結果

常見錯誤 可能分析原因
TPS波動較大 網路波動其他服務資源競爭以及垃圾回收問題
高並行下大量報錯 短連線導致的埠被完全佔用以及執行緒池最大執行緒數設定較小超時時間較短導致。
叢集類系統,各服務節點負載不均衡 SLB服務設定了對談保持
並行數不斷增加,TPS上不去,CPU使用率較低 SQL沒有建立索引/SQL語句篩選條件不明確、程式碼中設有同步鎖,高並行時出現鎖等待;
connection reset、服務重啟、timeout 引數設定、服務策略、阻塞及各種鎖導致

(四). locust 核心元件

核心元件: 2類 ,4個

  • User : 在locust中User類表示一個使用者。locust將為每一個被模擬的使用者生成一個User類範例,而我們可以在User類中定義一些常見的屬性來定義使用者的行為。

    • HttpUser
  • Task: 使用者行為

    • SequentialTaskSet
    • TaskSet:
      • tasks屬性將多個TaskSet子類巢狀在一起
      • 類中相互巢狀
      • User類中嵌入TaskSet類,作為User的子類
  • Events : locust提供的事件勾點,用於一些再執行過程中執行特定時間的操作。

重要的屬性:

  • wait_time

    ​ > 三種時間間隔表示式

  • task: 任務(使用者行為)

    • tasks :
      • 使用者類的使用者行為方法上新增@task修飾
      • 參照外部使用者行為方法時 使用tasks實現
  • weight

    測試中,存在多個User Class,預設情況下locust將為每個User Class的範例的數量是相同的。通過設定weight屬性,來控制locust為我們的User Class生成不同數量的範例。

    • locustfile07.py

(五). locust 擴充套件增強

	→  Python程式碼 

1. 錄製用例

​ 不支援 , →外掛增強

2. 資料關聯

locustfile05.py

使用變數方式進行傳遞

3. 引數化

locustfile09.py

locust引數化:引入佇列的概念 queue ,實現方式是將引數推入佇列,測試時依次取出,全部取完後 locust 會自動停止。若是使用引數迴圈壓測,需要將取出的引數再推入隊尾。

  • 變數
  • CSV
  • 佇列
  • 。。

4. 檢查點

locust預設情況下會使用預設的檢查點,比如當介面超時、連結失敗等原因是,會自動判斷失敗

原理

  • 使用self.client提供的catch_response=True`引數, 新增locust提供的ResponseContextManager類的上下文方法手動設定檢查點。
  • ResponseContextManager裡面的有兩個方法來宣告成功和失敗,分別是successfailure。其中failure方法需要我們傳入一個引數,內容就是失敗的原因。

locustfile10.py

from requests import codes
from locust import HttpUser, task, between


class DemoTest(HttpUser):
    host = 'https://www.baidu.com'
    wait_time = between(2, 15)

    def on_start(self): 
        # 通過手動傳入catch_response=True 引數手動設定檢查點
        with self.client.get('/', catch_response=True) as r: 
            if r.status_code == codes.bad:
                r.success()
            else:
                r.failure("請求百度首頁失敗了哦哦")

    @task
    def search_locust(self):
        with self.client.get('/s?ie=utf-8&wd=locust', catch_response=True) as r:
            if r.status_code == codes.ok:
                r.success()
            else:
                r.failure("搜尋locust失敗了哦哦")

5. 思考時間

  • wait_time
    • between
    • constant

6. 權重

  • 第一種:方法上指定

locust預設是隨機執行taskset裡面的task的。

權重通過在@task引數中設定,如程式碼中hello:world:item是1:3:2,實際執行時的程式碼,在user中tasks會將任務生成列表[hello,world,world,world,item,item]

@tag 裝飾器 :# locust -f locustfile06.py --tags tag1

task不止一個時,可以通過@tag給task打標籤進行分類,在執行測試時,通過--tags name執行指定帶標籤的task

# locustfile06.py
import time
from locust import HttpUser, task, between, TaskSet, tag


class QuickstartUser(TaskSet):
    wait_time = between(1, 5)
    # wait_time = constant(3)  #固定時間
    @task
    def hello_world(self):
        self.client.get("/hello")
        self.client.get("/world")

    @tag("tag1", "tag2")
    @task(3)
    def view_items(self):
        for item_id in range(10):
            #self.client.request_name="/item?id=[item_id]"#分組請求
            # 將統計的10條資訊分組到名為/item條目下
            self.client.get(f"/item?id={item_id}", name="/item")
            time.sleep(1)

    def on_start(self):
        self.client.post("/login", json={"username": "foo", "password": "bar"})

class MyUserGroup(HttpUser):
    """ 定義執行緒組 """
    tasks = [QuickstartUser]   # tasks 任務列表
    host = "http://www.baidu.com"
  • 第二種:在屬性中指定

    檔案中存在多個使用者類場景,

    • 命令列上沒有指定使用者類,Locust 將生成相同數量的每個使用者類。
    • 可以通過將它們作為命令列引數傳遞來指定要使用同一 locustfile 中的哪些使用者類: locust -f locustfile07.py QuickstartUser2
# locustfile07.py
import time
from locust import HttpUser, task, between, TaskSet, tag, constant


class QuickstartUser1(HttpUser):
    host = "http://www.baidu.com"
    wait_time = constant(4)
    weight = 3   #屬性中指定
    @task
    def hello_world(self):
        self.client.get("/hello1")
        self.client.get("/world1")

    def on_start(self):
        self.client.post("/login1", json={"username": "foo", "password": "bar"})


class QuickstartUser2(HttpUser):
    host = "http://www.baidu.com"
    wait_time = between(1, 5)
    weight = 1
    @task
    def hello_world(self):
        self.client.get("/hello2")
        self.client.get("/world2")

    def on_start(self):
        self.client.post("/login2", json={"username": "foo", "password": "bar"})


7. 集合點

什麼是集合點?

集合點用以同步虛擬使用者,以便恰好在同一時刻執行任務。在[測試計劃]中,可能會要求系統能夠承受1000 人同時提交資料,可以通過在提交資料操作前面加入集合點,這樣當虛擬使用者執行到提交資料的集合點時,就檢查同時有多少使用者執行到集合點,如果不到1000 人,已經到集合點的使用者在此等待,當在集合點等待的使用者達到1000 人時,1000 人同時去提交資料,從而達到測試計劃中的需求。

注意:框架本身沒有直接封裝集合點的概念 ,間接通過gevent並行機制,使用gevent的鎖來實現

semaphore是一個內建的計數器:
每當呼叫acquire()時,內建計數器-1
每當呼叫release()時,內建計數器+1
計數器不能小於0,當計數器為0時,acquire()將阻塞執行緒直到其他執行緒呼叫release()

兩步驟:

  1. all_locusts_spawned 建立勾點函數
  2. 將locust範例掛載到監聽器 events.spawning_complete.add_listener
  3. Locust範例準備完成時觸發

範例程式碼:

# locustfile08.py
import os

from locust import HttpUser, TaskSet, task,between,events
from gevent._semaphore import Semaphore


all_locusts_spawned = Semaphore()
all_locusts_spawned.acquire()# 阻塞執行緒


def on_hatch_complete(**kwargs):
    """
    Select_task類的勾點方法
    :param kwargs:
    :return:
    """
    all_locusts_spawned.release() # # 建立勾點方法


events.spawning_complete.add_listener(on_hatch_complete) #掛在到locust勾點函數(所有的Locust範例產生完成時觸發)


n = 0
class UserBehavior(TaskSet):

    def login(self):
        global n
        n += 1
        print("%s個虛擬使用者開始啟動,並登入"%n)

    def logout(self):
        print("退出登入")



    def on_start(self):
        self.login()

        all_locusts_spawned.wait() # 同步鎖等待

    @task(4)
    def test1(self):
      

        url = '/list'
        param = {
            "limit":8,
            "offset":0,
        }
        with self.client.get(url,params=param,headers={},catch_response = True) as response:
            print("使用者瀏覽登入首頁")

    @task(6)
    def test2(self):
        

        url = '/detail'
        param = {
            'id':1
        }
        with self.client.get(url,params=param,headers={},catch_response = True) as response:
            print("使用者同時執行查詢")

    @task(1)
    def test3(self):
        """
        使用者檢視查詢結果
        :return:
        """

        url = '/order'
        param = {
            "limit":8,
            "offset":0,
        }
        with self.client.get(url,params=param,headers={},catch_response = True) as response:
            print("使用者檢視查詢結果")

    def on_stop(self):
        self.logout()


class WebsiteUser(HttpUser):
    host = 'http://www.baidu.com'
    tasks = [UserBehavior]

    wait_time = between(1, 2)

if __name__ == '__main__':
    os.system("locust -f locustfile08.py")

8. 分散式

Locust 通過協程實現單機大量並行,但對多核 CPU 的支援並不好,可通過在一臺機器上啟動多個 Locust 範例實現對多核 CPU 的利用(單機分散式) ,同理:單臺計算機不足以模擬所需的使用者數量,Locust 也支援在多臺計算機上進行分散式負載測試。

一種是單機設定master和slave模式,另外一種是有多個機器,其中一個機器設定master,其它機器設定slave節點

注意:主節點master計算機和每個work工作節點計算機都必須具有 Locust 測試指令碼的副本。

單機主從模式

其中 slave 的節點數要小於等於本機的處理器數

步驟: 以單臺計算機為例(既當做主控機,也當做工作機器)

  1. Step1:→ 啟動locust master節點

    locust -f locustfile07.py --master

  2. Step2:→ 每個工作節點 locust -f locustfile07.py --worker

多機主從模式

  1. 選擇其中一臺電腦,啟動master節點,因為主節點無法操作別的節點,所以必須在其它機器上啟動從屬Locust節點,後面跟上--worker引數,以及 --master-host(指定主節點的IP /主機名)。
locust -f locustfile07.py --master
  1. 其它機器上(環境和主節點環境一致,都需要有locust的執行環境和指令碼),啟動 slave 節點,設定 --master-host
locust -f locustfile.py --worker --master-host=192.168.x.xx

更多引數介紹

  • --master

將 locust 設定為 master 模式。Web 介面將在此節點上執行。

  • --worker

將locuster設定為worker模式。

  • --master-host= X. X. X. X

可選擇與-- worker一起使用,以設定主節點的主機名/IP (預設值為127.0.0.1)

  • --master-port

可選地與-- worker一起用於設定主節點的埠號(預設值為5557)。

  • -master-bind-host= X. X. X. X
    可選擇與--master一起使用。 確定主節點將繫結到的網路介面。 預設為*(所有可用介面)。

  • --master-bind-port=5557
    可選擇 與--master一起使用。 確定主節點將偵聽的網路埠。 預設值為5557。

  • --expect-workers= X
    在使用--headless啟動主節點時使用。 然後主節點將等待,直到 X worker節點已經連線,然後測試才開始。

9. 資源監控

10. docker 執行locust

拉取映象:

docker pull locustio/locust

執行容器:

docker run -p 8089:8089 -v $PWD:/mnt/locust locustio/locust -f /mnt/locust/locustfile.py

Docker Compose:

version: '3'

services:
  master:
    image: locustio/locust
    ports:
     - "8089:8089"
    volumes:
      - ./:/mnt/locust
    command: -f /mnt/locust/locustfile.py --master -H http://master:8089
  
  worker:
    image: locustio/locust
    volumes:
      - ./:/mnt/locust
    command: -f /mnt/locust/locustfile.py --worker --master-host master

11. 高效能 FastHttpUser

Locust 的預設 HTTP 使用者端使用python-requests。如果您計劃以非常高的吞吐量執行測試並且執行 Locust 的硬體有限,那麼它有時效率不夠。Locust 還附帶FastHttpUser使用geventhttpclient代替。它提供了一個非常相似的 API,並且使用的 CPU 時間顯著減少,有時將給定硬體上每秒的最大請求數增加了 5 到 6 倍。

在相同的並行條件下使用FastHttpUser能有效減少對負載機的資源消耗從而達到更大的http請求。


比對結果如下:

locustfile11.py

HttpUser:

對比:FastHttpUser

(六)附外

0. 程序、執行緒、協程區別

程序:程序是具有一定獨立功能的程式關於某個資料集合上的一次執行活動,程序是系統進行資源分配和排程的一個獨立單位。每個程序都有自己的獨立記憶體空間,不同程序通過程序間通訊來通訊。由於程序比較重量,佔據獨立的記憶體,所以上下文程序間的切換開銷(棧、暫存器、虛擬記憶體、檔案控制程式碼等)比較大,但相對比較穩定安全。

執行緒: 執行緒是程序的一個實體,是CPU排程和分派的基本單位,它是比程序更小的能獨立執行的基本單位.執行緒自己基本上不擁有系統資源,只擁有一點在執行中必不可少的資源(如程式計數器,一組暫存器和棧),但是它可與同屬一個程序的其他的執行緒共用程序所擁有的全部資源。執行緒間通訊主要通過共享記憶體,上下文切換很快,資源開銷較少,但相比程序不夠穩定容易丟失資料。

協程: 協程是一種使用者態的輕量級執行緒,協程的排程完全由使用者控制。協程擁有自己的暫存器上下文和棧。協程排程切換時,將暫存器上下文和棧儲存到其他地方,在切回來的時候,恢復先前儲存的暫存器上下文和棧,直接操作棧則基本沒有核心切換的開銷,可以不加鎖的存取全域性變數,所以上下文的切換非常快。

  1. 程序與執行緒比較:

    • 程序用於獨立的地址空間 ;執行緒依附於程序(先有程序後有執行緒) ,可以共用程序的地址空間

    • 程序之間不共用全域性變數 , 執行緒之間共用全域性變數

    • 執行緒是cpu 排程的基本單位; 程序是作業系統分配資源資源的最小單位

    • 程序之間相互獨立 ,都可並行執行 (核數大於執行緒數)

    • 多程序執行其中某個程序掛掉不會影響其他程序執行, 多執行緒開發中當前程序掛掉 依附於當前程序中的多執行緒進行銷燬

  2. 執行緒與協程比較

    • 一個執行緒可包含多個協程 ,一個程序也可單獨擁有多個協程

    • 執行緒、程序 同步機制 ,協程非同步

    • 協程保留最近一次呼叫時狀態,每次過程重入相當於喚醒

    • 執行緒的切換由作業系統負責排程,協程由使用者自己進行排程

    • 資源消耗:執行緒的預設Stack大小是1M,而協程更輕量,接近1K。

執行緒: 輕量級的程序 協程: 輕量級的執行緒 (使用者態)

更多查閱:

1. 更多命令

如果Locust檔案位於與locustfile.py在不同的子目錄/或者檔名不一樣,則使用引數-f+檔名:
$ locust -f locust_files/my_locust_file.py
要在多個程序中執行Locust,我們可以通過指定--master:
$ locust -f locust_files/my_locust_file.py --master
啟動任意數量的從屬程序:
$ locust -f locust_files/my_locust_file.py --slave
如果要在多臺機器上執行Locust,則在啟動從屬伺服器時還必須指定主伺服器主機(在單臺計算機上執行Locust時不需要,因為主伺服器主機預設為127.0.0.1):
$ locust -f locust_files/my_locust_file.py --slave --master-host=192.168.0.100
還可以在組態檔(locust.conf或~/.locust.conf)或以LOCUST_字首的env vars中設定引數
例如:(這將與上一個命令執行相同的操作)
$ LOCUST_MASTER_HOST=192.168.0.100 locust

注意:要檢視所有可用選項,請鍵入:locust —help

2. 學習路線

https://docs.locust.io/en/stable/what-is-locust.html

3. WebSocket與HTTP的關聯和差異

相同:

  1. 建立在TCP之上,通過TCP協定來傳輸資料。
  2. 都是可靠性傳輸協定
  3. 都是應用層協定。

不同:

  1. WebSocket是HTML5中的協定,支援持久連線,HTTP不支援持久連線
  2. HTTP是單向協定,只能由使用者端發起,做不到伺服器主動向使用者端推播資訊

4. 延伸閱讀&知識庫

參考資料:https://github.com/locustio/locust/wiki/Articles