locust压测工具:启动概述

locust压测工具启动概述

本文环境python3.5.2
locust版本0.9.0

locust概述

locust是一个简单易用、分布式的用户压测工具,并确定系统可以处理多少并发用户。在测试的时候,让你定义的每一个测试用例都执行,并且可以从web界面来监控所有执行的用例的执行过程。这将有助于您在让真正的用户进入之前对您的代码进行测试和识别瓶颈。locust基于事件驱动,因此可以在一台机器上支持数千个并发用户,与许多其他基于事件的应用程序相比,它不使用回调。相反,它通过gevent,让每个测试用例都高效独立运行。locust提供了纯净的Python的编程环境,支持分布式与可扩展可支持数十万用户的并发,提供了人性化的web界面进行监控和操作让使用更简单,能够测试很多系统。locust是一款基于好多组件来实现运行的压测工具,包括依赖于gevent,requests和flask等许多成熟的第三方组件。本文就简单的分析一下locust的简单的启动的执行流程。

locust的启动准备

首先引用官方文档的示例代码:

from locust import HttpLocust, TaskSet, task

class UserBehavior(TaskSet):
    def on_start(self):
        """ on_start is called when a Locust start before any task is scheduled """
        self.login()

    def on_stop(self):
        """ on_stop is called when the TaskSet is stopping """
        self.logout()

    def login(self):
        self.client.post("/login", {"username":"ellen_key", "password":"education"})

    def logout(self):
        self.client.post("/logout", {"username":"ellen_key", "password":"education"})

    @task(2)
    def index(self):
        self.client.get("/")

    @task(1)
    def profile(self):
        self.client.get("/profile")

class WebsiteUser(HttpLocust):
    task_set = UserBehavior
    min_wait = 5000
    max_wait = 9000

然后将示例代码保存为test_locust.py文件,配置好待测试的服务器的ip,在终端命令行中输入:

locust -f ./test_locust.py --host=http://127.0.0.1:8000 --no-web

此时就启动了locust来执行,压测的功能,此时终端上就会输出当前的执行的信息。

locust的启动流程与概述

从执行的locust过程可知,在终端中调用了locust来执行,从locust的setup.py文件中可知,启动的入口函数locust.main:main函数;

entry_points={
    'console_scripts': [
        'locust = locust.main:main',
    ]
},

此时查看main相关代码:

