Locust 源碼理解與分析

前言

相信不少小夥伴會選擇Locust做爲壓測工具輔助測試,本文從Locust源碼開始分析它的優劣,結論在最後,最終我仍是選擇了Jmeter前端

主要分析了Locust源碼的兩個文件:main.py 和 runners.pyjava

 

main.py

它執行程序的入口,如下代碼僅包含核心代碼。python

parse_options()

它用來解析傳入的參數,閱讀它能夠了解Locust都接受哪些參數,而且大體是什麼做用。web

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

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

# Initialize ,-H 和 --host是相同的,默認是None,提示是help
 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" )
 

find_locustfile(locustfile) 和 load_locustfile(path)

  做用:找到並加載咱們手寫的locust用例,即-f 傳入的文件,py結尾。
  核心代碼(節選):shell

# 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。flask

 

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。app

  • 若是是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 類,都是其中的方法實現。

 

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)


runners.py

weight_locusts(self, amount, stop_timeout = None)  

根據權重計算出要使用的用戶數

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 是用例中的全部weight值的綜合,weight表明權重值。
    weight_sum = sum((locust.weight for locust in self.locust_classes if locust.task_set))
        # 能夠有多個用例。
    for locust in self.locust_classes:
                # 一些判斷略過
        if not locust.task_set:
            warnings.warn("Notice: Found Locust class (%s) got no task_set. Skipping..." % locust.__name__)
            continue

        if self.host is not None:
            locust.host = self.host
        if stop_timeout is not None:
            locust.stop_timeout = stop_timeout

        # create locusts depending on weight
                # 在循環中這是一個用例,percent 意味着這個用例在整體權重中的比例。
        percent = locust.weight / float(weight_sum)
                # 好比是設置了1000個用戶,根據權重比例,計算出1000個用戶中的多少個用戶來執行這個用例。
        num_locusts = int(round(amount * percent))
                # 複製並添加到結果集中
        bucket.extend([locust for x in xrange(0, num_locusts)])
    return bucket

 

spawn_locusts(self, spawn_count=None, stop_timeout=None, wait=False)

利用了sleep來達到每秒運行多少用戶的效果。

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)
    spawn_count = len(bucket)
    if self.state == STATE_INIT or self.state == STATE_STOPPED:
        self.state = STATE_HATCHING
        self.num_clients = spawn_count
    else:
        self.num_clients += spawn_count
  # hatch_rate 的解釋:The rate per second in which clients are spawned. Only used together with --no-web
    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:
                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))
            occurence_count[locust.__name__] += 1
                    # 定義啓動的方法,能夠看到是執行run()方法
            def start_locust(_):
                try:
                    locust().run()
                except GreenletExit:
                    pass

                    # 協程的執行方法,也是Group()的spawn
            new_locust = self.locusts.spawn(start_locust, locust)
            if len(self.locusts) % 10 == 0:
                logger.debug("%i locusts hatched" % len(self.locusts))
                    # 睡眠即等待指定時間。
            gevent.sleep(sleep_time)

    hatch()
    if wait:
        self.locusts.join()
        logger.info("All locusts dead\n")

kill_locusts(self, kill_count)

  1.根據權重計算出要幹掉多少個用戶。
  2.被幹掉的用戶在協程池子中停掉,並從權重池子中彈出。

bucket = self.weight_locusts(kill_count)
kill_count = len(bucket)
self.num_clients -= kill_count
logger.info("Killing %i locusts" % kill_count)
dying = []
for g in self.locusts:
    for l in bucket:
        if l == g.args[0]:
            dying.append(g)
            bucket.remove(l)
            break
for g in dying:
    self.locusts.killone(g)
# 收尾工做,主要是提示給頁面和打日誌
events.hatch_complete.fire(user_count=self.num_clients)

 

Locust的一些特色及思考,與Jmeter對比

作過性能測試的都知道Jmeter是一個繞不開的工具,那麼Locust和它比起來有什麼優缺點?

  
  Jmeter幾乎天天都在更新,Locust幾乎沒啥更新。

  Locust的實現是前端的,在 chart.js 中,LocustLineChart,仍是比較簡陋的。
  Jmeter的能夠安裝插件顯示,也簡陋。

  

  Jmeter也是安裝插件實現服務端性能指標監控,簡陋。
  Locust就沒有。

 

  Locust也沒有測試報告。
  Jmeter3.0開始支持報告生成,可是有硬傷。

測試用例部分:

python腳本是亮點,畢竟代碼能夠實現一切需求。
但不足之處很明顯:
1.util包沒有,複雜用例編寫代碼工做量很大,維護成本很大,同時考驗代碼功力。
2.沒有錄製用例,保存用例功能,即使使用HttpRunner支持的錄製保存,也只是基礎用例。
實際上性能測試剛需的如參數化,仍是要手寫python腳本。
以上對於時間較緊的測試需求,用Locust明顯是撞牆。

Jmeter明顯好不少,自己GUI界面簡單明瞭,各類內置函數幫助你寫腳本。
就算用例編寫很複雜,還提供了beanshell,可使用Java代碼實現(雖然調試比較費勁)。
同時Jmeter擁有各類協議的插件,仍是不錯的。

併發能力

Locust使用4個slave,形成的壓力是1.3k,Jmeter是13k,差了10倍。

Locust做爲施壓側 能力太弱了 通過實驗最終得出的結論是 單核只能承載500左右的RPS

 

總結:使用Locust要慎重,慎重。

相關文章
相關標籤/搜索