本文环境python3.5.2
locust版本0.9.0
上文大概描述了locust的启动了流程,本文主要是接着上文继续分析,示例代码中的http的测试的执行过程,是如何去访问远端的http接口的流程。接着就分析如何通过传入的运行时间参数来停止locust的运行。
示例代码中访问url的代码如下:
@task(2)
def index(self):
self.client.get("/")
@task(1)
def profile(self):
self.client.get("/profile")
其中的client就是继承自HttpLocust初始化过程中创建的,由于示例代码里面的WebsiteUser继承自HttpLocust,在初始化的过程的时候;
class HttpLocust(Locust):
"""
Represents an HTTP "user" which is to be hatched and attack the system that is to be load tested.
The behaviour of this user is defined by the task_set attribute, which should point to a
:py:class:`TaskSet ` class.
This class creates a *client* attribute on instantiation which is an HTTP client with support
for keeping a user session between requests.
"""
client = None
"""
Instance of HttpSession that is created upon instantiation of Locust.
The client support cookies, and therefore keeps the session between HTTP requests.
"""
def __init__(self):
super(HttpLocust, self).__init__()
if self.host is None:
raise LocustError("You must specify the base host. Either in the host attribute in the Locust class, or on the command line using the --host option.")
self.client = HttpSession(base_url=self.host) # 初始化一个client实例
此时的client就是一个HttpSession的实例,分析该类:
class HttpSession(requests.Session): # 继承自requests.Session
"""
Class for performing web requests and holding (session-) cookies between requests (in order
to be able to log in and out of websites). Each request is logged so that locust can display
statistics.
This is a slightly extended version of `python-request `_'s
:py:class:`requests.Session` class and mostly this class works exactly the same. However
the methods for making requests (get, post, delete, put, head, options, patch, request)
can now take a *url* argument that's only the path part of the URL, in which case the host
part of the URL will be prepended with the HttpSession.base_url which is normally inherited
from a Locust class' host property.
Each of the methods for making requests also takes two additional optional arguments which
are Locust specific and doesn't exist in python-requests. These are:
:param name: (optional) An argument that can be specified to use as label in Locust's statistics instead of the URL path.
This can be used to group different URL's that are requested into a single entry in Locust's statistics.
:param catch_response: (optional) Boolean argument that, if set, can be used to make a request return a context manager
to work as argument to a with statement. This will allow the request to be marked as a fail based on the content of the
response, even if the response code is ok (2xx). The opposite also works, one can use catch_response to catch a request
and then mark it as successful even if the response code was not (i.e 500 or 404).
"""
def __init__(self, base_url, *args, **kwargs):
super(HttpSession, self).__init__(*args, **kwargs) # 调用父类的初始化方法
self.base_url = base_url # 设置Host
# Check for basic authentication
parsed_url = urlparse(self.base_url) # 解析url
if parsed_url.username and parsed_url.password: # 检查是否配置了用户名与密码
netloc = parsed_url.hostname
if parsed_url.port:
netloc += ":%d" % parsed_url.port
# remove username and password from the base_url
self.base_url = urlunparse((parsed_url.scheme, netloc, parsed_url.path, parsed_url.params, parsed_url.query, parsed_url.fragment))
# configure requests to use basic auth
self.auth = HTTPBasicAuth(parsed_url.username, parsed_url.password)
def _build_url(self, path):
""" prepend url with hostname unless it's already an absolute URL """
if absolute_http_url_regexp.match(path): # 检查是否是绝对路径
return path # 如果是绝对路径则直接返回
else:
return "%s%s" % (self.base_url, path) # 返回完整的url
def request(self, method, url, name=None, catch_response=False, **kwargs):
"""
Constructs and sends a :py:class:`requests.Request`.
Returns :py:class:`requests.Response` object.
:param method: method for the new :class:`Request` object.
:param url: URL for the new :class:`Request` object.
:param name: (optional) An argument that can be specified to use as label in Locust's statistics instead of the URL path.
This can be used to group different URL's that are requested into a single entry in Locust's statistics.
:param catch_response: (optional) Boolean argument that, if set, can be used to make a request return a context manager
to work as argument to a with statement. This will allow the request to be marked as a fail based on the content of the
response, even if the response code is ok (2xx). The opposite also works, one can use catch_response to catch a request
and then mark it as successful even if the response code was not (i.e 500 or 404).
:param params: (optional) Dictionary or bytes to be sent in the query string for the :class:`Request`.
:param data: (optional) Dictionary or bytes to send in the body of the :class:`Request`.
:param headers: (optional) Dictionary of HTTP Headers to send with the :class:`Request`.
:param cookies: (optional) Dict or CookieJar object to send with the :class:`Request`.
:param files: (optional) Dictionary of ``'filename': file-like-objects`` for multipart encoding upload.
:param auth: (optional) Auth tuple or callable to enable Basic/Digest/Custom HTTP Auth.
:param timeout: (optional) How long in seconds to wait for the server to send data before giving up, as a float,
or a (`connect timeout, read timeout `_) tuple.
:type timeout: float or tuple
:param allow_redirects: (optional) Set to True by default.
:type allow_redirects: bool
:param proxies: (optional) Dictionary mapping protocol to the URL of the proxy.
:param stream: (optional) whether to immediately download the response content. Defaults to ``False``.
:param verify: (optional) if ``True``, the SSL cert will be verified. A CA_BUNDLE path can also be provided.
:param cert: (optional) if String, path to ssl client cert file (.pem). If Tuple, ('cert', 'key') pair.
"""
# prepend url with hostname unless it's already an absolute URL
url = self._build_url(url) # 获取访问路径
# store meta data that is used when reporting the request to locust's statistics
request_meta = {} # 请求头部信息
# set up pre_request hook for attaching meta data to the request object
request_meta["method"] = method # 请求的方法
request_meta["start_time"] = time.time() # 请求开始的时间
response = self._send_request_safe_mode(method, url, **kwargs) # 访问请求并获取返回值
# record the consumed time
request_meta["response_time"] = (time.time() - request_meta["start_time"]) * 1000 # 记录响应处理的时间
request_meta["name"] = name or (response.history and response.history[0] or response).request.path_url
# get the length of the content, but if the argument stream is set to True, we take
# the size from the content-length header, in order to not trigger fetching of the body
if kwargs.get("stream", False):
request_meta["content_size"] = int(response.headers.get("content-length") or 0) # 获取返回的文件长度
else:
request_meta["content_size"] = len(response.content or b"") # 否则获取响应的整体大小
if catch_response: # 如果要包含请求信息
response.locust_request_meta = request_meta
return ResponseContextManager(response) # 用ResponseContextManager包裹response
else:
try:
response.raise_for_status() # 检查返回状态
except RequestException as e:
events.request_failure.fire(
request_type=request_meta["method"],
name=request_meta["name"],
response_time=request_meta["response_time"],
exception=e,
) # 如果失败则通知所有的失败请求事件执行
else:
events.request_success.fire(
request_type=request_meta["method"],
name=request_meta["name"],
response_time=request_meta["response_time"],
response_length=request_meta["content_size"],
) # 如果成功则通知所有的成功事件执行
return response
def _send_request_safe_mode(self, method, url, **kwargs):
"""
Send an HTTP request, and catch any exception that might occur due to connection problems.
Safe mode has been removed from requests 1.x.
"""
try:
return requests.Session.request(self, method, url, **kwargs) # 调用requests的Session去请求接口
except (MissingSchema, InvalidSchema, InvalidURL):
raise
except RequestException as e:
r = LocustResponse()
r.error = e
r.status_code = 0 # with this status_code, content returns None
r.request = Request(method, url).prepare()
return r
由该代码可知,处理的请求都是通过requests库中的Session来进行请求的,示例代码中的client都是通过requests的代码进行请求,并且还可以使用session来保持会话,从而使接口请求的时候能够带上权限检查等额外信息。
由于可以在启动locust可以指定执行的时间,可以到时间退出,我们分析一下该退出函数的执行,
def spawn_run_time_limit_greenlet():
logger.info("Run time limit set to %s seconds" % options.run_time)
def timelimit_stop():
logger.info("Time limit reached. Stopping Locust.")
runners.locust_runner.quit()
gevent.spawn_later(options.run_time, timelimit_stop)
等到了run_time之后,就会执行timelimit_stop函数,而该函数就是调用了实例化的locust类实例的quit方法;
def stop(self):
# if we are currently hatching locusts we need to kill the hatching greenlet first
if self.hatching_greenlet and not self.hatching_greenlet.ready(): # 检查是否有hatch_greenlet并且没有准备好 就杀掉该协程
self.hatching_greenlet.kill(block=True)
self.locusts.kill(block=True) # 杀死所有的协程组
self.state = STATE_STOPPED # 更改状态为停止态
events.locust_stop_hatching.fire() # 通知所有locust_stop_hatching的函数执行
def quit(self):
self.stop() # 停止执行所有的greenlet
self.greenlet.kill(block=True) # 主greenlet杀死
此时就是停止所有的协程执行,终止该测试用例的执行。其中events.locust_stop_hatching使用了典型的观察者设计模式:
locust_stop_hatching = EventHook()
class EventHook(object):
"""
Simple event class used to provide hooks for different types of events in Locust.
Here's how to use the EventHook class::
my_event = EventHook()
def on_my_event(a, b, **kw):
print "Event was fired with arguments: %s, %s" % (a, b)
my_event += on_my_event
my_event.fire(a="foo", b="bar")
If reverse is True, then the handlers will run in the reverse order
that they were inserted
"""
def __init__(self):
self._handlers = [] # 所有待处理的handlers
def __iadd__(self, handler):
self._handlers.append(handler) # 添加到处理的Handler列表中
return self
def __isub__(self, handler):
self._handlers.remove(handler) # 移除handler
return self
def fire(self, reverse=False, **kwargs):
if reverse:
self._handlers.reverse() # 是否排序
for handler in self._handlers: # 依次遍历handler并执行
handler(**kwargs)
本文主要是继续分析了locust启动之后,http的请求的处理与定时退出的功能,其中http的请求都是基于requests.Session来实现的,定时退出的功能主要还是依赖于gevent中的Group来控制所有已经运行的协程,通过停止所有运行的协程来达到关闭停止运行的目的,其中还有些许细节并没有详细说明,大家有兴趣可自行查阅相关源码。鉴于本人才疏学浅,如有疏漏请批评指正