您的位置:首页 > 其它

基于locust的性能测试优化

2017-06-02 16:43 183 查看

问题产生的背景

以往在测试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
to
bb9f
tal_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 性能测试