1. 项目概述:为什么是Python 3.11与Locust?

最近在做一个新项目的性能摸底,团队里有人提了一嘴:“要不还用老办法,写个脚本跑跑?”我直接给否了。脚本压测不是不行,但数据不准、场景模拟不真实、报告简陋,最后性能瓶颈定位全靠猜,这种苦头吃过太多次了。这次我坚持要用专业的压测工具,目标很明确:要能模拟真实用户行为,要能产生清晰直观的报告,还要能方便地做分布式压测,把服务端的真实负载能力给“榨”出来。

选型过程没花太多时间,JMeter功能强大但略显笨重,写个复杂逻辑还得折腾BeanShell;Gatling基于Scala,学习曲线对团队不太友好。一圈看下来, Locust 再次进入了视野。它是一个用Python编写的开源负载测试工具,最大的特点就是测试场景完全用代码(Python)来定义,这给了我们极大的灵活性。你可以像写业务逻辑一样,去定义用户登录、浏览商品、下单等一系列复杂操作,而不是在UI上拖拽组件。而且,它的分布式压测能力是原生支持的,扩展起来非常方便。

但这次,我决定玩点不一样的: 基于Python 3.11来部署和调优Locust 。你可能要问,Python 3.8、3.9不也能用吗?没错,但3.11有个“杀手锏”——显著的性能提升。官方数据显示,Python 3.11比3.10平均快了25%左右。这意味着什么?意味着同样一台压测机,用Python 3.11来跑Locust的压测脚本,其本身作为“压力发生器”的开销更小,能腾出更多的CPU和内存资源去模拟更多用户(即“蝗虫”),或者更准确地说,在模拟同等数量用户时,数据更精确,对被测系统的压力波形更“干净”。这对于我们精准定位性能瓶颈至关重要。毕竟,我们不希望因为压测工具自身的性能瓶颈,而错误地判断了被测服务的性能。

所以,这个案例的核心,就是围绕 “Python 3.11” “Locust” 这两个关键词,从零开始搭建一个高性能、易扩展的压测环境,并分享在部署、脚本编写、执行和结果分析全链路中,那些真正影响效率和准确性的调优点。无论你是刚接触性能测试的新手,还是想优化现有压测体系的老手,这篇从实战中踩坑总结出来的经验,应该都能给你一些直接的参考。

2. 环境部署:打造高性能压测基地

压测工具自身的运行环境是否稳定、高效,是决定测试结果可信度的第一步。一个配置不当的环境,可能会引入额外的延迟或资源竞争,导致“测不准”。我们的目标是搭建一个纯净、高性能的Locust运行环境。

2.1 Python 3.11的安装与隔离

我强烈建议不要使用系统自带的Python,也不要随意用 sudo pip 安装包。最佳实践是使用 虚拟环境 进行隔离。这里我推荐使用 pyenv 来管理多个Python版本,并用 venv 创建项目专属环境。

首先,安装Python 3.11。如果你使用 pyenv ,操作非常简洁:

# 安装pyenv(如果未安装)
curl https://pyenv.run | bash
# 将pyenv初始化命令添加到shell配置文件中,如 ~/.bashrc 或 ~/.zshrc
echo 'export PATH="$HOME/.pyenv/bin:$PATH"' >> ~/.bashrc
echo 'eval "$(pyenv init -)"' >> ~/.bashrc
echo 'eval "$(pyenv virtualenv-init -)"' >> ~/.bashrc
source ~/.bashrc

# 安装Python 3.11
pyenv install 3.11.9  # 建议指定一个具体的次版本号,如3.11.9
pyenv global 3.11.9   # 或仅在当前目录使用 pyenv local 3.11.9

如果你在纯净的Linux服务器上,也可以直接通过包管理器安装,例如在Ubuntu 22.04或更新版本上:

sudo apt update
sudo apt install software-properties-common -y
sudo add-apt-repository ppa:deadsnakes/ppa -y
sudo apt update
sudo apt install python3.11 python3.11-venv python3.11-dev -y

这里多安装了 python3.11-dev ,是为了后续某些Python包(如 gevent ,Locust的并发基础)可能需要编译原生扩展。

安装完成后,为我们的压测项目创建一个独立的虚拟环境:

# 进入你的项目目录
cd /path/to/your/locust-project
# 使用Python3.11创建虚拟环境
python3.11 -m venv venv
# 激活虚拟环境
source venv/bin/activate

激活后,你的命令行提示符前通常会显示 (venv) ,表示你正处在这个隔离的环境中。之后所有的 pip install 操作都只影响这个环境。

注意 :生产环境的压测机建议使用Linux系统。Windows虽然也能运行,但在高并发网络处理和资源调度上,Linux通常表现更稳定、更可预测。如果你必须在Windows上操作,可以使用WSL2(Windows Subsystem for Linux)来获得接近Linux的体验。

2.2 Locust及其依赖的精准安装

环境准备好后,安装Locust。但别急着一个 pip install locust 就完事。为了获得最佳性能和避免潜在的依赖冲突,我建议进行更精细的控制。

首先,升级 pip setuptools 到最新版本,这能保证包安装过程更顺畅:

pip install --upgrade pip setuptools wheel

接下来,安装Locust。这里有个小技巧:直接 pip install locust 会安装最新版,但有时最新版可能包含我们不想要的实验性功能或未知的Bug。对于生产压测,我倾向于选择一个经过一段时间检验的稳定版本。同时,我们明确指定不安装GUI依赖( locust 包默认会安装),因为我们很可能在无界面的服务器上以 --headless 模式运行。

# 安装一个特定的稳定版本,例如2.20.0,并排除GUI
pip install "locust==2.20.0" --no-deps
# 然后单独安装其核心依赖
pip install gevent>=22.10.2 psutil>=5.9.5 flask>=2.3.3 werkzeug>=2.3.7 msgpack>=1.0.5

--no-deps 参数告诉pip先不安装Locust声明的依赖,然后我们手动安装指定版本的依赖。这样做的好处是,我们可以控制一些关键依赖的版本。例如 gevent 是一个基于协程的高性能网络库,是Locust高并发的基石,手动指定一个较新且稳定的版本有助于提升性能。

安装完成后,验证一下:

locust --version

如果正确显示版本号(如 locust 2.20.0 ),说明安装成功。

2.3 压测脚本结构与基础模板

Locust的测试逻辑全部写在一个Python文件中。一个结构清晰的脚本是高效压测的开始。下面是一个最基础的模板,我称之为 locustfile.py

from locust import HttpUser, task, between, events
import logging
import time

# 可选的初始化钩子,在所有测试开始前运行一次
@events.init.add_listener
def on_locust_init(environment, **kwargs):
    logging.info("Locust测试初始化完成。目标主机: %s", environment.host)

class QuickstartUser(HttpUser):
    """
    模拟一个快速启动用户的行为。
    HttpUser是Locust内置的用于HTTP测试的用户类。
    """
    # 用户在每个任务执行后,等待1到3秒(均匀分布)
    wait_time = between(1, 3)

    # 当用户开始运行时执行(只执行一次),常用于登录等初始化操作
    def on_start(self):
        # 示例:登录并获取token
        # response = self.client.post("/login", json={"username":"foo", "password":"bar"})
        # self.token = response.json().get("token")
        pass

    # 当用户停止运行时执行(只执行一次)
    def on_stop(self):
        pass

    # @task装饰器定义了一个任务,权重默认为1
    @task
    def get_index_page(self):
        # self.client是HttpUser内置的请求客户端,用法类似requests
        with self.client.get("/", catch_response=True, name="01_获取首页") as response:
            # 自定义响应验证:状态码为200且响应时间小于2秒才算成功
            if response.status_code == 200 and response.elapsed.total_seconds() < 2:
                response.success()
            else:
                response.failure(f"状态码异常或响应过慢: {response.status_code}, 耗时: {response.elapsed.total_seconds()}s")

    # 权重为3,表示这个任务被执行的频率是get_index_page的3倍
    @task(3)
    def view_items(self):
        for item_id in range(10):
            # 注意:这里每个item_id的请求在统计中会被分开,不利于聚合分析
            # 更好的做法是使用参数化,见后续章节
            self.client.get(f"/item?id={item_id}", name="02_查看商品")
            time.sleep(0.5) # 模拟用户思考时间

这个模板包含了几个关键部分:

  1. 用户类(QuickstartUser) :代表一类虚拟用户的行为模式。
  2. 等待时间(wait_time) :定义用户执行完一个任务后到下一个任务开始前的等待时间,用于模拟真实用户的思考或浏览间隔。 between 是均匀分布,还有 constant (固定间隔)等。
  3. 生命周期钩子(on_start/on_stop) :用于模拟用户登录/登出。
  4. 任务(@task) :核心,定义用户具体做什么。权重值决定了任务执行的相对概率。
  5. 请求与断言 :使用 self.client 发起请求,并通过 catch_response success()/failure() 自定义成功/失败条件,这是Locust比很多工具强大的地方,你可以定义业务层面的失败(如响应里某个字段不符合预期)。

把这个文件保存到你的项目根目录,命名为 locustfile.py (这是默认文件名),一个最简单的压测就准备好了。

3. 核心脚本编写技巧与高级用法

有了基础模板,我们可以开始雕琢压测脚本,使其能模拟更真实、更复杂的业务场景。脚本的质量直接决定了压测场景的真实性和数据的价值。

3.1 参数化与数据驱动测试

上面的模板中, view_items 任务循环请求了10个固定的商品ID,这很不真实。真实场景中,用户访问的商品ID应该是随机的、动态的。我们需要参数化。

方法一:从列表中随机选取 这是最简单的方式,适用于数据量不大、可全部加载到内存的情况。

from locust import task
import random

class ApiUser(HttpUser):
    # 在类级别定义或从文件加载测试数据
    product_ids = [1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008, 1009, 1010]

    @task
    def get_product(self):
        pid = random.choice(self.product_ids)
        # 使用name参数将不同id的请求归类为同一个统计项,非常重要!
        self.client.get(f"/api/product/{pid}", name="/api/product/[id]")

关键在于 name="/api/product/[id]" ,它告诉Locust将所有不同 pid 的请求在统计时归为一类。否则,你会得到 /api/product/1001 /api/product/1002 ...等无数个独立的统计条目,报告会变得无法阅读。

方法二:从文件中循环读取(CSV) 当测试数据量很大(如十万级用户账号)时,需要从文件读取。Locust没有内置的数据驱动机制,但我们可以利用Python轻松实现。

import csv
from locust import task, events
from itertools import cycle

class DataDrivenUser(HttpUser):
    # 在测试初始化时加载数据
    def on_start(self):
        # 假设我们有一个CSV文件,包含username和password
        with open('user_credentials.csv', 'r') as f:
            reader = csv.DictReader(f)
            self.user_list = list(reader)
        # 使用cycle创建一个无限循环的迭代器,当列表用完时会从头开始
        self.user_iter = cycle(self.user_list)

    @task
    def login_and_do_something(self):
        user = next(self.user_iter)
        # 使用获取到的用户数据
        with self.client.post("/login", json=user, catch_response=True, name="用户登录") as resp:
            if resp.status_code == 200:
                resp.success()
                self.token = resp.json().get('token')
                # 后续携带token的请求...
            else:
                resp.failure(f"登录失败: {resp.text}")

这里使用了 itertools.cycle 来循环使用用户数据。注意,在分布式模式下,每个工作进程(worker)都会独立加载并循环这份数据。如果你需要全局唯一且不重复地使用数据(例如模拟10万个独立用户登录一次),就需要更复杂的分布式数据共享机制,比如使用队列服务(Redis),这属于高级话题。

3.2 关联请求与状态保持

很多API请求需要依赖上一个请求的响应结果,最常见的就是登录后的 token 。我们需要在用户实例间保持这个状态。

class UserWithSession(HttpUser):
    wait_time = between(2, 5)
    token = None

    def on_start(self):
        # 登录,获取token
        resp = self.client.post("/auth/login", json={"email":"test@example.com", "pass":"123456"})
        if resp.status_code == 200:
            self.token = resp.json()['access_token']
            self.headers = {"Authorization": f"Bearer {self.token}"}
        else:
            # 登录失败,可以触发停止,或者标记该用户实例为失败
            logging.error("用户登录失败,停止该用户行为")
            self.stop(force=True) # 强制停止这个用户实例

    @task
    def get_profile(self):
        if not self.token:
            return
        # 在请求头中携带token
        self.client.get("/api/user/profile", headers=self.headers, name="获取用户资料")

    @task
    def create_order(self):
        if not self.token:
            return
        order_data = {"product_id": 123, "quantity": 1}
        with self.client.post("/api/order", json=order_data, headers=self.headers, name="创建订单") as resp:
            if resp.status_code == 201:
                order_id = resp.json()['order_id']
                # 可以基于这个order_id发起后续请求,如支付
                self.client.post(f"/api/order/{order_id}/pay", headers=self.headers, name="订单支付")

这里的关键点是:

  1. token headers 保存为 self.token self.headers ,它们是用户实例的属性,在该用户的所有任务中共享。
  2. on_start 中执行登录,并做好错误处理。登录失败的用户实例应该被停止,避免其继续执行无意义的请求污染数据。
  3. 在每个需要认证的任务中,先检查 token 是否存在。

3.3 自定义客户端与复杂协议支持

Locust默认的 HttpUser 只能测HTTP/HTTPS。如果你的系统使用WebSocket、gRPC、TCP自定义协议呢?Locust的架构是开放的,你可以通过继承 User 类并定义自己的 client 属性来实现。

以WebSocket为例,你可以结合 websocket-client 库:

from locust import User, task, constant
import websocket
import json
import time

class WebSocketUser(User):
    wait_time = constant(1) # 固定等待时间
    host = "ws://your-websocket-server.com"

    def on_start(self):
        # 建立WebSocket连接
        self.ws = websocket.WebSocket()
        try:
            self.ws.connect(self.host)
            self.ws.send(json.dumps({"type": "auth", "token": "xxx"}))
        except Exception as e:
            logging.error(f"WebSocket连接失败: {e}")
            self.stop(force=True)

    @task
    def send_message(self):
        message = {"type": "chat", "content": f"Hello from Locust at {time.time()}"}
        self.ws.send(json.dumps(message))
        # 接收响应(假设服务端会立即回复)
        try:
            response = self.ws.recv()
            # 这里可以解析response并做断言
            # if json.loads(response).get("status") != "ok":
            #     raise Exception("响应异常")
        except Exception as e:
            logging.error(f"接收消息失败: {e}")

    def on_stop(self):
        # 关闭连接
        if self.ws:
            self.ws.close()

通过这种方式,Locust可以扩展到几乎任何基于网络的协议测试。你需要自己处理协议的连接、发送、接收和异常,但并发调度、数据统计和UI展示仍然由Locust强大的核心来负责。

4. 分布式压测部署与执行策略

单机Locust能模拟的用户数受限于CPU、内存和网络端口数(每个用户一个协程,但大量连接会占用文件描述符)。要产生更大的压力,必须进行分布式压测。Locust原生支持Master-Worker架构。

4.1 分布式架构解析

分布式模式下,你需要启动一个 Master 节点和多个 Worker 节点。

  • Master节点 :负责协调测试、分发任务、收集所有Worker的统计数据并在Web UI中展示。它 模拟任何用户。
  • Worker节点 :负责实际执行测试脚本,模拟用户并发请求。它们连接到Master,接收指令,并实时将请求数据发送回Master进行聚合。

所有节点必须能够访问到相同的测试脚本( locustfile.py )及其依赖(如自定义的Python模块或数据文件)。网络互通是必须的。

4.2 启动命令与实战配置

假设我们有三台服务器: master-server (192.168.1.100), worker1 (192.168.1.101), worker2 (192.168.1.102)。

步骤1:在Master节点启动Master进程 master-server 上执行:

cd /path/to/locust-project
source venv/bin/activate
locust -f locustfile.py --master --host=http://your-target-system.com
  • --master :指定以Master模式运行。
  • --host :指定被测系统的基地址。Worker节点会使用这个地址。
  • 默认情况下,Master的Web UI监听在 0.0.0.0:8089 。你可以通过 --web-host --web-port 修改。

步骤2:在每个Worker节点启动Worker进程 worker1 worker2 上分别执行:

cd /path/to/locust-project  # 必须保证脚本路径和内容与Master一致
source venv/bin/activate
locust -f locustfile.py --worker --master-host=192.168.1.100
  • --worker :指定以Worker模式运行。
  • --master-host :指定Master节点的IP地址或主机名。

启动成功后,在Master的Web UI( http://master-server:8089 )上,你会在“Workers”选项卡看到已连接的Worker数量。此时,你就可以像单机模式一样,在UI上设置用户数、生成速率并启动测试了。压力将由所有Worker共同产生。

4.3 无头模式与自动化集成

对于CI/CD流水线或自动化测试,我们不需要Web UI。Locust提供了强大的 无头模式(Headless Mode)

# 单机无头模式
locust -f locustfile.py --headless --users 100 --spawn-rate 10 --run-time 1m --host=http://your-target-system.com

# 分布式无头模式(在Master节点执行)
locust -f locustfile.py --master --headless --expect-workers 2 --users 1000 --spawn-rate 100 --run-time 5m --host=http://your-target-system.com

关键参数:

  • --headless :启用无头模式。
  • --users :要模拟的总用户数(峰值)。
  • --spawn-rate :每秒启动的用户数(爬升速率)。
  • --run-time :测试运行时长,例如 5m (5分钟)、 1h30m
  • --expect-workers (仅Master):在启动测试前,等待指定数量的Worker连接。这确保了所有Worker就绪后才开始施压,避免数据不完整。
  • --csv / --html :可以指定前缀,自动生成CSV格式的数据报告和HTML报告。

自动化集成示例 : 你可以将上述命令写入Shell脚本或CI配置文件(如Jenkinsfile、GitLab CI)。测试结束后,收集生成的CSV和HTML报告,进行分析或归档。还可以结合Locust的 --check-failures --check-rps 等参数,在CI中设置性能通过/失败的质量门禁。

5. 性能调优实战:让压测引擎火力全开

使用Python 3.11和正确的部署方式只是基础。要让Locust这台“压力发生器”本身运行得更高效、更稳定,还需要进行一系列调优。这些调优主要围绕 操作系统 Locust配置 脚本编写 三个层面。

5.1 操作系统级调优

压测机,尤其是Worker节点,会产生大量网络连接。默认的系统限制可能成为瓶颈。

1. 文件描述符限制 每个TCP连接都会消耗一个文件描述符。模拟成千上万的用户,需要提高系统的最大文件描述符数量。

# 查看当前限制
ulimit -n

# 临时提高(对当前会话有效)
ulimit -n 65535

# 永久修改(需要root权限)
# 编辑 /etc/security/limits.conf,在文件末尾添加:
* soft nofile 65535
* hard nofile 65535
# 编辑 /etc/systemd/system.conf 和 /etc/systemd/user.conf,确保有:
DefaultLimitNOFILE=65535
# 重启系统或重新登录后生效。

2. 网络端口范围与TIME_WAIT Locust作为客户端,会使用大量本地端口发起连接。这些连接关闭后,会处于 TIME_WAIT 状态(默认2*MSL,约60秒),占用端口资源。在高并发短连接场景下,可能导致端口耗尽。

# 扩大本地端口范围
sudo sysctl -w net.ipv4.ip_local_port_range="1024 65535"

# 减少TIME_WAIT等待时间(激进,需评估)
sudo sysctl -w net.ipv4.tcp_fin_timeout=30

# 启用TIME_WAIT端口快速回收和重用(对客户端机器很有效)
sudo sysctl -w net.ipv4.tcp_tw_reuse=1
sudo sysctl -w net.ipv4.tcp_tw_recycle=1  # 注意:在Linux 4.12+内核中已移除,新版本无需设置

# 使配置永久生效,将上述行添加到 /etc/sysctl.conf 中

3. 网络缓冲区 增加TCP缓冲区大小可以提升网络吞吐量。

sudo sysctl -w net.core.rmem_max=134217728
sudo sysctl -w net.core.wmem_max=134217728
sudo sysctl -w net.ipv4.tcp_rmem="4096 87380 134217728"
sudo sysctl -w net.ipv4.tcp_wmem="4096 65536 134217728"

5.2 Locust配置与启动参数调优

1. 协程数量限制 Locust基于 gevent ,每个模拟用户是一个 greenlet (协程)。默认情况下,Python进程能创建的线程/协程数量有限(受 threading.stack_size 等影响)。在模拟数万用户时,可能需要调整。 实际上,对于纯协程模式,这个限制通常很高。更常见的瓶颈是前面提到的文件描述符。但如果你遇到 Resource temporarily unavailable 错误,可以尝试在脚本开头设置:

import gevent
# 设置gevent使用的libev后端(某些系统上可能性能更好)
from gevent import monkey
monkey.patch_all()

通常,保持默认即可。真正的用户数上限更多由机器CPU和内存决定。

2. 请求超时与连接池 HttpUser 使用的客户端基于 requests geventhttplib )。默认情况下,每个请求可能新建连接。对于高并发压测同一主机,使用 HTTP连接池 能大幅提升性能。 Locust的 HttpSession (即 self.client )默认会为每个 host 维护一个连接池。但你可以调整其参数,通过自定义客户端实现:

