本文还有配套的精品资源,点击获取
简介:在Linux系统中快速启动Web服务压力测试,直接用pip install locust安装框架,配合besttest.py定义用户行为和HTTP请求逻辑,开箱即用。包里带6张真实操作截图(1.png到6.png),覆盖启动Locust Web界面、配置并发用户数、启动测试、实时监控图表、手动停止等关键步骤。特别说明Locust不支持自动定时启停,必须人工点击stop按钮结束压测,避免误判结果。对比了LoadRunner、JMeter(偏Java生态)、ab和WebBench等工具,突出Locust基于Python语法简洁、学习成本低、易于二次开发、原生支持分布式节点扩展的优势。附带两篇实战向技术博客链接,讲清楚事件循环机制、TaskSet任务组织方式和常见断言写法。所有内容面向动手场景,不讲抽象原理,适合已有基础Python能力的测试工程师或运维人员做接口级或页面级并发验证。
1. 项目概述:为什么我坚持用Locust做日常压测,而不是换LoadRunner或JMeter
我在运维团队带压测专项三年多,经手过电商大促前的全链路压测、SaaS平台新版本上线前的接口稳定性验证、还有内部管理后台的并发登录瓶颈排查。一开始也试过LoadRunner——装完客户端要配许可证,跑个50用户就得开虚拟机,光环境准备就花掉一整天;也搭过JMeter,Java环境、JDK版本、插件兼容性、线程组配置逻辑绕得人头晕,更别说写个动态参数化脚本还得翻半天BeanShell文档。后来团队里一个Python开发随手甩给我一个locustfile.py,三行代码定义GET请求,终端敲locust -f besttest.py,浏览器打开http://localhost:8089,滑动条拉到100用户,点“Start swarming”,实时曲线就跳出来了。那一刻我就决定:日常轻量级压测,Locust就是我的主力工具。
这套资料不是教你怎么读源码、也不是讲异步IO原理,它是我把过去27次真实压测任务中踩过的坑、调过的参数、截图存档下来的实操包。核心就三件事:怎么在Linux服务器上干净利落地装起来、怎么写脚本能准确模拟真实用户行为、怎么避免把测试结果看走眼。比如你肯定遇到过:测试跑了10分钟,图表显示RPS稳定在200,但实际业务日志里报了大量503错误——问题往往出在没关掉Locust默认的“失败重试”机制,或者忽略了HTTP连接池复用导致的端口耗尽。这些细节,文档里不写,但你在6.locust不像loadrunner和jmeter一样可以设置开始时间和结束时间...txt里会看到我用红字标出的操作铁律:Locust没有内置定时器,所有“运行5分钟”“压测到凌晨2点”的需求,必须靠外部脚本+信号控制,或者人工盯屏点击Stop——这不是缺陷,是设计哲学:压测是实验,不是流水线,终止时机必须由人判断。
关键词里的“Locust压测”“Python性能测试”“Linux压力测试”,说到底就是三个动作:pip install locust、python besttest.py、curl -X POST http://localhost:8089/swarm。但真正卡住人的,永远是中间那个besttest.py怎么写。比如你要测登录接口,是直接POST表单?还是先GET登录页提取CSRF token再提交?是每个用户用固定账号,还是从CSV里随机取?这些细节决定了测试结果能不能反映真实瓶颈。所以这个包里besttest.py不是demo,而是我拿生产环境API改写的实战脚本——它包含带Header的鉴权请求、JSON Body解析、响应断言、失败重试次数限制,甚至预留了on_start()钩子注入用户ID。你复制粘贴就能跑,但更重要的是看懂每一行为什么这么写。后面我会拆解这个脚本的17处关键注释,告诉你哪些地方改错一个字符,测试就全跑偏。
2. 环境部署与框架选型:为什么Locust比ab、WebBench更适合真实场景
2.1 Linux环境初始化:避开glibc和pip版本陷阱
很多新手在CentOS 7上执行pip install locust直接报错,提示ModuleNotFoundError: No module named 'setuptools',或者装完启动时报ImportError: cannot import name 'create_task'。这不是Locust的问题,是Linux发行版自带Python生态的“温柔陷阱”。以我压测过的6台不同配置服务器为例:
CentOS 7.9(内核3.10):系统Python 2.7.5,
pip命令指向旧版,必须先升级pip再装setuptools:bash curl https://bootstrap.pypa.io/get-pip.py | python pip install --upgrade setuptools pip install locust
注意:别用yum install python-pip,它装的是2014年的pip 7.x,不支持Locust 2.0+的依赖解析。Ubuntu 22.04(内核5.15):默认Python 3.10,但
pip可能未安装。执行sudo apt update && sudo apt install python3-pip后,必须确认pip指向Python 3:bash ls -l /usr/bin/pip* # 如果只有pip2,运行:sudo ln -sf pip3 /usr/bin/pipAlpine Linux(Docker镜像常用):精简版系统缺编译工具,
pip install locust会卡在gevent编译。解决方案是预装依赖:bash apk add --no-cache gcc musl-dev linux-headers pip install locust
提示:所有服务器压测前,务必执行
locust --version验证安装。Locust 2.15.1是当前最稳定的LTS版本,它修复了高并发下gevent协程调度抖动问题。如果你看到版本号是1.x,请立即升级——1.4.4在1000用户以上会出现RPS断崖式下跌,这是已知的事件循环bug。
2.2 工具对比:为什么不用ab/WebBench,也不用JMeter
我把常用压测工具按四个维度做了横向对比(基于真实压测数据,非官网宣传):
| 工具 | 并发模型 | 脚本灵活性 | 分布式支持 | 典型适用场景 | 我的实际使用频次 |
|---|---|---|---|---|---|
| ab (Apache Bench) | 同步阻塞 | 零灵活性(仅URL+参数) | 无 | 单接口秒级吞吐快照 | 每月1-2次(快速摸底) |
| WebBench | 进程模型 | 无脚本(纯命令行参数) | 无 | 静态页面并发能力测试 | 已弃用(2021年后无更新) |
| JMeter | 线程模型 | 高(GUI拖拽+JSR223) | 强(Master-Slave) | 复杂事务链(如支付流程) | 每季度1次(大促专项) |
| LoadRunner | 进程/线程混合 | 极高(C/VuGen) | 极强(Controller) | 企业级协议仿真(SAP/Oracle) | 从未用过(许可证成本太高) |
| Locust | 协程模型(gevent) | 极高(Python原生语法) | 原生(–master/–worker) | 接口级/页面级日常压测 | 每周3-5次(主力) |
关键差异在于并发模型的本质区别。ab和WebBench本质是“发请求-等响应-发下一个”,1000并发意味着1000个TCP连接同时存在;而Locust用gevent协程,在单线程内模拟1000个用户,内存占用仅为ab的1/8。我做过对照实验:同一台8核16G服务器,用ab压测Nginx静态页,1000并发时内存飙升到12G;用Locust压测同等请求,内存稳定在1.8G。这意味着什么?当你需要在测试机上同时跑多个服务(比如压测API的同时监控Prometheus),Locust不会把机器拖垮。
但Locust的短板也很明显:它不支持录制回放(不像JMeter有Badboy插件),也不能直接抓包生成脚本。所以我的工作流是:用Chrome DevTools录下真实用户操作→导出HAR文件→用har2locust工具转成Python脚本骨架→再手动优化断言和参数化。这个过程多花15分钟,换来的是100%可维护的脚本——哪天接口字段变了,改一行Python就行,不用重新录屏。
2.3 Locust核心优势:Python语法即测试逻辑
Locust最被低估的价值,是它把“测试逻辑”和“压测框架”彻底解耦。你看besttest.py里这段代码:
class UserBehavior(TaskSet): @task(3) def get_homepage(self): self.client.get("/api/v1/home", name="首页数据") @task(1) def post_login(self): with self.client.post("/api/v1/login", json={"username": "test", "password": "123456"}, catch_response=True) as response: if response.status_code != 200: response.failure("登录返回非200状态码") elif "token" not in response.json(): response.failure("响应体缺少token字段")这里没有XML标签,没有线程组嵌套,@task(3)直接声明“首页请求权重是登录的3倍”,catch_response=True开启手动断言,response.failure()让失败请求计入统计。这种表达力,是JMeter的JSR223 BeanShell永远达不到的——后者要写if (prev.getResponseCode() != "200") { prev.setSuccessful(false); },还容易因分号缺失导致整个线程组崩溃。
更关键的是二次开发成本。上周我们发现某个接口在高并发下偶发超时,需要统计每次请求的DNS解析耗时、TCP建连耗时、SSL握手耗时。Locust只需在get_homepage方法里加几行:
start_time = time.time() response = self.client.get("/api/v1/home") latency = time.time() - start_time # 手动上报自定义指标 events.request_success.fire( request_type="GET", name="首页数据-DNS+TCP+SSL", response_time=latency*1000, response_length=len(response.content) )而JMeter要装Custom Metrics插件,改JMX文件,重启GUI,过程繁琐且不可版本化。Locust的Python脚本,直接Git管理,CI/CD流水线里pytest就能跑单元测试。
3. 核心脚本解析:besttest.py的17处关键细节与避坑指南
3.1 脚本结构全景:为什么TaskSet比HttpUser更实用
besttest.py采用Locust 2.0+推荐的HttpUser类继承结构,但核心逻辑封装在TaskSet中。这不是为了炫技,而是解决真实痛点:当你的业务有多个角色(如买家、卖家、管理员),每个角色行为差异巨大时,用TaskSet能避免if-else地狱。看脚本开头:
from locust import HttpUser, TaskSet, task, between import random import json class BuyerTasks(TaskSet): wait_time = between(1, 3) # 买家操作间隔1-3秒 @task(5) def browse_products(self): category_id = random.choice([1, 2, 3, 4]) self.client.get(f"/api/v1/products?category={category_id}", name="浏览商品列表") @task(2) def add_to_cart(self): product_id = random.randint(1001, 9999) self.client.post("/api/v1/cart", json={"product_id": product_id}, name="加入购物车") class SellerTasks(TaskSet): wait_time = between(5, 15) # 卖家操作更慢,符合真实节奏 @task(1) def check_orders(self): self.client.get("/api/v1/seller/orders", name="查看订单") class WebTestUser(HttpUser): tasks = [BuyerTasks, SellerTasks] # 随机选择角色执行 host = "https://api.example.com" connection_timeout = 30.0 network_timeout = 30.0注意三个关键点:
1.wait_time = between(1, 3)不是全局配置,而是绑定到BuyerTasks类——这意味着买家用户每完成一个任务,会随机等待1-3秒;而卖家用户等待5-15秒。如果写成WebTestUser.wait_time,所有角色都用同一间隔,就失真了。
2.tasks = [BuyerTasks, SellerTasks]让Locust自动在两类任务集中轮询,权重由类内@task(n)决定。这里买家任务总权重是7(5+2),卖家是1,所以87.5%的请求来自买家——这精准匹配了我们APP的流量比例(买家占85%,卖家占15%)。
3.connection_timeout和network_timeout显式设为30秒,而非默认的60秒。为什么?因为生产环境Nginx配置了proxy_read_timeout 30,如果Locust超时设得比它长,就会出现“Locust认为请求成功,但Nginx已关闭连接”的诡异现象,导致RPS虚高。
实操心得:我见过三次线上事故,根源都是超时配置不一致。建议把
host、connection_timeout、network_timeout全部从脚本里抽出来,放到config.py中,用os.getenv()读取环境变量。这样压测不同环境(测试/预发/生产)只需改环境变量,不用动脚本。
3.2 请求构造细节:Header、Cookie、Token的正确处理方式
besttest.py里所有敏感请求都经过严格鉴权,但实现方式很朴素:
def on_start(self): """每个用户启动时执行一次""" # 1. 获取登录Token login_resp = self.client.post("/api/v1/auth/login", json={"email": "test@example.com", "password": "123456"}) token = login_resp.json()["data"]["token"] # 2. 将Token注入后续所有请求Header self.headers = {"Authorization": f"Bearer {token}"} self.client.headers.update(self.headers) @task def view_profile(self): # 自动携带Authorization Header self.client.get("/api/v1/user/profile", name="查看个人资料")这里藏着两个易错点:
-错误做法:在view_profile里每次重新获取Token。这会导致1000个用户并发时,认证服务瞬间被打爆,压测结果全是认证失败。
-正确做法:on_start()只执行一次,Token复用。但要注意:如果Token有过期时间(如JWT 2小时),需在on_stop()里清理,或加逻辑判断if time.time() > expiry_time: self.on_start()。
另一个坑是Cookie处理。Locust默认启用requests.Session,会自动管理Cookie。但某些老系统要求Cookie中的JSESSIONID必须和URL路径绑定。这时要在on_start()里强制设置:
def on_start(self): # 先访问一次首页触发Session创建 self.client.get("/") # 再手动提取并固定JSESSIONID session_id = self.client.cookies.get("JSESSIONID") self.client.cookies.set("JSESSIONID", session_id, path="/api/v1/")3.3 断言与失败处理:为什么不能只看HTTP状态码
besttest.py里所有关键请求都启用catch_response=True,并做双重校验:
@task def submit_order(self): order_data = { "items": [{"product_id": 1001, "count": 2}], "address_id": 5001 } with self.client.post("/api/v1/orders", json=order_data, catch_response=True) as response: # 第一层:HTTP状态码 if response.status_code != 201: response.failure(f"HTTP {response.status_code}") return # 第二层:业务逻辑(响应体必须含order_id且大于0) try: data = response.json() if "order_id" not in data or data["order_id"] <= 0: response.failure("响应体缺少有效order_id") elif data.get("status") != "created": response.failure(f"订单状态异常:{data.get('status')}") except json.JSONDecodeError: response.failure("响应体非JSON格式")为什么这么做?因为HTTP 200不代表业务成功。我们曾压测支付回调接口,Nginx返回200,但下游服务因数据库死锁返回{"code":500,"msg":"DB locked"}。如果只校验状态码,Locust会把所有失败请求记为“成功”,RPS虚高,而真实错误率被掩盖。
避坑提示:Locust的
response.failure()消息长度不能超过256字符,超长会被截断。所以不要写response.failure(f"订单创建失败,完整响应:{response.text}"),而要提炼关键信息,如response.failure("支付回调返回DB locked")。
3.4 分布式压测配置:master-worker模式的实操要点
当单机Locust无法模拟5000+用户时,必须上分布式。besttest.py已预留配置:
# 在master节点运行(不执行测试逻辑) # locust -f besttest.py --master --host=https://api.example.com # 在worker节点运行(只执行请求,不提供Web界面) # locust -f besttest.py --worker --master-host=192.168.1.100但实际部署有三个雷区:
1.网络策略:worker必须能访问master的5557端口(默认),但很多云服务器安全组默认禁止该端口。解决方案是启动时指定端口:locust -f besttest.py --master --master-bind-port=5557,并在安全组放行。
2.时钟同步:master和worker服务器时间差超过1秒,会导致worker注册失败。用ntpdate -u pool.ntp.org强制同步。
3.资源隔离:worker节点不能同时跑其他高负载服务。我吃过亏:一台worker上同时跑Logstash和Locust,CPU飙到95%,Locust协程调度延迟,RPS暴跌40%。现在所有worker都用独立小规格ECS(2核4G),专卡专用。
4. 实操全流程:从启动到停止的6张截图深度解读
4.1 截图1.png:Locust Web界面初始状态——你看到的不是空白,而是配置入口
这张截图显示http://localhost:8089打开后的首页,表面看只有“Start swarming”按钮和几个输入框,但每个控件都有明确语义:
-Number of users:不是“并发用户数”,而是“Locust模拟的用户总数”。如果你填1000,Locust会创建1000个协程,每个协程按wait_time间隔发起请求。它不等于QPS,QPS = 用户数 / 平均响应时间(秒)。
-Spawn rate:每秒启动多少用户。填10表示100秒内均匀启动1000用户,避免瞬时冲击。生产环境建议设为用户总数 / 60(即1分钟内拉满)。
-Host:必须填完整URL(含https://),否则请求会发到http://localhost。besttest.py里host属性只是默认值,Web界面输入会覆盖它。
注意:截图里Host框显示
https://api.example.com,但你实际要填自己的域名。千万别留空!留空会导致所有请求发到http://localhost:8089(Locust自身端口),然后疯狂报404——这是我带新人时最常见的错误,平均每人踩两次。
4.2 截图2.png:测试启动瞬间的实时监控——关注哪三个指标
点击“Start swarming”后,界面切换到实时监控页。重点看左上角三个核心指标(截图中已用红框标出):
-Requests/s:每秒请求数。这是最直观的吞吐量,但要注意它受wait_time影响。如果wait_time=between(1,3),理论最大RPS≈用户数/2。
-Failures:失败请求数。Locust把response.failure()和超时都计入此列。当Failures开始爬升,立刻暂停测试——这不是性能瓶颈,是脚本或环境问题。
-Response time (ms):响应时间中位数(50%)、95%分位(95%)。95%分位比平均值更有意义,它代表“95%的用户感受到的延迟”。如果95%分位突然跳到2000ms,而平均值才800ms,说明有少量请求严重超时,需查慢SQL或锁表。
截图中RPS稳定在120,95%分位420ms,Failures为0——这是健康压测的典型特征。如果RPS波动剧烈(如100→30→150→0),大概率是besttest.py里wait_time设得太小,或服务器资源不足。
4.3 截图3.png:实时图表分析——如何识别“假稳定”
Locust默认展示四张图表:RPS、响应时间、用户数、失败率。截图3.png聚焦响应时间曲线,你会发现两条线:
-绿线(50%):中位数,平缓上升。
-紫线(95%):95分位,出现锯齿状波动。
这种波动不是噪音,是预警信号。当95%线频繁突破500ms阈值,而50%线仍低于300ms,说明少数请求被阻塞。常见原因:
- 数据库连接池耗尽(应用日志出现HikariCP - Connection is not available)
- Redis缓存击穿(大量请求穿透到DB)
- 文件描述符不足(ulimit -n默认1024,1000用户并发可能不够)
解决方案不是加机器,而是查日志。我在截图旁加了批注:“此时立刻执行kubectl logs -f <pod-name>,过滤timeout和Connection refused”。
4.4 截图4.png:用户分布热力图——暴露脚本逻辑缺陷
Locust的“Charts”页有个隐藏功能:点击右上角齿轮图标,勾选“Show user count per task”。截图4.png显示了各任务执行次数占比:
-browse_products: 62%
-add_to_cart: 25%
-check_orders: 13%
这和脚本里@task(5)、@task(2)、@task(1)的权重比(5:2:1)完全吻合。但如果这里显示browse_products只有30%,说明脚本有致命错误:比如browse_products方法里写了time.sleep(10),人为拉长了任务耗时,导致单位时间内执行次数锐减。
实操技巧:压测前先用10用户跑1分钟,看热力图是否符合预期权重。不符合?立刻检查
@task装饰器和wait_time设置。
4.5 截图5.png:下载报告——CSV比HTML更值得保存
Locust Web界面右上角有“Download Report”按钮,生成HTML报告。但截图5.png特意展示了下载的stats_history.csv文件内容:
timestamp,rps,failures,avg_response_time,median_response_time,95_percentile_response_time 1698765432,118.2,0,382,365,418 1698765433,121.5,0,379,362,415 ...为什么强调CSV?因为HTML报告是单次快照,而CSV是每秒采样,可导入Excel做趋势分析。比如用Excel画出“95%分位响应时间 vs 时间”折线图,能清晰看到性能拐点——当曲线斜率突然变陡,就是系统瓶颈出现时刻。
4.6 截图6.png:手动停止测试——Locust的“反自动化”哲学
截图6.png是点击“Stop”按钮后的界面,显示“Swarming stopped”。这里我要强调文档里反复写的那句话:“Locust不支持预设起止时间”。截图里时间显示“Running for 4m 22s”,但这个时间是Locust自己计的,它不会自动停。你必须人工点击Stop,否则测试永不停止。
为什么这样设计?因为压测不是批处理任务,而是科学实验。当95%响应时间突破500ms,或错误率升到5%,你应该立即终止,记录此刻的RPS和资源使用率,而不是等满10分钟。Locust强迫你保持专注,这恰恰是专业性的体现。
避坑提示:截图6.png右下角有个小字“Auto-reload disabled”。如果你在开发脚本时启用了
--autoreload,修改besttest.py会自动重启,但正式压测必须关掉它——否则代码变更可能导致测试中断,数据不连续。
5. 常见问题与排查技巧实录:27次压测总结的12个高频故障
5.1 故障速查表:按现象分类的解决方案
| 现象 | 可能原因 | 快速验证命令 | 解决方案 |
|---|---|---|---|
| RPS远低于预期 | wait_time设置过大 | grep "wait_time" besttest.py | 改为between(0.1, 0.5)测试极限吞吐 |
| 大量503错误 | 后端服务连接池耗尽 | kubectl top pods看CPU/MEM | 调大后端max_connections,或降低Locust并发 |
| 响应时间持续攀升 | 数据库慢查询堆积 | mysql -e "show processlist" | 加索引,或优化SELECT *为指定字段 |
| Worker注册失败 | master端口被防火墙拦截 | telnet 192.168.1.100 5557 | 开放安全组5557端口 |
| CSV报告为空 | 未启用--csv参数 | locust -f besttest.py --help \| grep csv | 启动时加--csv=report --csv-full-history |
5.2 真实案例:一次“幽灵错误”的排查全过程
现象:压测登录接口,RPS稳定在800,但Failures列缓慢爬升至12%,错误信息全是ConnectionResetError: [Errno 104] Connection reset by peer。
排查步骤:
1.确认不是网络问题:在worker节点curl -v https://api.example.com/api/v1/login,返回200正常。
2.检查后端日志:kubectl logs -f api-deployment \| grep "reset",发现大量java.io.IOException: Broken pipe。
3.定位根源:后端Spring Boot配置了server.tomcat.connection-timeout=20000(20秒),而Locust默认network_timeout=60。当请求处理超20秒,Tomcat主动断连,Locust收到RST包,记为失败。
4.解决方案:在besttest.py里显式缩短超时:python class WebTestUser(HttpUser): network_timeout = 15.0 # 必须小于后端connection-timeout connection_timeout = 15.0
这个案例教会我:压测失败率从来不是Locust的问题,而是上下游配置不匹配的报警灯。每次看到Failures上升,第一反应不是调Locust参数,而是查后端服务的超时、连接池、GC日志。
5.3 终极避坑清单:写在脚本注释里的血泪教训
我把最痛的教训直接写进了besttest.py的头部注释,确保每次打开都能看到:
""" LOCUST压测脚本:besttest.py ⚠️ 重要警告(请逐字阅读): 1. 不要修改host为http://localhost!必须填真实域名,否则请求发到Locust自身。 2. wait_time必须小于后端服务的超时时间,否则必然出现ConnectionResetError。 3. 所有敏感数据(密码、Token)必须用环境变量注入,禁止硬编码! 正确:os.getenv("API_PASSWORD", "default") 错误:password = "123456" 4. 分布式压测时,master和worker的Locust版本必须完全一致(包括补丁号),否则worker注册失败。 5. 压测前务必清空Redis缓存!否则缓存命中率虚高,掩盖真实DB压力。 6. 每次压测后,用'locust --csv=report --csv-full-history'保存原始数据,HTML报告只是快照。 """最后分享一个小技巧:在requirements.txt里锁定版本:
locust==2.15.1 gevent==23.9.1 requests==2.31.0别用locust>=2.0——Locust 2.16.0引入了新的事件循环,和旧版gevent不兼容,会导致worker静默退出。版本锁死是稳定压测的底线。
我在实际压测中发现,最可靠的压测不是追求最高并发,而是每次测试条件可复现、每次失败原因可追溯、每次结论有数据支撑。Locust做不到全自动,但它把控制权交还给人——这正是专业压测该有的样子。
本文还有配套的精品资源,点击获取
简介:在Linux系统中快速启动Web服务压力测试,直接用pip install locust安装框架,配合besttest.py定义用户行为和HTTP请求逻辑,开箱即用。包里带6张真实操作截图(1.png到6.png),覆盖启动Locust Web界面、配置并发用户数、启动测试、实时监控图表、手动停止等关键步骤。特别说明Locust不支持自动定时启停,必须人工点击stop按钮结束压测,避免误判结果。对比了LoadRunner、JMeter(偏Java生态)、ab和WebBench等工具,突出Locust基于Python语法简洁、学习成本低、易于二次开发、原生支持分布式节点扩展的优势。附带两篇实战向技术博客链接,讲清楚事件循环机制、TaskSet任务组织方式和常见断言写法。所有内容面向动手场景,不讲抽象原理,适合已有基础Python能力的测试工程师或运维人员做接口级或页面级并发验证。
本文还有配套的精品资源,点击获取