6000字Locust入門詳解

一、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("//baidu.com/123")
        self.client.get("//baidu.com/456")

    pass

總結三步:

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

3. 啟動測試

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

GUI 模式啟動 locust

啟動locust

​ 訪問://[::1]:8089/

指標詳解:

- Number of users  模擬用戶數
- Spawn rate  : 生產數 (每秒)、   =>jmeter : Ramp-Up Period (in seconds)
- Host (e.g. //www.example.com)  => 取決腳本中 絕對地址
- ![](//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("//baidu.com/123")  #服務錯誤、網路錯誤
              self.client.get("//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 = '//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 = "//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 = "//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 = "//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 = '//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 //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. 學習路線

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

3. WebSocket與HTTP的關聯和差異

相同:

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

不同:

  1. WebSocket是HTML5中的協議,支援持久連接,HTTP不支援持久連接
  2. HTTP是單向協議,只能由客戶端發起,做不到伺服器主動向客戶端推送資訊

4. 延伸閱讀&知識庫

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

Tags: