一、概述
Locust
的原意是蝗虫,作者之所以选择这个名字,是因为Locust工具生成的并发请求就跟一大群蝗虫一样,对我们的被测系统发起攻击,以此检测系统在高并发压力下是否能够正常运转。
Locust
能够在较少压力机的前提下支持极高并发数的测试。
二、简单示例
from locust import HttpUser, TaskSet, task
class WebsiteTasks(TaskSet):
def on_start(self):
self.client.post("/login", {
"username": "test",
"password": "123456"
})
@task(2)
def index(self):
self.client.get("/")
@task(1)
def about(self):
self.client.get("/about/")
class WebsiteUser(HttpUser):
"""
# 主类
"""
# task_set 指向定义了用户行为的类
# min_wait模拟负载的任务之间执行时的最小等待时间,单位为毫秒
# max_wait模拟负载的任务之间执行时的最大等待时间,单位为毫秒
task_set = WebsiteTasks
min_wait = 0
max_wait = 0
host = "https://debugtalk.com"
上面的示例中,定义了针对https://debugtalk.com 这个网站的测试场景:先模拟用户登录系统,然后随机访问首页(/)和关于页面(/about/)
脚本主要包含两个类:
一个是WebsiteUser
,继承自HttpUser
,而HttpUser
继承自Locust
另一个是WebsiteTasks
,继承自TaskSet
,而TaskSet
继承自Locust
事实上,在Locust的测试脚本中,所有业务测试场景都是在Locust和TaskSet两个类的继承子类中进行描述的
如何理解Locust
和TaskSet
这两个类呢?
简答说,Locust
类就好比是一群蝗虫,而每一只蝗虫就是一个类的实例。相应的,TaskSet
就好比是蝗虫的大脑,控制着蝗虫的具体行为,即实际业务场景测试对应的任务集
三、class HttpLocust(Locust)、HttpUser
1、在Locust类中,具有一个client属性,它对应着虚拟用户作为客户端所具备的请求能力,也就是我们常说的请求方法。通常情况下,我们不会直接使用Locust类,因为其client属性没有绑定任何方法。因此在使用Locust时,需要先继承Locust类,然后在继承子类即HttpLocust中的client属性中绑定客户端的实现类
2、对于常见的HTTP(S)协议,Locust已经实现了HttpLocust类,其client属性绑定了HttpSession类,而HttpSession又继承自requests.Session。因此在测试Locust脚本中,我们可以使用client属性来使用Python requests库的所有方法,包括GET/POST/HEAD/PUT/DELETE等,调用方式也与requests完全一致。另外,由于requests.Session的使用,因此client的方法调用之间就自动具有了状态记忆的功能,常见的场景就是,在登陆系统后可以维持登录状态的Session,后续HTTP请求操作都能带上登录态
3、在Locust类中,除了client属性,还有几个属性需要关注下:
task_set
: 指向一个TaskSet类,TaskSet类定义了用户的任务信息,该属性为必填;
max_wait/min_wait
: 每个用户执行两个任务间隔时间的上下限(毫秒),具体数值在上下限中随机取值,若不指定则默认间隔时间固定为1秒;
host
:被测系统的host,当在终端中启动locust时没有指定--host参数时才会用到;
weight
:同时运行多个Locust类时会用到,用于控制不同类型任务的执行权重
4、测试开始后,每个虚拟用户(Locust实例)的运行逻辑都会遵循如下规律:
先执行WebsiteTasks中的on_start(对于每个用户只执行一次)
,作为初始化
;
从WebsiteTasks中随机挑选(如果定义了任务间的权重关系,那么就是按照权重关系随机挑选)一个任务执行;
根据Locust类中min_wait和max_wait定义的间隔时间范围(如果TaskSet类中也定义了min_wait或者max_wait,以TaskSet中的优先),在时间范围中随机取一个值,休眠等待;
重复2~3步骤,直至测试任务终止。
5、从1.x版本
之后,locust将Httplocust类改为HttpUser类,User类是用于定义虚拟用户的,而HttpUser类则是用于定义User类下面Http请求的虚拟用户。HttpUser类下面主要设置等待时间wait_time=between(min,max)
和设置运行的Taskset类tasks={类名}
,也可以定义各个Taskset类的执行比重
6、HttpUser类的client属性是HttpSession类的一个实例,HttpSession是requests.Session的子类,requests就是常用来做接口测试的那个requests库,HttpSession没有对requests.Session做什么改动,主要是传递请求结果给Locust,比如success/fail,response time,response length,name
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")
由于requests.Session会暂存cookie,所以示例中登陆/login请求后可以继续请求/my-profile
四、class TaskSet
TaskSet类好比是蝗虫的大脑,控制着蝗虫的具体行为
TaskSet类实现了虚拟用户所执行任务的调度算法,包括规划任务执行顺序(schedule_task)、挑选下一个任务(execute_next_task)、执行任务(execute_task)、休眠等待(wait)、中断控制(interrupt)等等。在此基础上,我们就可以在TaskSet子类中采用非常简洁的方式来描述虚拟用户的业务测试场景,对虚拟用户的所有行为(任务)进行组织和描述,并可以对不同任务的权重进行配置。
在TaskSet子类中定义任务信息时,可以采取两种方式,@task
装饰器和tasks
属性。
采用@task装饰器定义任务信息时,描述形式如下:
from locust import TaskSet, task
class UserBehavior(TaskSet):
@task(1)
def test_job1(self):
self.client.get('/job1')
@task(2)
def test_job2(self):
self.client.get('/job2')
采用task属性定义任务信息时,描述形式如下:
from locust import TaskSet
def test_job1(obj):
obj.client.get('/job1')
def test_job2(obj):
obj.client.get('/job2')
class UserBehavior(TaskSet):
tasks = {test_job1:1, test_job2:2}
# tasks = [(test_job1,1), (test_job1,2)] # 两种方式等价
在如上两种定义任务信息的方式中,均设置了权重属性,即执行test_job2的频率是test_job1的两倍。
在TaskSet子类中除了定义任务信息,还有一个是经常用到的,那就是on_start
函数,主要用于完成一些初始化工作,每个用户只执行一次;还有一个是on_end
函数,在每次结束时执行,每个用户只执行一次。
五、脚本增强
1、关联
上下之间存在关联关系
Locust毕竟是python脚本,通过官方库函数re.search就能实现所有需求,甚至针对html页面,我们也可以采用lxml库,通过xpath优雅实现元素定位
2、参数化
参数化一般有三种情况
1)循环取数据,数据可重复使用:eg.模拟3用户并发请求网页,总共有100个URL地址,每个虚拟用户都会依次循环加载这100个URL地址
2)保证并发测试数据唯一性,不循环取数据:eg.模拟3用户并发注册账号,共有90个账号,要求账号注册不重复,注册完毕后结束测试
3)保证并发测试数据唯一性,循环取数据:eg.模拟3用户并发登陆账号,要求并发登陆账号不相同,但数据可循环使用
通过以上归纳,可以确信的说,以上三种类型基本上可以覆盖我们日常性能测试工作中的所有参数化场景
Locust使用python的list和queue数据结构即可,在WebsiteUser定义一个数据集,然后所有虚拟用户在WebsiteTasks中就可以共享该数据集了。如果不要求数据唯一性,数据集选择list数据结构,从头到尾循环遍历即可;如果要求数据唯一性,数据集选择queue数据结构,取数据时进行queue.get()操作即可,并且这也不会循环取数据;至于涉及到需要循环取数据的情况,那也简单,每次取完数据后再将数据插入到队尾即可,queue.put_nowait(data)
六、Locust运行模式
两种运行模式,单进程运行和多进程分布式运行
1、单进程运行模式的意思是,Locust所有的虚拟并发用户均运行在单个Python进程中,具体从使用形式上,又分为no_web和web两种形式。该种模式由于单进程的原因,并不能完全发挥压力机所有处理器的能力,因此主要用于调试脚本和小并发压测的情况。
2、当并发压力要求较高时,就需要用到Locust的多进程分布式运行模式。从字面意思上看,大家可能第一反应就是多台压力机同时运行,每台压力机分担负载一部分的压力生成。的确,Locust支持任意多台压力机(一主多从)的分布式运行模式,但这里说到的多进程分布式运行模式还有另外一种情况,就是在同一台压力机上开启多个slave的情况。这是因为当前阶段大多数计算机的CPU都是多处理器(multiple processor cores),单进程运行模式下只能用到一个处理器的能力,而通过在一台压力机上运行多个slave,就能调用多个处理器的能力了。比较好的做法是,如果一台压力机有N个处理器内核,那么就在这台压力机上启动一个master,N个slave。当然,我们也可以启动N的倍数个slave,但是根据我的试验数据,效果跟N个差不多,因此只需要启动N个slave即可。
七、脚本调试
在Locust的单进程no_web运行模式中,我们可以通过--no_web参数,指定并发数(-c)和总执行次数(-n),直接在Terminal中执行脚本。
在此基础上,当我们想要调试Locust脚本时,就可以在脚本中需要调试的地方通过print打印日志,然后将并发数和总执行次数都指定为1,执行形式如下所示。
locust -f locustfile.py --no_web -c 1 -n 1
八、执行测试
1、单进程 no_web形式
-c , --clients
:指定并发用户数
-n , --num-request
:指定总执行测试
-r , --hatch-rate
:指定并发加压速率,默认值为1
locust -H https://debugtalk.com -f demo.py --no-web -c1 -n2
2、单进程web形式
如果采用web形式,通常情况下无需指定其他额外参数,Locust默认采用8089端口启动web;如果要启用其他端口,就可以使用如下参数进行指定
-P,--port:指定web端口,默认为8089
此时,并没有开始真正执行测试,还需要在web页面中配置参数后进行启动
在Locust的Web管理页面
中,需要配置的参数只有两个:
1)Number of users to simulate
: 设置并发用户数,对应中no_web模式的-c, --clients参数;
2)Hatch rate (users spawned/second)
: 启动虚拟用户的速率,对应着no_web模式的-r, --hatch-rate参数,1秒启动多少用户。
参数配置完毕后,点击【Start swarming】即可开始测试。
3、多进程分布式
不管是单机多进程还是多机负载模式,运行方式都是一样的,先运行一个master
,再启动多个slave
1)启动master,需要使用--master参数,同样的,如果要使用8089以外的端口,还需要使用-P,--port参数
locust -H https://debugtalk.com -f demo.py --master --port=8088
2)master启动后,还需要启动slave才能执行测试任务
启动slave时需要使用--slave参数,在slave中,就不需要再指定端口了
locust -H https://debugtalk.com -f demo.py --slave
3)如果slave和master不在同一台机器上,还需要通过--master-host参数再指定master的IP地址
locust -H https://debugtalk.com -f demo.py --slave --master-host=<locust_machine_ip>
master和slave都启动完毕后,就可以在浏览器中通过http://locust_machine_ip:8089进入Locust的Web管理页面了。使用方式跟单进程web形式完全相同,只是此时是通过多进程负载来生成并发压力,在web管理界面中也能看到实际的slave数量。
九、测试结果展示
Locust
的结果展示十分简单,主要就四个指标:并发数
、RPS
、响应时间
、异常率
。但对于大多数场景来说,这几个指标已经足够了。
在上图中,RPS
和平均响应时间
这两个指标显示的值都是根据最近2秒请求响应数据计算得到的统计值,我们也可以理解为瞬时值。
RPS
=Request Per Second
十、脚本展示
from locust import HttpUser, TaskSet, task
from logzero import logger
class WebSiteTasks(TaskSet):
"""
执行任务
"""
@task(1)
def test_task(self):
try:
with self.client.get(url, name='address_search', `catch_response=True`) as response:
if response.status_code == 200 and json.loads(response.text).get('code') == 0:
logger.info(json.loads(response.text))
response.success()
else:
response.failure('请求失败,返回码不为200或者返回信息的code不为0')
except Exception as e:
response.failure('请求异常,异常信息为:{}'.format(str(e)))
class AddressSearch(HttpUser):
"""
主类
"""
task_set = WebSiteTasks
tasks = [WebSiteTasks]
host = 'http://hisearch_debug.weiyun.baidu.com'
min_wait = 0
max_wait = 0
运行命令展示
nohup locust -f ./test_locust_group_search.py --web-host=0.0.0.0 -P 8009
注意:在主类中,一般会指定 tasks = [WebSiteTasks],但有时候也会指定 tasks = [WebSiteTasks.test_task],如果按照后者的方式,这个类里面的其他函数都无法访问,只会访问指定的test_task方法,实际中遇到一直提示某些值未定义,值是在on_start方法中定义的,但是找不到,原因就是在tasks中指定了具体的方法
网友评论