一、为什么Locust自带的Web界面不够用

用Locust做压力测试的同学应该都有过这样的体验:测试跑完之后打开Web界面,页面上只显示了请求次数、响应时间、失败率这些基础指标。但是,你真正关心的业务指标呢?比如用户下单成功了几笔、库存扣减耗时多少、第三方接口回调成功率如何——这些统统看不到。

这不是你用法不对,而是Locust的设计定位决定的。它本质上是一个通用的HTTP压力测试工具,自带的Web界面展示的是通用的网络层指标。当你想追踪业务层的自定义数据时,就必须自己动手扩展。

这篇文章就来手把手教大家,怎么在Locust里面加上自定义指标的采集,并且让这些数据能直接在Web界面上展示出来。整个过程不需要改动Locust的源码,只需要写几行代码就能搞定。

二、深入理解Locust的指标采集机制

2.1 Locust的指标体系是怎么回事

要搞明白怎么加自定义指标,首先得搞清楚Locust现有的指标是怎么工作的。简单说一下,Locust内部维护着一套统计系统,每次发请求都会往里面写数据,Web界面定期从这套系统里拉数据展示。

Locust提供了一个叫做events的机制,相当于一个事件总线。在测试运行过程中,各种事件会往这个总线上发,任何模块都可以订阅这些事件。这就给了我们一个口子——我们可以在这个总线上塞进自己的自定义事件。

2.2 事件总线的工作原理

事件总线就像是快递站的包裹分拣系统。发件人把包裹(事件)扔进去,标注好类型,分拣系统就会把对应类型的包裹送到对应的收件人(处理器)手里。

在Locust里,常见的事件类型包括:REQUEST(请求事件)、LOGIN(登录事件)、CUSTOM(自定义事件)等。我们要用的就是CUSTOM这个类型,它是专门留给用户扩展的。

技术栈:Python

# 技术栈:Python
# 文件:custom_events_demo.py
# 演示Locust事件总线的基本用法

from locust import events, task, FastHttpUser
from time import sleep
import random

# 定义一个事件处理函数,监听Locust的CUSTOM事件
# 每次有自定义事件发出时,这个函数就会被调用
@events.custom.add_listener
def handle_custom_event(environment, **kwargs):
    """
    处理自定义事件
    :param environment: Locust的运行环境对象
    :param kwargs: 事件中携带的所有数据,键值对形式
    """
    # 打印事件名称和携带的数据,方便调试
    print(f"[自定义事件捕获] 名称: {kwargs.get('name')}, 值: {kwargs.get('value')}, 额外数据: {kwargs.get('extra')}")

# 定义用户行为类
class MyUser(FastHttpUser):
    """模拟用户的测试类"""
    
    # 等待时间设置:最小5秒,最大15秒
    wait_time = lambda self: random.uniform(5, 15)
    
    @task
    def my_task(self):
        """
        测试任务:模拟一个业务流程
        """
        # 模拟发送一个请求
        start_time = time.time()
        self.client.get("/api/orders")
        elapsed = time.time() - start_time
        
        # 发出一个自定义事件,告诉Locust这次业务操作的结果
        # 注意:发事件要用 events.custom.fire()
        events.custom.fire(
            name="业务处理耗时",       # 自定义指标名称
            value=elapsed,             # 指标数值
            unit="秒",                  # 单位
            extra={"order_id": "12345"} # 额外信息(可选)
        )

2.3 指标数据在内部是怎么存储的

Locust内部有一个StatsEntry类,每个指标都对应一个StatsEntry实例。这个实例里记录了请求次数、平均响应时间、最大响应时间、最小响应时间、失败次数等等。

这些StatsEntry实例被存放在一个叫做RequestStats的对象里面,可以理解为一张大的统计表。Web界面就是通过访问这张表来获取数据的。

技术栈:Python

# 技术栈:Python
# 文件:stats_inspection.py
# 演示如何查看Locust内部的统计数据

from locust.stats import RequestStats, StatsEntry
import time

# 创建一个新的统计数据对象
stats = RequestStats()

# 向统计表里添加一条记录,模拟一次请求
# total_count: 总请求数
# num_failures: 失败次数
# total_response_time: 总响应时间(毫秒)
# 返回值是当前的StatsEntry对象
stats_entry = stats.log_request(
    method="GET",           # HTTP方法
    name="/api/orders",     # 接口名称
    response_time=125.3,    # 本次响应时间(毫秒)
    content_length=512,     # 响应体大小(字节)
    error=None              # 错误信息,None表示成功
)

# 获取统计对象的摘要信息
# request_stats属性包含了按请求名分组的所有StatsEntry
print("统计摘要信息:")
print(f"总请求数: {stats.total.request_count}")       # 总请求次数
print(f"平均响应时间: {stats.total.avg_response_time}") # 平均响应时间
print(f"失败请求数: {stats.total.num_failures}")        # 失败次数
print(f"请求总数: {stats.total.num_requests}")          # 请求数(包括成功和失败)

三、自定义指标采集的完整实现方案

3.1 创建自定义统计类

要采集自定义指标,最干净的做法是自己写一个统计类,继承Locust的RequestStats,然后往里面加新字段。不过考虑到兼容性和维护成本,我们这里采用一个更简洁的方案:利用事件总线加一个内存计数器。

这个方案的好处是简单、不容易出错,坏处是你得自己管数据的生命周期。对于大多数场景来说,足够了。

技术栈:Python

# 技术栈:Python
# 文件:custom_stats_collector.py
# 自定义指标采集器的完整实现

import time
import threading
from collections import defaultdict
from locust import events

class CustomStatsCollector:
    """
    自定义指标采集器
    负责收集和统计Locust测试过程中产生的业务级自定义指标
    """
    
    def __init__(self):
        # 线程锁,保证多线程环境下数据读写安全
        self._lock = threading.Lock()
        
        # 用字典存储所有指标的统计数据
        # 结构: { 指标名称: { "total": 总数, "sum": 值之和, "max": 最大值, "min": 最小值, "min_time": 首次记录时间 } }
        self._data = defaultdict(lambda: {
            "total": 0,        # 记录条数
            "sum": 0.0,        # 所有值的总和
            "max": 0.0,        # 最大值
            "min": float("inf"), # 最小值
            "timestamps": []   # 时间戳列表,用于计算趋势
        })
        
        # 注册事件监听器,自动接收发来的自定义事件
        events.custom.add_listener(self._on_custom_event)
    
    def _on_custom_event(self, environment, **kwargs):
        """
        事件回调函数:每次收到自定义事件时自动调用
        """
        name = kwargs.get("name", "unnamed")   # 指标名称
        value = kwargs.get("value", 0)          # 指标值
        extra = kwargs.get("extra", {})         # 额外数据
        
        with self._lock:
            # 获取或创建该指标的统计记录
            entry = self._data[name]
            entry["total"] += 1                 # 计数加1
            entry["sum"] += float(value)        # 累加总值
            entry["max"] = max(entry["max"], float(value))  # 更新最大值
            entry["min"] = min(entry["min"], float(value))  # 更新最小值
            entry["timestamps"].append(time.time())  # 记录时间戳
            
            # 如果时间戳太多,只保留最近1000条,防止内存爆炸
            if len(entry["timestamps"]) > 1000:
                entry["timestamps"] = entry["timestamps"][-1000:]
    
    def get_stats(self):
        """
        获取所有自定义指标的统计汇总
        :return: 一个字典,包含每个指标的详细统计数据
        """
        result = {}
        with self._lock:
            for name, data in self._data.items():
                result[name] = {
                    "count": data["total"],
                    "avg": data["sum"] / data["total"] if data["total"] > 0 else 0,
                    "max": data["max"],
                    "min": data["min"] if data["min"] != float("inf") else 0,
                    "total_sum": data["sum"]
                }
        return result
    
    def reset(self):
        """清空所有统计数据,开始新一轮统计"""
        with self._lock:
            self._data.clear()

# 全局实例,测试脚本中直接引用这个对象
custom_stats = CustomStatsCollector()

3.2 在测试脚本中使用自定义指标

采集器建好了,接下来就是在测试脚本里用它。每次你的业务操作完成后,发一个事件就行。

技术栈:Python

# 技术栈:Python
# 文件:locustfile.py
# 完整的Locust测试脚本,包含自定义指标采集