from locust import HttpUser, task
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

class OptimizedUser(HttpUser):
    def on_start(self):
        # 创建一个自定义的Session
        session = requests.Session()
        # 配置重试策略(谨慎使用,压测时通常不希望重试掩盖问题)
        retry = Retry(total=0, connect=0, read=0, redirect=0) # 禁用重试
        adapter = HTTPAdapter(pool_connections=100, pool_maxsize=100, max_retries=retry)
        session.mount('http://', adapter)
        session.mount('https://', adapter)
        # 替换掉默认的client.session
        self.client.session = session

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

这里将连接池大小设置为100。 pool_connections 是缓存的连接池数量, pool_maxsize 是每个连接池的最大连接数。根据你的目标并发数调整。

3. 启动参数优化

  • --skip-log-setup :禁用Locust的日志设置,如果你有自己的日志配置,可以使用此参数避免冲突。
  • --loglevel / --logfile :控制日志级别和输出文件,在生产环境将日志级别设为 WARNING ERROR 可以减少I/O开销。
  • --csv :即使运行无头模式,也建议生成CSV报告,便于后续分析。

5.3 脚本层面的性能陷阱与规避

1. 避免在任务循环中执行耗时或阻塞操作 Locust是单进程、协程模型。如果一个任务中执行了CPU密集型计算或阻塞型I/O(如读写大文件、同步网络请求),会阻塞整个事件循环,导致所有虚拟用户“卡住”。

# 错误示例
@task
def slow_calculation(self):
    result = some_very_slow_python_function() # 同步CPU密集型函数
    self.client.get(f"/api?result={result}")

# 正确做法:将耗时操作移到任务外部,或使用缓存。
# 如果必须执行,考虑将其放到一个单独的线程池中执行,并使用gevent的线程适配。
from gevent.threadpool import ThreadPool
pool = ThreadPool(10) # 一个小的线程池

