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

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

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 条回复 时间 点赞
1楼 已删除

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

3楼 已删除

向请教一下楼主,如何知道 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楼 已删除
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册