Locust 是一个易于使用、可编写脚本和可扩展的性能测试工具。官方文档
$ pip3 install locust
$ locust -V
这个用户将一次又一次地向/hello 和/world 发出 HTTP 请求。
编写 locustfile.py
from locust import HttpUser, task
class HelloWorldUser(HttpUser):
@task
def hello_world(self):
self.client.get("/hello")
self.client.get("/world")
启动 locust
# 在 locustfile.py 所在目录
$ locust
[2021-07-24 09:58:46,215] .../INFO/locust.main: Starting web interface at http://*:8089
[2021-07-24 09:58:46,285] .../INFO/locust.main: Starting Locust 2.8.6
访问 UI 界面【可选】
只使用命令行输出测试报告,不启用 UI 界面
$ locust --headless --users 10 --spawn-rate 1 -H http://your-server.com
[2021-07-24 10:41:10,947] .../INFO/locust.main: No run time limit set, use CTRL+C to interrupt.
[2021-07-24 10:41:10,947] .../INFO/locust.main: Starting Locust 2.8.6
[2021-07-24 10:41:10,949] .../INFO/locust.runners: Ramping to 10 users using a 1.00 spawn rate
Name # reqs # fails | Avg Min Max Median | req/s failures/s
----------------------------------------------------------------------------------------------
GET /hello 1 0(0.00%) | 115 115 115 115 | 0.00 0.00
GET /world 1 0(0.00%) | 119 119 119 119 | 0.00 0.00
----------------------------------------------------------------------------------------------
Aggregated 2 0(0.00%) | 117 115 119 117 | 0.00 0.00
[2021-07-24 10:44:42,484] .../INFO/locust.runners: All users spawned: {"HelloWorldUser": 10} (10 total users)
参数 -u -r
# 最初只有 1 个用户(140205318976400)在重复执行任务
# 大约 1s 左右,产生了第 2 个用户(140205319277392),此时是两个用户在重复执行任务
# 再过 1s 左右,产生了第 3 个用户(140205318975696),此后是三个用户在重复执行任务
# 至此,达到了 -u 3 指定的高峰期用户数量,对应日志:All users spawned: {"RequestMilvusUser": 3} (3 total users)
$ locust -u 3 -r 1 --headless
[2022-04-17 18:23:47,604] 192.168.1.5/INFO/locust.runners: Ramping to 3 users at a rate of 1.00 per second
[2022-04-17 18:23:47,731] 192.168.1.5/INFO/root: id(User instance): 140205318976400, duration: 125.33854100000002, resp: (Status(code=0, message='Add vectors successfully!'), [1514942100066791424])
...
[2022-04-17 18:23:48,622] 192.168.1.5/INFO/root: id(User instance): 140205318976400, duration: 33.88720899999997, resp: (Status(code=0, message='Add vectors successfully!'), [1514942100066791424])
[2022-04-17 18:23:48,645] 192.168.1.5/INFO/root: id(User instance): 140205319277392, duration: 40.38904100000007, resp: (Status(code=0, message='Add vectors successfully!'), [1514942100066791424])
[2022-04-17 18:23:48,655] 192.168.1.5/INFO/root: id(User instance): 140205318976400, duration: 32.91200000000005, resp: (Status(code=0, message='Add vectors successfully!'), [1514942100066791424])
[2022-04-17 18:23:48,681] 192.168.1.5/INFO/root: id(User instance): 140205319277392, duration: 35.22329199999996, resp: (Status(code=0, message='Add vectors successfully!'), [1514942100066791424])
...
[2022-04-17 18:23:49,605] 192.168.1.5/INFO/locust.runners: All users spawned: {"RequestMilvusUser": 3} (3 total users)
[2022-04-17 18:23:49,631] 192.168.1.5/INFO/root: id(User instance): 140205318976400, duration: 36.88016599999999, resp: (Status(code=0, message='Add vectors successfully!'), [1514942100066791424])
[2022-04-17 18:23:49,635] 192.168.1.5/INFO/root: id(User instance): 140205319277392, duration: 33.87429200000014, resp: (Status(code=0, message='Add vectors successfully!'), [1514942100066791424])
[2022-04-17 18:23:49,642] 192.168.1.5/INFO/root: id(User instance): 140205318975696, duration: 37.08516600000023, resp: (Status(code=0, message='Add vectors successfully!'), [1514942100066791424])
[2022-04-17 18:23:49,672] 192.168.1.5/INFO/root: id(User instance): 140205318976400, duration: 41.167166000000144, resp: (Status(code=0, message='Add vectors successfully!'), [1514942100066791424])
# 最初就会产生 3 个用户(140272847696080、140272847696272、140272847696784),它们三个都在重复执行任务
# 至此,达到了 -u 3 指定的高峰期用户数量,对应日志:All users spawned: {"RequestMilvusUser": 3} (3 total users)
$ locust -u 3 -r 3 --headless
[2022-04-17 18:15:17,621] 192.168.1.5/INFO/locust.runners: Ramping to 3 users at a rate of 3.00 per second
[2022-04-17 18:15:17,621] 192.168.1.5/INFO/locust.runners: All users spawned: {"RequestMilvusUser": 3} (3 total users)
[2022-04-17 18:15:17,706] 192.168.1.5/INFO/root: id(User instance): 140272847696080, duration: 83.56029100000006, resp: (Status(code=0, message='Add vectors successfully!'), [1514942100066791424])
[2022-04-17 18:15:17,707] 192.168.1.5/INFO/root: id(User instance): 140272847696272, duration: 82.622417, resp: (Status(code=0, message='Add vectors successfully!'), [1514942100066791424])
[2022-04-17 18:15:17,711] 192.168.1.5/INFO/root: id(User instance): 140272847696784, duration: 86.21625000000004, resp: (Status(code=0, message='Add vectors successfully!'), [1514942100066791424])
[2022-04-17 18:15:17,740] 192.168.1.5/INFO/root: id(User instance): 140272847696080, duration: 34.20625, resp: (Status(code=0, message='Add vectors successfully!'), [1514942100066791424])
[2022-04-17 18:15:17,745] 192.168.1.5/INFO/root: id(User instance): 140272847696272, duration: 38.10945799999998, resp: (Status(code=0, message='Add vectors successfully!'), [1514942100066791424])
[2022-04-17 18:15:17,750] 192.168.1.5/INFO/root: id(User instance): 140272847696784, duration: 38.60574999999999, resp: (Status(code=0, message='Add vectors successfully!'), [1514942100066791424])
Demo
import time
from locust import HttpUser, task, between
class QuickstartUser(HttpUser):
wait_time = between(1, 5)
@task
def hello_world(self):
self.client.get("/hello")
self.client.get("/world")
@task(3)
def view_items(self):
for item_id in range(10):
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"})
QuickstartUser
wait_time
@task
self.client
name=“/item”
on_startup
User 类 & 类属性
一个用户类代表一个用户,Locust 将为每个被模拟的用户产生一个 User 类的实例。
wait_time
例如,下面的 User 类将休眠一秒钟,然后两秒钟,然后三秒钟,等等。
class MyUser(User):
last_wait_time = 0
def wait_time(self):
self.last_wait_time += 1
return self.last_wait_time
...
weight & fixed_count
如果 locustfile 中存在多个用户类,并且命令行上没有指定任何用户类,Locust 将为每个用户类生成相同数量的用户。你也可以通过传递命令行参数来指定在同一个 locustfile 中使用哪个用户类。
$ locust -f locust_file.py WebUser MobileUser
weight 属性可以用于调整不同用户类的占比。
class WebUser(User):
weight = 3
...
class MobileUser(User):
weight = 1
...
fixed_count 属性用于指定用户数量的精确值,此时 weight 属性将被忽略户。这些用户会首先产生。
class AdminUser(User):
wait_time = constant(600)
fixed_count = 1
@task
def restart_app(self):
...
class WebUser(User):
...
host
tasks
environment
environment 是对用户运行环境的引用,用来与环境或其中包含的 runner 进行交互。
例如,在任务方法中停止 runner,如果正在运行的是一个独立的 locust 实例,这将停止整个运行。如果是在 worker 节点上运行,则将停止该特定节点。
self.environment.runner.quit()
on_start & on_stop
Tasks
当性能测试启动时,将为每个模拟用户创建一个 User 类的实例,并且它们将在自己的绿色线程中运行。当这些用户运行时,他们选择一些任务执行,睡一会儿,然后再选择一个新的任务,如此反复。
@task 装饰器
为 User 声明任务的最简单方法是使用 @task 装饰器。
@task 接受一个可选的 weight 参数,用于指定任务的执行比率。在下面的例子中,task2被选中的几率是 task1的两倍:
from locust import User, task, between
class MyUser(User):
wait_time = between(5, 15)
@task(3)
def task1(self):
pass
@task(6)
def task2(self):
pass
User.tasks
@tag 装饰器
from locust import User, constant, task, tag
class MyUser(User):
wait_time = constant(1)
@tag('tag1')
@task
def task1(self):
pass
@tag('tag1', 'tag2')
@task
def task2(self):
pass
@tag('tag3')
@task
def task3(self):
pass
@task
def task4(self):
pass
Events
如果您想运行一些设置代码作为测试的一部分,那么通常将其放在 locustfile 的模块级就足够了,但有时您需要在运行过程中的特定时间做一些事情。为此,Locust 提供了事件挂钩。
test_start & test_stop
如果您需要在性能测试的开始或停止时运行一些代码,则应该使用 test_start 和 test_stop 事件。您可以在 locustfile 的模块级设置这些事件的侦听器:
from locust import events
@events.test_start.add_listener
def on_test_start(environment, **kwargs):
print("A new test is starting")
@events.test_stop.add_listener
def on_test_stop(environment, **kwargs):
print("A new test is ending")
init
init 事件在每个 Locust 进程开始时触发。这在分布式模式中特别有用,因为每个 worker 进程(不是每个用户)都需要一次机会来进行一些初始化。
例如,假设你有一个全局状态,所有产生的用户都需要使用这个全局状态:
from locust import events
from locust.runners import MasterRunner
@events.init.add_listener
def on_locust_init(environment, **kwargs):
if isinstance(environment.runner, MasterRunner):
print("I'm on master node")
else:
print("I'm on a worker or standalone node")
其他事件请参考: extending locust using event hooks
HttpUser 类
HttpUser 是最常用的用户,它添加了一个用于发出 HTTP 请求的 client 属性。
client / HttpSession
例如,发出一个 POST 请求,查看响应并隐式重用我们为第二个请求获得的任何会话 cookie
response = self.client.post("/login", {"username":"testuser", "password":"secret"})
print("Response status code:", response.status_code)
print("Response text:", response.text)
response = self.client.get("/my-profile")
验证响应
如果 HTTP 响应代码是 OK 的(< 400) ,那么请求被认为是成功的,但是对响应进行一些额外的验证通常是有用的。
通过使用 catch_response 参数、 with-statement 和对 response.failure ()的调用,可以将请求标记为 failed
with self.client.get("/", catch_response=True) as response:
if response.text != "Success":
response.failure("Got wrong response")
elif response.elapsed.total_seconds() > 0.5:
response.failure("Request took too long")
你也可以将一个请求标记为成功,即使响应代码不好:
with self.client.get("/does_not_exist/", catch_response=True) as response:
if response.status_code == 404:
response.success()
您甚至可以通过抛出异常,然后在 with-block 外捕获异常,从而完全避免记录请求。或者你可以抛出一个 Locust 异常,就像下面的例子一样,然后让 Locust 捕捉它。
from locust.exception import RescheduleTask
...
with self.client.get("/does_not_exist/", catch_response=True) as response:
if response.status_code == 404:
raise RescheduleTask()
REST/JSON APIs
下面是一个如何调用 REST API 并验证响应的例子:
from json import JSONDecodeError
...
with self.client.post("/", json={"foo": 42, "bar": None}, catch_response=True) as response:
try:
if response.json()["greeting"] != "hello":
response.failure("Did not get expected value in greeting")
except JSONDecodeError:
response.failure("Response could not be decoded as JSON")
except KeyError:
response.failure("Response did not contain expected key 'greeting'")
Locust-plugins 有一个现成的类用于测试 REST API: RestUser
对请求分组
网址包含某种动态参数(s),将这些 url 进行分组后再进行用户数据统计通常是有意义的。这可以通过向 HttpSession 的不同请求方法传递一个 name 参数来实现。
例如:
# Statistics for these requests will be grouped under: /blog/?id=[id]
for i in range(10):
self.client.get("/blog?id=%i" % i, name="/blog?id=[id]")
在某些情况下,不可能将参数传入请求函数,例如在与包装了请求会话的库/SDK 进行交互时。通过设置 client.request_name 属性提供了分组请求的另一种方式。
# Statistics for these requests will be grouped under: /blog/?id=[id]
self.client.request_name="/blog?id=[id]"
for i in range(10):
self.client.get("/blog?id=%i" % i)
self.client.request_name=None
如果希望用最小的样板文件链接多个组,可以使用 client.rename_request()上下文管理器。
@task
def multiple_groupings_example(self):
# Statistics for these requests will be grouped under: /blog/?id=[id]
with self.client.rename_request("/blog?id=[id]"):
for i in range(10):
self.client.get("/blog?id=%i" % i)
# Statistics for these requests will be grouped under: /article/?id=[id]
with self.client.rename_request("/article?id=[id]"):
for i in range(10):
self.client.get("/article?id=%i" % i)
HTTP Proxy 设置
连接池
当每个 HttpUser 创建新的 HttpSession 时,每个用户实例都有自己的连接池。这类似于真实用户与 web 服务器的交互方式。
但是,如果希望在所有用户之间共享连接,可以使用单个池管理器。为此,将 pool_manager 类属性设置为 urllib3.PoolManager 的实例。
from locust import HttpUser
from urllib3 import PoolManager
class MyUser(HttpUser):
# All users will be limited to 10 concurrent connections at most.
pool_manager = PoolManager(maxsize=10, block=True)
有关更多的配置选项,请参考 urllib3 documentation.
TaskSets
如何组织测试代码
记住这一点很重要,locustfile.py 只是 Locust 导入的一个普通的 Python 模块。从这个模块中,您可以像在任何 Python 程序中一样自由地导入其他 Python 代码。当前的工作目录文件会自动添加到 python 的 sys.path 中,因此任何位于 python 工作目录中的 python 文件/模块/包都可以使用 python import 语句导入。
对于小型测试,将所有测试代码保存在一个 locustfile.py 中应该可以,但是对于较大的测试套件,您可能希望将代码分割成多个文件和目录。
如何构造测试源代码当然完全取决于您,但是我们建议您遵循 Python 最佳实践。
单个 locustfile 示例
Project root
common/
__init__.py
auth.py
config.py
locustfile.py
requirements.txt
多个 locustfile 示例
Project root
common/
__init__.py
auth.py
config.py
my_locustfiles/
api.py
website.py
requirements.txt
命令行选项
$ locust --help
环境变量
大多数可以通过命令行参数设置的选项也可以通过环境变量设置。例如:
$ LOCUST_LOCUSTFILE=custom_locustfile.py locust
配置文件
可以通过命令行参数设置的任何选项也可以通过配置文件格式的配置文件设置。
Locust 将默认查找 ~/.locust.conf 和 ./locust.conf,您可以使用 --config 命令行参数指定其他位置的配置文件。
Demo
# master.conf in current directory
locustfile = locust_files/my_locust_file.py
headless = true
master = true
expect-workers = 5
host = http://target-system
users = 100
spawn-rate = 10
run-time = 10m
$ locust --config=master.conf
配置值按以下顺序读取(覆盖) : ~/locust.conf -> ./locust.conf -> (file specified using --conf) -> env vars -> cmd args
所有可用的配置选项
要导入的 Python 模块,例如 “. ./other_test. py”。要么是. py 文件,要么是包目录。默认为"locustfile"
性能测试的主机地址:http://10.21.32.33
Locust 并发用户高峰人数。主要与-headless 或-autostart 连用。可以在测试期间通过键盘输入改变: w 产生1 个用户、W产生10个用户、s停止1个用户、S停止10个用户)
产生新用户的速率(每秒几个新用户)。主要与-headless 或-autostart 一起使用
【废弃】–hatch-rate
在指定时间后停止,例如: 300秒、20分、3小时、1小时30分等。只与-headless 或-autostart 一起使用。默认为永久运行。
web 界面绑定的主机地址。默认为 ‘*’ (all interfaces)
运行 web 界面的主机端口
禁用 web 界面,并立即启动测试。使用-u 和-t 来控制用户数和运行时间
立即启动测试(不禁用 web UI)。使用-u 和-t 来控制用户数和运行时间
完全退出 Locust,x 秒后运行结束。仅与-autostart 一起使用。默认是让 Locust 运行,直到你使用 ctrl + c 关闭它
【废弃】–headful
打开 web 界面的 Basic 认证。应该以下列格式提供: username:password
可选的 TLS 证书路径,用于服务于 HTTPS
可选的 TLS 私钥路径,用于通过 HTTPS 提供服务
设置 Locust 以分布式模式运行,并以此进程为 master 节点
Locust master 应绑定的接口 (hostname, ip) ,只与-master 一起使用,默认值为 * (所有可用接口)。
Locust master 应绑定到的端口,只与-master 一起使用,默认值为5557。
master 节点将等待,直到指定个数的 worker 节点已连接,然后才开始测试。(只有在使用-headless/autostart 时才有效)。
master 等待 worker 来连接的超时,默认永远等待。
将 locust 设置为以分布式模式运行,并使用此进程作为 worker
分布式模式时,master 的主机地址或 IP,只与-worker 一起使用,默认为127.0.0.1。
分布式模式时,master 的主机端口,只在与-worker 一起使用,默认值为5557。
在测试中包含的 tag 列表,因此只执行具有任何匹配 tag 的任务
从测试中排除的 tag 列表,因此只执行没有匹配 tag 的任务
将当前请求状态以 CSV 格式存储到文件中。设置此选项将生成三个文件: [CSV_PREFIX]_stats.csv, [CSV_PREFIX]_stats_history.csv and [CSV_PREFIX]_failures.csv
–csv
LOCUST_CSV
csv
以 CSV 格式存储每个统计数据条目到 _stats_history.csv 文件。必须指定-csv参数来启用这个选项。
在控制台中打印统计数据
只打印摘要统计数据
一旦生成完成,重置统计信息。在分布式模式下,应该在 master 和 worker 上都设置。
将 HTML 报表存储到指定的文件路径
禁用 Locust 的日志记录设置,配置是由 Locust 测试或 Python 默认提供的。
日志级别,可选范围 DEBUG/INFO/WARNING/ERROR/CRITICAL,默认 INFO。
日志文件的路径。如果没有设置,日志将输出到 stderr
设置测试结果包含任何故障或错误时要使用的进程退出代码
在退出前,等待模拟用户执行完任务的超时秒数。默认是立即终止。在分布式模式下,只需要为 master 指定此参数。
自定义参数
统计设置
Locust 统计信息的默认配置在 stats.py 文件的常量中。可以通过重写这些值来调整它以满足特定的需求。为此,导入 locust.stats 模块并覆盖所需的设置
import locust.stats
locust.stats.CONSOLE_STATS_INTERVAL_SEC = 15
可以修改的统计参数列表如下:
运行 Locust 的单个进程可以模拟相当高的吞吐量。对于一个简单的测试计划,它应该能够每秒发出数百个请求,如果使用 FastHttpUser,则可以发出数千个请求。
但是,如果您的测试计划很复杂,或者您希望运行更多的负载,那么您将需要扩展到多个进程,甚至是多台机器。
为此,您在主节点上使用 --master 标志启动 Locust 的一个实例,并使用 --worker 标志启动多个工人实例。如果这些 worker 与您使用的主机不在同一台计算机上,使用 --master-host 将他们指向 master 节点的主机 IP/hostname。
master 实例会运行 Locust 的 web 界面,并告诉 worker 何时产生/停止用户。worker 运行您的用户并将统计数据发送回去。master 实例本身不运行任何 Users。
master 和 worker 在分布式运行 Locust 时,都必须有 locustfile 的副本。
因为 Python 不能完全利用每个进程多于一个核心(请参见 GIL ) ,所以通常应该在每个处理器核心上运行一个 worker 实例,以便充分利用它们的计算能力。
每个 worker 可以运行多少个用户几乎没有限制。Locust/gevent 可以在每个进程中运行数千甚至数万个用户,只要他们的总请求率/RPS 不是太高。
如果 Locust 即将耗尽 CPU 资源,它将记录一个警告。
Demo
# 在 master 节点上启动 locust
$ locust -f my_locustfile.py --master
# 然后对每个 worker (用 master的 IP 替换192.168.0.14,或者如果你的 worker 和 master 在同一台机器上,则完全省略参数)
$ locust -f my_locustfile.py --worker --master-host=192.168.0.14
相关选项参数
跨节点通信
在分布式模式下运行 Locust 时,您可能希望在主节点和工作节点之间进行通信以协调数据。通过使用内置的消息挂钩,可以很容易地实现自定义消息:
from locust import events
from locust.runners import MasterRunner, WorkerRunner
# Fired when the worker receives a message of type 'test_users'
def setup_test_users(environment, msg, **kwargs):
for user in msg.data:
print(f"User {user['name']} received")
environment.runner.send_message('acknowledge_users', f"Thanks for the {len(msg.data)} users!")
# Fired when the master receives a message of type 'acknowledge_users'
def on_acknowledge(msg, **kwargs):
print(msg.data)
@events.init.add_listener
def on_locust_init(environment, **_kwargs):
if not isinstance(environment.runner, MasterRunner):
environment.runner.register_message('test_users', setup_test_users)
if not isinstance(environment.runner, WorkerRunner):
environment.runner.register_message('acknowledge_users', on_acknowledge)
@events.test_start.add_listener
def on_test_start(environment, **_kwargs):
if not isinstance(environment.runner, MasterRunner):
users = [
{"name": "User1"},
{"name": "User2"},
{"name": "User3"},
]
environment.runner.send_message('test_users', users)
请注意,在本地运行(即非分布式)时,这个功能将得到保留; 消息将由发送它们的同一个运行程序简单地处理。
在 Locust 源代码的 examples 目录 中可以找到一个更完整的示例。
在调试器中运行 Locust 在开发测试时非常有用。除此之外,你可以检查一个特定的回复或者检查一些用户/实例变量。
但是调试器有时会遇到类似 Locust 这样的复杂 gevent 应用程序的问题,而且框架本身还有许多您可能不感兴趣的内容。为了简化这个问题,Locust 提供了一个名为 run_single_user 的方法。请注意,这是一个相当新的特性,api 可能会发生变化。
from locust import HttpUser, task, run_single_user
class QuickstartUser(HttpUser):
host = "http://localhost"
@task
def hello_world(self):
with self.client.get("/hello", catch_response=True) as resp:
pass # maybe set a breakpoint here to analyze the resp object?
# if launched directly, e.g. "python3 debugging.py", not "locust -f debugging.py"
if __name__ == "__main__":
run_single_user(QuickstartUser)
它隐式地为请求事件注册一个事件处理程序,以打印关于每个请求的一些统计信息:
type name resp_ms exception
GET /hello 38 ConnectionRefusedError(61, 'Connection refused')
GET /hello 4 ConnectionRefusedError(61, 'Connection refused')
您可以通过指定参数来为 run_single_ user 配置打印的内容。
确保在调试器设置中启用了 gevent。
VS Code launch.json 示例
{
"version": "0.2.0",
"configurations": [
{
"name": "Python: Current File",
"type": "python",
"request": "launch",
"program": "${file}",
"console": "integratedTerminal",
"gevent": true
}
]
}
在 PyCharm 中也有类似的设置
VS Code/pydev 可能会警告您:sys.settrace() should not be used when the debugger is being used,它可以安全地被忽略
debugging_advanced 示例
官方的 Docker 镜像位于 locustio/locust 。
像这样使用它(假设locustfile. py 存在于当前的工作目录文件中) :
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
上面的组合配置可以用以下命令启动一个主节点和4个工人:
$ docker-compose up --scale worker=4
使用 docker 镜像作为基础镜像
$ FROM locustio/locust
$ RUN pip3 install some-python-package
在 Kubernetes 上运行分布式负载测试
你可以在没有 web UI 的情况下运行 locust ——例如,如果你想在一些自动化的流程中运行它,比如一个 CI 服务器——通过使用 --headless 标志和-u 和-r:-u 指定要生成的用户数,-r 指定生成率(每秒启用的用户数)。
$ locust -f locust_files/my_locust_file.py --headless -u 1000 -r 100
当测试运行时,您可以手动更改用户数,即使在坡道启动完成之后也是如此。按 w 添加1个用户,按 W 添加10个用户。按 s 键删除1或 S 键删除10。
为测试设置时间限制:使用 --run-time 或-t。一旦时间到,Locust 就会关闭。
$ locust -f --headless -u 1000 -r 100 --run-time 1h30m
允许任务在关闭时完成迭代:默认情况下,Locust 会立即停止你的任务(甚至不等待请求完成)。如果您想让任务完成它们的迭代,可以使用 --stop-timeout 。
$ locust -f --headless -u 1000 -r 100 --run-time 1h30m --stop-timeout 99
在没有 web UI 的情况下分布式运行 Locust:启动主节点时应该指定 --expect-workers 选项,以指定预期连接的工作节点数。然后,它将等待直到足够的工作节点已经连接后才开始测试。
控制 Locust 进程的退出代码:在 CI 环境中运行 Locust 时,您可能需要控制 Locust 进程的退出代码。您可以在测试脚本中通过设置 Environment 实例的 process_exit_code 来实现这一点。
下面是一个例子,如果符合以下任何一个条件,就可以将退出代码设置为非零:
# 这段代码可以写入 locustfile.py 或在 locustfile 中导入的任何其他文件中)
import logging
from locust import events
@events.quitting.add_listener
def _(environment, **kw):
if environment.stats.total.fail_ratio > 0.01:
logging.error("Test failed due to failure ratio > 1%")
environment.process_exit_code = 1
elif environment.stats.total.avg_response_time > 200:
logging.error("Test failed due to average response time ratio > 200 ms")
environment.process_exit_code = 1
elif environment.stats.total.get_response_time_percentile(0.95) > 800:
logging.error("Test failed due to 95th percentile response time > 800 ms")
environment.process_exit_code = 1
else:
environment.process_exit_code = 0
class MyCustomShape(LoadTestShape):
time_limit = 600
spawn_rate = 20
def tick(self):
run_time = self.get_run_time()
if run_time < self.time_limit:
# User count rounded to nearest hundred.
user_count = round(run_time, -2)
return (user_count, spawn_rate)
return None
你可能希望通过 CSV 文件消费 Locust 的测试结果。在这种情况下,有两种方法可以做到这一点。
首先,在使用 web UI 运行 Locust 时,可以在 Download Data 选项卡下检索 CSV 文件。
其次,你可以运行 Locust 与一个标志,将定期保存三个 CSV 文件。
$ locust -f examples/basic.py --csv=example --headless -t10m
这些文件将被命名为 example_stats.csv、example_failures.csv、example_history.csv (在使用 --csv=example 时)。
前两个文件将包含整个测试运行的统计数据和失败数据,每个统计数据条目(URL 端点)和一个聚合行。
example_history.csv 将在整个测试运行过程中添加当前(10秒滑动窗口)状态来获取新行。
默认情况下,只有聚合行定期添加到历史统计数据中,但是如果 Locust 以 --csv-full-history 标志开始,每次写入统计数据时(默认情况下每2秒钟一次)就会为每个统计数据条目(和聚合)添加一行。
如果你想写得更快(或者更慢) ,你也可以自定义写入的频率:
import locust.stats
locust.stats.CSV_STATS_INTERVAL_SEC = 5 # default is 1 second
locust.stats.CSV_STATS_FLUSH_INTERVAL_SEC = 60 # Determines how often the data is flushed to disk, default is 10 seconds
假设我们有一个 XML-RPC 服务器,我们想要进行负载测试
import random
import time
from xmlrpc.server import SimpleXMLRPCServer
def get_time():
time.sleep(random.random())
return time.time()
def get_random_number(low, high):
time.sleep(random.random())
return random.randint(low, high)
server = SimpleXMLRPCServer(("localhost", 8877))
print("Listening on port 8877...")
server.register_function(get_time, "get_time")
server.register_function(get_random_number, "get_random_number")
server.serve_forever()
我们可以通过包装 xmlrpc.client.ServerProxy 来构建一个通用的 XML-RPC client
import time
from xmlrpc.client import ServerProxy, Fault
from locust import User, task
class XmlRpcClient(ServerProxy):
"""
XmlRpcClient is a wrapper around the standard library's ServerProxy.
It proxies any function calls and fires the *request* event when they finish,
so that the calls get recorded in Locust.
"""
def __init__(self, host, request_event):
super().__init__(host)
self._request_event = request_event
def __getattr__(self, name):
func = ServerProxy.__getattr__(self, name)
def wrapper(*args, **kwargs):
request_meta = {
"request_type": "xmlrpc",
"name": name,
"start_time": time.time(),
"response_length": 0, # calculating this for an xmlrpc.client response would be too hard
"response": None,
"context": {}, # see HttpUser if you actually want to implement contexts
"exception": None,
}
start_perf_counter = time.perf_counter()
try:
request_meta["response"] = func(*args, **kwargs)
except Fault as e:
request_meta["exception"] = e
request_meta["response_time"] = (time.perf_counter() - start_perf_counter) * 1000
self._request_event.fire(**request_meta) # This is what makes the request actually get logged in Locust
return request_meta["response"]
return wrapper
class XmlRpcUser(User):
"""
A minimal Locust user class that provides an XmlRpcClient to its subclasses
"""
abstract = True # dont instantiate this as an actual user when running Locust
def __init__(self, environment):
super().__init__(environment)
self.client = XmlRpcClient(self.host, request_event=environment.events.request)
# The real user class that will be instantiated and run by Locust
# This is the only thing that is actually specific to the service that we are testing.
class MyUser(XmlRpcUser):
host = "http://127.0.0.1:8877/"
@task
def get_time(self):
self.client.get_time()
@task
def get_random_number(self):
self.client.get_random_number(0, 100)
唯一重要的区别是,您需要使 gRPC gevent 兼容,在打开通道之前执行以下代码:
import grpc.experimental.gevent as grpc_gevent
grpc_gevent.init_gevent()
要测试的 Dummy server:
import hello_pb2_grpc
import hello_pb2
import grpc
from concurrent import futures
import logging
import time
logger = logging.getLogger(__name__)
class HelloServiceServicer(hello_pb2_grpc.HelloServiceServicer):
def SayHello(self, request, context):
name = request.name
time.sleep(1)
return hello_pb2.HelloResponse(message=f"Hello from Locust, {name}!")
def start_server():
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
hello_pb2_grpc.add_HelloServiceServicer_to_server(HelloServiceServicer(), server)
server.add_insecure_port("localhost:50051")
server.start()
logger.info("gRPC server started")
server.wait_for_termination()
gRPC client, base User 示例:
# make sure you use grpc version 1.39.0 or later,
# because of https://github.com/grpc/grpc/issues/15880 that affected earlier versions
import grpc
import hello_pb2_grpc
import hello_pb2
from locust import events, User, task
from locust.exception import LocustError
from locust.user.task import LOCUST_STATE_STOPPING
from hello_server import start_server
import gevent
import time
# patch grpc so that it uses gevent instead of asyncio
import grpc.experimental.gevent as grpc_gevent
grpc_gevent.init_gevent()
@events.init.add_listener
def run_grpc_server(environment, **_kwargs):
# Start the dummy server. This is not something you would do in a real test.
gevent.spawn(start_server)
class GrpcClient:
def __init__(self, environment, stub):
self.env = environment
self._stub_class = stub.__class__
self._stub = stub
def __getattr__(self, name):
func = self._stub_class.__getattribute__(self._stub, name)
def wrapper(*args, **kwargs):
request_meta = {
"request_type": "grpc",
"name": name,
"start_time": time.time(),
"response_length": 0,
"exception": None,
"context": None,
"response": None,
}
start_perf_counter = time.perf_counter()
try:
request_meta["response"] = func(*args, **kwargs)
request_meta["response_length"] = len(request_meta["response"].message)
except grpc.RpcError as e:
request_meta["exception"] = e
request_meta["response_time"] = (time.perf_counter() - start_perf_counter) * 1000
self.env.events.request.fire(**request_meta)
return request_meta["response"]
return wrapper
class GrpcUser(User):
abstract = True
stub_class = None
def __init__(self, environment):
super().__init__(environment)
for attr_value, attr_name in ((self.host, "host"), (self.stub_class, "stub_class")):
if attr_value is None:
raise LocustError(f"You must specify the {attr_name}.")
self._channel = grpc.insecure_channel(self.host)
self._channel_closed = False
stub = self.stub_class(self._channel)
self.client = GrpcClient(environment, stub)
class HelloGrpcUser(GrpcUser):
host = "localhost:50051"
stub_class = hello_pb2_grpc.HelloServiceStub
@task
def sayHello(self):
if not self._channel_closed:
self.client.SayHello(hello_pb2.HelloRequest(name="Test"))
time.sleep(1)
如果您要测试的目标系统已经有一个构建好的 SDK 可用,Locust 支持将其集成到您的负载测试工作中使用。
要实现这一点,唯一的先决条件是:SDK 需要有一个可访问的 request.Sessions 类。
下面的示例显示 locust 客户端在启动期间覆盖了 Archivist SDK 内部的 _session 对象。
import locust
from locust.user import task
from archivist.archivist import Archivist # Example SDK under test
class ArchivistUser(locust.HttpUser):
def on_start(self):
AUTH_TOKEN = None
with open("auth.text") as f:
AUTH_TOKEN = f.read()
# Start an instance of of the SDK
self.arch: Archivist = Archivist(url=self.host, auth=AUTH_TOKEN)
# overwrite the internal _session attribute with the locust session
self.arch._session = self.client
@task
def Create_assets(self):
"""User creates assets as fast as possible"""
while True:
self.arch.assets.create(behaviours=["Builtin", "RecordEvidence", "Attachments"], attrs={"foo": "bar"})
只需要子类化 FastHttpUser 而不是 HttpUser
from locust import task, FastHttpUser
class MyUser(FastHttpUser):
@task
def index(self):
response = self.client.get("/")
FastHttpUser/geventhttpclient 与 HttpUser/python-requests 非常相似,但有时会有细微的差别。如果您需要使用客户端库的内部机制,例如手动管理 cookie,那么这种情况尤其明显。
事件挂钩
Locust 带有一些事件挂钩,可以用于在多种不同方式上扩展 Locust。
例如,下面是如何设置一个事件侦听器,该侦听器将在请求完成后触发。
from locust import events
@events.request.add_listener
def my_request_handler(request_type, name, response_time, response_length, response,
context, exception, start_time, url, **kwargs):
if exception:
print(f"Request to {name} failed with exception {exception}")
else:
print(f"Successfully made a request to: {name})
print(f"The response was {response.text}")
在上面的示例中,通配符关键字参数(** kwargs)将为空,因为我们已处理所有参数,但是如果 Locust 在未来版本中添加了新的参数,它可以防止代码因异常而中断。
另外,完全可以实现一个不为此事件提供所有参数的客户端。例如,非 http 协议甚至可能没有 url 或响应对象的概念。从侦听器函数定义中删除所有这类缺少的字段或使用默认参数。
在分布式模式下运行 locust 时,在运行测试之前 worker 节点上做一些设置可能会很有用。你可以通过检查节点的 runner 类型来确保你没有在 master 节点上运行。
from locust import events
from locust.runners import MasterRunner
@events.test_start.add_listener
def on_test_start(environment, **kwargs):
if not isinstance(environment.runner, MasterRunner):
print("Beginning test setup")
else:
print("Started test from Master node")
@events.test_stop.add_listener
def on_test_stop(environment, **kwargs):
if not isinstance(environment.runner, MasterRunner):
print("Cleaning up test data")
else:
print("Stopped test from Master node")
您还可以使用事件来添加 自定义命令行参数 。
若要查看可用事件的完整列表,请参见 事件挂钩 。
请求上下文
request event 有一个上下文参数,使您能够传递有关请求的数据(例如用户名、标签等)。
可以在对 request 方法的调用中直接设置它。
class MyUser(HttpUser):
@task
def t(self):
self.client.post("/login", json={"username": "foo"})
self.client.get("/other_request", context={"username": "foo"})
@events.request.add_listener
def on_request(context, **kwargs):
if context:
print(context["username"])
也可以在 User 级别上设置,通过重写 User.context ()方法。
class MyUser(HttpUser):
def context(self):
return {"username": self.username}
@task
def t(self):
self.username = "foo"
self.client.post("/login", json={"username": self.username})
@events.request.add_listener
def on_request(context, **kwargs):
print(context["username"])
添加 Web 路由
Locust 使用 Flask 服务来产生网页用户界面,因此可以很容易添加 web end-points 到网页用户界面。通过监听 init 事件,我们可以获取到 Flask app 实例的引用,并使用它来设置一个新的路由。
from locust import events
@events.init.add_listener
def on_locust_init(environment, **kw):
@environment.web_ui.app.route("/added_page")
def my_added_page():
return "Another page"
启动 locust 后就可以浏览 http://127.0.0.1:8089/added_page 。
扩展 Web 用户界面
运行一个后台 greenlet
因为 locust 文件只是“代码”,所以没有什么可以阻止您生成自己的 greenlet 来与实际的负载/用户并行运行。
例如,你可以监视测试的失败率,如果超过某个阈值,你可以停止运行。
from locust import events
from locust.runners import STATE_STOPPING, STATE_STOPPED, STATE_CLEANUP, MasterRunner, LocalRunner
def checker(environment):
while not environment.runner.state in [STATE_STOPPING, STATE_STOPPED, STATE_CLEANUP]:
time.sleep(1)
if environment.runner.stats.total.fail_ratio > 0.2:
print(f"fail ratio was {environment.runner.stats.total.fail_ratio}, quitting")
environment.runner.quit()
return
@events.init.add_listener
def on_locust_init(environment, **_kwargs):
# dont run this on workers, we only care about the aggregated numbers
if isinstance(environment.runner, MasterRunner) or isinstance(environment.runner, LocalRunner):
gevent.spawn(checker, environment)
将 locustfiles 参数化
像任何程序一样,你可以使用环境变变量。
# 在 linux/mac 上
MY_FUNKY_VAR=42 locust ...
# 在 windows 上
SET MY_FUNKY_VAR=42
locust ...
然后在你的 locustfiles 里找到它们。
import os
print(os.environ['MY_FUNKY_VAR'])
您可以使用 init_command_line_parser Event 向 Locust 添加自己的命令行参数。自定义参数也可以在 web UI 中显示和编辑。
from locust import HttpUser, task, events
@events.init_command_line_parser.add_listener
def _(parser):
parser.add_argument("--my-argument", type=str, env_var="LOCUST_MY_ARGUMENT", default="", help="It's working")
# Set `include_in_web_ui` to False if you want to hide from the web UI
parser.add_argument("--my-ui-invisible-argument", include_in_web_ui=False, default="I am invisible")
@events.test_start.add_listener
def _(environment, **kw):
print(f"Custom argument supplied: {environment.parsed_options.my_argument}")
class WebsiteUser(HttpUser):
@task
def my_task(self):
print(f"my_argument={self.environment.parsed_options.my_argument}")
print(f"my_ui_invisible_argument={self.environment.parsed_options.my_ui_invisible_argument}")
在运行 Locust 分布式时,自定义参数会在运行开始时自动转发给 worker(但不是在此之前,因此在测试实际开始之前不能依赖于转发的参数)。
测试数据管理
更多示例参见 locust-plugins
Locust 使用 Python 内置的 logging 框架 来处理日志记录。
Locust 默认的日志配置是直接将日志消息写入 stderr。–loglevel 和 --logfile 可用于更改日志级别程和/或将日志输出方式转换为文件。
默认的日志配置会安装 root logger 即 locust.* logger,因此在您自己的测试脚本中使用 root logger 以及 --logfile,将把日志输出到文件。
import logging
logging.info("this log message will go wherever the other locust log messages go")
还可以使用 --skip-log-setup 选项在您自己的测试脚本中控制整个日志配置。然后,您必须自己配置
logging 。
Locust loggers
可以从您自己的 Python 代码启动负载测试,而不是使用 locust 命令运行 Locust。
首先创建一个 Environment 实例。
from locust.env import Environment
env = Environment(user_classes=[MyTestUser])
Environment 实例的 create_local_runner, create_master_runner, create_worker_runner 可用于启动一个 Runner 实例,它可用于启动负载测试。
env.create_local_runner()
env.runner.start(5000, spawn_rate=20)
env.runner.greenlet.join()
还可以绕过分派和分发逻辑,手动控制产生的用户。
new_users = env.runner.spawn_users({MyUserClass.__name__: 2})
new_users[1].my_custom_token = "custom-token-2"
new_users[0].my_custom_token = "custom-token-1"
上面的例子只能在独立模式下工作,并且是一个实验性的特性,这意味着它可以在未来的版本中被删除。但是,如果您希望对产生的用户进行细粒度控制,那么它是非常有用的。
不要试图在相同的 Python 进程中创建 master runner 和 worker(s)。它不起作用,即使起作用,也不会比只运行一个 LocalRunner 给你带来更好的性能。每个 worker 都必须在自己的进程中运行,这是没有办法的。
我们还可以使用 Environment 实例的 create_web_ui 方法来启动一个 Web UI,用于查看统计数据,并控制 runner (例如启动和停止负载测试) 。
env.create_local_runner()
env.create_web_ui()
env.web_ui.greenlet.join()
完整实例
import gevent
from locust import HttpUser, task, between
from locust.env import Environment
from locust.stats import stats_printer, stats_history
from locust.log import setup_logging
setup_logging("INFO", None)
class User(HttpUser):
wait_time = between(1, 3)
host = "https://docs.locust.io"
@task
def my_task(self):
self.client.get("/")
@task
def task_404(self):
self.client.get("/non-existing-path")
# setup Environment and Runner
env = Environment(user_classes=[User])
env.create_local_runner()
# start a WebUI instance
env.create_web_ui("127.0.0.1", 8089)
# start a greenlet that periodically outputs the current stats
gevent.spawn(stats_printer(env.stats))
# start a greenlet that save current stats to history
gevent.spawn(stats_history, env.runner)
# start the test
env.runner.start(1, spawn_rate=10)
# in 60 seconds stop the runner
gevent.spawn_later(60, lambda: env.runner.quit())
# wait for the greenlets
env.runner.greenlet.join()
# stop the web server for good measures
env.web_ui.stop()