@task
def slow_calculation(self):
    # 将阻塞函数提交到线程池,避免阻塞协程
    future = pool.spawn(some_very_slow_python_function)
    result = future.get() # 这里会切换协程,不会阻塞事件循环
    self.client.get(f"/api?result={result}")

2. 谨慎使用 time.sleep time.sleep() 是阻塞的,会挂起当前协程。在Locust中,应该使用 gevent.sleep() ,它是非阻塞的。

import gevent

class CorrectUser(HttpUser):
    wait_time = between(1, 3) # Locust内置的wait_time函数已经是gevent友好的

    @task
    def my_task(self):
        self.client.get("/step1")
        gevent.sleep(0.5) # 非阻塞等待,模拟用户思考
        self.client.get("/step2")

3. 减少不必要的响应内容解析 如果你只关心请求的响应时间或状态码,而不需要响应体内容,可以在请求时设置 stream=True ,并立即关闭响应,避免将整个响应体读入内存。

@task
def get_large_file(self):
    # 对于大文件响应,使用stream模式
    response = self.client.get("/download/large.zip", stream=True)
    # 立即关闭连接,不读取内容
    response.close()
    # 只根据状态码判断成功
    if response.status_code == 200:
        response.success()

但注意,这会影响Locust统计的响应大小( Response Size )数据。

6. 监控、结果分析与问题排查

压测执行过程中和结束后,如何解读数据、定位问题,是性能测试的价值所在。

6.1 关键监控指标解读

Locust的Web UI和CSV报告提供了丰富的指标:

  1. RPS(Requests per Second) :每秒请求数。这是衡量系统吞吐量的核心指标。观察其曲线是否平稳,是否达到预期。
  2. 响应时间(Response Times) :重点关注 平均响应时间 中位数(Median) 95/99分位值(95%ile, 99%ile)
    • 平均响应时间 :容易受极端值影响。
    • 中位数 :有一半的请求快于这个值,更能代表“典型”体验。
    • 95/99分位值 :例如95%ile=500ms,意味着95%的请求响应时间在500ms以内。 这是评估用户体验和SLA(服务等级协议)的关键指标 。即使平均响应时间很好,如果99%分位值很高,也意味着有少量用户遭遇了极差的体验。
  3. 失败率(Failures) :任何非2xx/3xx的HTTP状态码,或者在 catch_response 中手动调用 failure() 的请求,都会计入失败。压测过程中失败率应接近0%。突然升高的失败率往往是系统出现瓶颈(如连接池耗尽、数据库死锁)的信号。
  4. 用户数(Number of Users) :当前活跃的模拟用户数。结合RPS和响应时间,可以绘制出系统在不同并发下的性能表现曲线。

6.2 分布式数据聚合与报告生成

在分布式压测中,Master会聚合所有Worker的数据。Web UI展示的是全局视图。但有时你需要更精细的数据,比如每个Worker的负载是否均衡。

Locust的CSV报告默认包含聚合数据。如果你想分析每个Worker,可以在启动Worker时使用 --csv 参数指定不同的前缀,让每个Worker生成独立的CSV文件(但这通常不是好主意,因为Master已经做了聚合)。

更好的做法是: 同时监控压测机(Worker)本身的资源使用情况 。使用 htop nmon prometheus+node_exporter 来监控Worker的CPU、内存、网络流量。确保压测机本身没有成为瓶颈(例如CPU持续100%)。如果某个Worker的CPU明显低于其他Worker,可能意味着网络问题或负载分配不均。

生成HTML报告 : 在无头模式或Web UI测试结束后,可以点击“Download Report”生成一个漂亮的HTML报告。这个报告包含了所有关键指标的图表和表格,非常适合归档和分享。

# 在无头模式中自动生成HTML报告
locust -f locustfile.py --headless --users 100 --spawn-rate 10 --run-time 2m --host=http://example.com --html report.html

6.3 常见问题与排查清单

在Locust压测过程中,你可能会遇到以下典型问题:

