Python 3.11与Locust:构建高性能分布式压测环境实战指南
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) # 模拟用户思考时间
这个模板包含了几个关键部分:
- 用户类(QuickstartUser) :代表一类虚拟用户的行为模式。
- 等待时间(wait_time) :定义用户执行完一个任务后到下一个任务开始前的等待时间,用于模拟真实用户的思考或浏览间隔。
between是均匀分布,还有constant(固定间隔)等。 - 生命周期钩子(on_start/on_stop) :用于模拟用户登录/登出。
- 任务(@task) :核心,定义用户具体做什么。权重值决定了任务执行的相对概率。
- 请求与断言 :使用
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="订单支付")
这里的关键点是:
- 将
token和headers保存为self.token和self.headers,它们是用户实例的属性,在该用户的所有任务中共享。 - 在
on_start中执行登录,并做好错误处理。登录失败的用户实例应该被停止,避免其继续执行无意义的请求污染数据。 - 在每个需要认证的任务中,先检查
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报告提供了丰富的指标:
- RPS(Requests per Second) :每秒请求数。这是衡量系统吞吐量的核心指标。观察其曲线是否平稳,是否达到预期。
- 响应时间(Response Times) :重点关注 平均响应时间 、 中位数(Median) 和 95/99分位值(95%ile, 99%ile) 。
- 平均响应时间 :容易受极端值影响。
- 中位数 :有一半的请求快于这个值,更能代表“典型”体验。
- 95/99分位值 :例如95%ile=500ms,意味着95%的请求响应时间在500ms以内。 这是评估用户体验和SLA(服务等级协议)的关键指标 。即使平均响应时间很好,如果99%分位值很高,也意味着有少量用户遭遇了极差的体验。
- 失败率(Failures) :任何非2xx/3xx的HTTP状态码,或者在
catch_response中手动调用failure()的请求,都会计入失败。压测过程中失败率应接近0%。突然升高的失败率往往是系统出现瓶颈(如连接池耗尽、数据库死锁)的信号。 - 用户数(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 来控制节奏。 |
一个实用的排查流程 :
- 缩小范围 :先用单用户、低并发运行脚本,确保业务逻辑正确,请求能成功。
- 逐步加压 :从低并发(如10用户)开始,逐步增加,观察RPS和响应时间曲线。找到性能拐点。
- 内外结合 :在增加负载的同时,同时监控 压测机 和 被测服务 的资源使用情况。先确定瓶颈出现在哪一侧。
- 日志分析 :关注Locust输出的失败信息,以及被测服务的应用日志和错误日志。
- 工具深挖 :对于被测服务,使用专业的APM工具、数据库慢查询日志、JVM监控工具(如Arthas)等进行深度剖析。
性能调优是一个“假设-验证”的循环过程。Locust帮你发现了性能瓶颈(如“在200并发下,下单接口的99%响应时间超过2秒”),而真正的优化工作,则需要你深入到被测系统的代码、数据库、中间件和架构中去寻找根本原因并解决它。Locust是一个出色的“压力施加器”和“数据收集器”,但它不直接解决问题,而是照亮问题所在的位置。
更多推荐
所有评论(0)