def main():
    parser, options, arguments = parse_options()                # 解析传入的参数

    # setup logging
    setup_logging(options.loglevel, options.logfile)            # 建立日志文件
    logger = logging.getLogger(__name__)                        # 获取日志的Logger
    
    if options.show_version:                                    # 检查是否是显示Locust的版本号
        print("Locust %s" % (version,))
        sys.exit(0)                                             # 打印版本后退出

    locustfile = find_locustfile(options.locustfile)            # 寻找传入的测试文件

    if not locustfile:                                          # 如果没有找到传入的测试文件则报错
        logger.error("Could not find any locustfile! Ensure file ends in '.py' and see --help for available options.")
        sys.exit(1)

    if locustfile == "locust.py":                               # 如果传入的测试文件名就是locust.py文件则报错
        logger.error("The locustfile must not be named `locust.py`. Please rename the file and try again.")
        sys.exit(1)

    docstring, locusts = load_locustfile(locustfile)            # 加载传入的实例文件并查找相关的Locust类

    if options.list_commands:
        console_logger.info("Available Locusts:")
        for name in locusts:
            console_logger.info("    " + name)
        sys.exit(0)

    if not locusts:                                             # 如果文件中没有Locust类被找到则退出
        logger.error("No Locust class found!")
        sys.exit(1)

    # make sure specified Locust exists
    if arguments:                                               # 检查是否存在Locust类
        missing = set(arguments) - set(locusts.keys())          # 减去已经找到的Locust类
        if missing:                                             # 如果有缺失的则报错退出
            logger.error("Unknown Locust(s): %s\n" % (", ".join(missing)))
            sys.exit(1)
        else:
            names = set(arguments) & set(locusts.keys())        # 取并集
            locust_classes = [locusts[n] for n in names]            
    else:
        # list() call is needed to consume the dict_view object in Python 3
        locust_classes = list(locusts.values())                 # 获取locust类列表
    
    if options.show_task_ratio:                                 # 是否显示任务信息
        console_logger.info("\n Task ratio per locust class")
        console_logger.info( "-" * 80)
        print_task_ratio(locust_classes)
        console_logger.info("\n Total task ratio")
        console_logger.info("-" * 80)
        print_task_ratio(locust_classes, total=True)
        sys.exit(0)                                         
    if options.show_task_ratio_json:                            # 是否是以json的方式导出执行信息
        from json import dumps
        task_data = {
            "per_class": get_task_ratio_dict(locust_classes), 
            "total": get_task_ratio_dict(locust_classes, total=True)
        }
        console_logger.info(dumps(task_data))
        sys.exit(0)
    
    if options.run_time:                                       # 是否设置了运行结束时间
        if not options.no_web:                                 # 检查是否启动了web界面展示
            logger.error("The --run-time argument can only be used together with --no-web")
            sys.exit(1)
        try:
            options.run_time = parse_timespan(options.run_time)     # 解析成时间戳
        except ValueError:
            logger.error("Valid --run-time formats are: 20, 20s, 3m, 2h, 1h20m, 3h30m10s, etc.")
            sys.exit(1)
        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)    # 指定时间后调用退出方法

    if not options.no_web and not options.slave:                    # 检查是否是开启了web与从模式
        # spawn web greenlet
        logger.info("Starting web monitor at %s:%s" % (options.web_host or "*", options.port))
        main_greenlet = gevent.spawn(web.start, locust_classes, options)        # 启动web并开始
    
    if not options.master and not options.slave:                                # 如果既不是主也不是从模式
        runners.locust_runner = LocalLocustRunner(locust_classes, options)      # 默认locust_runner为LocalLocustRunner类
        # spawn client spawning/hatching greenlet
        if options.no_web:                                                      # 如果没有web展示模式
            runners.locust_runner.start_hatching(wait=True)                     # 则开始执行
            main_greenlet = runners.locust_runner.greenlet                      # 设置main_greenlet
        if options.run_time:                                                    # 是否设置了运行结束时间
            spawn_run_time_limit_greenlet()                                     # 如果设置了运行结束时间则在指定时间后退出
    elif options.master:                                                        # 是否是主进程模式
        runners.locust_runner = MasterLocustRunner(locust_classes, options)     # 设置启动类为MasterLocustRunner
        if options.no_web:                                                      # 如果没有web展示
            while len(runners.locust_runner.clients.ready)

从该函数可知,首先会解析传入参数,然后根据传入的参数依次去查找待执行的示例文件,通过加装的示例文件查找相关的Locust类,然后根据传入的是否展示web界面等信息执行执行运行启动,在本例的示例代码中,由于不使用web来控制,所以就直接运行,此时我们继续分析查找locust文件的方法,find_locustfile函数;

def find_locustfile(locustfile):
    """
    Attempt to locate a locustfile, either explicitly or by searching parent dirs.
    """
    # Obtain env value
    names = [locustfile]                                                            # 传入的路径名称
    # Create .py version if necessary
    if not names[0].endswith('.py'):                                                # 检查是否已.py结束的文件
        names += [names[0] + '.py']                                                 # 没有已.py结束则添加后缀
    # Does the name contain path elements?
    if os.path.dirname(names[0]):                                                   # 是否是文件夹
        # If so, expand home-directory markers and test for existence
        for name in names:                                                          # 查找路径名
            expanded = os.path.expanduser(name)
            if os.path.exists(expanded):                                            # 如果存在
                if name.endswith('.py') or _is_package(expanded):                   # 是否已.py结尾或者是否可导入    
                    return os.path.abspath(expanded)                                # 返回文件的绝对路径
    else:
        # Otherwise, start in cwd and work downwards towards filesystem root  
        path = os.path.abspath('.')                                                 # 从当前文件夹开始查找
        while True:
            for name in names:                                                      # 遍历
                joined = os.path.join(path, name)                                   # 添加到文件名称中
                if os.path.exists(joined):                                          # 检查是否存在该路径
                    if name.endswith('.py') or _is_package(joined):                 # 是否已.py结尾或者是可导入
                        return os.path.abspath(joined)                              # 返回文件绝对路劲
            parent_path = os.path.dirname(path)                                     # 获取自目录
            if parent_path == path:                                                 # 依次遍历检查
                # we've reached the root path which has been checked this iteration
                break
            path = parent_path
    # Implicit 'return None' if nothing was found

该函数就是一直去寻找定义的示例文件并返回文件的绝对路径,当找到了绝对路径之后就会去导入该模块,此时我们继续分析load_locustfile函数的执行过程:

def is_locust(tup):
    """
    Takes (name, object) tuple, returns True if it's a public Locust subclass.
    """
    name, item = tup                        
    return bool(
        inspect.isclass(item)                           # 是否是类
        and issubclass(item, Locust)                    # 是否是Locust的子类
        and hasattr(item, "task_set")                   # 是否有task_set属性
        and getattr(item, "task_set")                   # 获取task_set属性
        and not name.startswith('_')                    # 不以'_'名称开头
    )


def load_locustfile(path):
    """
    Import given locustfile path and return (docstring, callables).

    Specifically, the locustfile's ``__doc__`` attribute (a string) and a
    dictionary of ``{'name': callable}`` containing all callables which pass
    the "is a Locust" test.
    """

    def __import_locustfile__(filename, path):
        """
        Loads the locust file as a module, similar to performing `import`
        """
        try:
            # Python 3 compatible
            source = importlib.machinery.SourceFileLoader(os.path.splitext(locustfile)[0], path)    # 导入moudle获取导入的属性
            imported = source.load_module()
        except AttributeError:
            # Python 2.7 compatible
            import imp
            imported = imp.load_source(os.path.splitext(locustfile)[0], path)

        return imported

    # Get directory and locustfile name
    directory, locustfile = os.path.split(path)                                         # 分离路径
    # If the directory isn't in the PYTHONPATH, add it so our import will work
    added_to_path = False
    index = None
    if directory not in sys.path:                                                       # 检查文件夹是否在系统路径中
        sys.path.insert(0, directory)                                                   # 不在系统路径中则插入
        added_to_path = True
    # If the directory IS in the PYTHONPATH, move it to the front temporarily,
    # otherwise other locustfiles -- like Locusts's own -- may scoop the intended
    # one.
    else:
        i = sys.path.index(directory)                                                   # 获取文件夹的索引
        if i != 0:
            # Store index for later restoration
            index = i
            # Add to front, then remove from original position
            sys.path.insert(0, directory)                                               # 如果不为0 则插入第一个路径
            del sys.path[i + 1]
    # Perform the import
    imported = __import_locustfile__(locustfile, path)                                  # 导入该module
    # Remove directory from path if we added it ourselves (just to be neat)
    if added_to_path:
        del sys.path[0]
    # Put back in original index if we moved it
    if index is not None:
        sys.path.insert(index + 1, directory)
        del sys.path[0]
    # Return our two-tuple
    locusts = dict(filter(is_locust, vars(imported).items()))                           # 获取导入module的所有的环境的值并依次传入is_locust函数进行检查
    return imported.__doc__, locusts                                                    # 返回导入的module的注释,并返回找到的Locust类

此时就找出了在示例文件test_locust.py中的WebsiteUser,因为该类继承自HttpLocust,此时在main的执行流程中就会继续执行到

if not options.master and not options.slave:
    runners.locust_runner = LocalLocustRunner(locust_classes, options)
    # 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()

此时,locust_runner就是LocalLocustRunner,并且调用了该实例的start_hatching(wait=True)函数;

class LocalLocustRunner(LocustRunner):
    def __init__(self, locust_classes, options):
        super(LocalLocustRunner, self).__init__(locust_classes, options)

        # register listener thats logs the exception for the local runner
        def on_locust_error(locust_instance, exception, tb):
            formatted_tb = "".join(traceback.format_tb(tb))
            self.log_exception("local", str(exception), formatted_tb)
        events.locust_error += on_locust_error

    def start_hatching(self, locust_count=None, hatch_rate=None, wait=False):
        self.hatching_greenlet = gevent.spawn(lambda: super(LocalLocustRunner, self).start_hatching(locust_count, hatch_rate, wait=wait))                                   # 调用了父类LocustRunner的start_hatching方法 
        self.greenlet = self.hatching_greenlet                                  # 设置了greenlet




