在本文中,我將介紹一個名為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,這個是第三方維護的庫,支援 Kafka
、mqtt
,webdriver
等測試。
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(10, 300)
# 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(5, 15)
# 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=True) as 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。
執行案例1:locust -f locustfile.py
,通過Web頁面,可以看到以下結果: