性能测试工具locust使用

部分内容转自https://my.oschina.net/u/1433482/blog/634047

什么是locustio?

  • Locust是易于使用、分布式的用户负载测试工具。用于网站(或其他系统)的负载测试,计算出系统可以处理并发用户数。
    测试时大量蝗虫会攻击你的网站。每只蝗虫(或叫测试用户)可以自定义、测试过程由web界面实时监控。这能帮助测试并确定瓶颈。
    Locust 完全基于的事件,单机可以支持数千用户。它不使用回调,而是基于轻量进程gevent, 能简单地实线各种场景。

特点

  • Python书写场景
    • 无需笨重的UI或XML。仅仅是代码,协程而不是回调。
  • 分布式,可扩展和,支持成千上万的用户
  • 基于Web的用户界面(基于flask)
    • Locust有整洁HTML + JS用户界面,实时展示测试细节,跨平台和易于扩展。
  • 可以测试任何系统
  • 可控制
    • 事件完全由gevent处理。

      背景

  • 我们研究了现有的解决方案,都不符合要求。比如Apache JMeter和Tsung。JMeter基于UI操作,容易上手,但基本上不具备编程能力。其次JMeter基于线程,要模拟数千用户几乎不可能。 Tsung基于Erlang,能模拟上千用户并易于扩展,但它它基于XML的DSL,描述场景能力弱,且需要大量的数据处理才知道测试结果。
    无论如何,我们试图解决创建蝗虫,当这些问题。希望以上都不是painpoints应该存在。
    我想你可以说我们真的只是想在这里从头开始自己的痒。我们希望其他人会发现,因为我们做的是有益的。

    安装

  • github地址:https://github.com/locustio/locust 可找到最新版本,支持python2.7~3.6
  • pip install locustio or easy_install locustio
  • 校验

    • 执行”locust –help”能看到如下信息表示安装成功:
      1
      2
      3
      4
      5
      6
      # locust --help
      Usage: locust [options] [LocustClass [LocustClass2 ... ]]
      Options:
      -h, --help show this help message and exit
      ...

    分布式测试还需要安装pyzmq。尽管locustio可以在Windows运行,但是考虑效率不推荐。

    快速入门

  • 直接查看官方demo
  • 官方手册地址:http://docs.locust.io/en/latest/index.html

项目实战

  • 下面我们对一个 socket.io(基于websocket,nodejs)的im项目进行实践
  • 涉及第三方库:
    • socketIO-client-2
  • 通过locust编写socketio客户端

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    # -*- coding:utf-8 -*-
    import requests
    import json
    import time
    import types
    import sys
    import os
    import random
    import logging
    from socketIO_client import SocketIO, LoggingNamespace, BaseNamespace
    from locust import HttpLocust, TaskSet, task, events
    logger = logging.getLogger('requests')
    logging.getLogger('requests').setLevel(logging.WARNING)
    logging.basicConfig(level=logging.DEBUG)
    a = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z']
    did = "did"+str(long(time.time()*1000)) + random.choice(a)
    #print did
    def on_message(*args):
    print("msg receive:", args)
    hosts = 'http://talk.xxxx.com'
    port = 80
    """
    流程:
    1、创建会话;
    2、发送消息;
    """
    class WebsiteTasks(TaskSet):
    def on_start(self):
    self.conversationid = None
    token = self.gettoken()
    start = time.time()
    try:
    self.sk = SocketIO(hosts,port=port,params={'token': token})
    end = time.time()
    events.request_success.fire(
    request_type="Send",
    name="Connection",
    response_time=int((end - start) * 1000),
    response_length=0,
    )
    except Exception as err:
    events.request_failure.fire(
    request_type="Send",
    name="Connection",
    response_time=int((time.time() - start) * 1000),
    response_length=0,
    )
    self.sk.on('message', self.create_conversation)
    data = {
    "sn": 0,
    "ver": 2,
    "time": int(time.time()),
    "cmid": "".join(map(lambda xx:(hex(ord(xx))[2:]),os.urandom(16))),
    "confirm": 1,
    "type": "Conversation",
    "payload": {
    "cmd": "createConversation",
    "data": {"Id":""}
    }
    }
    self.sk.wait(seconds=1)
    #print(data)
    try:
    start = time.time()
    self.sk.send(data, self.create_conversation)
    self.sk.wait_for_callbacks(seconds=1)
    end = time.time()
    events.request_success.fire(
    request_type="Send",
    name="CreateConversation",
    response_time=int((end - start) * 1000),
    response_length=0,
    )
    except Exception as err:
    logger.exception("send create conversation fail:%s", err)
    raise Exception("send create conversation fail:{0}".format(err))
    if self.conversationid is None:
    raise Exception("create conversation failed!!")
    def create_conversation(self, *args):
    if type(args[0]) is types.DictType:
    rp = args[0]
    if rp["payload"]["cmd"] == 'createConversation':
    self.conversationid = rp["payload"]["data"]["conversationId"]
    #print "conversationid", self.conversationid
    def gettoken(self):
    uid = 0
    try:
    # 通过flask写了一个用户id的排号器
    url = "http://192.168.149.34:5000/getuser"
    # r = requests.get(url)
    r = self.client.get("/getuser")
    uid = eval(r.text)[1]
    except Exception as e:
    raise Exception("getUid Error:{0} content:{1}".format(e, r.text))
    try:
    #print "uid:",uid
    # 获取登录token
    url = 'http://im.xxxx.com/chat'
    payload = {
    ....
    }
    r = requests.get(url, params=payload)
    token = r.json().get(u'data').get(u'token')
    except Exception as e:
    raise Exception("getToken Error:{0} content:{1}".format(e, r.content))
    #print token
    return token
    @task
    def sendmssage(self):
    # 参考wiki的消息体
    data = {
    "sn": 0,
    "ver": 2,
    "time": int(time.time()),
    "cmid": "".join(map(lambda xx:(hex(ord(xx))[2:]),os.urandom(16))),
    "confirm": 0,
    "type": "",
    "payload": {
    "cmd": "chat",
    "data": {
    "conversationId": self.conversationid,
    "msgType": 'rich',
    "msgContent": "load testing...."
    }
    }
    }
    #print(data)
    try:
    start = time.time()
    self.sk.send(data, on_message)
    self.sk.wait_for_callbacks(seconds=1)
    end = time.time()
    events.request_success.fire(
    request_type="Send",
    name="Chating",
    response_time=int((end - start) * 1000),
    response_length=0,
    )
    except Exception as err:
    logger.exception("send message fail:%s", err)
    raise Exception("send message fail:{0}".format(err))
    class WebsiteUser(HttpLocust):
    task_set = WebsiteTasks
    min_wait = 5000
    max_wait = 15000
    一般http请求demo
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    # -*- coding:utf-8 -*-
    import requests
    import json
    import time
    import sys
    import os
    import random
    from locust import HttpLocust, TaskSet, task, events
    from requests.exceptions import (RequestException, MissingSchema,
    InvalidSchema, InvalidURL)
    class WebsiteTasks(TaskSet):
    def on_start(self):
    pass
    def check_response(self, response):
    try:
    response.raise_for_status()
    except RequestException as e:
    events.request_failure.fire(
    request_type=response.locust_request_meta["method"],
    name=response.locust_request_meta["name"],
    response_time=response.locust_request_meta["response_time"],
    exception=e,
    )
    else:
    code = response.json().get("code")
    if code == 1000:
    events.request_success.fire(
    request_type=response.locust_request_meta["method"],
    name=response.locust_request_meta["name"],
    response_time=response.locust_request_meta["response_time"],
    response_length=response.locust_request_meta["content_size"],
    )
    else:
    events.request_failure.fire(
    request_type=response.locust_request_meta["method"],
    name=response.locust_request_meta["name"],
    response_time=response.locust_request_meta["response_time"],
    response_length=response.locust_request_meta["content_size"],
    exception="Response Code Error! Code:{0}".format(code)
    )
    @task
    def requests_Msgs(self):
    cookies= {...}
    url = "/business"
    payload = {...}
    headers = {
    'Content-Type':'application/x-www-form-urlencoded'
    }
    try:
    rsp = self.client.post(url, data=payload, headers=headers, catch_response=True, cookies=cookies)
    # print r.text
    self.check_response(rsp)
    except Exception as err:
    # logger.exception("request Msgs fail:%s", err)
    raise Exception("request Msgs fail:{0}".format(err))
    class WebsiteUser(HttpLocust):
    task_set = WebsiteTasks
    min_wait = 5000
    max_wait = 15000
    host = "http://chat.xxxx.com"