class LocustRunner(object):
    def __init__(self, locust_classes, options):
        self.options = options                                                  # 解析的传入参数
        self.locust_classes = locust_classes                                    # 找到的locust
        self.hatch_rate = options.hatch_rate                                    # 执行速率
        self.num_clients = options.num_clients                                  # 启动的客户端数量
        self.host = options.host                                                # 访问的host
        self.locusts = Group()
        self.greenlet = self.locusts
        self.state = STATE_INIT
        self.hatching_greenlet = None
        self.exceptions = {}
        self.stats = global_stats
        
        # 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

    def weight_locusts(self, amount, stop_timeout = None):
        """
        Distributes the amount of locusts for each WebLocust-class according to it's weight
        returns a list "bucket" with the weighted locusts
        """
        bucket = []
        weight_sum = sum((locust.weight for locust in self.locust_classes if locust.task_set))  # 计算总的权重
        for locust in self.locust_classes:                                                      # 遍历locust列表
            if not locust.task_set:                                                             # 如果locust没有task_set属性则跳过
                warnings.warn("Notice: Found Locust class (%s) got no task_set. Skipping..." % locust.__name__)
                continue

            if self.host is not None:                                                           # 检查是否有host
                locust.host = self.host
            if stop_timeout is not None:                                                        # 检查是否有体制时间
                locust.stop_timeout = stop_timeout

            # create locusts depending on weight
            percent = locust.weight / float(weight_sum)                                         # 计算权重
            num_locusts = int(round(amount * percent))                                          # 获取需要启动的客户端数量
            bucket.extend([locust for x in xrange(0, num_locusts)])                             # 生成对应数量的locusts数量
        return bucket

    def spawn_locusts(self, spawn_count=None, stop_timeout=None, wait=False):
        if spawn_count is None:                                                                 # 获取生成客户端的数量
            spawn_count = self.num_clients

        bucket = self.weight_locusts(spawn_count, stop_timeout)                                 # 传入数量并计算权重之后哦返回待执行的locust实例列表
        spawn_count = len(bucket)
        if self.state == STATE_INIT or self.state == STATE_STOPPED:                             # 如果是初始化状态或停止状态
            self.state = STATE_HATCHING                                                         # 修改为hatching状态
            self.num_clients = spawn_count                                                      # 设置数量
        else:
            self.num_clients += spawn_count                                                     # 否则添加数量

        logger.info("Hatching and swarming %i clients at the rate %g clients/s..." % (spawn_count, self.hatch_rate))
        occurence_count = dict([(l.__name__, 0) for l in self.locust_classes])
        
        def hatch():
            sleep_time = 1.0 / self.hatch_rate                                                  # 通过速率获取睡眠的时间
            while True:
                if not bucket:                                                                  # 如果为0则执行完成
                    logger.info("All locusts hatched: %s" % ", ".join(["%s: %d" % (name, count) for name, count in six.iteritems(occurence_count)]))
                    events.hatch_complete.fire(user_count=self.num_clients)
                    return

                locust = bucket.pop(random.randint(0, len(bucket)-1))                           # 随机获取一个locust实例
                occurence_count[locust.__name__] += 1                                           # 记录该locust实例执行的次数
                def start_locust(_):
                    try:
                        locust().run(runner=self)                                               # 调用locust实例的run方法
                    except GreenletExit:
                        pass
                new_locust = self.locusts.spawn(start_locust, locust)                           # 执行start_locust函数
                if len(self.locusts) % 10 == 0:
                    logger.debug("%i locusts hatched" % len(self.locusts))
                gevent.sleep(sleep_time)                                                        # 休息执行时间
        
        hatch()                                                                                 # 调用hatch函数运行
        if wait:
            self.locusts.join()
            logger.info("All locusts dead\n")

    def start_hatching(self, locust_count=None, hatch_rate=None, wait=False):
        if self.state != STATE_RUNNING and self.state != STATE_HATCHING:                # 不是运行状态并且不是hatching状态
            self.stats.clear_all()
            self.stats.start_time = time()
            self.exceptions = {}
            events.locust_start_hatching.fire()

        # Dynamically changing the locust count
        if self.state != STATE_INIT and self.state != STATE_STOPPED:                    # 是否初始化状态并且不是停止状态
            self.state = STATE_HATCHING
            if self.num_clients > locust_count:
                # Kill some locusts
                kill_count = self.num_clients - locust_count
                self.kill_locusts(kill_count)
            elif self.num_clients < locust_count:
                # Spawn some locusts
                if hatch_rate:
                    self.hatch_rate = hatch_rate
                spawn_count = locust_count - self.num_clients
                self.spawn_locusts(spawn_count=spawn_count)
            else:
                events.hatch_complete.fire(user_count=self.num_clients)
        else:
            if hatch_rate:                                                              # 是否设置了速率
                self.hatch_rate = hatch_rate
            if locust_count is not None:                                                # 如果传入了数量
                self.spawn_locusts(locust_count, wait=wait)                             # 传入参数
            else: 
                self.spawn_locusts(wait=wait)                                           # 启动

