locustfile.py
是 locust 运行的脚本文件,就像 jmeter 的 jmx 文件一样。
locustfile 是普通的 python 文件。
唯一的要求是它声明至少一个类,我们称之为用户类属于User
的子类。
用户类 User class
一个用户类别代表一个用户(如果你愿意,也可以认为是一群蝗虫)。
Locust 将为每个正在模拟的用户生 User 类的一个实例。User 类通常必须定义如下这些属性。
wait_time 属性
除了tasks
属性外,还应该声明一种wait_time
方法。它用于确定虚拟用户在执行任务之间等待多长时间。
wait_time
,也就是常见的性能工具中提到的 “思考时间”,模拟用户在实际操作过程中的停顿。比如用户点击一个商品,进入商品详情后,用户会看看商品描述、商品图片等,对系统来说这一段时间此用户并未产生任何压力。
Locust 带有一些内置函数,这些函数返回一些常用的 wait_time 方法。
最常见的一种是between
。它用于使模拟用户在每次执行任务后等待介于最小值和最大值之间的随机时间。
使用以下 locustfile,每个用户将在任务之间等待 5 到 15 秒:
from locust import User, task, between
class MyUser(User):
@task
def my_task(self):
print("executing my_task")
wait_time = between(5, 15)
wait_time 方法应返回秒数(或几分之一秒),也可以在 TaskSet 类上声明,在这种情况下,它将仅用于该 TaskSet。
也可以直接在 User 或 TaskSet 类上声明自己的 wait_time 方法。下面的 User 类将开始休眠一秒钟,然后休眠1, 2, 3,依此类推。
class MyUser(User):
last_wait_time = 0
def wait_time(self):
self.last_wait_time += 1
return self.last_wait_time
...
weight 属性(可以理解为执行比例或任务执行权重)
如果文件中存在多个用户类,并且在命令行上未指定任何用户类,则 Locust 将产生相等数量的每个用户类。你还可以通过将它们作为命令行参数传递,来指定要从同一 locustfile 中使用哪些用户类:
$ locust -f locust_file.py WebUser MobileUser
如果你希望模拟更多特定类型的用户,则可以在这些类上设置一个 weight 属性。像下面的例子,WebUser 的可能性是 MobileUser 的三倍:
class WebUser(User):
weight = 3
...
class MobileUser(User):
weight = 1
...
host 属性
host 属性是要加载的主机的 URL 前缀(即“ http://google.com”)。通常,当 Locust 启动时,这是在Locust 的 Web UI 或命令行中使用--host
选项指定的。
如果在 user 类中声明了 host 属性,则在命令行或 Web 请求中未指定--host
的情况下将使用该属性。
tasks 属性
user 类可以使用@task
装饰器声明为任务的方法,但也可以指定任务使用tasks
属性,该属性在下面详细介绍。
Tasks
启动负载测试后,将为每个模拟用户创建一个 User 类的实例,并且它们将在其自己的 green 线程中运行。这些用户运行时,他们选择执行的任务(task 方法),休眠一会儿,然后选择一个新任务,依此类推。
这些任务是普通的 python 可调用对象。 如果我们正在对拍卖网站进行负载测试,则它们可以执行诸如“加载起始页”,“搜索某些产品”,“竞标” 之类的场景。
声明 tasks
使用task
装饰器为 User 类(或 TaskSet)声明任务的典型方法。
Here is an example:
以下是一个典型的示例:
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
具有可选的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
tasks 属性
使用@task
装饰器声明任务是一种快捷的方式,通常也是声明任务的最佳方法。但是,也可以通过设置tasks
属性(使用来定义 User 或 TaskSet 的任务)@task
装饰器实际上只会填充 tasks 属性)。
tasks 属性可以是 Task 的列表,也可以是 <Task:int>
dict 的列表,其中 Task 是可以调用的 python 或 TaskSet 类(在下文中有更多介绍)。如果任务是普通的 python 函数,则它们会收到一个参数,即正在执行任务的 User 实例。
以下是声明普通 python 函数为 User 任务的示例:
from locust import User, constant
def my_task(l):
pass
class MyUser(User):
tasks = [my_task]
wait_time = constant(1)
如果将 task 属性指定为列表,则每次执行任务时,都会从 tasks 属性中随机选择该任务。但是,如果 tasks 是一个 dict 而非 list,将可调用对象作为 key,将 ints 作为值,将随机选择要执行的任务,但将 int 作为执行比率。因此,任务看起来像这样:
{my_task: 3, another_task: 1}
my_task would be 3 times more likely to be executed than another_task.
my_task 执行的可能性是其他任务的 3 倍。
在内部,上面的 dict 实际上将扩展为一个看起来像这样的列表(并且 task 属性被更新):
[my_task, my_task, my_task, another_task]
然后使用 Python 的random.choice()
从列表中选择任务。
标记 tasks
通过使用标记<locust.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
开始此测试,则在测试过程中将仅执行 task1 和 task2。如果你以--tags tag2 tag3
开始测试,则只会执行 task2 和 task3。
--exclude-tags
的行为恰恰相反。因此,如果你以--exclude-tags tag3
开始测试,则只会执行 task1,task2 和 task4。
排除总是胜于包含。因此,如果一个任务同时被指定执行和排除,则该任务将不会执行。
TaskSet class
由于实际的网站通常以分层的方式构建,包括多个子部分,因此 Locust 具有 TaskSet 类。Locust 任务不仅可以是Python 可调用的,还可以是 TaskSet 类。TaskSet 是 Locust 任务的集合,将像直接在 User 类上声明的任务一样执行,使用户在两次任务执行之间处于休眠状态。这是一个带有 TaskSet 的 Locust 文件的简短示例:
from locust import User, TaskSet, between
class ForumSection(TaskSet):
@task(10)
def view_thread(self):
pass
@task(1)
def create_thread(self):
pass
@task(1)
def stop(self):
self.interrupt()
class LoggedInUser(User):
wait_time = between(5, 120)
tasks = {ForumSection: 2}
@task
def index_page(self):
pass
也可以使用@task
装饰器直接在 User/TaskSet 类下内联 TaskSet:
class MyUser(User):
@task(1)
class MyTaskSet(TaskSet):
...
TaskSet 类的任务方法可以是其他 TaskSet 类,从而可以将它们嵌套任何数量的级别。这使我们能够定义更能模拟实际用户使用的场景。
例如,我们可以使用以下结构定义 TaskSet:
- Main user behaviour
- Index page
- Forum page
- Read thread
- Reply
- New thread
- View next page
- Browse categories
- Watch movie
- Filter movies
- About page
- 主场景
- 首页
- 论坛页
- 阅读帖子
- 回复帖子
- 创建帖子
- 查看下一页
- 浏览分类
- 观看视频
- 过滤视频
- 查看帮助
当正在运行的虚拟用户线程选择 TaskSet 类执行时:
- 将创建该类的实例,然后从该 TaskSet 类中选取一个任务并执行;
- 然后将根据
wait_time
方法指定的等待时间休眠; - 然后从 TaskSet 的任务方法中选择一个新的任务执行;
- 然后再次等待,依次类推。
中断 TaskSet
有关 TaskSet 的重要一件事是,它们永远不会停止执行其任务,也就意味着任务控制权不会自动回到父 User/TaskSet。开发人员必须通过调用TaskSet.interrupt()
方法来完成此操作。
-
interrupt
(self, reschedule=True)中断 TaskSet 并将执行控制移交给父 TaskSet。如果 reschedule 为 True(默认值),则父 User 将立即重新计划并执行新任务。
在以下示例中,如果我们没有调用self.interrupt()
的停止任务,则模拟用户一旦进入论坛任务集中就永远不会停止运行该任务:
class RegisteredUser(User):
@task
class Forum(TaskSet):
@task(5)
def view_thread(self):
pass
@task(1)
def stop(self):
self.interrupt()
@task
def frontpage(self):
pass
使用中断功能,我们可以与任务权重一起定义虚拟用户离开论坛的可能性。
TaskSet 和 User 类中的任务之间的差异
与直接驻留在User下的任务相比,驻留在TaskSet下的任务的一个区别是,执行时传递的参数是对 TaskSet 实例的引用,而不是对 User 实例的引用。可以通过TaskSet.user
从 TaskSet 实例内部访问 User 实例。TaskSets 还包含一个方便的client
属性,该属性引用 User 实例上的 client 属性。
引用 User 实例或父 TaskSet 实例
TaskSet 实例包含一个 user
属性指向引用它的 User 实例,parent
属性指向其父类 TaskSet 实例。
标记任务集
你可以使用标签<locust.tag>
装饰器为 TaskSet 进行标记,其方式与普通任务类似,如上述<tagging-tasks>
所述,但是有一些细微差别值得一提。标记 TaskSet 会将标记自动应用于所有 TaskSet 的任务。此外,如果被标记的 TaskSet 存在嵌套 TaskSet,即使没有标记嵌套的子 TaskSet,Locust 也会执行该任务。
顺序任务集
SequentialTaskSet
是 TaskSet,但其任务将按照声明的顺序执行。SequentialTaskSet 类上任务的权重将被忽略。可以将 SequentialTaskSets 嵌套在 TaskSet 中,反之亦然。
def function_task(taskset):
pass
class SequenceOfTasks(SequentialTaskSet):
@task
def first_task(self):
pass
tasks = [functon_task]
@task
def second_task(self):
pass
@task
def third_task(self):
pass
在上面的示例中,任务以声明的顺序执行:
first_task
function_task
second_task
third_task
然后它将再次从first_task
开始循环。
on_start 和 on_stop 方法
User 类和 TaskSet 类都可以声明 on_start
方法和 on_stop
方法。
User 类会在启动运行时运行 on_start
方法,并在停止运行时执行 on_stop
方法。
对于 TaskSet 来说,on_start
会在虚拟用户开始执行该 TaskSet 时运行,而在虚拟用户停止执行 TaskSet 时执行 on_stop
方法(即当 interrupt()
被调用时,或者虚拟用户被干掉时)。
测试开始与结束事件
如果你需要在开始或结束负载测试时需要执行某些代码,你可以使用 test_start
和 test_stop
事件。你可以在 locustfile 模块级别为这些事件设置监听器:
from locust import events
@events.test_start.add_listener
def on_test_start(**kwargs):
print("A new test is starting")
@events.test_stop.add_listener
def on_test_stop(**kwargs):
print("A new test is ending")
在运行 Locust 分布式执行时,只会在主节点中触发test_start
和test_stop
事件。
构造 HTTP 请求
到目前为止,我们仅介绍了用户的任务计划部分。为了实际负载测试系统,我们需要发出 HTTP 请求。HttpLocust
的存在就是为了帮助我们做到这一点。当使用 HttpLocust
类时,每个该类的实例会获得一个 client
属性(HttpSession
的实例,HttpSession
用来构造 HTTP 请求)。
-
class
HttpUser
(*args, **kwargs)一个虚拟用户表示要要进行负载测试的系统的一系列 HTTP 请求。此用户的行为由其任务定义,可以通过在方法上使用
@task decorator
或通过设置tasks attribute
进行定义。此类在实例化时创建了一个 client 属性,该属性是一个 HTTP 客户端,用于在请求之间保持用户的 session。
从 HttpUser 类继承时,我们可以使用其 client 属性对服务器发出 HTTP 请求。以下是一个 locustfile 的示例,可用于对具有两个 URL 的站点 / 和 /about/ 进行负载测试:
from locust import HttpUser, task, between
class MyUser(HttpUser):
wait_time = between(5, 15)
@task(2)
def index(self):
self.client.get("/")
@task(1)
def about(self):
self.client.get("/about/")
使用上面的 User 类,每个虚拟用户将在请求之间等待 5 到 15 秒,而 / 发出的请求将是 /about/ 的两倍。
使用 HTTP client
每个 HttpUser 实例在 client 属性中都包含 HttpSession
的实例。HttpSession 类其实是 requests.Session
的子类,用来构造 HTTP 请求,并会收集统计信息,可以使用 get
, post
, put
, delete
, head
, patch
和 options
方法。
HttpSession 的实例将在请求之间保留 cookie,以便可用于登录网站并在请求之间保持会话。client 属性可以被 User 实例中的 TaskSet 实例引用,以便轻松地从任意地方提取 client 并发出HTTP请求。
以下一个简单的示例,它向 /about 发出 GET 请求(在这种情况下,我们假设 self 是一个 TaskSet
实例或者 HttpUser
实例:
response = self.client.get("/about")
print("Response status code:", response.status_code)
print("Response content:", response.text)
这是发出 POST 请求的示例:
response = self.client.post("/login", {"username":"testuser", "password":"secret"})
安全模式
HTTP 客户端配置为以 safe_mode 运行,则因连接错误、超时或类似原因而失败的任何请求都不会引发异常,而是返回一个空的虚拟 Response 对象。该请求将在用户统计信息中报告为失败。返回的虚拟响应的 content 属性将设置为None,其 status_code 将为 0。
手动控制请求的成功与失败
默认情况下,除非 HTTP 响应代码为 OK(<400),否则请求将被标记为失败。
大多数情况下,此默认值是你想要的。但是有时,例如,当测试预期返回 404 时,或者你在测试一个设计不好的系统(即使发生错误,状态码也返回 200 OK )时,此时需要手动控制 Locust 是否应将请求视为成功或失败。
通过使用 catch_response 参数和 with 语句,即使状态码正确,也可以将请求标记为失败。
with self.client.get("/", catch_response=True) as response:
if response.content != b"Success":
response.failure("Got wrong response")
正如可以将带有 OK 状态码的请求标记为失败一样,也可以将 catch_response 参数与 with 语句一起使用,以使导致 HTTP 错误代码的请求在统计信息中仍被报告为成功:
with self.client.get("/does_not_exist/", catch_response=True) as response:
if response.status_code == 404:
response.success()
使用动态参数将对请求的分组
Web 站点上的 URL 中包含某种动态参数是很常见的。通常,将这些 URL 归为“用户”统计信息是有意义的。这可以通过向HttpSession's
不同的请求方法传递 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]")
HTTP 代理设置
为了提高性能,我们通过将 request.Session 的 trust_env 属性设置为 False,将请求配置为不在环境中查找 HTTP 代理设置。
如果你不希望这样做,可以手动将locust_instance.client.trust_env
设置为True
。有关更多详细信息,请参阅requests文档。
如何构建测试代码
重要的是要记住:locustfile.py
只是由 Locust 导入的普通 Python 模块。从该模块中,你可以像导入任何 Python 程序一样自由地导入其他 python 代码。当前的工作目录会自动添加到 python 的 sys.path 中,因此可以使用 python 的 import 语句导入驻留在工作目录中的所有 python 文件/模块/软件包。
对于小型测试,将所有测试代码保存在一个locustfile.py
中应该可以正常工作,但是对于大型测试套件,你可能需要将代码拆分为多个文件和目录。
当然,如何构造测试源代码完全取决于你,但是我们建议你遵循 Python 最佳实践。以下是一个虚构的 Locust 项目的示例文件结构:
-
Project root
-
common/
__init__.py
auth.py
config.py
-
locustfile.py
-
requirements.txt
(用到第三方扩展库及其版本说明保存在 requirements.txt)
-
具有多个不同 locustfiles 的项目也可以将它们保存在单独的子目录中:
-
Project root
-
common/
__init__.py
auth.py
config.py
-
locustfiles/
api.py
website.py
-
requirements.txt
-
使用上述任何项目结构,你的 locustfile 都可以使用以下方法导入公共库:
import common.auth
网友评论