性能测试工具 Locust 源码阅读及特点思考 (一)

zyanycall · 2018年01月29日 · 最后由 老马 回复于 2018年02月02日 · 11232 次阅读

Locust 在性能测试界已经比较出名了,本身是 python 写的,代码量不多,学习起来挺好。

github:[https://github.com/locustio/locust]

我本身是想搞一个网页版的性能测试平台,希望使用 python 语言实现,以方便 app 端 ui 测试代码迁移。

Locust 源码阅读

Locust 源码主要的文件有两个:main.py 和 runners.py

main.py

它执行程序的入口,以下代码仅包含核心代码。

parse_options()

它用来解析传入的参数,阅读它可以了解 Locust 都接受哪些参数,并且大致是什么作用。
基本代码:

"""
Handle command-line options with optparse.OptionParser.

Return list of arguments, largely for use in `parse_arguments`.
"""

# Initialize
parser = OptionParser(usage="locust [options] [LocustClass [LocustClass2 ... ]]")

parser.add_option(
    '-H', '--host',
    dest="host",
    default=None,
    help="Host to load test in the following format: http://10.21.32.33"
)

写的挺清楚了,-H 和 --host 是相同的,默认是 None,提示是 help。

find_locustfile(locustfile) 和 load_locustfile(path)

找到并加载我们手写的 locust 用例,即-f 传入的文件,py 结尾。
核心代码(节选):

# Perform the import (trimming off the .py)
imported = __import__(os.path.splitext(locustfile)[0])
# Return our two-tuple
locusts = dict(filter(is_locust, vars(imported).items()))

上面是:1.将自身的 import 导入 locust 用例文件。2.得到其中的用例类。is_locust 是布尔返回类型的方法,用于判断是否继承了 TaskSet。

main()

很长的条件分支,根据输入的参数来走不同的逻辑代码。

options 对象代表着传入的参数。
locusts 对象代表着我们的用例 TaskSet 类。

  • 如果使用了 --run-time 参数,则调用:
def timelimit_stop():
    logger.info("Time limit reached. Stopping Locust.")
    runners.locust_runner.quit()
gevent.spawn_later(options.run_time, timelimit_stop)

使用了协程来执行。

  • 如果没有 no-web 参数:
main_greenlet = gevent.spawn(web.start, locust_classes, options)

也是用协程,启动了一个 web 程序,本身是 flask 的。
locust_classes 和 options 是 web 程序参数,包含了 host port。

  • 如果是 master
# spawn client spawning/hatching greenlet
if options.no_web:
    runners.locust_runner.start_hatching(wait=True)
    main_greenlet = runners.locust_runner.greenlet
if options.run_time:
    spawn_run_time_limit_greenlet()

会执行 master 对应的 runners,hatching 是孵化,即开始启动。
main_greenlet 是协程的主体。是协程的池子,Group() ,我理解类似于众多任务的一个集合(from gevent.pool import Group)。
协程就不解释了,这里一个 main_greenlet 就是一个协程的主体,至于你是 4 核的 CPU 最好是 4 个协程,这是定义和启动 4 个 slave 实现的,代码不会判断这些。
runners.locust_runner 是另一个重要文件的内容,后面再解释。

后面代码都很类似。
master runner 和 slave runner 都是继承的 LocustRunner 类,都是其中的方法实现。

runners.py(后续)

Locust 执行的方法类。
由于内容比较多,后面另开一个帖子介绍吧。先介绍一下 events,便于后面的 runners.py 阅读。

events.py

Locust 事件的框架,简单来说,就是声明一个方法,加入到指定的 events 中。
只要是同样的方法(参数不同),都可以加入到这个 events 中。
之后调用 events 的 fire(self, **kwargs) ,调用到之前声明定义的方法,完成触发动作。

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")
    """

    def __init__(self):
        self._handlers = []

    def __iadd__(self, handler):
        self._handlers.append(handler)
        return self

    def __isub__(self, handler):
        self._handlers.remove(handler)
        return self

    def fire(self, **kwargs):
        for handler in self._handlers:
            handler(**kwargs)

# 一个例子
request_success = EventHook()

使用的代码举例:

# register listener that resets stats when hatching is complete
def on_hatch_complete(user_count):
    self.state = STATE_RUNNING
    if self.options.reset_stats:
        logger.info("Resetting stats\n")
        self.stats.reset_all()
events.hatch_complete += on_hatch_complete

如上,events.hatch_complete 相当于一个触发的任务链(使用 += 添加任务)。
使用下面代码调用:

events.hatch_complete.fire(user_count=self.num_clients)

Locust 的一些特点及思考(后续)

Locust 并不完美,或者说离 Jmeter 的程度都是非常遥远的。
Jmeter 几乎每天都在更新,Locust 几乎没啥更新。

相关 Locust 的思考后续写一下吧,使用 Locust 要慎重,慎重。

Locust 的源码分析二链接:

Locust 源码阅读及特点思考 (二)

共收到 7 条回复 时间 点赞
11楼 已删除

1.你到底什么理解,我到底什么误解,你说出来,想喷我就勇敢点儿。
2.“locust 更新的少可能是因为没有太多高并发场景能达到 locust 应用级别,jmeter 就足够了”。这句暴露了你的实力。我可以这么讲,Jmeter3.3 目前的功能,甩开 locust 太远,基本就没有 locust 什么事情了。locust 的作者是有自知自明,不更新很明智。

9楼 已删除

向请教一下楼主,如何知道 Jmeter 单台服务器压测最多能设置多少个 sample?是通过看电脑的内存消耗么?

对 韩将 回复

我写的是 Locust 的你竟然来问我 Jmeter 的问题……让我一阵心酸啊。
我开始以为你问的是 Threads(users) 的数量,后来仔细看了一下,是问的 sampler 即用例中的请求数。
最终能设置多少个 sampler 确实是依赖内存的消耗的,这里解释一下。
Jmeter 编辑用例增加 sampler 的时候,用例是会保存成 jmx 用例文件的,jmx 用例文件中即包含了每个 sampler 的信息。
而 Jmeter 性能测试时,会将正常状态(没有灰化隐藏)的 sampler 信息(很多个)加载到本身的内存中,确切说是 JVM 的 Heap 中,用来当做原始测试数据。
那么这会遇到第一个内存障碍,如果你的有效的 jmx 用例文件就 100MB,Jmeter 的内存 Heap 才设置了 50MB,那这个用例可能是加载不起来的。
性能测试时,由于不能污染原始的内存中的 jmx 文件,比如做参数化等细节,每一个 jmx 会被复制出去,由于具体 Jmeter 是使用线程来实现压力机制的,所以复制的 jmx 文件会到线程的内存中,这次是在 JVM 的永久代中,而每个线程的内存可以使用-Xss 参数配置。
那么这回遇到第二个内存障碍,如果 jmx 有效的比较大,而-Xss 设置的比较小,那么性能测试时也是有可能失败的。
注意:
1.Jmeter 加载/运行 用例/sampler 时,如果遇到问题,都会抛出 JVM 级别的异常的,肯定是会打到日志上的,这部分定位问题十分简单,而解决也比较简单,增加内存就好了。
2.真正的用例在内存中不会像 jmx 文件那么大的,Jmeter 会提炼用例数据的,内存占用会比文件占用的空间小得多。(具体提炼多少需要看源码,这里无法尽述)。
总结:
sampler 多少确实是内存决定的,内存够大就没事,有问题就看日志,注意调整-Xms -Xmx -Xss 参数。

相对于 jmeter,locust 确实有一些自己的优势,比如脚本更易于维护,且内存占用相对小等。但是目前综合实力和生态确实不如 jmeter。
楼主从源码分析推断出的观点还是挺有说服力的,希望同学在评论时向楼主学习。尽量客观不要凭感觉进行臆测。

真心不好意思,不是来喷的,不过我觉得吧,写性测试工具不用 C 至少也要用 java/scala 吧,python 本身的性能会给性能测试结果分析带来很大的干扰~

对 槽神 回复

你说的有道理。不过我还没写网页版的性能测试平台呢,这里仅是在看 Locust。
@debugtalk 他阿里同事实现了 GO 语言的 Locust 客户端 boomer,我未来还会继续分析 GO 语言的可行性的。boomer 的源码我也在学习。

支持楼主的分析精神 鼓捣精神。就是需要您这样的先行者。

10楼 已删除
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册。