最近打算学习LocustIO,但是介于英文水平一般,英文文档读起来还是不太顺畅,于是花了点时间把整个英文文档翻译了一遍,以供学习之用。翻译过程尽量终于原文,但是由于水平有限,难免会有错失遗漏,如有发现,请不吝指正,谢谢!
Locust
是一个易于使用的分布式用户负载测试工具。它用于对Web站点(或其他系统)进行负载测试,并计算出一个系统可以处理多少并发用户。
Locust
这个工具的灵感来自于,在测试期间,一群蝗虫(Locust)会攻击你的网站。每个蝗虫(或者测试用户)的行为由您定义,群集过程由Web UI实时监控。这可以帮助您在让真正的用户进入系统之前进行测试并识别代码中的瓶颈。
Locust
完全基于事件,因此可以在一台机器上支持数千个并发用户。与许多其他基于事件的应用程序相比,它不使用回调,而是使用基于 gevent 的轻量级进程。每个 Locust 都在自己的进程中运行(正确的说法是greenlet)。这允许您用Python编写非常有表现力的场景,而不用使用回调使代码复杂化。
使用纯Python代码编写用户测试场景
不需要笨重的UI或臃肿的XML—只需要像通常那样编写代码即可。基于协程而不是回调,您的代码看起来和行为都像正常的Python代码。
分布式和可伸缩-支持成千上万的用户
Locust
支持在多台机器上运行负载测试。由于基于事件,即使一个 Locust 节点也可以在一个进程中处理数千个用户。这背后的部分原因是,即使您模拟了那么多用户,也不是所有用户都在积极地攻击您的系统。通常,用户都在无所事事地考虑下一步该做什么。使得每秒请求数不等于在线用户数。
基于Web的UI
Locust
有一个简洁的由 HTML+JS 生成的用户界面,可以实时显示相关的测试细节。由于UI是基于Web的,所以它是跨平台的,并且易于扩展。
可以测试任何系统
尽管 Locust
是面向Web的,但它几乎可以用于测试任何系统。无论你想测试什么,只要编写一个客户端,然后让它像蝗虫一样成群结队!这是超级简单的!
Locust
很小,很容易对付,我们打算让它保持这种状态。所有事件I/O和协程的重载都委托给 gevent。替代测试工具的脆弱性是我们创建Locust的原因。替代测试工具的脆弱性,是我们创建 Locust 的原因。
Locust
的产生是因为我们厌倦了现有的解决方案。它们都没有解决正确的问题,对我来说,它们没有抓住重点。我们已经尝试了Apache JMeter 和 Tsung,这两种工具都可以使用。我们已经在工作中多次使用了前者来进行基准测试。JMeter附带一个UI,您可能会认为这是一件好事。但是您很快就会意识到,通过一些简单的点击界面来 “编码” 您的测试场景是一个陷阱。其次,JMeter 是线程绑定的。这意味着对于您想要模拟的每个用户,都需要一个单独的线程。不用说,在一台机器上使用千上万的虚拟用户进行基准测试是不可行的。
另一方面,Tsung 没有这些线程问题,因为它是用 Erlang 编写的。它可以利用 BEAM 自身提供的轻量级进程,并愉快地进行扩展。但是在定义测试场景时,Tsung 和 JMeter 一样受到限制。它提供了一个基于XML的DSL来定义用户在测试时应该如何表现。我想你可以想象 “编码” 这个XML文件的恐怖。在完成时显示任何类型的图表或报告都要求您对测试生成的日志文件进行后处理。只有这样你才能了解测试的情况。
无论如何,我们在创造 Locust
的过程中已经尝试解决这些问题。希望以上这些痛点都不存在。
我想你可能会说,我们只是解决了我们自己测试工作中的痛点。但是,我们希望其他人能够像我们一样,享受到这个工具所带来的便利。
Open source licensed under the MIT license (see LICENSE file for details)。
下面是一个简单的 locustfile.py 的小例子:
from locust import HttpLocust, TaskSet
def login(l):
l.client.post('/login_action/', {'username': 'ellen_key', 'password': 'education'})
def logout(l):
l.client.post('/logout/', {'username': 'ellen_key', 'password': 'education'})
def index(l):
l.client.get('/')
def profile(l):
l.client.get('/profile')
class UserBehavior(TaskSet):
tasks = {index: 2, profile: 1}
def on_start(self):
login(self)
def on_stop(self):
logout(self)
class WebsiteUser(HttpLocust):
task_set = UserBehavior
min_wait = 5000
max_wait = 9000
在这里,我们定义了许多 Locust 任务,这些任务是普通的Python可调用函数,它们只接受一个参数(一个 Locust 类实例)。这些 Locust 任务集中在 TaskSet 类的子类的 tasks 属性中。然后我们定义了一个 HttpLocust 类的子类,它代表一个用户。在这个类中,我们定义了一个模拟用户在执行任务之间应该等待多长时间,以及哪个TaskSet类定义了用户的“行为”。其中,TaskSet 类可以嵌套。
声明任务的另一种方法(通常更方便)是使用 @task
装饰器。以下代码相当于上述代码:
from locust import HttpLocust, TaskSet, task
class UserBehavior(TaskSet):
def on_start(self):
""" on_start is called when a Locust start before any task is scheduled """
self.login()
def on_stop(self):
""" on_stop is called when the TaskSet is stopping """
self.logout()
def login(self):
self.client.post("/login", {"username": "ellen_key", "password": "education"})
def logout(self):
self.client.post("/logout", {"username": "ellen_key", "password": "education"})
@task(2)
def index(self):
self.client.get("/")
@task(1)
def profile(self):
self.client.get("/profile")
class WebsiteUser(HttpLocust):
task_set = UserBehavior
min_wait = 5000
max_wait = 9000
Locust 类(以及 HttpLocust,因为它是 Locust 类的子类)还允许指定每个模拟用户在执行任务(min_wait 和 max_wait)和其他用户行为之间的最小和最大等待时间(以毫秒为单位)。默认情况下,时间是在 min_wait 和 max_wait 之间随机均匀地选择的,但是可以通过将 wait_function 设置为一个函数来自定义这个等待时间。例如,对于平均为1秒的指数分布等待时间:
import random
class WebsiteUser(HttpLocust):
task_set = UserBehavior
wait_function = lambda self: random.expovariate(1) * 1000
如果要运行上面的 Locust 文件,假设它的名称是 locustfile.py,并且位于当前工作目录中,可以在命令行下使用如下的命令:
$ locust --host=http://example.com
如果 Locust 文件位于一个子目录或者使用了其他的名称,那么可以使用 -f
参数来指定:
$ locust -f locust_files/my_locust_file.py --host=http://example.com
要运行分布在多个进程中的 Locust,我们可以通过 --master
参数来指定并启动一个主进程:
$ locust -f locust_files/my_locust_file.py --master --host=http://example.com
然后,我们就可以启动任意数量的从属进程了:
$ locust -f locust_files/my_locust_file.py --slave --host=http://example.com
如果我们想在多台机器上运行 Locust,我们还必须在启动从机时指定主机(在一台机器上运行 Locust 时不需要这样做,因为主机默认为 127.0.0.1):
$ locust -f locust_files/my_locust_file.py --slave --master-host=192.168.0.100 --host=http://example.com
注意:要查看所有可用选项,请使用命令
locust --help
使用上面的命令行启动 Locust 之后,可以在浏览器打开地址 http://127.0.0.1:8089 (如果您在本地运行Locust)。然后,就可以看到如下的画面:
locustfile 是一个普通的Python文件。惟一的要求在这个文件中必须至少定义一个继承自 Locust 类(我们称它为locust类)的类。
Locust 类表示一个用户(或者一个 locust 的集群)。Locust 将为每个被模拟的用户生成(孵化)一个 locust 类的实例。一个 locust 类通常应该定义如下一些属性:
task_set 属性应该指向一个定义了用户行为的 TaskSet 类,下面将对其进行更详细的描述。
除了 task_set 属性外,通常还需要声明 min_wait 和 max_wait 属性。这些分别是模拟用户在执行每个任务之间等待的最小时间和最大时间,单位为毫秒。min_wait 和 max_wait 默认值均为1000,因此,如果没有声明 min_wait 和 max_wait,则 locust 将在每个任务之间始终等待1秒。
使用以下 locustfile,每个用户将在任务之间等待5到15秒:
from locust import Locust, TaskSet, task
class MyTaskSet(TaskSet):
@task
def my_task(self):
print("executing my_task")
class MyLocust(Locust):
task_set = MyTaskSet
min_wait = 5000
max_wait = 15000
min_wait 和 max_wait 属性也可以在 TaskSet 类中重写。
你可以像下面这样从一个文件中运行两个 locust:
$ locust -f locust_file.py WebUserLocust MobileUserLocust
如果你想让其中一个运行的更频繁,你可以在这些类中设置一个 weight 属性。下面的例子中,web user 运行的频率将是 mobile user 的3倍:
class WebUserLocust(Locust):
weight = 3
...
class MobileUserLocust(Locust):
weight = 1
...
host 属性是要加载主机的 URL 前缀(例如 https://google.com)。通常,URL前缀是在命令行中启动 locust 时通过参数 --host
指定的。当在 locust 类中声明一个 host 属性,并且在命令行中启动 locust 时未使用 --host
参数,那么将使用 host 属性的值。
如果 Locust 代表一个 locust 的集群,那么 TaskSet
类则代表 locust 的大脑。每个 Locust 类必须有一个指向TaskSet 类的 task_set 属性集。
TaskSet 就像它的名字一样,是一组任务,这些任务都是普通的Python可调用对象。如果我们对一个拍卖的网站进行负载测试,那么可以执行诸如 “加载其实页面”、“搜索某些产品” 和 “出价” 等操作。
当启动负载测试时,派生的 Locust 类的每个实例将开始执行它们的 TaskSet。然后,每个 TaskSet 将选择一个任务并执行。之后等待若干毫秒,这个等待时间是均匀分布在 Locust 类的 min_wait 和 max_wait 属性值之间的一个随机数(如果 TaskSet 设置了自己的 min_wait 和 max_wait 属性,则将使用它自己设置的值)。然后它将再次选择要执行的任务,再次等待。以此类推。
定义 TaakSet 的 tasks 的典型方式是使用 @task
装饰器。
下面是一个例子:
from locust import Locust, TaskSet, task
class MyTaskSet(TaskSet):
@task
def my_task(self):
print("Locust instance (%r) executing my_task" % (self.locust))
class MyLocust(Locust):
task_set = MyTaskSet
@task
可以接受一个可选参数 weight,用来指定任务的执行比率。在下面的例子中, task2 的执行比率是 task1 的 2 倍:
from locust import Locust, TaskSet, task
class MyTaskSet(TaskSet):
min_wait = 5000
max_wait = 15000
@task(3)
def task1(self):
pass
@task(6)
def task2(self):
pass
class MyLocust(Locust):
task_set = MyTaskSet
使用 @task
装饰器定义任务是一种方便的方法,通常也是最好的方法。但是,也可以通过设置 tasks 属性来定义TaskSet 的任务(使用 @task
装饰器实际上就是用来设置 tasks 属性)。
tasks 属性要么是Python可调用对象的列表,要么是元素是以可调用对象为键、以 int 类型的值为值的字典。tasks 属性中的这些可调用对象接受一个表示正在执行任务的 TaskSet 类的实例作为参数。下面是一个极其简单的 locustfile 例子(这个 locustfile 实际上不会对任何东西执行负载测试):
from locust import Locust, TaskSet
def my_task(l):
pass
class MyTaskSet(TaskSet):
tasks = [my_task]
class MyLocust(Locust):
task_set = MyTaskSet
如果将 tasks 属性指定为列表,那么每次执行任务时,都将从 tasks 属性中随机选择任务。但是,如果任务是一个以可调用对象为键、以 int 类型的值为值的字典,则将随机选择要执行的任务,并以 int 类型的值为比率。对于下面这样的任务:
{my_task: 3, another_task: 1}
my_task 的执行比率是 another_task 的三倍。
TaskSet 的一个非常重要的特性是它们可以嵌套,因为真正的网站通常是用分层的方式构建的,包含多个子部分。因此,嵌套的 TaskSet 将允许我们定义一个行为,以更现实的方式模拟用户。例如,我们可以用下面的结构来定义 TaskSet:
- Main user behaviour
- Index page
- Forum page
- Read thread
- Replay
- New thread
- View next page
- Browse categories
- Watch movie
- Filter movies
- About page
嵌套 TaskSet 的方法就像使用 tasks 属性指定任务一样,但不是引用Python函数,而是引用另一个TaskSet:
class ForumPage(TaskSet):
@task(20)
def read_thread(self):
pass
@task(1)
def new_thread(self):
pass
@task(5)
def stop(self):
self.interrupt()
class UserBehaviour(TaskSet):
tasks = {ForumPage:10}
@task
def index(self):
pass
因此,在上面的示例中,如果在执行 UserBehaviour
任务集时选择 ForumPage
执行,那么 ForumPage
任务集将开始执行。然后,ForumPage任务集将选择它自己的任务之一,执行,然后等待,以此类推。
关于上面的示例,有一件重要的事情需要注意,那就是在 ForumPage
的 stop 方法中调用了 self.interrupt() 。这样做的本质上是停止执行 ForumPage
任务集,并在 UserBehavior
实例中继续执行。如果在 ForumPage
中没有对interrupt() 方法的调用,Locust 在启动 ForumPage
任务之后就不会停止运行它。但是通过使用中断功能,我们可以与任务权重一起定义模拟用户离开论坛的可能性。
也可以使用 @task
装饰器在类中像声明普通任务一样内联声明嵌套的 TaskSet :
class MyTaskSet(TaskSet):
@task
class SubTaskSet(TaskSet):
@task
def my_task(self):
pass
TaskSet 实例的属性 locust 指向它的 locust 实例,属性 parent 指向它的父 TaskSet(它将指向父类 TaskSet 中的 loocust 实例)。
TaskSequence
类是一个 TaskSet,但是它的任务将按顺序执行。要定义这个顺序,您应该执行以下操作:
class MyTaskSequence(TaskSequence):
@seq_task(1)
def first_task(self):
pass
@seq_task(2)
def second_task(self):
pass
@seq_task(3)
@task(10)
def third_task(self):
pass
在上面的例子中,顺序被定义:
执行一次 first_task → 执行一次 second_task → 执行10次 third_task
可以看到,可以将 @task
装饰器和 @seq_task
组合起来使用,当然也可以在 TaskSequence 中嵌套 TaskSet,反之亦然。
Locust 还以可选的方式支持 Locust 级别的 setup 和 teardown,TaskSet 级别的 setup 和 teardown,以及 TaskSet 级别的 on_start 和 on_stop。
无论是运行在 Locust 还是 TaskSet 上,setup 和 teardown 都是只运行一次的方法。setup 在任务开始运行之前运行,而 teardown 在所有任务完成并退出 Locust 之后运行。这使您能够在任务开始运行前执行一些准备工作(如创建数据库),并在 Locust 退出之前进行清理(如删除数据库)。
要使用它,只需在 Locust 或 TaskSet 类上声明一个 setup 和/或 teardown 方法即可。这些方法将会自动运行。
TaskSet 类可以声明 on_start 方法或 on_stop 方法。on_start 方法在虚拟用户开始执行 TaskSet 类时调用,而on_stop
(locust.core.TaskSet.on_stop())方法在 TaskSet 停止时调用。
设置和清理操作可能是相互依赖的,因此他们的执行必须有一定的顺序,下面是它们的运行顺序:
一般来说,setup 和 teardown 方法在功能上应该是互补的。
到目前为止,我们只讨论了 Locust 用户的任务调度部分。为了对系统进行真是的负载测试,我们需要发出HTTP请求。而 HttpLocust 类的存在可以帮助我们做到这一点。当使用这个类时,每个实例都获得一个 client 属性,该属性将是 HttpSession 的一个实例,可用于发起HTTP请求。
class HttpLocust
表示一个策划并攻击要进行负载测试的系统的 HTTP “用户”。这个HTTP “用户” 的行为由 task_set 属性定义,该属性应该指向一个 TaskSet 类。这个类在实例化时会创建一个 client 属性,这个属性的值是一个支持在请求间保持用户会话(user session)的 HTTP 客户端。
client=None
在 locust
实例化时创建的 HttpSession 实例。客户端支持 cookies
,因此在 HTTP 请求间保持会话。
当继承 HttpLocust 类时,我们可以使用它的 client 属性对服务器发出HTTP请求。下面的 locust 文件,可以通过两个URLs(/ 和 /about/)对一个网站进行负载测试:
from locust import HttpLocust, TaskSet, task
class MyTaskSet(TaskSet):
@task(2)
def index(self):
self.client.get("/")
@task(1)
def about(self):
self.client.get("/about/")
class MyLocust(HttpLocust):
task_set = MyTaskSet
min_wait = 5000
max_wait = 15000
使用上面的 Locust 类,每个模拟用户将在请求之间等待5到15秒,并且URL /
将被请求的时间是 /about/
的两倍。
细心的读者会发现,我们可以在 TaskSet 内使用 self.client 而不是 self.locust.client 引用 HttpSession 实例,这看起来很奇怪。之所以可以这样做,是因为 TaskSet 类有一个返回 self.locust.client 的属性 client。
HttpLocust 的每个实例都有一个值为HttpSession 实例的 client 属性。HttpSession 类实际上是requests.Session 类的子类,这个类的实例可以使用 get,post, head, put,delete, options 和 patch 方法发出HTTP请求并报告给 Locust 的统计数据。HttpSession 实例将在请求之间保存 cookie,以便用于登录网站并在请求之间保持会话。client 属性还可以被来自 Locust 实例的 TaskSet 实例引用,这样就很容易检索客户端并在任务中发出HTTP请求。
下面是一个简单的例子,它向 /about
路径发出 GET 请求(在本例中,我们假设 self 是 TaskSet 或 HttpLocust 类的一个实例:
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 对象。该请求在 Locust 的统计数据中将被报告为失败。返回的虚拟 Response 的 content 属性将被设置为 None,其 status_code 将为0。
默认情况下,除非HTTP响应代码是 OK(2xx),否则将被标记为失败。大多数情况下,这个默认的行为即使你想要的结果。然而,有的时候,例如,当测试一个 URL 端点时,您希望返回404,或者测试一个设计糟糕的系统,即使出现错误,也可能返回 200 OK,这样就产生了一个需求,即需要手动控制一个请求应该是成功或者失败。
要实现这个需求,可以使用 catch_response 参数和 with 语句将响应代码是 OK 的请求标记为失败:
with client.get("/", catch_response=True) as response:
if response.content != b"Success":
response.failure("Got wrong response")
正如可以将响应代码为 OK 的请求标记为失败一样,也可以使用 catch_response 参数和 with 语句将响应代码不是 OK 的请求在统计数据中标记为成功。
with client.get("/does_not_exist/", catch_response=True) as response:
if response.status_code == 404:
response.success()
网站的某些页面的 URL 中包含一些动态参数是很常见的。通常,在 Locust 的统计数据中将这些 URL 组合在一起是有意义的。可以通过将 name 参数传递给 HttpSession 的不同请求方法来实现这个功能。
例如:
# Statistics for these requests will be grouped under: /blog/?id=[id]
for i in range(10):
client.get("/blog?id=%i" % i, name="/blog?id=[id]")
通常,人们希望对共享公共库的多个 locustfile 进行分组。在这种情况下,重要的是将项目根目录定义为调用locust 的目录,并且建议所有 locustfiles 都位于项目根下的某个位置。
一个开箱即用的平面文件结构如下:
commonlib_config.py
commonlib_auth.py
locustfile_web_app.py
locustfile_api.py
locustfile_ecommerce.py
locustfiles 可以使用以下命令导入公共库,例如,import commonlib_auth
。但是,这种方法并不能将公共库与locust 文件清晰地分开。
使用子目录是一种更清晰的方法(参见下面的示例),但是 locust 只导入与运行的 locustfile 所在目录相关的模块。如果希望从项目根目录(即运行 locust 命令的位置)导入,在导入任何公共库之前,请确保在 locust 文件中写入 sys.path.append(os.getcwd())
,这将使项目根目录(即当前工作目录)可导入。
项目根目录
__init__.py
common/
__init__.py
config.py
auth.py
locustfiles/
__init__.py
web_app.py
api.py
ecommerce.py
使用如上的项目结构,你的 locust 文件可以使用如下的方式导入公共库:
sys.path.append(os.getcwd())
import common.auth
Locust 支持跨多台机器运行负载测试。
为此,您可以使用 --master
参数在 master 模式下启动一个 Locust实例。这个实例将运行 Locust 的 Web 接口,您可以在其中启动测试并查看实时统计数据。主节点本身不模拟任何用户,您必须使用 —slave
参数启动一个或多个从属 Locust 节点并使用 --master-host
参数指定主节点的IP/主机名。
一种常见的设置是在一台机器上运行一个主程序,然后在从属机器上为每个处理器内核运行一个从属实例。
注意:无论是主机还是从机,在运行分布式 Locust 测试脚本时,都必须有一份 Locust 测试脚本的副本。
在主节点上启动 locust:
$ locust -f my_locustfile.py --master
然后,再启动每个从属节点上的 locust(使用主节点的IP替换 192.168.0.14):
$ locust -f my_locustfile.py --slave --master-host=192.168.0.14
--master
在主节点上设置 locust。Web界面会在这个节点上运行。
--slave
在从属节点上设置 locust。
--master-host:X.X.X.X
可选。与 --slave
参数一起使用时,用来设置主节点的 hostname/IP。(默认为 127.0.0.1)
--master-port=5557
可选。与 --slave
参数一起使用时,用来设置主节点的端口(默认端口为 5557)。注意,locust 不光会使用指定的这个端口,还会使用比这个端口的数值大1的端口,也就是说,如果指定端口为 5557,那么端口 5557 和 5558 都会被使用。
--master-bind-host=X.X.X.X
可选。与 --master
参数一起使用时,将决定主节点将会绑定的网络接口。默认值是 *
,表示所有的可用接口。
--master-bind-port=5557
可选。与 --master
参数一起使用时,将决定主节点要监听的网络端口。默认监听的端口是 5557。注意,locust 不光会监听指定的这个端口,还会监听比这个端口的数值大1的端口,也就是说,如果指定端口为 5557,则端口 5557 和 5558 都会被监听。
--expect-slaves=X
当启动主节点时使用了 --no-web
参数时使用。主节点会在测试开始之前一直等待,直到 X 从属节点成功连接。
请参考下一节的内容。
您可以在没有 Web UI的情况下运行 locust ,例如,如果您想在一些自动化流程中运行它,比如CI服务器中,那么可以使用 --no-web
参数和 -c
以及 -r
。
$ locust -f locust_files/my_locust_file.py --no-web -c 1000 -r 100
-c
指定要生成的 Locust 用户的数量。-r
指定每秒钟要生成的 Locust 用户的数量。注意:这是 v0.9 的新功能,对于 v0.8,可以使用
-n
参数指定请求的数量。
假如你想要为一个测试指定运行时间,可以使用 --run-time
或 -t
。
$ locust -f --no-web -c 1000 -r 100 --run-time 1h30m
当到达设置的时间时,Locust 将自动关闭。
如果你要在没有Web UI的情况下以分布式方式运行 Locust,必须在启动主节点的时候使用 --expect-slaves
参数指定期望连接的从属节点的数量。然后,主节点会等到连接了相应数量的从属节点之后才开始测试。
如果您希望通过CSV文件使用 Locust 结果,可以使用下面两种方法。
首先,当使用Web UI运行 Locust 时,您可以在 Download Data 选项卡下检索CSV文件。
其次,您可以使用一个参数运行 Locust,该参数将定期保存两个CSV文件。如果您计划使用 --no-web
参数以自动化的方式运行 Locust,这将特别有用:
$ locust -f examples/basic.py --csv=example --no-web -t10m
这些文件将命名为 example_distribution.csv 和 example_requests.csv (当使用 --csv=example
时),并在 stat页面中镜像 Locust 的构建。
如果你想写得更快(或更慢),你也可以自定义写入频率:
import locust.stats
locust.stats.CSV_STATS_INTERVAL_SEC = 5 # default is 2 seconds
该数据将写入文件名为你指定的名称分别加上 _distribution.csv 和 _requests.csv 的两个文件:
$ cat example_distribution.csv
"Name","# requests","50%","66%","75%","80%","90%","95%","98%","99%","100%"
"GET /",31,4,4,4,4,4,4,4,4,4
"/does_not_exist",0,"N/A","N/A","N/A","N/A","N/A","N/A","N/A","N/A","N/A"
"GET /stats/requests",38,3,4,4,4,4,5,5,5,5
"None Total",69,3,4,4,4,4,4,5,5,5
和:
$ cat example_requests.csv
"Method","Name","# requests","# failures","Median response time","Average response time","Min response time","Max response time","Average Content Size","Requests/s"
"GET","/",51,0,4,3,2,6,12274,0.89
"GET","/does_not_exist",0,56,0,0,0,0,0,0.00
"GET","/stats/requests",58,0,3,3,2,5,1214,1.01
"None","Total",109,56,3,3,2,6,6389,1.89
Locust 是以HTTP为主要目标构建的。但是,通过编写一个触发 request_success 和 request_failure 事件的自定义客户端,可以很容易地将其扩展为能够对基于 请求/响应
的系统进行负载测试的工具。
以下是一个 Locust 类:XmlRpcLocust
的例子,它提供一个 XML-RPC 客户端 xmlrpclient,并跟踪所有发出的请求:
import time
import xmlrpclib
from locust import Locust, TaskSet, events, task
class XmlRpcClient(xmlrpclib.ServerProxy):
"""
Simple, sample XML RPC client implementation that wraps
xmlrpclib.ServerProxy and fires locust events on request_success
and request_failure, so that all requests gets tracked in locust's
statistics.
"""
def __getattr__(self, name):
func = xmlrpclib.ServerProxy.__getattr__(self, name)
def wrapper(*args, **kwargs):
start_time = time.time()
try:
result = func(*args, **kwargs)
except xmlrpclib.Fault as e:
total_time = int((time.time() - start_time) * 1000)
events.request_failure.fire(request_type="xmlrpc",
name=name,
response_time=total_time,
exception=e)
else:
total_time = int((time.time() - start_time) * 1000)
events.request_success.fire(request_type="xmlrpc",
name=name,
response_time=total_time,
response_length=0)
# In this example, I've hardcoded response_length=0.
# If we would want the response length to be
# reported correctly in the statistics, we would probably
# need to hook in at a lower level
return wrapper
class XmlRpcLocust(Locust):
"""
This is the abstract Locust class which should be subclassed.
It provides an XML-RPC client that can be used to make XML-RPC
requests that will be tracked in Locust's statistics.
"""
def __init__(self, *args, **kwargs):
super(XmlRpcLocust, self).__init__(*args, **kwargs)
self.client = XmlRpcClient(self.host)
class ApiUser(XmlRpcLocust):
host = "http://127.0.0.1:8877/"
min_wait = 100
max_wait = 1000
class task_set(TaskSet):
@task(10)
def get_time(self):
self.client.get_time()
@task(5)
def get_random_number(self):
self.client.get_random_number(0, 100)
如果您以前编写过 Locust测试,您应该知道一个名为 ApiUser 的类,它是一个普通的 Locust 类,它的 task_set属性是一个 TaskSet 类的子类,而这个子类带有多个 task。
然而,ApiUser 继承自 XmlRpcLocust,您可以在 ApiUser 的正上方看到它。XmlRpcLocust 类在 client 属性下提供 XmlRpcClient 的实例。XmlRpcClient 是标准库的 xmlrclib. serverproxy 的装饰器。它基本上只是代理函数调用,但是添加了触发用于将所有调用报告给 Locust 统计数据的 locust.events.request_success 和 locust.events.request_failure 的重要功能。
下面是 XML-RPC 服务器的实现,它可以作为上述代码的服务器:
import random
import time
from SimpleXMLRPCServer 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()
class Locust
表示一个策划并发起负载测试的 “用户”。这个 “用户” 的行为由 task_set 属性定义,该属性应该指向一个 TaskSet 类。该个类通常应该由定义某种客户端的类来子类化,例如,对 HTTP 系统进行负载测试时,你可能希望使用 HttpLocust 类。
max_wait=1000
执行 locust
任务时的最长时间间隔。
min_wat=1000
执行 locust 任务时的最短时间间隔。
task_set=None
定义 locust
行为的 TaskSet 类。
wait_function()
计算执行 locust
任务时的时间间隔的函数,单位为毫秒。
weight=10
locust
被选中的概率。权重越大,被选中的几率就越大。
class HttpLocust
表示一个策划并攻击要进行负载测试的系统的 HTTP “用户”。这个HTTP “用户” 的行为由 task_set 属性定义,该属性应该指向一个 TaskSet 类。这个类在实例化时会创建一个 client 属性,这个属性的值是一个支持在请求间保持用户会话(user session)的 HTTP 客户端。
client=None
在 locust
实例化时创建的 HttpSession 实例。客户端支持 cookies
,因此在 HTTP 请求间保持会话。
class TaskSet(parent)
定义 locust
用户将要执行的一组任务。
当 TaskSet 开始运行时,它会从 tasks 属性中选择一个任务并执行,然后调用这个任务的 wait_function 方法,之后再调用另一个任务,以此类推。其中 wait_function 方法定义并返回一个以毫秒为单位的睡眠时间,wait_function 方法定义的睡眠时间的默认是介于 min_wait 和 max_wait 之间且均匀分布的随机数。
TaskSets 可以嵌套,这意味着一个 TaskSet 的 tasks 属性可以包含其他的 TaskSet。如果计划执行嵌套的 TaskSet ,则将实例化它并从当前执行的 TaskSet 进行调用。然后,当前运行的 TaskSet 中的执行将被移交给嵌套的 TaskSet ,嵌套的 TaskSet 将继续运行,直到遇到由 TaskSet.interrupt() 方法抛出的 InterruptTaskSet 异常时终止。而后,将继续在第一个 TaskSet 中执行。
client
引用根 locust
实例的 client 属性。
interrupt(reschedule=True)
中断 TaskSet 并将执行控制权交给父 TaskSet。
如果 reschedule 的值为 True,父 locust 将立即重新调度并执行下一个任务。
这个方法不应该由根 TaskSet (即立即附加到 Locust 类的 task_set 属性)调用,而应该由层次结构中更深层次的嵌套 TaskSet 类调用。
locust=None
在 TaskSet 实例化后将引用根 Locust 类的实例。
max_wait=None
执行 locust 任务时的最长时间间隔。可以用来覆盖根 Locust 类中定义的 max_wait。如果 TaskSet 没有设置这个属性,那么将使用根 Locust 类的 max_wait 属性值。
min_wait=None
执行 locust 任务时的最短时间间隔。可以用来覆盖根 Locust 类中定义的 min_wait。如果 TaskSet 没有设置这个属性,那么将使用根 Locust 类的 min_wait 属性值。
parent=None
当 TaskSet 实例化以后,将引用父 TaskSet 或 Locust 类的实例。适用于嵌套的 TaskSet 类。
schedule_task(task_callable, args=None, kwargs=None, first=False)
添加一个任务到 locust 的任务执行队列。
参数:
tasks=[]
列表中包含表示 locust 用户任务的可调用对象。
如果该参数值是一个列表,那么将从中随机挑选任务进行执行。
如果该参数值是一个元素为二元组 (callable, int) 的列表或元素为 callable: int 的字典,那么将随机选择要执行的任务,但是每个任务将根据其对应的 int 类型的值进行加权。所以在下面的例子中,ThreadPage
被选中的可能性是 write_post
的15倍:
class ForumPage(TaskSet):
tasks = {Threadpage: 15, write_post: 1}
wait_function=None
用于计算 locust 任务执行时中间的间隔时间的函数,单位为毫秒。可以用于覆盖根 Locust 类中定义 wait_function 方法。如果 TaskSet 没有设置这个属性,那么将使用根 Locust 类的 wait_function 方法。
task(weight=1)
用于在类中内联声明 TaskSet 的任务。
例如:
class ForumPage(TaskSet):
@task(100)
def read_thread(self):
pass
@task(7)
def create_thread(self):
pass
class TaskSequence(parent)
定义 locust 用户将要执行的任务序列。
当 TaskSequence 开始执行时,它将从 tasks 属性值中根据任务的索引选择一个任务进行执行,然后调用它的定义了一个睡眠时间的 wait_fucntion 方法。wait_function 定义的睡眠时间默认为介于 min_wait 和 max_wait 之间且均匀分布的一个随机数,单位为毫秒。然后再调用索引为 index + 1 / % 的任务,以此类推。
TaskSequence 可以与 TaskSet 嵌套,这意味着 TaskSequence 的 tasks 属性可以包含 TaskSet 实例和其他TaskSequence 实例。如果计划执行嵌套的 TaskSet,则将实例化它并从当前执行的 TaskSet 调用它。然后,当前运行的 TaskSet 中的执行将被移交给嵌套的 TaskSet ,这个嵌套的 TaskSet 将继续运行,直到遇到由 TaskSet.interrupt() 抛出 InterruptTaskSet 异常时终止,然后在第一个 TaskSet 中继续执行。
在这个类中,任务应该被定义成一个列表,或者简单地由 task_seq 装饰器定义。
client
引用根 Locust 实例的 client 属性。
interrupt(reschedule=True)
中断 TaskSet 并将执行控制权交给父 TaskSet。
如果 reschedule 的值为 True,父 locust 将立即重新调度并执行下一个任务。
这个方法不应该由根 TaskSet (即立即附加到 Locust 类的 task_set 属性)调用,而应该由层次结构中更深层次的嵌套 TaskSet 类调用。
schedule_task(task_callable, args=None, kwargs=None, first=False)
添加一个任务到 locust 的任务执行队列。
参数:
seq_task(order)
用于在类中内联声明 TaskSequence 的任务。
例如:
class NormalUser(TaskSequence):
@seq_task(1)
def login_first(self):
pass
# You can also set the weight in order to execute the task
# for 'weight' times one after another.
@seq_task(2)
@task(25)
def then_read_thread(self):
pass
@seq_task(3)
def then_logout(self):
pass
class HttpSession(base_url, *args, **kwargs)
用于执行 Web 请求并在请求之间保持会话(以便能够登录和退出网站)。每个请求都将被记录下来,以便 locust 可以显示统计数据。
这是Python的 requests
库的 requests.Session 类的拓展,工作原理与是极其相似的。然而,发送请求的方法(get、post、delete、put、head、options、patch、request)现在可以接受一个 url 参数,这个参数只是 URL的路径部分,在这种情况下,URL的主机部分将取 HttpSession.base_url (继承自一个 Locust 类的 host 属性)的值。
发送请求的每个方法还接受两个额外的可选参数,这些参数是特定于 Locust ,在Python的 requests 库中不存在的:
参数:
name
可选参数。可以指定为 Locust 的统计信息中的标签,用于代替 URL 路径。这可以用于将被请求的不同 URL 分组到 Locust 统计数据中的一个条目中。
catch_response
可选参数。如果要设置,可以是一个布尔值。可以用来使请求返回为作为with 语句的参数的上下文管理器。这将允许根据响应内容将请求标记为失败,即使响应代码是 ok (2xx) ,反之亦然。可以使用 catch_response 捕捉请求,然后将其标记为成功,即使响应代码不是 ok (例如 500 或 404)。
该类中包含如下几个常用的实例方法:
delete(url, **kwargs)
发送一个 DELETE 请求,返回一个 Response
对象。
参数:
Request
对象的URL。request
的可选参数。返回值类型:requests.Response
对象。
get(url, **kwargs)
发送一个 GET 请求,返回一个 Response
对象。
参数:
Request
对象的URL。request
的可选参数。返回值类型:requests.Response
对象。
head(url, **kwargs)
发送一个 HEAD 请求,返回一个 Response
对象。
参数:
Request
对象的URL。request
的可选参数。返回值类型:requests.Response
对象。
options(url, **kwargs)
发送一个 OPTIONS 请求,返回一个 Response
对象。
参数:
Request
对象的URL。request
的可选参数。返回值类型:requests.Response
对象。
patch(url,data=None , **kwargs)
发送一个 PATCH 请求,返回一个 Response
对象。
参数:
Request
对象的URL。request
的可选参数。返回值类型:requests.Response
对象。
post(url,data=None , json=None, **kwargs)
发送一个 POST 请求,返回一个 Response
对象。
参数:
Request
对象的URL。request
的可选参数。返回值类型:requests.Response
对象。
put(url,data=None , **kwargs)
发送一个 PUT 请求,返回一个 Response
对象。
参数:
Request
对象的URL。request
的可选参数。返回值类型:requests.Response
对象。
request(method, url, name=None , catch_response=False, **kwargs)
构造并发送一个 requests.Request
。返回 requests.Response
对象。
参数:
method
新 Request
对象的方法。
url:新 Request
对象的URL。
name:可选参数。
可以指定为 Locust 的统计信息中的标签,用于代替 URL 路径。这可以用于将被请求的不同 URL 分组到 Locust 统计数据中的一个条目中。
catch_response
可选参数。如果要设置,可以是一个布尔值。可以用来使请求返回为作为with 语句的参数的上下文管理器。这将允许根据响应内容将请求标记为失败,即使响应代码是 ok (2xx) ,反之亦然。可以使用 catch_response 捕捉请求,然后将其标记为成功,即使响应代码不是 ok (例如 500 或 404)。
params
可选参数。要发送到 Request
的查询字符串的字典或 bytes 对象。
data:可选参数。要发送到 Request
主体中的字典或 bytes 对象。
headers:可选参数。与 Request
一起发送的表示 HTTP headers 的字典。
cookies:可选参数。与 Request
一起发送的表示 cookies 的 dict 或 CookieJar 对象。
files:可选参数。用于多部分编码上传的元素为 filename: file-like-objects 的字典。
auth:可选参数:用于启用 Basic、Digest 或自定义的 HTTP Auth 的元组或可调用对象。
timeout:
可选参数。以浮点数或(连接超时、读取超时)元组的形式等待服务器发送数据的时间(以秒为单位)。
allow_redirects:可选参数。布尔类型。默认值为 True。 表示是否允许重定向。
proxies:可选参数。字典类型。键表示代理使用的协议,键值表示代理的URL。
stream:可选参数。是否立即下载响应内容。默认值为 False。
verify:可选参数。如果为 True,则会验证 SSL 证书。也可以提供一个 CA_BUNDLE 路径。
cert:可选参数。如果提供一个字符串。那么应该是指向SSL 客户端证书(.pem文件)的路径;如果是一个元组,则应该是 (‘cert’, ‘key’)。
这个类其实是位于 requests
库中的,但是由于 Locust
在构造HTTP 请求的时候要用到这个类,并且在编写 Locust
测试时,这个类也非常重要,所以就把这个类包含在了 API 文档里。
class Response
包含服务器对HTTP请求的响应的 Response
对象。
该类的实例包含如下的实例属性与特性(property):
实例属性:
cookies=None
服务器返回的 CookieJar 或 Cookie 对象。
elapsed=None
发送请求到响应到达之间的时间间隔(使用 timedelta 对象表示)。此属性专门度量从发送请求的第一个字节到完成对报头的解析所花费的时间。因此,它不受响应内容或 stream 关键字参数值的影响。
encoding=None
访问 r.text 时解码操作要用到的编码方式。
headers=None
不区分大小写的响应头字典。例如,headers['content-encoding']
将会返回响应头中键为 Content-Encoding 的键值。
history=None
请求历史记录中的响应对象列表。任何重定向响应都将在这里结束。该列表从最早的请求到最近的请求进行排序。
reason=None
与 HTTP 状态码相对应的文本格式的原因,例如 Not Found 或 OK。
request=None
使用 PreparedRequest 表示的对应于当前响应的原始请求。
status_code=None
使用整数表示的 HTTP 响应状态码。例如 404 或 200。
url=None
响应的最终URL位置。
只读特性:
content
bytes 类型的响应内容。
apparent_encoding
由Python的 chardet 库提供的猜测到的响应的 content 使用的编码格式。
is_permanent_redirect
如果此响应是重定向的永久版本之一,则返回 True,否则返回 False。
is_redirect
如果此响应是可以自动处理的格式良好的HTTP重定向(通过 session.resolve_reredirect() 判断),则返回True,否则返回 False。
links
返回已解析的响应头链接(如果有的话)。
next
返回一个PreparedRequest 对象,用于表示重定向链中的下一个请求(如果有的话)。
ok
如果 status_code 小于400,返回 True;如果不小于400,返回 False。
此属性检查响应的状态代码是否在400到600之间,以查看是否存在客户端错误或服务器错误。如果状态码在200到400之间,则返回 True ,而不是检查响应代码是否为 200 OK
。
text
使用Unicode字符表示的响应的内容。
如果 Response.encoding 是 None,则使用 chardet 猜测编码。
响应内容的编码按照 RFC 2616 的规定,由 HTTP headers 唯一确定。如果可以利用非 HTTP 知识更好地猜测编码,应该在访问该特性之前为 r.encoding 设置合适的值。
实例方法:
close()
用于释放链接。一旦调用了次方法,就不能再访问底层原始对象了。
注意:这个方法通常不需要显示调用。
raise_for_status()
如果发生错误,则抛出已存储的 HTTPError 异常。
iter_content(chunk_size=1, decode_unicode=False)
用于遍历响应数据。当请求的 stream 参数的值设置为 True 时,使用这个方法可以避免立即将内容读入内存而获得较大的响应。chunk_size 参数设置每次读入内存的字节数。这个值并不一定是解码时返回的每个项的长度。
chunk_size 的类型必须是 int 或 None。如果是 None,则将根据 stream 的值来确定具体的行为。当 stream=True 时,将在数据以任何大小到达时读取数据。如果 stream=False ,数据将作为单个块返回。
如果 decode_unicode 为 True,那么将使用基于响应的最佳可用编码对内容进行解码。
iter_lines(chunk_size=512, decode_unicode=None, delimiter=None)
迭代响应数据,每次一行。当请求的 stream 参数为 True 时,可以避免立即将内容读入内存而获得较大的响应。
注意:这个方法不是可重入安全的
json(**kwargs)
返回响应的 json 编码内容(如果又的话)。
**kwargs 表示要传给 jason.loads 函数的可选参数。
如果响应的主体中不包含有效的 json 数据,则将引发 ValueError
异常。
class ResponseContextManager(response)
可以充当上下文管理器的 Response 类,提供手动控制HTTP 请求在在 Locost 的统计数据中应该标记为成功还是失败的能力。
这个类是 Response 类的子类。包含两个额外的方法:success 和 failure。
failure(exc)
将响应报告为失败。
其中参数 exc 可以是一个Python的异常类或者一个字符串。如果是一个字符串,那么将使用这个字符串来实例化 CatchResponseError 类。
例如:
with self.client.get('/', catch_response=True) as response:
if response.content = b'':
response.failure('No data')
success()
将响应报告为成功。
例如:
with self.client.get('/does/not/exist', catch_response=True) as response:
if response.status_code = 404:
response.success()
exception InterruptTaskSet(reschedule=True)
在 Locust 任务内抛出这个异常时,将会中断这个 Locust 正在执行的当前任务。
事件钩子都是 locust.events.EventHook 类的实例。
class EventHook
简单事件类,用于为 locust 中不同类型的事件提供钩子。
下面的代码演示如何使用这个类:
my_event = EventHook()
def on_my_event(a, b, **kw):
print(f'Event was fired with arguments: {a!s}, {b!s}')
my_event += on_my_event
my_event.fire(a='foo', b='bar')
如果 reverse 的值为 True,则处理程序将按照插入时的相反顺序运行。
下面的事件钩子在 locust.events 模块下可用:
request_success=
当一个请求成功完成时触发。
监听者应该使用如下参数:
request_failure=
当一个请求失败时触发。
事件触发式将使用如下参数:
locust_error=
当 Locust 类的执行过程中出现异常时触发。
事件触发式将使用如下参数:
report_to_master=
当 Locust 在 -slave 模式下运行时使用。用于将数据附加到定期发送给主服务器的数据字典上。当报告要发送到主服务器时,它会定期触发。
注意: Locust 使用的键 ‘stats’ 和 ‘errors’ 不应该被覆盖。
事件触发式将使用如下参数:
slave_report=
当 locust 在 -master 模式下运行使用。并在 Locust 主服务器从从属服务器收到报告时触发。
此事件可用于聚合来自 Locust 从属服务器的数据。
事件触发式将使用如下参数:
hatch_complete=
当所有 locust 用户都已经生成时触发。
事件触发式将使用如下参数:
quitting=
在退出 locust 进程时触发。
Locust 附带了一些事件,这些事件提供了以不同方式扩展 Locust 的钩子。
事件监听器可以在模块级注册到 Locust 文件中。这里有一个例子:
from locust import events
def my_success_handler(request_type, name, response_time, response_length, **kw):
print "Successfully fetched: %s" % (name)
events.request_success += my_success_handler
注意:
强烈建议在侦听器中添加通配符关键字参数(上面代码中的
**kw
),以防止在未来版本中添加新参数时代码崩溃。
另外:
要查看所有可用的事件,请查看 事件钩子 一节。
Locust 使用 Flask 作为 web UI 的服务器,因此很容易向Web UI添加web端点。只需导入你的 locustfile 中的Flask应用程序,并设置一个新的路径:
from locust import web
@web.app.route("/added_page")
def my_added_page():
return "Another page"
现在,你应该能够启动 locust 并导航到 http://127.0.0.1:8089/added_page。
Locust 主机和 Locust 从机通过交换 msgpack 消息进行通信,许多语言都支持 msgpack 消息。所以,你可以用任何你喜欢的语言来写 Loucst 任务。为了方便起见,一些库充当从运行器。他们会执行你的 Locust 任务,并定期向主机汇报。