从该类的分析可知,最终调用的都是Locust的run方法,

class Locust(object):
    """
    Represents a "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 should usually be subclassed by a class that defines some kind of client. For 
    example when load testing an HTTP system, you probably want to use the 
    :py:class:`HttpLocust ` class.
    """
    
    host = None
    """Base hostname to swarm. i.e: http://127.0.0.1:1234"""
    
    min_wait = 1000
    """Minimum waiting time between the execution of locust tasks"""
    
    max_wait = 1000
    """Maximum waiting time between the execution of locust tasks"""

    wait_function = lambda self: random.randint(self.min_wait,self.max_wait) 
    """Function used to calculate waiting time between the execution of locust tasks in milliseconds"""
    
    task_set = None
    """TaskSet class that defines the execution behaviour of this locust"""
    
    stop_timeout = None
    """Number of seconds after which the Locust will die. If None it won't timeout."""

    weight = 10
    """Probability of locust being chosen. The higher the weight, the greater is the chance of it being chosen."""
        
    client = NoClientWarningRaiser()
    _catch_exceptions = True
    _setup_has_run = False  # Internal state to see if we have already run
    _teardown_is_set = False  # Internal state to see if we have already run
    _lock = gevent.lock.Semaphore()  # Lock to make sure setup is only run once
    
    def __init__(self):
        super(Locust, self).__init__()
        self._lock.acquire()
        if hasattr(self, "setup") and self._setup_has_run is False:
            self._set_setup_flag()
            self.setup()
        if hasattr(self, "teardown") and self._teardown_is_set is False:
            self._set_teardown_flag()
            events.quitting += self.teardown
        self._lock.release()

    @classmethod
    def _set_setup_flag(cls):
        cls._setup_has_run = True

    @classmethod
    def _set_teardown_flag(cls):
        cls._teardown_is_set = True
    
    def run(self, runner=None):
        task_set_instance = self.task_set(self)                                         # 实例化task_set实例
        try:
            task_set_instance.run()                                                     # 调用task_set实例的run方法
        except StopLocust:
            pass
        except (RescheduleTask, RescheduleTaskImmediately) as e:
            six.reraise(LocustError, LocustError("A task inside a Locust class' main TaskSet (`%s.task_set` of type `%s`) seems to have called interrupt() or raised an InterruptTaskSet exception. The interrupt() function is used to hand over execution to a parent TaskSet, and should never be called in the main TaskSet which a Locust class' task_set attribute points to." % (type(self).__name__, self.task_set.__name__)), sys.exc_info()[2])
        except GreenletExit as e:
            if runner:
                runner.state = STATE_CLEANUP
            # Run the task_set on_stop method, if it has one
            if hasattr(task_set_instance, "on_stop"):
                task_set_instance.on_stop()
            raise  # Maybe something relies on this except being raised?

