基于locust的性能测试优化

本文针对使用Locust等工具进行Web服务性能测试时遇到的实际并发请求与模拟并发请求不一致的问题,提出了一种改进方案。通过对Locust的二次开发,实现虚拟用户并发请求,以更准确地模拟真实用户的并发行为。

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

问题产生的背景

以往在测试web服务的性能时,使用的工具有loadrnner、tsung、locust、jmeter等。这些工具的基本思路都相同,在一个文件里面定义一个用户所要发起的请求,之后交给工具来模拟多个用户重复执行我们定义的行为,最后返回平均请求的响应时长。
近期在测试几个web服务的性能时,发现了一个问题,就是当一个页面需要加载多个接口的数据时,浏览器所发出的请求是并发的。虽然我们测试时,在文件中定义了单个用户索要发出的所有请求,但是进行性能测试时,对于一个测试用户来说,所有的请求都是顺序发出的。由此引出了如下两个问题:

a.  性能测试过程中工具所虚拟出来的用户数,产生的压力要小于实际环境下同等数量的真实用户。
比如loadrunner,一个文件中定义了两个请求,性能测试时我们将用户数设置成100,那么在压测时,同一时间段并发的请求数是100个,因为每个用户是顺序发起请求。但是实际环境中,100个用户同时访问页面,同一时间的并发请求数应该是200个,因为浏览器会并行发起多个请求。这样,测试报告中的用户数,就不能直接用来作为实际用户数量进行评估了

b.  接口的平均响应时长,并不能用来评估页面的加载时间。
一个页面有两个接口a  b,我们模拟以一个用户访问了两次,这个时候,以接口响应时长为维度的数据报告大致是如下形式:

如果以平均响应最慢的那个接口来评估,那么可以说页面的平均加载时间是3.5s。
但是实际上,第一次访问时,接口a b,最大的响应时长是5s,这个时候页面的加载时间是5s。第二次访问时间是4s。那么平均的页面响应时间应该是4.5s。4.5s和3.5s之间的差距,在性能测试中还是无法忽略的,在数据波动较大的情况下可能会出现更极端的结果。

解决办法

a.  这里可能会有人想到可以起多个用户,每个用户向不同的接口发起请求。假如一个页面有两个接口,要模拟一个真实用户的话,那么测试工具就跑两个用户,这两个用户分别对这两个接口发起请求,这样同一时间的请求并发数就和实际的一致了。这样应该也可以,但是在一些请求频率的细节上,还是和实际的单个用户并发访问有些差别。再有就是最后我们得到的测试结果,还是以接口的响应时长为维度的。
b.  另外一种比较完美的解决方法就是我们自己实现一个html&js解析器,完全模拟浏览器的行为。不过这种方式实现难度较大,且性能测试时,高并发环境下要运行那么多的解析器,资源消耗也是一个很大的问题。
c.  最后一种解决方案,就是利用现有的性能测试工具,修改内部任务调度方式,修改任务统计时间逻辑。把每个用户下的任务改为并发执行,时间统计为所有任务全部执行完毕的时间,作为一次单个事件进行统计。

基于locust进行性能测试优化

本次二次开发之所以选择locust,是因为这个工具开源,基于python,轻量。稍微花些心思就能看懂它的源码。关于locust的内部原理介绍这里不多说。

简单介绍一下locust内部各个代码文件的功能:

Runners.py
主要实现虚拟用户的调度,集群方案下与slave通信,根据压力配置创建对应数量的虚拟用户,并实现测试任务的启动、停止等操 作。
Core.py
定义虚拟用户,每个虚拟用户在运行时所执行的任务以及任务调度都在这个文件中完成。其中ClassTaskSet主要实现每个用户下任务的调度功能。后续我们修改并发执行任务时,也是主要修改这里的代码。
Stats.py
用于维护任务中每个请求的具体信息,包括平均响应时长、最大最小响应时长、请求个数、频率灯、等信息。运行时RequestStats类会为每个请求都创建一个StatsEntry实例,后续所有该请求所产生的数据都由这个实例来维护。
Clients.py
主要用来重写requests中一些类和函数,方便测试web接口时调用。
Main.py
入口文件,解析用户自定义的文件,解析命令行参数等。
Web.py
Locust运行时的web界面,实现前后台交互。后续增加统计数据类型时,需要修改此文件。


过多的介绍代码内容太枯燥了,所以感兴趣的话大家可以自己下载一下源码看一下。原始的locust中,单个虚拟用户的任务执行主要是在core.py中的TaskSet类,run函数中实现了任务的调度执行,主要流程就是在一个while循环中,持续维护一个非空的任务队列,然后每次pop一个任务并执行。当前任务执行完后,进入下一次循环。这里如果我们想要单个用户能够并发执行他的任务的话,就要修改这个函数,将原来的顺序执行改为一次并发执行所有任务,等到当前所有任务都执行完毕后,记录下本次循环执行的时长,再次进入下一个循环。
部分代码内容如下:
  • core.py, 修改run函数中的循环内容,增加execute_tasks函数,修改execute_task函数以及其它一些函数。主要是将原来的顺序执行任务,改为一次性并发执行多个任务。
    def run(self, *args, **kwargs):
        .....
        .....
        while (True):
            try:
                .....
                self.schedule_task()
                try:
                    self.execute_tasks()
                except RescheduleTaskImmediately:
                    pass
                self.wait()
            except InterruptTaskSet as e:
                .....
    
    def execute_tasks(self):
        start_time = round(time()*1000,0)
        self._task_pool = Group()
        for task in self._task_queue:
            self.execute_task(task["callable"], *task["args"], **task["kwargs"])
        self._task_pool.join()
        end_time = round(time()*1000,0)
        jobtime = end_time - start_time
        events.job_finish.fire(name=self.locust.name, jobtime=jobtime)
    
    def execute_task(self, task, *args, **kwargs):
        if hasattr(task, "__self__") and task.__self__ == self:
            self._task_pool.spawn(task, *args, **kwargs)
        elif hasattr(task, "tasks") and issubclass(task, TaskSet):
            task(self).run(*args, **kwargs)
        else:
            self._task_pool.spawn(task, self, *args, **kwargs)
    
    def schedule_task(self):
        self._task_queue = []
        for task in self.tasks:
            task = {"callable":task,"args":[],"kwargs":{} }
            self._task_queue.append(task)
        random.shuffle(self._task_queue)

  • stats.py  增加MyEntry类,用来维护并发任务执行下的各种数据,后续统计测试结果的时候会用到。
class MyEntry(object):
    name = None
    num_jobs = None
    job_times = None
    total_job_time = None
    min_job_time = None
    max_job_time = None

    def __init__(self,name):
        .....
        ....
    def log(self,jobtime):
        self.num_jobs += 1
        self.total_job_time += jobtime
        if self.min_job_time == 0:
            self.min_job_time = jobtime
        else:
            self.min_job_time = min( self.min_job_time, jobtime)
        self.max_job_time = max( self.max_job_time, jobtime)

        if jobtime < 100:
            rounded_jobtime = jobtime
        elif jobtime < 1000:
            rounded_jobtime = round(jobtime,-1)
        elif jobtime < 10000:
            rounded_jobtime = round(jobtime,-2)
        else:
            rounded_jobtime = round(jobtime,-3)

        self.job_times.setdefault(rounded_jobtime,0)
        self.job_times[rounded_jobtime] += 1
    @property
    def avg_job_time(self):
        if self.num_jobs == 0:
            return 0
        else:
            return float(self.total_job_time)/self.num_jobs
  • web.py 修改requests_stats函数,增加针对并发任务的数据统计与计算。
    for k,v in runners.locust_runner.job_stats.items():
        stats.append({
            "name": v.name,
            "num_requests": v.num_jobs,
            "avg_response_time": v.avg_job_time,
            "min_response_time": v.min_job_time,
            "max_response_time": v.max_job_time,
            "num_failures": 0,
            "current_rps": 0,
            "median_response_time": 0,
            "avg_content_length": 0
        })

除了以上三个地方之外还有其它一些地方的代码也需要修改,主要是用来支持任务并发执行后,一些数据统计与记录等内容。整体来说本次优化所做的修改,修改的代码量在几百行左右。

验证测试

自己搭建了一个测试的php页面,用于进行修改后的效果验证。根据url的参数值,sleep对应的时间之后再进行响应,页面的代码如下:
<?php
   sleep($_GET['a']);
?>
编写locust测试脚本,对接口发起请求,分别传入参数1 2 4,也就是这三个请求的响应时间分别为1s 2s 4s.


在MyTest中给name变量赋值为MyBrowser,之后在测试结果页面,针对MyBrower的时长统计,都是该用户每次并发请求所消耗的时长。结果如下:

因为并发请求时,每次统计的是所有请求全部响应完成的时长,所以MyBrowser的结果中,平均时长是4s。

接下来我们验证一下,在第一章节中反馈的问题b。 修改被测试页面的代码,当请求接口a时,随机sleep 1s或4s。请求接口b时,随机sleep 2s或5s。
在locust测试脚本中,对接口a b发起请求。


我们来看一下测试结果:

这里可以看到,接口a的平均响应是2.5s。接口b的平均响应时长是3.5s。如果我们取最慢的那个接口,那么按照老的思维方式,最终的性能结果是3.5s。
但是,如果以并发任务执行的时长为统计维度,我们看到MyBrower中统计的平均时长是4s。
### 使用Locust进行Web应用的性能和负载测试 #### 准备环境 安装Locust可以通过pip命令轻松完成。确保已安装Python环境之后,在终端输入如下命令来安装Locust: ```bash pip install locust ``` 这一步骤为后续创建并执行基于Python编写的自定义化负载测试脚本提供了必要的工具支持[^2]。 #### 编写测试脚本 编写一个简单的`locustfile.py`文件,其中包含了描述用户行为的任务类。下面是一个基本的例子,展示了如何模拟用户的登录操作以及浏览页面的行为: ```python from locust import HttpUser, TaskSet, task, between class UserBehavior(TaskSet): @task(1) def index(self): self.client.get("/") @task(2) def profile(self): self.client.get("/profile") class WebsiteUser(HttpUser): tasks = [UserBehavior] wait_time = between(5, 9) ``` 此代码片段定义了一组任务集,其中包括两个HTTP GET请求分别指向根路径和用户资料页;同指定了每个虚拟用户之间的等待间间隔范围为5到9秒之间随机选取[^3]。 #### 启动Locust服务 通过命令行启动Locust服务器,并指定刚才创建好的`.py`文件作为入口点。如果是在本地环境中,则可以直接运行以下指令: ```bash locust -f locustfile.py --host=http://example.com ``` 上述命令会开启一个内置的Web UI,默认监听于8089端口,允许使用者调整并发数、查看实统计图表等交互式控制面板功能[^4]。 #### 执行分布式测试 当面对更大规模的压力需求,可以利用多台机器组成集群来进行分布式的压测实验。具体做法是先在一个节点上以Master身份启动Locust实例,再让其他Worker节点连接至该中心节点共同参与测试过程: - **主节点**:`locust -f locustfile.py --master --host=http://target-site.com` - **工作节点**:`locust -f locustfile.py --worker --master-host=<MASTER_IP> --host=http://target-site.com` 这种方式能够显著提升整体吞吐量水平,适用于更复杂的生产环境下进行全面深入的功能验证与优化建议收集[^1]。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值