import random
from time import sleep
from locust import FastHttpUser, task, between, events
from custom_stats_collector import custom_stats  # 导入自定义采集器

class ECommerceUser(FastHttpUser):
    """
    电商场景压力测试用户
    模拟用户浏览商品、加入购物车、下单的完整流程
    """
    
    # 用户请求间隔时间:5秒到20秒之间随机
    wait_time = between(5, 20)
    
    @task(3)  # 权重3,表示这个任务被选中的概率是其他的3倍
    def browse_products(self):
        """浏览商品列表,这个任务执行频率较高"""
        # 记录开始时间,用来算接口耗时
        start = time.time()
        
        response = self.client.get(
            "/api/products",
            params={"page": random.randint(1, 10), "size": 20},
            catch_response=True  # 开启响应捕获,可以手动判定成功或失败
        )
        
        # 计算接口耗时(毫秒)
        api_cost = (time.time() - start) * 1000
        
        # 发送自定义指标事件:记录商品列表接口的响应时间
        events.custom.fire(
            name="商品列表接口耗时",
            value=round(api_cost, 2),
            unit="ms"
        )
        
        # 检查HTTP状态码
        if response.status_code != 200:
            response.failure("商品列表接口返回异常状态码: %d" % response.status_code)
        
        # 如果商品列表有数据,模拟点击进入详情页
        if len(response.json().get("data", [])) > 0:
            product_id = response.json()["data"][0]["id"]
            self._view_product_detail(product_id)
    
    @task(1)  # 权重1,低频任务
    def add_to_cart(self):
        """加入购物车操作"""
        start = time.time()
        
        product_id = random.randint(1, 1000)
        quantity = random.randint(1, 5)
        
        response = self.client.post(
            "/api/cart",
            json={"product_id": product_id, "quantity": quantity},
            catch_response=True
        )
        
        # 发送自定义指标:购物车操作耗时
        cart_cost = (time.time() - start) * 1000
        events.custom.fire(
            name="购物车操作耗时",
            value=round(cart_cost, 2),
            unit="ms"
        )
        
        # 手动判定成功或失败
        if response.status_code == 200 and response.json().get("success"):
            response.success()  # 标记为成功
        else:
            response.failure("加入购物车失败: %s" % response.text[:200])
    
    @task(1)  # 权重1,低频任务
    def place_order(self):
        """
        下单操作——核心业务,需要采集更多细粒度的指标
        """
        start_total = time.time()
        
        # 第一步:校验库存
        inventory_start = time.time()
        self.client.get("/api/inventory/check", params={"sku": "SKU-2024-001"})
        inventory_cost = (time.time() - inventory_start) * 1000
        events.custom.fire(
            name="库存校验耗时",
            value=round(inventory_cost, 2),
            unit="ms"
        )
        
        # 第二步:创建订单
        order_start = time.time()
        response = self.client.post(
            "/api/orders",
            json={
                "items": [{"product_id": 101, "quantity": 2}],
                "shipping_address": "北京市朝阳区xxx路xxx号"
            },
            catch_response=True
        )
        order_cost = (time.time() - order_start) * 1000
        
        # 发送自定义指标:订单创建耗时
        events.custom.fire(
            name="订单创建耗时",
            value=round(order_cost, 2),
            unit="ms"
        )
        
        # 第三步:支付处理
        if response.status_code == 200:
            order_id = response.json().get("order_id")
            payment_start = time.time()
            self.client.post("/api/payment", json={"order_id": order_id})
            payment_cost = (time.time() - payment_start) * 1000
            events.custom.fire(
                name="支付处理耗时",
                value=round(payment_cost, 2),
                unit="ms"
            )
        
        # 第四步:记录整个下单流程的总耗时
        total_cost = (time.time() - start_total) * 1000
        events.custom.fire(
            name="下单流程总耗时",
            value=round(total_cost, 2),
            unit="ms"
        )
        
        if response.status_code != 200:
            response.failure("下单失败")
    
    def _view_product_detail(self, product_id):
        """查看商品详情页(内部方法)"""
        start = time.time()
        response = self.client.get(
            f"/api/products/{product_id}",
            catch_response=True
        )
        detail_cost = (time.time() - start) * 1000
        events.custom.fire(
            name="商品详情接口耗时",
            value=round(detail_cost, 2),
            unit="ms"
        )
        if response.status_code != 200:
            response.failure("商品详情接口异常")


# 测试结束时,打印自定义统计摘要
@events.test_stop.add_listener
def print_custom_summary(environment, **kwargs):
    """测试结束钩子:打印所有自定义指标的汇总数据"""
    print("\n" + "=" * 60)
    print("自定义指标统计汇总")
    print("=" * 60)
    for name, stats in custom_stats.get_stats().items():
        print(f"【{name}】")
        print(f"  采集次数: {stats['count']}")
        print(f"  平均值:   {stats['avg']:.2f}ms")
        print(f"  最大值:   {stats['max']:.2f}ms")
        print(f"  最小值:   {stats['min']:.2f}ms")
        print(f"  总耗时:   {stats['total_sum']:.2f}ms")
        print()

3.3 扩展Web界面展示自定义指标

采集到了数据,下一步就是让这些数据在Locust的Web界面上显示出来。Locust的Web界面是Flask应用,它支持自定义页面路由。我们可以加一个新的路由,返回一个JSON接口,然后在界面上显示。

技术栈:Python

# 技术栈:Python
# 文件:web_extension.py
# 扩展Locust Web界面,展示自定义指标

from locust import events
from flask import jsonify, render_template_string
from custom_stats_collector import custom_stats
import json

# 自定义页面的HTML模板
CUSTOM_STATS_TEMPLATE = """
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>自定义指标监控 - Locust</title>
    <style>
        body { font-family: Arial, sans-serif; margin: 20px; background: #f5f5f5; }
        .container { max-width: 1200px; margin: 0 auto; }
        h1 { color: #333; border-bottom: 2px solid #4CAF50; padding-bottom: 10px; }
        .stats-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 16px; }
        .stat-card { background: white; border-radius: 8px; padding: 20px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
        .stat-name { font-size: 16px; font-weight: bold; color: #4CAF50; margin-bottom: 12px; }
        .stat-row { display: flex; justify-content: space-between; margin: 8px 0; font-size: 14px; }
        .stat-label { color: #666; }
        .stat-value { font-weight: bold; color: #333; }
        .refresh-btn { margin: 20px 0; padding: 10px 24px; background: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; }
        .refresh-btn:hover { background: #45a049; }
        .refresh-info { font-size: 12px; color: #999; margin-left: 12px; }
    </style>
</head>
<body>
    <div class="container">
        <h1>📊 自定义指标监控面板</h1>
        <div>
            <button class="refresh-btn" onclick="refreshStats()">🔄 刷新数据</button>
            <span class="refresh-info">数据每5秒自动刷新一次</span>
        </div>
        <div class="stats-grid" id="stats-container">
            <!-- JavaScript动态填充内容 -->
        </div>
    </div>

    <script>
        // 页面加载时自动刷新
        document.addEventListener('DOMContentLoaded', function() {
            refreshStats();
            // 每5秒自动刷新一次
            setInterval(refreshStats, 5000);
        });

        // 从后端API获取统计数据并渲染
        async function refreshStats() {
            try {
                const response = await fetch('/custom-stats');
                const data = await response.json();
                renderStats(data);
            } catch (error) {
                document.getElementById('stats-container').innerHTML = '<p style="color:red;">数据获取失败: ' + error.message + '</p>';
            }
        }

        // 渲染统计卡片
        function renderStats(stats) {
            const container = document.getElementById('stats-container');
            if (Object.keys(stats).length === 0) {
                container.innerHTML = '<p style="grid-column: 1/-1; text-align:center; color:#999; padding:40px;">暂无自定义指标数据</p>';
                return;
            }
            let html = '';
            for (const [name, stat] of Object.entries(stats)) {
                html += `
                <div class="stat-card">
                    <div class="stat-name">${name}</div>
                    <div class="stat-row"><span class="stat-label">采集次数</span><span class="stat-value">${stat.count}</span></div>
                    <div class="stat-row"><span class="stat-label">平均值</span><span class="stat-value">${stat.avg.toFixed(2)} ms</span></div>
                    <div class="stat-row"><span class="stat-label">最大值</span><span class="stat-value">${stat.max.toFixed(2)} ms</span></div>
                    <div class="stat-row"><span class="stat-label">最小值</span><span class="stat-value">${stat.min.toFixed(2)} ms</span></div>
                    <div class="stat-row"><span class="stat-label">总耗时</span><span class="stat-value">${stat.total_sum.toFixed(2)} ms</span></div>
                </div>
                `;
            }
            container.innerHTML = html;
        }
    </script>
</body>
</html>
"""

@events.init.add_listener
def add_custom_web_routes(environment, **kwargs):
    """
    Locust启动时注册自定义Web路由
    这个函数在Locust的Web服务器初始化时被调用
    """
    
    # 获取Flask应用对象
    app = environment.runner.web_ui if environment.runner else environment._web_ui
    if app is None:
        return
    
    # 注册自定义统计数据的API接口
    @app.route('/custom-stats')
    def custom_stats_api():
        """返回所有自定义指标的JSON数据"""
        return jsonify(custom_stats.get_stats())
    
    # 注册自定义统计面板页面
    @app.route('/custom-stats-panel')
    def custom_stats_panel():
        """渲染自定义指标展示页面"""
        return render_template_string(CUSTOM_STATS_TEMPLATE)

3.4 整合所有模块运行

把上面的几个文件整合起来,用一条命令就能启动完整的测试:

技术栈:Python

# 技术栈:Bash命令行
# 启动Locust测试,带上自定义指标采集和Web界面扩展

# 基本启动命令
# --headless 表示无头模式(不弹浏览器),如果需要浏览器交互可以去掉
# --users 指定并发用户数
# --spawn-rate 每秒启动的用户数
# --run-time 测试持续时间
# --host 被测服务的地址

locust -f locustfile.py \
    --headless \
    --users 50 \
    --spawn-rate 10 \
    --run-time 5m \
    --host http://your-server.com

# 如果想看到Web界面,去掉 --headless 参数,然后浏览器打开 http://localhost:8089
# 自定义面板地址:http://localhost:8089/custom-stats-panel

如果不想用命令行参数,也可以在代码里配置,这样更方便团队协作:

技术栈:Python

# 技术栈:Python
# 文件:settings.py
# Locust全局配置

# 测试目标主机
HOST = "http://your-server.com"

# 并发用户数
NUM_USERS = 50

# 每秒启动的用户数(控制压力增长的速度)
SPAWN_RATE = 10

# 测试持续时间(5分钟)
RUN_TIME = "5m"

# 是否显示HTML格式的统计报告
HTML_STATS = True

技术栈:Python

# 技术栈:Python
# 文件:main.py
# 程序入口:整合所有模块

import locust
from locust import events
from settings import HOST, NUM_USERS, SPAWN_RATE, RUN_TIME
from custom_stats_collector import custom_stats
from web_extension import add_custom_web_routes

# 注册自定义Web路由
events.init.add_listener(add_custom_web_routes)

# 注册测试结束时的数据清理
@events.test_stop.add_listener
def cleanup(environment, **kwargs):
    """测试结束后清理资源"""
    # 将统计数据导出到文件,方便后续分析
    stats = custom_stats.get_stats()
    with open("custom_stats_export.json", "w", encoding="utf-8") as f:
        json.dump(stats, f, ensure_ascii=False, indent=2)
    print("自定义指标已导出到 custom_stats_export.json")

# 启动Locust
# 使用环境变量传入配置,便于CI/CD集成
if __name__ == "__main__":
    import os
    os.environ["LOCUST_HOST"] = HOST
    os.environ["LOCUST_USERS"] = str(NUM_USERS)
    os.environ["LOCUST_SPAWN_RATE"] = str(SPAWN_RATE)
    os.environ["LOCUST_RUN_TIME"] = RUN_TIME
    locust.main()

四、进阶:把自定义指标接入Prometheus

上面我们做的指标展示只是一个简单的HTML页面,如果你需要和现有的监控体系(比如Grafana)对接,那就要把指标数据以Prometheus格式暴露出来。

Locust其实已经内置了对Prometheus的支持,我们只需要让自定义指标也走Prometheus的格式就行。

技术栈:Python

# 技术栈:Python
# 文件:prometheus_export.py
# 将自定义指标以Prometheus格式暴露

from prometheus_client import Counter, Histogram, Gauge, start_http_server
from locust import events

# 创建Prometheus指标对象
# Counter: 只增不减的计数器(比如请求总数)
custom_request_total = Counter(
    "locust_custom_request_total",
    "自定义指标累计值",
    ["name"]  # 标签维度
)

# Histogram: 直方图,用于记录耗时分发
custom_response_time = Histogram(
    "locust_custom_response_time_seconds",
    "自定义指标响应时间分布(秒)",
    ["name"],
    buckets=(0.1, 0.5, 1.0, 2.0, 5.0, 10.0, 30.0, 60.0)
)

# Gauge: 可以是任意值的指标(比如当前在线用户数)
custom_gauge_metric = Gauge(
    "locust_custom_gauge",
    "自定义Gauge指标",
    ["name"]
)

@events.init.add_listener
def start_prometheus_server(environment, **kwargs):
    """启动Prometheus指标暴露服务"""
    # 在9090端口启动Prometheus的metrics端点
    start_http_server(9090)
    print("Prometheus指标服务已启动,访问 http://localhost:9090/metrics")

@events.custom.add_listener
def push_to_prometheus(environment, **kwargs):
    """
    将自定义事件数据推送到Prometheus
    注意:Prometheus的Counter和Histogram的标签(labels)数量有限
    """
    name = kwargs.get("name", "unknown")
    value = kwargs.get("value", 0)
    
    # 更新Counter
    custom_request_total.labels(name=name).inc()
    
    # 更新Histogram(传入耗时,单位是秒)
    if isinstance(value, (int, float)):
        custom_response_time.labels(name=name).observe(float(value) / 1000.0)

对应的Grafana中可以用PromQL查询:

技术栈:Text(PromQL查询示例)

# 技术栈:PromQL查询语言
# 查询自定义指标的平均响应时间(按名称分组)
rate(locust_custom_response_time_seconds_sum{name="订单创建耗时"}[5m]) / rate(locust_custom_response_time_seconds_count{name="订单创建耗时"}[5m])

# 查询自定义指标的p99响应时间
histogram_quantile(0.99, sum(rate(locust_custom_response_time_seconds_bucket[5m])) by (le, name))

# 查询自定义指标的总请求次数
sum(increase(locust_custom_request_total[1h])) by (name)

Grafana的dashboard配置示例:

技术栈:JSON

{
    "title": "Locust自定义指标监控",
    "panels": [
        {
            "title": "各指标请求次数(每小时)",
            "type": "bar",
            "datasource": "Prometheus",
            "targets": [
                {
                    "expr": "sum(increase(locust_custom_request_total[1h])) by (name)",
                    "legendFormat": "{{name}}"
                }
            ],
            "fieldConfig": {
                "defaults": {
                    "unit": "short",
                    "color": { "mode": "palette-classic" }
                }
            }
        },
        {
            "title": "响应时间P99趋势",
            "type": "timeSeries",
            "targets": [
                {
                    "expr": "histogram_quantile(0.99, sum(rate(locust_custom_response_time_seconds_bucket[5m])) by (le, name))",
                    "legendFormat": "{{name}} - P99"
                },
                {
                    "expr": "histogram_quantile(0.50, sum(rate(locust_custom_response_time_seconds_bucket[5m])) by (le, name))",
                    "legendFormat": "{{name}} - P50"
                }
            ],
            "fieldConfig": {
                "defaults": {
                    "unit": "s",
                    "custom": {
                        "drawStyle": "line",
                        "lineInterpolation": "smooth",
                        "fillOpacity": 10,
                        "spanNulls": true
                    }
                }
            }
        }
    ]
}

五、应用场景分析

这套自定义指标采集方案,在以下几种场景下特别有用:

业务链路追踪场景:电商下单、支付、物流这几种核心流程,每一步都涉及多个微服务调用。光看HTTP层面的状态码和响应时间根本不够,你需要知道"库存校验花了多少毫秒"、"订单服务调用了多少次物流服务"这种细粒度的信息。

性能瓶颈定位场景:测试跑完之后,整体P95响应时间是2秒,但到底慢在哪里?是数据库查询慢、是第三方接口慢、还是你自己的业务逻辑有性能问题?自定义指标能把每个环节拆开来看。

容量规划场景:在扩容决策的时候,光看请求量是不够的。你需要知道不同业务模块的负载差异,比如"商品搜索接口QPS是1000,订单创建接口QPS只有100,但后者是前者的50倍耗时",这种信息才能指导你怎么分配资源。

回归测试对比场景:每次上线新版本后,除了看有没有bug,还要看性能有没有退化。有了自定义指标,你可以精确对比"上线前商品详情接口平均耗时120ms,上线后变成180ms",及时发现性能问题。

六、技术优缺点对比

说完了好处,也得说说这套方案有什么坑。

优点方面:

第一,非侵入式。不需要改Locust源码,不需要fork项目,只需要加几个Python文件就能实现。升级Locust版本的时候也不用担心兼容性问题。

第二,灵活度很高。采集什么指标、怎么采集,完全由你决定。可以按业务需要动态调整,不需要改框架。

第三,和现有工具链兼容。Prometheus和Grafana都是行业标准,接入之后可以和现有的监控告警体系打通。

缺点方面:

第一,内存开销。自定义采集器在内存里维护统计数据,如果指标种类太多或者采集频率太高,内存占用会增长。好在我们在采集器里加了1000条时间戳的上限保护,一般不会出问题。

第二,没有内置持久化。我们方案里的数据都是存在内存里的,测试结束后如果不主动导出就会丢失。如果需要长期存储,得配合Prometheus或者自己写数据库持久化逻辑。

第三,Web界面扩展有限。Locust原生的Web界面是Flask应用,我们加的路由能工作,但样式和原生界面不太统一,看起来会有点割裂。

七、注意事项

线程安全问题:Locust是多线程的,多个线程可能同时发事件。采集器里用threading.Lock做了保护,但如果你自己写采集逻辑,一定要记得加锁。不加锁的话,统计数据可能会出现脏读、数据不一致的问题。

指标名称命名规范:自定义指标的名称建议用中文或者带下划线的英文,避免使用特殊字符。名称会作为Prometheus的label值,特殊字符可能导致Prometheus解析失败。推荐格式:模块名_操作类型_度量单位,比如订单_创建耗时_ms。

性能开销:每次发事件都会触发一次事件总线的分发,虽然开销很小(微秒级别),但在超高QPS场景下(每秒几万请求以上)累积起来也是可观的。如果性能敏感,可以考虑批量上报,比如攒够100条再统一推送。

清理机制:Locust的事件监听器在测试过程中是持续有效的,但如果你多次启动测试(比如在同一次locust运行中重复start/stop),自定义采集器的数据可能会累积。建议在测试开始时调用reset()方法清空旧数据。

环境变量注入:在CI/CD环境中,通过环境变量传入配置比硬编码更灵活。Locust支持用LOCUST_HOST、LOCUST_USERS等环境变量覆盖命令行参数,自定义采集器也可以读取环境变量来控制行为。

八、文章总结

回到开头的问题:Locust的Web界面信息不全怎么办?

答案是,别指望Locust帮你展示所有东西,把自定义指标的采集和展示交给自己写代码去解决。核心思路就是三步:

第一步,用events.custom.fire()发事件,把你在测试脚本里计算出来的业务指标往事件总线上扔。

第二步,写一个采集器类,监听事件总线上的数据,做统计和汇总。

第三步,通过自定义Flask路由把数据展示在Web界面上,或者推送到Prometheus让Grafana来展示。

整个方案的关键在于利用Locust的事件总线机制。这个机制设计得很干净,不需要深入改源码就能实现扩展,而且和Locust的版本更新解耦。

如果你的测试场景很简单,只需要跑个接口看状态码和响应时间,那Locust原生功能就够用了。但如果你需要追踪业务级别的指标——耗时分布、成功率、调用次数、业务流水号——那就必须动手加自定义采集。

最后给个建议:如果你的项目是正经的产品级压测,建议从一开始就把自定义指标采集的框架搭好,不要等测试跑了几次觉得"好像还差点什么"才想起加。指标采集越早集成,后续的分析价值越大。