由该类可知,最终调用了tesk_set实例的run方法,该类TaskSet的代码如下:

class TaskSetMeta(type):
    """
    Meta class for the main Locust class. It's used to allow Locust classes to specify task execution 
    ratio using an {task:int} dict, or a [(task0,int), ..., (taskN,int)] list.
    """
    
    def __new__(mcs, classname, bases, classDict):
        new_tasks = []
        for base in bases:                                                      # 检查父类
            if hasattr(base, "tasks") and base.tasks:                           # 如果父类有tasks
                new_tasks += base.tasks                                         # 添加父类的tasks
        
        if "tasks" in classDict and classDict["tasks"] is not None:             # 检查tasks是否在属性列表中
            tasks = classDict["tasks"]                                          # 获取传入的tasks数据
            if isinstance(tasks, dict):
                tasks = six.iteritems(tasks)                                    # 迭代tasks
            
            for task in tasks:                                                  # 遍历tasks
                if isinstance(task, tuple):                                     # 如果是元组
                    task, count = task 
                    for i in xrange(0, count):
                        new_tasks.append(task)                                  # 添加到tasks列表中
                else: 
                    new_tasks.append(task)                                      # 直接添加到tasks列表中
        
        for item in six.itervalues(classDict):
            if hasattr(item, "locust_task_weight"):                             # 检查是否有locust_task_weight属性
                for i in xrange(0, item.locust_task_weight):                    # 添加对应数量的任务到列表中
                    new_tasks.append(item)
        
        classDict["tasks"] = new_tasks                                          # 重新设置tasks的列表
        
        return type.__new__(mcs, classname, bases, classDict)                   # 生成该类