问题现象 可能原因 排查思路与解决方案
RPS上不去,但压测机CPU/内存很低 1. 被测服务本身已达到性能瓶颈。
2. 网络延迟或带宽限制。
3. Locust脚本中存在同步阻塞操作(如错误的 time.sleep )。
4. 连接池配置过小或端口耗尽。
1. 先监控被测服务的资源(CPU、内存、IO、数据库连接等),定位瓶颈点。
2. 使用 ping traceroute iftop 检查网络。
3. 检查脚本,将 time.sleep 替换为 gevent.sleep ,将CPU密集型操作移出任务循环。
4. 检查压测机 ulimit -n 和`netstat -an
响应时间随着用户数增加线性增长 典型资源竞争瓶颈。可能是数据库连接池耗尽、应用服务器线程池满、某个外部API限流、或磁盘IO达到上限。 1. 监控数据库连接数、活跃线程数。
2. 检查应用日志是否有大量等待或超时错误。
3. 对被测服务进行链路追踪(如SkyWalking, Jaeger),找出最耗时的环节。
出现大量“Connection refused”或“Timeout”错误 1. 被测服务崩溃或拒绝连接。
2. 压测机端口耗尽。
3. 操作系统或中间件(如Nginx)连接数限制。
1. 检查被测服务进程是否存活,日志是否有异常。
2. 在压测机上执行 ss -s 查看TCP连接统计,调整 net.ipv4.ip_local_port_range tcp_tw_reuse
3. 检查Nginx的 worker_connections 和系统的 somaxconn
Worker节点与Master连接中断 1. 网络不稳定。
2. Worker进程因异常退出。
3. Master和Worker版本不一致。
1. 检查网络连通性。
2. 查看Worker节点的日志输出(Locust输出或系统日志)。
3. 确保所有节点使用相同版本的Locust和Python。
Web UI图表数据卡顿或不更新 1. 浏览器性能问题或打开的标签页太多。
2. 测试数据量极大,浏览器渲染吃力。
3. Master节点资源(CPU/内存)不足。
1. 尝试刷新页面或使用更轻量的浏览器。
2. 对于长时间压测,可以定期下载CSV数据用其他工具(如Grafana)分析。
3. 监控Master节点资源,Master本身开销很小,但若机器配置极低也可能有问题。
模拟用户数达不到设定值 1. 压测机资源(CPU、内存)不足,无法支撑更多协程。
2. wait_time 设置过长,导致大量用户处于“等待”状态,活跃用户数不足。
3. 脚本中任务执行太快,用户很快结束并退出(如果未设置 wait_time )。
1. 监控压测机资源,考虑增加Worker节点或使用更高配置机器。
2. 检查 wait_time 配置,确保其合理模拟了用户操作间隔。
3. 确保用户类有持续执行的任务(循环任务),或者使用 constant_pacing 来控制节奏。

一个实用的排查流程

  1. 缩小范围 :先用单用户、低并发运行脚本,确保业务逻辑正确,请求能成功。
  2. 逐步加压 :从低并发(如10用户)开始,逐步增加,观察RPS和响应时间曲线。找到性能拐点。
  3. 内外结合 :在增加负载的同时,同时监控 压测机 被测服务 的资源使用情况。先确定瓶颈出现在哪一侧。
  4. 日志分析 :关注Locust输出的失败信息,以及被测服务的应用日志和错误日志。
  5. 工具深挖 :对于被测服务,使用专业的APM工具、数据库慢查询日志、JVM监控工具(如Arthas)等进行深度剖析。

性能调优是一个“假设-验证”的循环过程。Locust帮你发现了性能瓶颈(如“在200并发下,下单接口的99%响应时间超过2秒”),而真正的优化工作,则需要你深入到被测系统的代码、数据库、中间件和架构中去寻找根本原因并解决它。Locust是一个出色的“压力施加器”和“数据收集器”,但它不直接解决问题,而是照亮问题所在的位置。

更多推荐