Locust使用手册--编写一个locustfile

本文介绍Locust性能测试工具的基本使用方法,包括locustfile的编写、任务定义、事件监听及HTTP请求处理等内容。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

编写一个locustfile

现在,让我们看一个更完整/更真实的测试示例:

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"})

代码分解解读

import time
from locust import HttpUser, task, between

locust文件只是一个python的普通库,它可以通过import代码来引用其他文件或包。

class QuickstartUser(HttpUser):

这里我们定义了class类来实现用户模拟操作。该类继承了HttpUser类,HttpUser为每个用户提供一个client属性,该属性是HttpSession的一个实例,可用于向我们要负载测试的目标系统发出HTTP请求。当开始测试时,locust将为每个模拟用户创建一个此类的实例,每个用户都在各自的协程中运行。
要使得测试case文件成为有效的locastfile,它必须至少包含一个从User继承的类。

wait_time = between(1, 5)

在我们的类中定义了一个wait_time用来模拟每个任务被执行的时间间隔在1到5秒之间。更多详细信息参考wait_time属性章节。

@task
def hello_world(self):
    ...

@task修饰的方法是locustfile的核心。对于每个正在运行的用户,Locust都会创建一个greenlet(微线程/协程),它将调用这些方法。

@task
def hello_world(self):
    self.client.get("/hello")
    self.client.get("/world")

@task(3)
def view_items(self):
...

我们用@task修饰了两个方法hello_world()view_items(),声明了两个任务,其中一个方法的权重更高(3)。当我们的QuickstartUser运行时,它将从已经声明的任务(在本例中是hello_worldview_items)中选择一个来执行。任务的选择是随机的,但你可以赋予它们不同的权重。上述配置将使Locust选择view_items的可能性是hello_world的三倍。当一个任务完成执行后,用户将在等待时间(本例是1到5秒)内休眠。在等待时间过后,它会选择一个新任务并不断重复以上操作。
请注意,只有用@task修饰的方法才会被选中,因此你可以任意自定义内部助手方法。

self.client.get("/hello")

self.client属性使得它可以执行HTTP调用并被Locust记录。有关如何发出其他类型的请求、验证响应等的信息,请参考使用HTTP客户端

批注
HttpUser不是真正的浏览器,因此不会解析HTML响应来加载资源或呈现页面。不过它会追踪cookie信息。

@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)

view_items任务中,我们使用变量查询参数加载10个不同的URL。为了不在Locust的统计数据中获得10个单独的条目-因为统计数据是在URL上分组的-我们使用name参数将所有这些请求分组到名为/item的条目下。

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

另外我们声明了一个on_start方法。当每个模拟用户启动时都会调用此名称的方法。更多信息参考on_start and on_stop 方法

自动生成locustfile

你可以通过har2locust将浏览器录制的HAR文件自动生成locustfile。
它对于不习惯于编写自己的locustfile的初学者特别有用,但对于更高级的用例来说,它也是高度可定制的。

批注
har2locust正处于测试阶段。它可能并不一定总能生成正确的locustfile,并且它的接口可能会在不同版本之间发生变化。

User类

一个user类表示系统里的一种用户\场景的类型。当你执行测试时,运行指定要模拟的并发用户数,Locust会为每个用户创建一个实例。你可以为这些类/实例添加任何你想使用的属性,不过有些属性对Locust是有特殊意义的。如:

wait_time属性

User类的wait_time方法实现了执行每个任务之间的延时控制。如果未指定wait_time,则任务一完成就立即执行下一个任务。

  • constant: 指定一个固定的时间值
  • between: 在指定最小值到最大值的范围随机获取一个值

例如,要让每个用户在每次任务执行之间等待0.5到10秒:

from locust import User, task, between

class MyUser(User):
    @task
    def my_task(self):
        print("executing my_task")

    wait_time = between(0.5, 10)
  • constant_throughput: 给定一个时间X以确保任务每秒运行(最多)X次。
  • constant_pacing: 给定一个时间X以确保每X秒运行一次(这个是constant_throughput的倒数)

批注
例如,如果你希望Locust在峰值负载下每秒运行500次任务迭代,可以使用mathematical inverse和5000个用户数。
等待时间只能限制吞吐量,不能通过启动新用户来达到目的。因此,在我们的示例中,如果任务执行时间间隔超过10秒,那么对应的吞吐量将小于500。
等待时间是在任务执行后应用的,因此,如果有较高的繁殖率/加速,则可能会在加速过程中超过目标。
等待时间适用于任务,而不是请求。例如,如果规定了wait_time = constant_throughput(2)和在任务中执行两个请求,则请求率(RPS)为4/User1

