编程开源技术交流,分享技术与知识

网站首页 > 开源技术 正文

深入理解、灵活应用,Python Locust 性能测试框架源码深度解析

wxchong 2024-06-23 19:07:42 开源技术 21 ℃ 0 评论

Locust寓意蝗虫,蝗虫过境,寸草不生;而Locust工具生成并发请求就和一大群蝗虫一般,向我们的被测系统发起攻击,以此测试系统在高并发压力下是否能正常运转。

Locust测试框架中,采用Python进行开发,对常见的Http(s)协议的系统,Locust采用Request库作为客户端,在发请求时和Request库使用方法一样。

在模拟并发时,Locust采用协程、非阻塞IO来实现网络层的并发请求,因此单台压力机也能产生数千并发请求,再加上对分布式运行的支持,Locust能在使用较少压力机的前提下支持极高的并发数测试。

本文深入Locust源码解析,梳理运行流程,帮助我们更好的使用Locust开展性能测试,以及基于Locust的二次开发。


实例

本文通过如下一个简单Locust性能测试脚本,进行后续的介绍:

首先,来看下Locust的执行命令及参数,命令如下:

启动参数:

  • --no-web:表示不使用Web界面运行测试,no_web =True 。
  • -c: 设置虚拟用户数,num_clients=10。
  • -r: 设置每秒启动虚拟用户数,hatch_rate=2。
  • -t:设置设置运行时间,run_time=20s。

还支持以下参数:


Locust程序执行流程源码解析

执行如下Locust运行命令,no_web模式 10个用户并发,每秒加载2个用户,运行20s:

接下来,按照Locust执行流程,我们依次看下具体执行细节。


1 Python进程

一个程序的执行实例就是一个进程。当我们执行上述命令时,实际上起了一个Python进程。


2 主线程

线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。对应的,在该Python进程中,存在一个线程负责具体的实际运行。


3 locust命令参数解析:main.parse_options()

我们看下程序的主函数,即入口函数:

源码如上,首先进行的参数解析,通过parse_options()解析“locust -f testlocust.py --no-web -c 10 -r 2 -t 20s”命令,使用find_locustfile()获取在testlocust.py测试脚本路径,同时locustfile不能命名为locust.py,然后使用load_locustfile()加载测试脚本。


4 加载性能测试脚本文件、解析VUser数量

如上,load_locustfile()中通过imp.load_source通过根据路径导入测试脚本,再通过dict(filter(is_locust, vars(imported).items())),源码如下。is_locust()获取测试脚本中继承自Locust且带有task_set属性的子类,一个子类相当于一个VUser(如测试脚本里的WebsiteUserOne、WebsiteUserTwo)。

当VUser类都检查完毕之后,会把这些VUser类收集到一个列表中去,源码如下;


5 协程池main_greenlet

之后mian.mian()通过多个条件语句根据命令中参数的选择启动模式(web、no_web、master/slave),来启动一个协程,并且会把VUser列表和解析后的命令行参数内容都作为参数传递过去,本例子采用no_web模式。源码如下,同时把核心方法调用函数整合如下:


6 计算各VUser的权重占比,生成VUser启动列表

VUser是测试脚本中testlocust.py继承locust的子类,根据Locust命令中-c 参数指定用户并发数以及VUser的权重(weight)属性,计算使用多少用户执行各VUser,具体的实现源码如下:

weight_locusts()方法的主要实现逻辑是,先对所有的VUser权重数求和,再计算每个VUser的权重占总权重比例,再将并发用户数乘以各VUser占比取整,就得到了各VUser需要启动数量,最后把指定数量的VUser都填充到bucket列表中返回,举个例子:


7 启动VUser协程组

通过weight_locusts()获取到VUser启动列表bucket之后,首先会计算sleep_time,通过此来控制每秒加载用户数,然后进入while True循环中,会随机取出VUser启动列表中的一个VUser类,然后新起一个协程来实例化它,实例之后调用它的run方法开始执行各VUser的对应任务/任务集,直到所有VUser都实例化完成。