@six.add_metaclass(TaskSetMeta)
class TaskSet(object):
    
    tasks = []

    def __init__(self, parent):
        self._task_queue = []
        self._time_start = time()
        
        if isinstance(parent, TaskSet):
            self.locust = parent.locust
        elif isinstance(parent, Locust):
            self.locust = parent
        else:
            raise LocustError("TaskSet should be called with Locust instance or TaskSet instance as first argument")

        self.parent = parent
        
        # if this class doesn't have a min_wait, max_wait or wait_function defined, copy it from Locust
        if not self.min_wait:
            self.min_wait = self.locust.min_wait
        if not self.max_wait:
            self.max_wait = self.locust.max_wait
        if not self.wait_function:
            self.wait_function = self.locust.wait_function

        self._lock.acquire()
        if hasattr(self, "setup") and self._setup_has_run is False:
            self._set_setup_flag()
            self.setup()
        if hasattr(self, "teardown") and self._teardown_is_set is False:
            self._set_teardown_flag()
            events.quitting += self.teardown
        self._lock.release()

    @classmethod
    def _set_setup_flag(cls):
        cls._setup_has_run = True

    @classmethod
    def _set_teardown_flag(cls):
        cls._teardown_is_set = True

    def run(self, *args, **kwargs):
        self.args = args
        self.kwargs = kwargs
        
        try:
            if hasattr(self, "on_start"):                                               # 检查是否有on_start属性,有则调用该函数
                self.on_start()
        except InterruptTaskSet as e:
            if e.reschedule:
                six.reraise(RescheduleTaskImmediately, RescheduleTaskImmediately(e.reschedule), sys.exc_info()[2])
            else:
                six.reraise(RescheduleTask, RescheduleTask(e.reschedule), sys.exc_info()[2])
        
        while (True):
            try:
                if self.locust.stop_timeout is not None and time() - self._time_start > self.locust.stop_timeout:                                       # 检查执行的时间是否已到,如果到了则返回
                    return
        
                if not self._task_queue:                                        # 检查任务列表是否有任务
                    self.schedule_task(self.get_next_task())                    # 有待执行的任务则调用该任务
                
                try: 
                    self.execute_next_task()                                    # 执行下一个待调度的任务
                except RescheduleTaskImmediately:
                    pass
                except RescheduleTask:
                    self.wait()                                                 # 等待
                else:
                    self.wait()
            except InterruptTaskSet as e:
                if e.reschedule:
                    six.reraise(RescheduleTaskImmediately, RescheduleTaskImmediately(e.reschedule), sys.exc_info()[2])
                else:
                    six.reraise(RescheduleTask, RescheduleTask(e.reschedule), sys.exc_info()[2])
            except StopLocust:
                raise
            except GreenletExit:
                raise
            except Exception as e:
                events.locust_error.fire(locust_instance=self, exception=e, tb=sys.exc_info()[2])
                if self.locust._catch_exceptions:
                    sys.stderr.write("\n" + traceback.format_exc())
                    self.wait()
                else:
                    raise
    
    def execute_next_task(self):
        task = self._task_queue.pop(0)                                              # 获取任务队列的第一个任务
        self.execute_task(task["callable"], *task["args"], **task["kwargs"])        # 执行该任务
    
    def execute_task(self, task, *args, **kwargs):
        # check if the function is a method bound to the current locust, and if so, don't pass self as first argument
        if hasattr(task, "__self__") and task.__self__ == self:                     # 检查是否有__self__属性并且__self__是否为自己
            # task is a bound method on self
            task(*args, **kwargs)                                                   # 是则直接调用task执行
        elif hasattr(task, "tasks") and issubclass(task, TaskSet):
            # task is another (nested) TaskSet class
            task(self).run(*args, **kwargs)                                         # 检查是否有tasks属性并且是否是TaskSet的子类如果是则继续调用run方法
        else:
            # task is a function
            task(self, *args, **kwargs)                                             # 如果task是个function则直接调用
    
    def schedule_task(self, task_callable, args=None, kwargs=None, first=False):
        """
        Add a task to the Locust's task execution queue.
        
        *Arguments*:
        
        * task_callable: Locust task to schedule
        * args: Arguments that will be passed to the task callable
        * kwargs: Dict of keyword arguments that will be passed to the task callable.
        * first: Optional keyword argument. If True, the task will be put first in the queue.
        """
        task = {"callable":task_callable, "args":args or [], "kwargs":kwargs or {}}
        if first:
            self._task_queue.insert(0, task)                                        # 如果是第一个任务则添加到_task_queue中
        else:
            self._task_queue.append(task)                                           # 否则就添加到_task_queue任务列表中
    
    def get_next_task(self):
        return random.choice(self.tasks)                                            # 随机抽取一个任务
    
    def get_wait_secs(self):
        millis = self.wait_function()                                               # 
        return millis / 1000.0

    def wait(self):
        self._sleep(self.get_wait_secs())

    def _sleep(self, seconds):
        gevent.sleep(seconds)

其中使用到了元类编程的相关知识,此时就将实例代码中的如下两个任务执行了;

@task(2)
def index(self):
    self.client.get("/")

@task(1)
def profile(self):
    self.client.get("/profile")

task的函数如下,

def task(weight=1):
    """
    Used as a convenience decorator to be able to declare tasks for a TaskSet 
    inline in the class. Example::
    
        class ForumPage(TaskSet):
            @task(100)
            def read_thread(self):
                pass
            
            @task(7)
            def create_thread(self):
                pass
    """
    
    def decorator_func(func):
        func.locust_task_weight = weight
        return func
    
    """
    Check if task was used without parentheses (not called), like this::
    
        @task
        def my_task()
            pass
    """
    if callable(weight):
        func = weight
        weight = 1
        return decorator_func(func)
    else:
        return decorator_func

给每个定义的函数都添加了locust_task_weight属性,让这些方法都在TaskSet初始化的时候都被添加到tasks属性中了,然后在task被调用的时候执行,至此,locust的基本的task的调用流程基本完成。

总结

locust压测工具支持多种的应用场景,是用Python实现的一个压测框架,本文首先只是简单的分析了一下该框架的基本的执行流程,就是通过执行定义的task来实现测试用例的执行,本文还有些许细节没有详细说明,只是简单的概述了启动执行的大致流程,启动执行的更为详细的流程大家有兴趣可自行查阅。鉴于本人才疏学浅,如有疏漏请批评指正。

你可能感兴趣的:(python,python,locust,压测工具)