一、Locust 性能测试

问题引言:

  1. 主流性能工具对比
  2. 为什么要用locust进行性能测试
  3. 如何对http接口进行性能测试
  4. 如何对websocket接口进行性能测试
  5. locust性能测试报告分析
  6. locust 核心部件了解
  7. locust 主要用法详解

(一). 性能测试工具

主流性能测试工具对比

  • loadrunner : 收费 昂贵
  • jmeter: 开源(二次开发) 、基于java、多线程 、使用gui设计用例 ,xml保存 、录制工具

loadRunner vs Jmeter

认识Locust

定义

Locust是一款易于使用的分布式负载测试工具,完全基于事件,即一个locust节点也可以在一个进程中支持数千并发用户,不使用回调,通过gevent使用轻量级过程(即在自己的进程内运行)。

locust: 开源 、基于python ,非多线程(协程)、“用例即代码” ; 无录制工具、

  • python的一个库 ,需要python3.6 及以上环境支持
  • 可用做性能测试
  • 基于事件,用协程 进行性能测试
  • 支持 图形 、no-gui、 分布式等多种运行方式

为什么选择locust

  • 基于协程 ,低成本实现更多并发

  • 脚本增强(“测试即代码”)

  • 使用了requests发送http请求

  • 支持分布式

  • 使用Flask 提供WebUI

  • 有第三方插件、 易于扩展

(二) locust 基本用法

约定大于配置

1.安装locust

pip install  locust
locust -v

2.编写用例

test_xxx (一般测试框架约定)

dockerfile (docker约定)

locustfile.py (locust约定)

# locustfile.py

eg: 入门示例

from locust import HttpUser, task, between


# User  ?
# function  包装task
class MyHttpUser(HttpUser):
    wait_time = between(1, 2)  # 执行任务 等待时长   检查点 思考时间

    @task
    def index_page(self):
        self.client.get("https://baidu.com/123")
        self.client.get("https://baidu.com/456")

    pass

总结三步:

  1. 创建 之类
  2. 为待测试用例添加@locust.task 装饰器
  3. 使用self.client 发送请求
  4. 指定 wait_time 属性

3. 启动测试

GUI 模式启动 locust

启动locust

​访问:http://[::1]:8089/

指标详解:

- Number of users  模拟用户数
- Spawn rate  : 生产数 (每秒)、   =>jmeter : Ramp-Up Period (in seconds)
- Host (e.g. http://www.example.com)  => 取决脚本中 绝对地址
- ![](https://s3.bmp.ovh/imgs/2022/06/14/22e82961a5609f42.png)
  • WebUI 模块说明:
    • New test:点击该按钮可对模拟的总虚拟用户数和每秒启动的虚拟用户数进行编辑;
    • Statistics:类似于jmeter中Listen的聚合报告;
    • Charts:测试结果变化趋势的曲线展示图,分别为每秒完成的请求数(RPS)、响应时间、不同时间的虚拟用户数;
    • Failures:失败请求的展示界面;
    • Exceptions:异常请求的展示界面;
    • Download Data:测试数据下载模块, 提供三种类型的CSV格式的下载,分别是:Statistics、responsetime、exceptions;

命令行模式启动 locust

locust -f locustfile.py --headless -u 500 -r 10  --host 123  -t 1h5m

​框架是通过命令locust运行的,常用参数有:

  • -H:指定测试的主机地址(注:会覆盖Locust类指定的主机地址)
  • -f:指定测试脚本地址(注:脚本中必须包含一个Locust的衍生类)
  • --no-web:不启动web网页,而是直接开始运行测试,需提供属性-c和-r
  • -u:并发的用户数,与--no-web一起使用
  • -r:每秒启动的用户数,与--no-web一起使用
  • -t:运行时间(单位:秒),与--no-web一起使用
  • -L:日志级别,默认为INFO
    调试命令:locust -f **.py --no-web -u 1 -t 1
    运行命令:locust -f **.py

4. locust概念

  1. 父类是个User ?

    • 性能测试 模拟真实用户
    • 每个user相当于一个协程链接 ,进行相关系统交互操作
  2. 为什么方法,要包装为task

    • task 表示用户要进行的操作

      • 访问首页 → 登录 → 增、删改查 → homPage
    • TaskSet : 定义用户将执行的一组任务的类。测试任务开始后,每个Locust用户会从TaskSet中随机挑选 一个任务执行

    • 具体的内容: 方法的代码

       class MyHttpUser(HttpUser):   #用户
          # wait_time = lambda self: random.expovariate(1)*1000
          wait_time = between(1, 2)  # 执行任务 等待时长   检查点 思考时间
      
          @task
          def index_page(self):   # 用户执行操作
              self.client.get("https://baidu.com/123")  #服务错误、网络错误
              self.client.get("https://baidu.com/456")
              # 断言 、 业务错误
      

      6000字Locust入门详解-LMLPHP

(三) locust 自定义压测协议 websocket

什么是websocket协议 ?

6000字Locust入门详解-LMLPHP

6000字Locust入门详解-LMLPHP

选择websocket 客户端

目前pypi , 2个ws库

  • websockets: 提供client , 提供了server ; 使用async 语法
  • websocket_client: 仅提供client 、使用(非async)同步语法
  1. 安装 pip install websocket_client

  2. 使用

    import asyncio
    import websockets
    
    async def hello():
        async with websockets.connect('ws://localhost:8765') as websocket:
            name = input("What's your name? ")
    
            await websocket.send(name)
            print(f" send:>>> {name}")
    
            greeting = await websocket.recv()
            print(f" recv: <<< {greeting}")
    
    asyncio.get_event_loop().run_until_complete(hello())
    

    创建WebSocketUser

    1. 创建专用客户端链接

    2. 设计统计结果

    3. 设定成功、失败条件

​ 初步结果:

​ 1. RPS xxxx 左右

​ 2. 最大用户数 xxx

​ > 表示:当前允许最大用户数请求,但是无法全部返回结果

(四). locust 核心组件

6000字Locust入门详解-LMLPHP

核心组件: 2类 ,4个

  • User : 在locust中User类表示一个用户。locust将为每一个被模拟的用户生成一个User类实例,而我们可以在User类中定义一些常见的属性来定义用户的行为。

    • HttpUser
  • Task: 用户行为

    • SequentialTaskSet
    • TaskSet:
      • tasks属性将多个TaskSet子类嵌套在一起
      • 类中相互嵌套
      • User类中嵌入TaskSet类,作为User的子类
  • Events : locust提供的事件钩子,用于一些再运行过程中执行特定时间的操作。

重要的属性:

  • wait_time

    ​> 三种时间间隔表达式

    • 固定时间, 由函数提供

    • 区间随机时间:

    • 自适应节奏时间: 用于确保任务每 X 秒(最多)运行一次

  • task: 任务(用户行为)

    • tasks :
      • 用户类的用户行为方法上添加@task修饰
      • 引用外部用户行为方法时 使用tasks实现
  • weight

    • locustfile07.py

(五). locust 扩展增强

	→  Python代码

1. 录制用例

2. 数据关联

使用变量方式进行传递

3. 参数化

  • 变量
  • CSV
  • 队列
  • 。。

4. 检查点

locust默认情况下会使用默认的检查点,比如当接口超时、链接失败等原因是,会自动判断失败

原理

  • 使用self.client提供的catch_response=True`参数, 添加locust提供的ResponseContextManager类的上下文方法手动设置检查点。
  • ResponseContextManager里面的有两个方法来声明成功和失败,分别是successfailure。其中failure方法需要我们传入一个参数,内容就是失败的原因。
from requests import codes
from locust import HttpUser, task, between


class DemoTest(HttpUser):
    host = 'https://www.baidu.com'
    wait_time = between(2, 15)

    def on_start(self):
        # 通过手动传入catch_response=True 参数手动设置检查点
        with self.client.get('/', catch_response=True) as r:
            if r.status_code == codes.bad:
                r.success()
            else:
                r.failure("请求百度首页失败了哦哦")

    @task
    def search_locust(self):
        with self.client.get('/s?ie=utf-8&wd=locust', catch_response=True) as r:
            if r.status_code == codes.ok:
                r.success()
            else:
                r.failure("搜索locust失败了哦哦")

6000字Locust入门详解-LMLPHP

5. 思考时间

  • wait_time
    • between
    • constant

6. 权重

  • 第一种:方法上指定

locust默认是随机执行taskset里面的task的。

权重通过在@task参数中设置,如代码中hello:world:item是1:3:2,实际执行时的代码,在user中tasks会将任务生成列表[hello,world,world,world,item,item]

# locustfile06.py
import time
from locust import HttpUser, task, between, TaskSet, tag


class QuickstartUser(TaskSet):
    wait_time = between(1, 5)
    # wait_time = constant(3)  #固定时间
    @task
    def hello_world(self):
        self.client.get("/hello")
        self.client.get("/world")

    @tag("tag1", "tag2")
    @task(3)
    def view_items(self):
        for item_id in range(10):
            #self.client.request_name="/item?id=[item_id]"#分组请求
            # 将统计的10条信息分组到名为/item条目下
            self.client.get(f"/item?id={item_id}", name="/item")
            time.sleep(1)

    def on_start(self):
        self.client.post("/login", json={"username": "foo", "password": "bar"})

class MyUserGroup(HttpUser):
    """ 定义线程组 """
    tasks = [QuickstartUser]   # tasks 任务列表
    host = "http://www.baidu.com"
  • 第二种:在属性中指定

# locustfile07.py
import time
from locust import HttpUser, task, between, TaskSet, tag, constant


class QuickstartUser1(HttpUser):
    host = "http://www.baidu.com"
    wait_time = constant(4)
    weight = 3   #属性中指定
    @task
    def hello_world(self):
        self.client.get("/hello1")
        self.client.get("/world1")

    def on_start(self):
        self.client.post("/login1", json={"username": "foo", "password": "bar"})


class QuickstartUser2(HttpUser):
    host = "http://www.baidu.com"
    wait_time = between(1, 5)
    weight = 1
    @task
    def hello_world(self):
        self.client.get("/hello2")
        self.client.get("/world2")

    def on_start(self):
        self.client.post("/login2", json={"username": "foo", "password": "bar"})


7. 集合点

注意:框架本身没有直接封装集合点的概念 ,间接通过gevent并发机制,使用gevent的锁来实现

semaphore是一个内置的计数器:
每当调用acquire()时,内置计数器-1
每当调用release()时,内置计数器+1
计数器不能小于0,当计数器为0时,acquire()将阻塞线程直到其他线程调用release()

两步骤:

  1. all_locusts_spawned 创建钩子函数
  2. 将locust实例挂载到监听器 events.spawning_complete.add_listener
  3. Locust实例准备完成时触发

示例代码:

# locustfile08.py
import os

from locust import HttpUser, TaskSet, task,between,events
from gevent._semaphore import Semaphore


all_locusts_spawned = Semaphore()
all_locusts_spawned.acquire()# 阻塞线程


def on_hatch_complete(**kwargs):
    """
    Select_task类的钩子方法
    :param kwargs:
    :return:
    """
    all_locusts_spawned.release() # # 创建钩子方法


events.spawning_complete.add_listener(on_hatch_complete) #挂在到locust钩子函数(所有的Locust示例产生完成时触发)


n = 0
class UserBehavior(TaskSet):

    def login(self):
        global n
        n += 1
        print("%s个虚拟用户开始启动,并登录"%n)

    def logout(self):
        print("退出登录")



    def on_start(self):
        self.login()

        all_locusts_spawned.wait() # 同步锁等待

    @task(4)
    def test1(self):


        url = '/list'
        param = {
            "limit":8,
            "offset":0,
        }
        with self.client.get(url,params=param,headers={},catch_response = True) as response:
            print("用户浏览登录首页")

    @task(6)
    def test2(self):


        url = '/detail'
        param = {
            'id':1
        }
        with self.client.get(url,params=param,headers={},catch_response = True) as response:
            print("用户同时执行查询")

    @task(1)
    def test3(self):
        """
        用户查看查询结果
        :return:
        """

        url = '/order'
        param = {
            "limit":8,
            "offset":0,
        }
        with self.client.get(url,params=param,headers={},catch_response = True) as response:
            print("用户查看查询结果")

    def on_stop(self):
        self.logout()


class WebsiteUser(HttpUser):
    host = 'http://www.baidu.com'
    tasks = [UserBehavior]

    wait_time = between(1, 2)

if __name__ == '__main__':
    os.system("locust -f locustfile08.py")

8. 分布式

一种是单机设置master和slave模式,另外一种是有多个机器,其中一个机器设置master,其它机器设置slave节点

单机主从模式

步骤: 以单台计算机为例(既当做主控机,也当做工作机器)

  1. Step1:→ 启动locust master节点

    locust -f locustfile07.py --master

  2. Step2:→ 每个工作节点 locust -f locustfile07.py --worker

多机主从模式

  1. 选择其中一台电脑,启动master节点,因为主节点无法操作别的节点,所以必须在其它机器上启动从属Locust节点,后面跟上--worker参数,以及 --master-host(指定主节点的IP /主机名)。
locust -f locustfile07.py --master
  1. 其它机器上(环境和主节点环境一致,都需要有locust的运行环境和脚本),启动 slave 节点,设置 --master-host
locust -f locustfile.py --worker --master-host=192.168.x.xx

更多参数介绍

  • --master

将 locust 设置为 master 模式。Web 界面将在此节点上运行。

  • --worker

将locuster设置为worker模式。

  • --master-host= X. X. X. X

可选择与-- worker一起使用,以设置主节点的主机名/IP (默认值为127.0.0.1)

  • --master-port

可选地与-- worker一起用于设置主节点的端口号(默认值为5557)。

  • -master-bind-host= X. X. X. X
    可选择与--master一起使用。 确定主节点将绑定到的网络接口。 默认为*(所有可用接口)。

  • --master-bind-port=5557
    可选择 与--master一起使用。 确定主节点将侦听的网络端口。 默认值为5557。

  • --expect-workers= X
    在使用--headless启动主节点时使用。 然后主节点将等待,直到 X worker节点已经连接,然后测试才开始。

9. 资源监控

6000字Locust入门详解-LMLPHP

6000字Locust入门详解-LMLPHP

6000字Locust入门详解-LMLPHP

10. docker 运行locust

拉取镜像:

docker pull locustio/locust

运行容器:

docker run -p 8089:8089 -v $PWD:/mnt/locust locustio/locust -f /mnt/locust/locustfile.py

6000字Locust入门详解-LMLPHP

Docker Compose:

version: '3'

services:
  master:
    image: locustio/locust
    ports:
     - "8089:8089"
    volumes:
      - ./:/mnt/locust
    command: -f /mnt/locust/locustfile.py --master -H http://master:8089

  worker:
    image: locustio/locust
    volumes:
      - ./:/mnt/locust
    command: -f /mnt/locust/locustfile.py --worker --master-host master

11. 高性能 FastHttpUser

Locust 的默认 HTTP 客户端使用python-requests。如果您计划以非常高的吞吐量运行测试并且运行 Locust 的硬件有限,那么它有时效率不够。Locust 还附带使用代替。它提供了一个非常相似的 API,并且使用的 CPU 时间显着减少,有时将给定硬件上每秒的最大请求数增加了 5 到 6 倍。

在相同的并发条件下使用FastHttpUser能有效减少对负载机的资源消耗从而达到更大的http请求。


比对结果如下:

HttpUser:

6000字Locust入门详解-LMLPHP

6000字Locust入门详解-LMLPHP

对比:FastHttpUser

6000字Locust入门详解-LMLPHP

6000字Locust入门详解-LMLPHP

(六)附外

0. 进程、线程、协程区别

进程:进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。每个进程都有自己的独立内存空间,不同进程通过进程间通信来通信。由于进程比较重量,占据独立的内存,所以上下文进程间的切换开销(栈、寄存器、虚拟内存、文件句柄等)比较大,但相对比较稳定安全。

线程: 线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。线程间通信主要通过共享内存,上下文切换很快,资源开销较少,但相比进程不够稳定容易丢失数据。

协程: 协程是一种用户态的轻量级线程,协程的调度完全由用户控制。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。

  1. 进程与线程比较:

    • 进程用于独立的地址空间 ;线程依附于进程(先有进程后有线程) ,可以共享进程的地址空间

    • 进程之间不共享全局变量 , 线程之间共享全局变量

    • 线程是cpu 调度的基本单位; 进程是操作系统分配资源资源的最小单位

    • 进程之间相互独立 ,都可并发执行 (核数大于线程数)

    • 多进程运行其中某个进程挂掉不会影响其他进程运行, 多线程开发中当前进程挂掉 依附于当前进程中的多线程进行销毁

  2. 线程与协程比较

    • 一个线程可包含多个协程 ,一个进程也可单独拥有多个协程

    • 线程、进程 同步机制 ,协程异步

    • 协程保留最近一次调用时状态,每次过程重入相当于唤醒

    • 线程的切换由操作系统负责调度,协程由用户自己进行调度

    • 资源消耗:线程的默认Stack大小是1M,而协程更轻量,接近1K。

6000字Locust入门详解-LMLPHP

线程: 轻量级的进程 协程: 轻量级的线程 (用户态)

更多查阅:

1. 更多命令

如果Locust文件位于与locustfile.py在不同的子目录/或者文件名不一样,则使用参数-f+文件名:
$ locust -f locust_files/my_locust_file.py
要在多个进程中运行Locust,我们可以通过指定--master:
$ locust -f locust_files/my_locust_file.py --master
启动任意数量的从属进程:
$ locust -f locust_files/my_locust_file.py --slave
如果要在多台机器上运行Locust,则在启动从属服务器时还必须指定主服务器主机(在单台计算机上运行Locust时不需要,因为主服务器主机默认为127.0.0.1):
$ locust -f locust_files/my_locust_file.py --slave --master-host=192.168.0.100
还可以在配置文件(locust.conf或~/.locust.conf)或以LOCUST_前缀的env vars中设置参数
例如:(这将与上一个命令执行相同的操作)
$ LOCUST_MASTER_HOST=192.168.0.100 locust

注意:要查看所有可用选项,请键入:locust —help

2. 学习路线

https://docs.locust.io/en/stable/what-is-locust.html

3. WebSocket与HTTP的关联和差异

相同:

  1. 建立在TCP之上,通过TCP协议来传输数据。
  2. 都是可靠性传输协议
  3. 都是应用层协议。

不同:

  1. WebSocket是HTML5中的协议,支持持久连接,HTTP不支持持久连接
  2. HTTP是单向协议,只能由客户端发起,做不到服务器主动向客户端推送信息

4. 延伸阅读&知识库

参考资料:https://github.com/locustio/locust/wiki/Articles

6000字Locust入门详解-LMLPHP

06-19 23:26