8 指定时间后停止

根据Locust命令 -r 指定的运行时间,时间结束时,触发runners.locust_runner.quit()停止VUser协程运行。


9 协程A:一个虚拟用户,他执行什么任务取决于task_set对应的实例中的任务集

在介绍接下来的Locust流程之前,我们再看一下文章开头的性能测试脚本testlocust.py,如下,其中WebsiteUserOne、WebsiteUserTwo就是上述文中的VUser,它具备成为VUser的2个充要条件:继承Locust(HttpLocust是Locust的子类)、具有task_set属性。

在上一个流程环节“启动VUser协程组”中提到,“实例之后调用它的run方法开始执行各VUser的对应任务/任务集”,那么一个协程中VUser都执行哪些任务,如何执行呢。

在测试脚本中,继承Locust的VUser类中task_set就是要执行的任务集合,这个集合里面可以有1或N个任务,还可以包含子任务集,自任务集还可以嵌套任务、子子任务集。在testlocust.py例子中,任务集包含tc_index、tc_user任务。


10 协程组 阻塞等待

实例化VUser的协程会在一个协程组内,该协程组会通过外部参数确定是否阻塞主线程,源码如下。


11 初始化 on_start

而VUser在实例化之后,通过调用run方法就会开始执行真正的请求任务集合。在TaskSet.run方法内,会先检查是否有on_start方法,如果有则最先执行它,然后会进入一个 while (True)循环,循环内每次会获取一个要执行的任务并执行,直到执行时间结束或者主动中断。


12 获取VUser任务集:core.TaskSetMeta()

在获取执行任务的逻辑中会分2种情况:一种是随机获取任务集中任务并执行,另一种是按顺序获取任务集任务并执行。这主要取决于对任务方法使用的是@task装饰器,还是@seq_task装饰器。除此之外,@task、@seq_task参数为执行权重,@task权重越大的task被执行的概率就越高,@seq_task权重越小,执行优先级越高,越早执行。

如上,是随机获取任务的源码,首先在生成tasks列表的时候,会根据任务的locust_task_weight属性值来添加同等数量的任务,之后在获取任务的时候,直接使用 get_next_task()随机从tasks列表中获取,因为权重越高在tasks列表中出现的次数就越多,所以被随机选到的概率就越高。

如上,是顺序获取任务的源码,首先在实例化时就根据 locust_task_order属性对任务进行排序,之后获取任务的 get_next_task()方法内会按照索引依次获取任务,并且支持循环的获取方式。


13 循环执行任务

每个VUser通过协程通过while (True)按照任务获取方式(随机、顺序)循环执行任务集中任务。


14 执行任务 A

首先介绍下Locust发送请求客户端是大家熟知的requests模块,源码如下:

testlocust.py测试脚本中,通过with self.client.request(…) as response 发送请求并获得请求响应体。

执行具体任务的逻辑,任务分3种:TaskSet实例的类方法、子任务集、普通函数。根据任务类型的不同,会执行相应的调用操作:

  • 如果是TaskSet类方法,会直接调用,task(*args, **kwargs)
  • 如果是子任务集,会递归调用子任务集的run方法,task(self).run(*args, **kwargs)
  • 如果是普通函数,会直接调用并把Locust实例作为第一参数,task(self, *args, **kwargs)

每个任务除了包含具体请求发送还应包含检查点、响应断言,如下测试脚本中,就设置检查点,同时考虑了请求参数获取方式。

最后每个VUser通过协程每执行完一个任务,就会执行wait()方法,实现思考时间等待。

附:性能测试脚本模版

实际应用中,为了方便快速构建微服务单接口性能测试用例,写了一个模版文件,每次替换模版文件中的 $host、$parameter_allocation_update、$url、$method、$share_data、$web_reg_find、$min_wait、$max_wait变量即可。如下

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表