HttpLocust继承自Locust,添加了client属性。client属性是HttpSession实例,可以用于生成HTTP请求。
on_start为client初始化时执行的步骤。
task表示下面方法是测试内容,里面的数字执行比例,这里about页面占三分之一,主页占三分之二。
task_set指定client执行的类。min_wait和max_wait为两次执行之间的最小和最长等待时间。

  • 启动

    • 启动locust后台,–port可自定义端口,默认8089
      1
      2
      3
      4
      locust -f imlocust.py --host http://192.168.149.34:5000 --port 80
      [root@localhost im]# locust -f imlocust3.py --host http://192.168.149.34:5000 --port 80
      [2017-04-21 15:34:36,366] localhost.localdomain/INFO/locust.main: Starting web monitor at *:80
      [2017-04-21 15:34:39,103] localhost.localdomain/INFO/locust.main: Starting Locust 0.8a2
  • 在浏览器启动locust

  • 命令行按Ctrl + c , 可以显示一些摘要,类似下图的

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    [2015-05-08 16:48:19,884] andrew-Hi-Fi-A88S2/INFO/locust.main: Shutting down (exit code 0), bye.
    Name # reqs # fails Avg Min Max | Median req/s
    --------------------------------------------------------------------------------------------------------------------------------------------
    GET / 36 0(0.00%) 260 206 411 | 250 0.90
    GET /about 17 0(0.00%) 199 146 519 | 170 0.10
    --------------------------------------------------------------------------------------------------------------------------------------------
    Total 53 0(0.00%) 1.00
    Percentage of the requests completed within given times
    Name # reqs 50% 66% 75% 80% 90% 95% 98% 99% 100%
    --------------------------------------------------------------------------------------------------------------------------------------------
    GET / 36 250 260 260 270 370 400 410 410 411
    GET /about 17 170 180 180 200 290 520 520 520 519
    --------------------------------------------------------------------------------------------------------------------------------------------
  • 网页上可以下载csv文件,一个是调配记录,一个是请求记录。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    # cat distribution_1431074713.45.csv
    "Name","# requests","50%","66%","75%","80%","90%","95%","98%","99%","100%"
    "GET /",36,250,260,260,270,370,400,410,410,411
    "GET /about",17,170,180,180,200,290,520,520,520,519
    "None Total",53,250,250,260,260,310,400,410,520,519
    # cat requests_1431074710.05.csv
    "Method","Name","# requests","# failures","Median response time","Average response time","Min response time","Max response time","Average Content Size","Requests/s"
    "GET","/",36,0,250,260,206,411,9055,0.76
    "GET","/about",17,0,170,199,146,519,4456,0.36
    "None","Total",53,0,250,241,146,519,7579,1.12
  • 其他:

  • locustfile
    • locustfile需要定义至少一个locust类。
  • Locust类

    • locust类代表用户。属性如下:
      • task_set属性
    • task_set属性指向定义了用户的行为的TaskSet类。
      • min_wait和max_wait属性
    • 两次执行之间的最小和最长等待时间,单位:毫秒,即执行各任务之间等待时间。默认为1000,并且因此蝗虫永远等待1秒各任务之间如果min_wait和MAX_WAIT未声明。
    • 用下面locustfile,每个用户将等待任务之间5到15秒:
      • weight属性
    • 可以同时执行同一文件的多个locust:

      1
      # locust -f locust_file.py WebUserLocust MobileUserLocust
    • 嵌套的python代码示例:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      class ForumPage(TaskSet):
      @task(20)
      def read_thread(self):
      pass
      @task(1)
      def new_thread(self):
      pass
      @task(5)
      def stop(self):
      self.interrupt()class UserBehaviour(TaskSet):
      tasks = {ForumPage:10}
      @task
      def index(self):
      pass
    • 类嵌套示例:

      1
      2
      3
      4
      5
      6
      class MyTaskSet(TaskSet):
      @task
      class SubTaskSet(TaskSet):
      @task
      def my_task(self):
      pass
    • HTTP请求

      • HttpLocust继承自Locust,添加了client属性。client属性是HttpSession实例,调用了requests,可以用于生成HTTP请求。 self.client设置了属性指向 self.locust.client。
      • get 和post示例:

        1
        2
        3
        4
        5
        6
        # get
        response = self.client.get("/about")
        print "Response status code:", response.status_code
        print "Response content:", response.content
        # post
        response = self.client.post("/login", {"username":"testuser", "password":"secret"})
      • 注意失败的任何请求如连接错误,超时等不产生异常,而是返回None,status_code是0。

      • 默认只要返回的不是200都是失败。也可以设置失败和成功:
        1
        2
        3
        4
        5
        6
        7
        with client.get("/", catch_response=True) as response:
        if response.content != "Success":
        response.failure("Got wrong response")
        with client.get("/does_not_exist/", catch_response=True) as response:
        if response.status_code == 404:
        response.success()
    • 动态参数:

      1
      2
      3
      # Statistics for these requests will be grouped under: /blog/?id=[id]
      for i in range(10):
      client.get("/blog?id=%i" % i, name="/blog?id=[id]")
    • 分布式测试

      • 主机使用–master,它不会模拟任何用户。实际测试机需要–slave和–master-host参数。通常一个cpu核可以执行一个从机。
        master的参数还有: “–master-bind-host=X.X.X.X”和–master-bind-host=X.X.X.X。
        从机的参数有:”–master-port=5557”,或者 –slave –master-host=X.X.X.X。