也可以在自定义的类中声明自定义的wait_time方法。例如,下面的MyUser类将休眠一秒钟,然后是两秒钟,然后三秒钟,等等。

class MyUser(User):
    last_wait_time = 0

    def wait_time(self):
        self.last_wait_time += 1
        return self.last_wait_time

    ...

weight和fixed_count属性

如果文件中存在多个用户类,并且命令行中未指定任何用户类,Locust将为每个用户类生成相同数量负载。还可以通过命令行传递参数的方式指定locustfile中的用户类名称来执行,如:

$locust -f locust_file.py WebUser MobileUser

如果希望模拟更多特定类型的用户,可以在这些类上设置权重属性。例如,WebUser的可能性是MobileUser的三倍:

class WebUser(User):
    weight = 3
    ...

class MobileUser(User):
    weight = 1
    ...

还可以设置fixed_count属性。在这种情况下,权重属性将被忽略,并且将产生精确的用户数。首先生成这些用户。在下面的示例中,将只生成一个AdminUser实例,以使某些特定的工作能够更准确地控制请求数,而不依赖于总用户数。

class AdminUser(User):
    wait_time = constant(600)
    fixed_count = 1

    @task
    def restart_app(self):
        ...

class WebUser(User):
    ...

host属性

host属性是被测系统的一个URL前缀(如: “http://google.com”)。通常这是在Locust的web UI中或在命令行启动Locust时使用–host参数来指定的。
如果在用户类中声明了host属性,则在命令行或web请求中未指定--host的情况下,将默认使用该属性的值。

tasks属性

User类可以使用@task装饰器将任务声明为它下面的方法,但也可以使用tasks属性指定任务,下面将详细介绍该属性使用。

environment属性

在用户运行时使用environment属性可实现运行环境的交互,例如通过runner属性控制阻止一个任务方法的执行:

self.environment.runner.quit()

如果在独立的Locust实例上运行,这将停止整个运行。如果在工作节点上运行,它将停止该特定节点。

on_start和on_stop方法

Users类(TaskSets类)可以声明on_start方法和on_stop方法。一个User类当它开始运行时将会调用on_start方法以及当它停止运行时on_stop方法被调用。对于TaskSet类,当一个模拟用户开始执行TaskSet类时将会调用on_start方法以及当模拟用户停止执行TaskSet类(当interrupt()被调用或运行用户被关闭)时on_stop方法被调用。

Tasks类

当负载测试开始时,将为每个模拟用户创建User类的实例,并它们将在自己的协程中开始运行。当这些用户运行时,选择自己执行的任务,休眠一会,然后选择一个新任务继续执行,依此类推。
这些任务是python普通的可调用程序,比如我们正在对一个拍卖网站做负载测试,可以将以下的操作当作任务来定义,就像“加载一个起始页面”、“搜索商品”、“出价”等操作。

@task 装饰器

为用户添加任务的最简单方法是使用task修饰符。

from locust import User, task, constant

class MyUser(User):
    wait_time = constant(1)

    @task
    def my_task(self):
        print("User instance (%r) executing my_task" % self)

@task采用可选的权重参数,可用于指定任务的执行率。在下面的示例中,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

tasks 属性

定义用户任务的另一种方法是设置task属性。
tasks属性要么是一个Tasks列表,要么是一个<Task: int> 字典,其中Task要么是一个python可调用类,要么是一个TaskSet类。如果task是一个普通的python函数,那么它将会接收一个参数,即为正在执行任务的User类实例。
下面是一个声明为普通python函数的User任务的示例:

from locust import User, constant

def my_task(user):
    pass

class MyUser(User):
    tasks = [my_task]
    wait_time = constant(1)

如果tasks-属性被定义为一个列表,则每次执行任务时,都会从tasks属性列表中随机选择一个来执行。然而如果tasks属性是一个字典(则可调用对象作为key值、int权重值为value),将随机选择要执行的任务,但将int作为权重比率。就像下面的示例那样:

{my_task: 3, another_task: 1}

my_task被执行的机率是another_task的3倍。
而实际上上面的示例在内部底层处理会被处理成列表(同时task属性也会被更新)为这样

[my_task, my_task, my_task, another_task]

然后通过python的random.choice()从列表中随机选择一个任务执行。

@tag 装饰器

通过使用@tag装饰器标记任务,可以使用--tags--exclude-tags参数对测试期间执行的任务进行自定义挑选。参考下面的示例:

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

如果你启动测试时使用了--tags tag1,则测试过程只有task1task2会被执行。如果使用了--tags tag2 tags3启动测试,则只有task2task3会被执行。
--exclude-tags是以完全相反的方式来执行。如果以--exclude-tags tag3来启动测试,则只有task1task2task4会被执行。根据排除总是胜于包含的原则,因此,如果一个任务同时包含一个已包含的标记和一个已排除的标记,则不会执行该任务。

Events类

如果希望运行一些设置代码作为测试的一部分,通常可以将其放在locustfile的模块级别,但有时需要在运行中的特定时间执行一些操作。为实现这个需求,Locust提供了事件钩子。

test_start和test_stop

如果你需要在负载测试启动或停止时运行一些特定代码,可以使用test_starttest_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进程开始时触发。这在分布式模式下尤其有用,因为每个工作进程(而不是每个用户)都需要一次初始化的机会。例如,假设你有一个全局状态,所有从该进程派生的用户都需要该状态:

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")

其他事件

请参考使用locust事件钩子扩展以获取其他事件以及有关如何使用它们的更多示例。

HttpUser类

HttpUser类是最常使用的User类。它添加了一个用于发送HTTP请求的client属性。

from locust import HttpUser, task, between

class MyUser(HttpUser):
    wait_time = between(5, 15)

    @task(4)
    def index(self):
        self.client.get("/")

    @task(1)
    def about(self):
        self.client.get("/about/")

client属性/HttpSession类

client属性是HttpSession类的一个实例。HttpSession类是requests.Session的子类/包装器,因此它的特性有很多文档记录,很多人都应该很熟悉。HttpSession添加的主要是向Locust报告请求结果(成功/失败、响应时间、响应长度、名称)。
它包含所有HTTP方法的方法: getpostput,…
就像requests.Session类那样,它在请求之间保存了cookie,因此可以很容易地用于登录网站。

进行`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")

HttpSession捕获任何requests.RequestException抛出的异常(如由连接错误、超时或类似原因引起),而不是返回一个虚拟的Response对象,其中status_code设置为0content设置为None

响应验证

如果HTTP的响应代码是OK(<400)则会认为请求成功,但是常常需要对响应做一些其他条件的验证。
通过使用catch_response参数、with-语句和对response.failure()的调用结果来将请求标记为失败。如:

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代码块外面捕获抛出的异常来避开记录请求结果。或者可以抛出一个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插件有一个现成的测试REST API的类,叫做RestUser

分组请求

网站的一些页面通过url的动态参数来获取是很常见的。通常在User的统计信息中会将这些进行url分组统计。这可以通过将name参数传递给HttpSession的不同请求方法来实现。
示例:

# 这些请求的统计数据将归为: /blog/?id=[id]
for i in range(10):
    self.client.get("/blog?id=%i" % i, name="/blog?id=[id]")

可能在某些场景下不能将参数传递到请求函数中,如和封装了请求会话的lib库/SDK包交互时。对请求进行分组的另一种方法是通过client.request_name属性来设置。

# 这些请求的统计数据将归为: /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

如果想在同一个locustfile里链接多个分组,可以使用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代理设置

为了提高性能,我们通过配置requests.Sessiontrust_env属性值为false来使得请求配置为不查找环境中的HTTP代理设置。如果不想这样做,可以手动设置locust_instance.client.trust_env的值为True。有关详细信息,请参考requests文档

连接池

当每个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文档

TaskSets

TaskSets是一种构建分层网站/系统测试的方法。可以通过TaskSet了解更多。

如何构造测试代码

重要的是要记住locustfile.py只是一个导入了Locust模块的python文件。从这个模块中,你可以像在任何python程序那样引入其他python代码。当前工作目录会自动添加到python的sys.path,因此可以使用pythonimport语句导入工作目录中的任何python文件/模块/包。
对于小型的测试,将所有测试代码保存在单个locustfile.py中应该可以,但对于较大的测试套件,可能需要将代码分割到多个文件和目录中。
当然,如何构造测试源代码完全取决于你,但我们建议你遵循Python最佳实践。下面是一个虚构的Locust项目的示例文件结构:

* Project Root
    * common/
        * __init__.py
        * auth.py
        * config.py
    * locustfile.py
    * requirements.txt (外部python依赖包通常都存放在requirements.txt中)

具有多个locostfile的项目也可以将它们保存在单独的子目录中:

    * common/
        * __init__.py
        * auth.py
        * config.py
    * my_locustfiles/
        * api.py
        * website.py
    * requirements.txt

使用以上任何一个项目结构,你的locustfile可以使用以下方法导入公共库:

import common.auth

  1. constant_throughput(2)等同于每秒最大执行2次,每个任务有个2个请求,则RPS=请求数/任务*最大执行数=2x2=4 ↩︎

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值