1. 项目概述:为什么我们需要分布式压测?
做性能测试的朋友,尤其是后端开发或者测试工程师,应该都遇到过这样的场景:你写好了一个接口,本地用JMeter或者Postman跑一下,响应速度嗖嗖的,感觉良好。但一上线,用户量稍微上来点,服务器就开始报警,CPU飙升,响应超时,甚至直接宕机。问题出在哪?很多时候,问题就出在测试环境和真实环境的压力根本不在一个量级上。单机发起的请求,对于现代动辄需要承载数万、数十万并发用户的系统来说,无异于隔靴搔痒。
这就是我们今天要聊的 分布式压力测试 的核心价值。它不再是“一台机器模拟几百个用户”,而是“多台机器协同,模拟成千上万个真实用户的行为”,从而在测试阶段就尽可能逼近甚至超越线上真实压力,提前发现系统的性能瓶颈和临界点。而 Locust ,正是实现这一目标的利器中的利器。它用Python编写,测试脚本就是普通的Python代码,对开发者极其友好;它自带Web UI,可以实时观察压测情况;更重要的是,它原生支持分布式运行,架构简单清晰,让你能轻松地将测试压力从“小打小闹”提升到“狂风暴雨”的级别。
简单来说,如果你还在为单机压测无法突破性能瓶颈而烦恼,或者想找一个比JMeter写脚本更灵活、比一些商业压测工具更轻量的方案,那么通过Locust搭建分布式压测集群,就是你接下来必须要掌握的技能。这不仅仅是工具的使用,更是一种贴近真实生产环境的性能验证思维。
2. Locust分布式架构核心原理解析
在动手搭建之前,我们必须先吃透Locust分布式模式的工作原理。这能帮助你在后续遇到各种“诡异”问题时,快速定位根因,而不是盲目地重启进程、修改配置。
Locust的分布式运行模式采用经典的 Master-Worker(主从)架构 。这个架构非常清晰:
- Master(主节点) :只有一个。它不负责模拟任何用户(即不产生任何请求压力)。它的核心职责是 协调 和 收集 。它启动一个Web UI服务器(默认端口8089),用于下发测试任务、启动/停止测试,并实时收集所有Worker节点汇报上来的统计数据,进行聚合后展示在Web界面上。
- Worker(工作节点) :可以有多个,理论上可以无限水平扩展。它们是真正的“压力产生器”。每个Worker节点会从Master节点获取测试任务(即要执行的Locust脚本),然后独立地运行脚本,模拟虚拟用户(User)执行任务,并发起HTTP(或其他协议)请求。每个Worker会定期将自己的测试数据(如请求数、响应时间、失败数等)发送回Master节点。
它们之间如何通信? Locust默认使用 ZeroMQ 或 TCP Socket 进行节点间通信。当你启动Master时,它会开启两个端口:一个用于Web UI(如8089),另一个用于与Worker通信(默认是5557)。Worker启动时,需要指定Master节点的IP和这个通信端口,从而建立连接。
一个关键概念:集中式与分布式。 很多人会混淆。Locust的分布式,指的是 压力生成器(Worker)是分布式的 ,它们可以分布在不同的物理机或虚拟机上,共同产生压力。但 测试协调和数据收集是集中式的 ,由唯一的Master节点负责。这保证了我们有一个统一的控制面和数据视图。与之相对的,像JMeter可能通过“远程启动”的方式,其控制节点(运行JMeter GUI的机器)本身也可能参与发压,架构上略有不同。Locust的这种明确分离,让架构更清晰,资源利用也更合理(Master可以是一台配置很低的机器)。
为什么分布式能提高压力? 压力(并发用户数、每秒请求数RPS)受限于单台机器的资源(CPU、内存、网络带宽、端口数)。单机Locust在模拟大量用户时,可能会因为Python的GIL(全局解释器锁)、网络连接数限制、或机器本身性能而达到瓶颈。分布式模式将模拟用户的任务分摊到多台Worker机器上,每台机器只负责一部分用户,从而轻松突破单机资源上限,实现压力叠加。例如,单机最多模拟5000用户,那么4台Worker理论上就能模拟20000用户。
3. 环境准备与Locust核心脚本编写
工欲善其事,必先利其器。分布式压测的第一步,不是急着启动多台机器,而是准备好一个清晰、可复用的测试脚本和环境。
3.1 环境搭建与依赖安装
首先,确保你的机器上安装了Python(建议3.7及以上版本)。Locust的安装极其简单:
pip install locust
这条命令会安装locust及其核心依赖。验证安装是否成功:
locust -V
如果输出类似
locust 2.20.0
的版本信息,说明安装成功。对于分布式压测,
所有节点(Master和所有Worker)都必须安装相同版本的Locust
,并且能够访问到相同的测试脚本。版本不一致是导致莫名错误的常见原因。
3.2 编写一个可分布式的Locust测试脚本
Locust的脚本本质就是一个Python文件。我们从一个经典的HTTP API压测脚本例子开始,并深入讲解其中每个部分对分布式运行的影响。
from locust import HttpUser, TaskSet, task, between
import json
class UserBehavior(TaskSet):
"""
定义虚拟用户的行为任务集。
在分布式环境下,每个Worker上的每个用户实例都会独立运行这些任务。
"""
# 每个用户执行任务前会调用一次on_start,常用于登录等初始化操作
def on_start(self):
"""模拟用户登录,获取token。注意:这个token是每个用户独立的。"""
login_payload = {"username": "test_user", "password": "test_pass"}
# 使用self.client发起请求,它会自动记录到Locust的统计中
with self.client.post("/api/login", json=login_payload, catch_response=True) as response:
if response.status_code == 200:
resp_json = response.json()
self.token = resp_json.get("token") # 将token存储在用户实例中
response.success()
else:
response.failure(f"Login failed with status code: {response.status_code}")
# 登录失败,可以停止这个用户的执行(可选)
self.interrupt()
# 用@task装饰器定义一个任务,权重值表示执行频率
@task(3) # 权重为3,意味着执行频率较高
def get_user_info(self):
"""任务1:获取用户信息"""
headers = {"Authorization": f"Bearer {self.token}"} if hasattr(self, 'token') else {}
with self.client.get("/api/user/profile", headers=headers, name="/api/user/profile [GET]") as response:
# 对响应进行断言,判断业务是否成功
if response.status_code == 200 and response.json().get("code") == 0:
response.success()
else:
response.failure(f"Unexpected response: {response.text}")
@task(1) # 权重为1,执行频率较低
def create_item(self):
"""任务2:创建一个新项目"""
headers = {"Authorization": f"Bearer {self.token}", "Content-Type": "application/json"}
item_data = {"name": f"TestItem_{self.user.id}", "value": 100}
with self.client.post("/api/items", json=item_data, headers=headers, name="/api/items [POST]") as response:
if response.status_code == 201:
response.success()
else:
response.failure(f"Create failed: {response.status_code}")
# 可以定义更多任务...
class WebsiteUser(HttpUser):
"""
定义虚拟用户类。每个模拟用户都是这个类的一个实例。
"""
# 指向上面定义的任务集
tasks = [UserBehavior]
# 用户执行完一个任务后,等待多长时间(秒)再执行下一个任务。
# between(1, 5) 表示等待1到5秒之间的一个随机时间。
# 这个设置对模拟真实用户思考时间、控制RPS至关重要。
wait_time = between(1, 5)
# 每个用户建立HTTP连接时的基础超时时间(秒)
connection_timeout = 10.0
# 网络请求超时时间(秒)
network_timeout = 10.0
分布式运行的关键脚本注意事项:
-
状态隔离
:
on_start中获取的self.token是每个User实例独立的。在分布式运行时,每个Worker上的每个用户都会独立执行on_start。 绝对不要在脚本顶层定义全局变量并在多个用户间共享状态 ,这会导致数据竞争和难以预料的行为。用户行为应该是无状态或实例状态隔离的。 -
任务权重与等待时间
:
@task(weight)和wait_time共同决定了压力模型。在分布式模式下,所有Worker遵循同一套模型,Master只是协调开始和停止,并不干涉每个Worker内部的任务调度。确保你的wait_time设置合理,between(1,5)能有效避免请求变成单纯的“循环轰炸”,更贴近真实用户操作间隔。 -
请求命名(
name参数) :在self.client.get/post中设置name参数非常重要。Locust的统计报表会按name聚合。如果你不设置name,默认会使用完整的URL路径。但像/api/items/123和/api/items/456会被统计为两个不同的条目,不利于分析。通过name="/api/items [GET]"可以将同一类请求归并,使报告更清晰。 -
主机地址(
host) :脚本里没有写死的host。这个地址需要在启动Locust时通过--host参数指定(如--host https://api.your-service.com)。在分布式场景下, 所有Worker必须使用相同的目标主机 。通常由Master在启动时指定,Worker会自动继承。
4. 分布式压测集群搭建与实战演练
现在,我们进入实战环节。假设你有三台Linux服务器(可以是物理机、虚拟机或云主机),IP分别为:
-
master_node_ip(用作Master) -
worker1_ip(用作Worker 1) -
worker2_ip(用作Worker 2)
我们的目标是在这三台机器上部署并运行分布式Locust压测。
4.1 第一步:准备与共享测试脚本
首先,将上一节编写好的Locust脚本(例如保存为
locustfile.py
)放到一个
所有节点都能访问的位置
。有几种常见做法:
- 版本控制仓库(推荐) :将脚本放在Git仓库中。在所有节点上拉取同一分支的同一版本代码。这是最规范、易于协同的方式。
- 共享网络存储 :如NFS、Samba共享目录,将脚本放在共享目录下。
-
手动同步
:如果机器不多,可以用
scp命令手动拷贝到各个节点相同路径下。
我们假设采用第一种方式,所有节点都已将脚本克隆到了
/home/user/loadtest/locustfile.py
。
确保所有节点Python和Locust环境一致
。在所有节点上执行
pip install locust
,并确认版本。
4.2 第二步:启动Master节点
在
master_node_ip
机器上,切换到脚本目录,执行以下命令:
cd /home/user/loadtest
locust -f locustfile.py --master --host=https://api.your-target.com
参数解析:
-
-f locustfile.py: 指定要运行的Locust脚本文件。 -
--master: 以Master模式运行。这是最关键的一个参数。 -
--host=https://api.your-target.com: 指定被压测系统的基地址。所有在脚本中使用的相对路径(如/api/login)都会拼接到这个host之后。 -
默认行为
:启动后,Master会开启两个服务:
-
Web UI服务:监听
0.0.0.0:8089
。你可以通过浏览器访问
http://master_node_ip:8089来打开控制界面。 - Worker通信服务:监听 0.0.0.0:5557 。等待Worker节点来连接。
-
Web UI服务:监听
0.0.0.0:8089
。你可以通过浏览器访问
注意 :如果你的Master节点有防火墙,必须放行 8089 (Web访问)和 5557 (Worker连接)端口。云服务器还需要在安全组规则中配置。
启动成功后,终端会显示类似以下信息:
[2024-05-XX ...] INFO/locust.main: Starting web interface at http://0.0.0.0:8089
[2024-05-XX ...] INFO/locust.main: Starting Locust 2.20.0
[2024-05-XX ...] INFO/locust.runners: Waiting for workers to be ready, 0 of 0 connected
此时,Web界面可以访问,但“SLAVES”部分显示为0,因为还没有Worker连接。
4.3 第三步:启动Worker节点
分别在
worker1_ip
和
worker2_ip
机器上,切换到脚本目录,执行Worker启动命令:
在
worker1_ip
上:
cd /home/user/loadtest
locust -f locustfile.py --worker --master-host=master_node_ip
在
worker2_ip
上:
cd /home/user/loadtest
locust -f locustfile.py --worker --master-host=master_node_ip
参数解析:
-
--worker: 以Worker模式运行。 -
--master-host=master_node_ip: 指定Master节点的IP地址或主机名。 这是Worker能找到Master并注册自己的关键 。 -
注意
:这里没有指定
--host参数。Worker会自动从Master那里继承--host的设置。确保Worker节点 网络能够访问--host指定的目标地址。
启动成功后,Worker终端会显示连接成功的信息:
[2024-05-XX ...] INFO/locust.main: Starting Locust 2.20.0
[2024-05-XX ...] INFO/locust.runners: Connecting to master at master_node_ip:5557
[2024-05-XX ...] INFO/locust.runners: Connected to master at master_node_ip:5557
同时,在Master节点的终端和Web界面中,你会看到Worker已连接。Web界面“SLAVES”部分会显示已连接的Worker数量(例如2个)。
4.4 第四步:通过Web UI执行分布式压测
-
打开浏览器,访问
http://master_node_ip:8089。 -
在Locust的Web界面中,你会看到:
- Number of total users to simulate : 要模拟的总用户数。这是 所有Worker节点共同模拟的用户总数 。比如你输入10000,Locust Master会协调两个Worker,让它们共同承担这10000个用户的模拟工作(通常会自动均衡分配)。
- Spawn rate (users spawned/second) : 每秒启动的用户数。控制用户压力爬升的速率。
-
Host
: 这里会显示你启动Master时设置的
--host,不可更改。 - SLAVES : 显示已连接的Worker数量(应为2)。
- 填写总用户数(如10000)和孵化率(如100),然后点击 “Start swarming” 。
-
观察与控制
:
- Statistics标签页 :查看实时聚合的RPS、响应时间、失败率等关键指标。这些数据是所有Worker汇总后的结果。
- Charts标签页 :查看用户数、RPS、响应时间随时间变化的曲线图。
- Workers标签页 :可以看到每个Worker节点的状态、CPU使用率(如果Worker是Locust 2.11+版本且运行在支持psutil的系统上)、当前模拟的用户数。 这是分布式特有的视图,用于监控各个压力生成器的负载是否均衡 。
- Failures/Exceptions标签页 :查看失败的请求和抛出的异常。
- 可以随时点击 “Stop” 停止测试,或者点击 “New test” 重置统计数据开始新一轮测试。
一个核心技巧:如何确定每个Worker能模拟多少用户? 这没有固定公式,取决于Worker机器本身的性能(CPU、内存、网络)和你的测试脚本复杂度(请求频率、响应处理逻辑)。一个实用的方法是 渐进式加压 。先在一个Worker上单独测试,逐步增加用户数,观察该Worker的CPU和内存使用率。当资源使用率达到70%-80%时,记录下此时的用户数,这大致就是该单机的能力上限。然后,在分布式模式下,将总用户数设定为(单机上限 * Worker数量),并留有一定余量。例如,单机上限是3000用户,有2个Worker,那么总用户数可以设置为5000-5500进行测试。
5. 高级配置、问题排查与性能优化
分布式压测搭建起来不难,但要跑得稳、数据准,就需要关注一些高级配置和常见坑点。
5.1 关键启动参数详解
除了基本的
--master
、
--worker
、
--host
,Locust提供了许多参数来精细控制压测行为,在分布式环境下尤其重要。
-
--web-host/--web-port: Master节点上,指定Web UI绑定的IP和端口。如果你想只允许本地访问Web界面,可以设置--web-host=127.0.0.1。 -
--master-bind-host/--master-bind-port: Master节点上,指定与Worker通信的Socket绑定的IP和端口(默认5557)。如果默认端口冲突,可以修改。 -
--worker: 如前所述,启动Worker模式。 -
--master-host/--master-port: Worker节点上,指定Master的地址和通信端口(默认5557)。 如果Master修改了--master-bind-port,Worker的--master-port必须与之对应 。 -
--expect-workers: 在Master启动时使用,例如--expect-workers=5。Master会等待指定数量的Worker全部连接成功后,才允许在Web UI上启动测试。这对于确保所有压力机就位后再开始非常有用。 -
--headless/-u/-r: 在无UI模式下运行。--headless表示不启动Web UI;-u指定用户数;-r指定孵化率。这在结合CI/CD进行自动化压测时常用。分布式模式下,可以在Master启动命令中加入这些参数,直接开始测试,无需点击Web UI。locust -f locustfile.py --master --expect-workers=2 --headless -u 10000 -r 100 --host=https://api.target.com --run-time 10m -
--run-time: 指定测试运行时长,例如--run-time 1h30m。时间一到,测试自动停止。在自动化场景中防止测试无限运行。 -
--csv/--html: 以CSV或HTML格式输出测试结果报告。分布式测试的聚合结果也会输出到这些文件中,便于后续分析。locust -f locustfile.py --master --host=... --csv=result --html=report.html
5.2 分布式压测常见问题与排查实录
即使按照步骤操作,你也可能会遇到一些问题。下面是我在实践中总结的“踩坑记录”:
问题1:Worker连接不上Master,提示
Connection refused
或超时。
-
排查思路
:
-
网络连通性
:在Worker节点上执行
telnet master_node_ip 5557或nc -zv master_node_ip 5557,检查端口是否能通。 -
防火墙/安全组
:确认Master节点的防火墙(如
firewalld、ufw)和云服务商安全组已放行 5557 端口。 -
Master绑定地址
:检查Master是否绑定到了
0.0.0.0。如果启动Master时使用了--master-bind-host=127.0.0.1,那么只有本机可以连接。分布式环境下必须绑定到0.0.0.0或特定网络IP。 -
命令错误
:确认Worker启动命令中的
--master-host参数值是否正确,不能是localhost或127.0.0.1(除非Worker和Master在同一台机器,但这失去了分布式意义)。
-
网络连通性
:在Worker节点上执行
问题2:Web UI上显示Worker已连接,但启动测试后,“Users”数量不增长或远低于设定值。
-
排查思路
:
- Worker日志 :查看Worker节点的终端输出或日志,是否有大量错误信息,例如连接目标服务器失败、脚本语法错误、导入模块失败等。一个常见的错误是脚本中引用了Worker机器上不存在的Python模块。
- 目标服务承受能力 :可能目标服务在压力下迅速崩溃或拒绝连接,导致虚拟用户无法成功启动。观察目标服务的监控和日志。
-
wait_time设置过大 :如果脚本中wait_time = between(10, 30),意味着每个用户执行一个任务后要等待10-30秒,那么用户数增长自然会非常缓慢。调整wait_time或提高孵化率(Spawn rate)。 -
Worker资源瓶颈
:通过
top或htop命令查看Worker节点的CPU和内存使用率。如果已经接近100%,说明单台Worker已经满载,无法孵化更多用户。需要增加Worker节点数量。
问题3:测试数据(如总RPS)波动非常大,或者明显低于预期。
-
排查思路
:
-
网络延迟与带宽
:Worker节点与被测服务之间的网络质量差,或者Worker节点本身的网络带宽已被打满。使用
ping、traceroute或iftop等工具检查网络状况。 -
“慢启动”效应
:如果设置了
wait_time,并且总用户数很大,孵化率相对较低,那么系统需要很长时间才能达到峰值压力。计算一下:10000用户,孵化率100用户/秒,需要100秒才能全部启动。在这100秒内,压力是逐渐上升的。 - 聚合延迟 :Master默认每3秒从Worker收集一次数据。在Web UI上看到的RPS是3秒内的平均值,并且有短暂延迟。这是正常现象。对于瞬时波动,可以关注更长周期的趋势。
-
检查脚本逻辑
:确认脚本中没有不必要的
time.sleep()或非常耗时的本地计算(如循环加密),这些操作会阻塞单个用户的任务执行,严重影响该用户线程/协程的吞吐能力。
-
网络延迟与带宽
:Worker节点与被测服务之间的网络质量差,或者Worker节点本身的网络带宽已被打满。使用
问题4:如何监控Worker节点本身的资源使用情况?
-
解决方案
:Locust自带的Worker列表在较新版本会显示CPU使用率。但更全面的监控,建议结合系统级工具:
-
使用
htop或nmon:在Worker节点上实时查看CPU、内存、负载。 -
使用
dstat或sar:查看详细的网络I/O、磁盘I/O历史数据。 - 关键指标 : CPU使用率 (是否持续高于80%)、 内存使用量 (是否接近耗尽)、 网络吞吐量 (是否达到网卡上限)、 系统负载(Load Average) (1分钟负载是否持续高于CPU核数太多)。任何一个指标成为瓶颈,都会限制该Worker的发压能力。
-
使用
5.3 性能优化与最佳实践
要让分布式Locust压测发挥最大威力,以下几点经验值得参考:
- Worker节点配置尽可能一致 :不同的硬件配置会导致发压能力不同,可能造成压力不均。尽量使用相同或配置相近的机器作为Worker。
-
优化Locust脚本性能
:
- 避免阻塞操作 :在任务方法中,避免使用同步的、耗时的I/O操作(如读写大文件、同步网络请求)。如果必须调用外部命令或库,考虑其是否阻塞。
-
谨慎使用
@task装饰器内的循环 :一个@task方法应该尽快结束。如果需要重复操作,应该拆分成多个@task,利用Locust的调度机制,而不是在一个任务里写for i in range(100)。 -
连接复用
:
HttpUser使用的client默认会保持HTTP连接(Session)。确保你的目标服务也支持HTTP Keep-Alive,这能大幅减少TCP连接建立的开销。
-
使用更高效的模式
:Locust默认使用基于
gevent的协程。对于计算密集型或特定I/O密集型的脚本,可以尝试切换到 多进程模式 (通过--processes参数,仅在类Unix系统有效),让每个Worker启动多个进程,充分利用多核CPU。但要注意,多进程模式下,用户状态在进程间不共享。 -
结果分析与瓶颈定位
:
- 关注分位数响应时间 :不要只看平均响应时间。Web UI上提供的50%(中位数)、95%、99%分位数响应时间更能反映用户体验。比如95%分位响应时间激增,说明有少量请求非常慢,可能是遇到了数据库锁、缓存击穿等问题。
- 结合系统监控 :压测时,一定要同时监控被测服务的各项指标(应用服务器线程池、数据库连接数、CPU、内存、磁盘IO、网络流量等)。当Locust报告响应时间变长、失败率升高时,去对照服务监控,往往能快速定位瓶颈所在(是应用代码问题?数据库慢查询?还是缓存服务过载?)。
-
安全与清理
:
- 压测数据隔离 :确保压测使用的是独立的测试数据库或测试环境,避免污染生产数据。
-
脚本包含清理逻辑
:对于创建资源的接口(如POST /api/items),可以考虑在
on_stop方法或使用Locust的events钩子,在测试结束后执行一些清理操作(如删除测试创建的数据),但这需要目标API提供相应的删除接口。更常见的做法是让测试环境定期自动重置。
373



被折叠的 条评论
为什么被折叠?



