从JMeter到Locust:接口压测实战全解析 如果你负责过一个稍微有点规模的接口项目迟早会面对同一个问题上线之前它到底扛不扛得住我第一次正经做接口压测用的是JMeter后来项目业务越来越复杂——登录要签名、下单要加密、每一步还要把上一步的返回值带过去——JMeter那套图形化方式反而变成了累赘。换成Locust之后压测脚本直接拿Python写整个压测干脆变成一个小工程来维护。十几轮压测做下来Locust已经是我做接口压测的首选工具。这篇文章就把我从零上手Locust到实际落地做接口压测的完整过程写出来包括怎么选型、怎么写脚本、怎么读报告、怎么做分布式以及那些文档里没写但你一定会踩的坑。不管你是刚接触性能测试的开发者还是已经在用JMeter但被业务逻辑折磨过的老手这篇都值得你花十分钟过一遍。1. 为什么偏偏是Locust和JMeter、wrk走一遭之后的选择1.1 Locust和JMeter的取舍点JMeter在接口压测界的普及度不用多说社区资料多、插件全、录脚本也方便。但它的核心是用XML保存测试计划一旦遇到每次请求都要动态计算签名先调A接口拿token再带token调B接口响应里的某字段要提取出来参与下一次计算这类真实业务场景你只能在BeanShell或者JSR223里写辅助代码那体验基本等同于在图形界面里做嵌入式开发调试一次能让人崩溃。Locust不一样。它的测试脚本就是普通Python文件整个测试逻辑就是你写的代码。你在业务代码里怎么组织函数、怎么处理依赖、怎么解析JSON在压测脚本里就怎么写工程能力可以完全复用。团队成员review压测脚本时也像是在review业务代码一样顺畅不用理解一堆图形组件的配置含义。还有一个很实际的问题JMeter的脚本保存下来是jmx文件给别人看的时候别人得先装JMeter打开才能明白你在测什么。Locust脚本就是代码文件丢到代码仓库里diff、版本管理、code review全都走正常研发流程。1.2 wrk很好但扛不住业务链路的复杂度wrk是轻量级压测的王者一台机器打几万QPS不是问题。但它的问题在于想模拟一条完整的业务链路很痛苦。虽然wrk也支持Lua脚本可一旦你要做鉴权、做参数关联、做服务端响应依赖Lua脚本的复杂度和可维护性就会迅速失控。接口压测和纯性能基准测试是两回事。基准测试只关心某个接口的极限QPS而接口压测更关心真实用户这么操作时系统会不会挂、响应会不会劣化。真实用户行为是带着思考时间、带着登录态、带着多步骤操作顺序的。wrk擅长前者Locust天生为后者设计。1.3 Locust适合的接口压测场景清单我给Locust划了一个使用边界放在这个场景下选它几乎不会错单接口或者多接口的混合压测需要按业务比例分配请求需要模拟登录态、携带动态Token、依赖上下游返回结果的链路压测需要把压测脚本纳入代码仓库做版本管理的团队需要和CI/CD集成每次发版前自动跑一轮轻量冒烟压测的团队要做长时稳定性测试观察是否存在内存泄漏和缓慢劣化反过来如果你只是要测某个静态资源的极限吞吐或者要打的是纯TCP私有协议那Locust就不是最优选wrk甚至直接写工具脚本更合适。工具选型没有万金油先想清楚自己测的目标是什么。2. 落地第一步装环境、写脚本、跑起来2.1 安装细节和版本选择直接给命令python3 -m venv venv source venv/bin/activate pip install locust安装完确认一下版本locust --version我建议压测环境用独立的虚拟环境不要和业务项目混在一起。原因很实际压测脚本依赖的第三方库和业务项目不一定兼容虚拟环境隔离能省掉很多依赖冲突的麻烦。当前Locust已经是2.x时代API和1.x差别不小。网上很多老教程还在写TaskSet、HttpLocust这些旧写法如果你安装的是新版照着老教程写会直接报错。判断标准很简单安装完成后执行locust --version如果是2.x直接看官方文档的Modern写法即可用HttpUser和task装饰器。2.2 第一份HttpUser脚本逐行解读一个能跑的最小脚本长这样from locust import HttpUser, task, between class ApiUser(HttpUser): wait_time between(1, 3) task def health_check(self): self.client.get(/health)逐行解释一下HttpUser代表一类虚拟用户每个虚拟用户是一个协程在Locust里叫做User实例。wait_time between(1, 3)表示每个用户每次执行完任务后会随机等待1到3秒再执行下一个任务。这个值模拟的是真实用户的操作间隙也叫思考时间。task装饰器标记一个方法为压测任务。任务方法每秒被调用的次数取决于用户数量和wait_time。self.client是一个基于requests.Session的HTTP客户端和requests库的用法完全一致支持get、post、put、delete这些常用方法。跑起来也非常直观locust -f locustfile.py --host https://api.example.com然后在浏览器打开http://localhost:8089填上用户数、每秒生成用户数、要压测的地址点开始就能看到实时图表。2.3 Web模式和headless模式怎么选Locust有两种运行模式我实际用下来各有分工。Web模式适合压测前期的探索阶段。你会启动一个本地Web服务在浏览器里可以实时调整并发用户数、观察图表、暂停和停止测试。这个模式的好处是灵活可以一边跑一边调参数适合第一次接触被测系统、不清楚它承受能力的时候。headless模式适合自动化场景。不启动Web界面直接在命令行用参数控制并发数、压测时长跑完自动退出locust -f locustfile.py --headless -u 200 -r 10 -t 10m --host https://api.example.com这条命令的含义是模拟200个虚拟用户每秒启动10个新用户持续压测10分钟后自动停止。这个模式我会把它做成一个shell脚本或者CI任务发版前自动执行严重程度一目了然。2.4 常用启动参数速查我整理了一份自己常用的参数表记不住全部没关系把这几个记住就够覆盖绝大多数场景了参数作用示例-f指定压测脚本文件-f locustfile.py-u虚拟用户总数-u 200-r每秒启动的用户数-r 10-t压测持续时间-t 2m、-t 1h--headless不启动Web界面直接跑无参数--host覆盖被测系统地址--host http://localhost:8000--csv定时输出CSV格式结果文件--csv result--only-summary只输出汇总数据不打点明细日志无参数有两点容易被忽略一是-t的单位支持s、m、h不写单位默认是秒二是--csv result会生成result_stats.csv和result_stats_history.csv前者是统计汇总后者是随时间变化的历史数据长压测后画趋势图特别有用。3. 建模才是压测的关键task、wait_time和用户生命周期3.1 任务权重怎么分真实的线上流量不会只打一个接口。用户可能先看列表再进详情偶尔才创建一个订单。Locust里的task权重就是干这个的。from locust import HttpUser, task, between class ShopUser(HttpUser): wait_time between(0.5, 2) task(5) def view_products(self): self.client.get(/api/products) task(2) def view_detail(self): self.client.get(/api/products/123) task(1) def create_order(self): self.client.post(/api/orders, json{product_id: 123, count: 1})权重为5的任务被选中的概率是权重为1的任务的5倍。也就是说每执行8次任务大约5次是浏览列表2次是查看详情1次是创建订单。这个比例应该来自业务分析而不是拍脑袋。我踩过的一个教训是权重分配失真会让压测结果严重偏离真实场景。有一回我只压了重接口下单逻辑结果下游库存服务先被打挂了但其实真实流量里下单占比低得多。所以做权重之前先去看线上接口的调用量分布没有监控数据的至少让运营或者后端同学给个估算。3.2 wait_time选不好压测就是自欺欺人wait_time看起来只是一个小参数实际上决定了压测模型是否合理。常见的有三种between(1, 3)每个任务执行后随机等待1到3秒最常用constant(2)每个任务后固定等待2秒constant_pacing(5)每个用户每隔5秒发起一次请求不管任务执行多久constant_pacing经常被误解它保证的不是每次都等5秒而是两次请求开始之间的间隔为5秒。如果任务本身执行了2秒那实际等待时间就是3秒如果任务执行了6秒那就几乎不等待立刻执行下一个任务。这个模式适合对请求频率有精确要求的场景。我的建议是模拟普通用户操作时用between差距不要拉太大1到3秒之间比较真实模拟接口轮询场景时用constant_pacing不要为了追求高RPS把wait_time设成0那测出来的是服务器的极限不是用户实际体验到的吞吐。3.3 on_start/on_stop和登录态管理接口压测绕不开登录态。Locust用on_start方法处理用户启动时的初始化最典型的就是登录拿Tokenimport uuid from locust import HttpUser, task, between class OrderUser(HttpUser): wait_time between(1, 3) def on_start(self): username fperf_{uuid.uuid4().hex[:8]} resp self.client.post(/api/login, json{ username: username, password: passw0rd }) token resp.json()[token] self.client.headers.update({Authorization: fBearer {token}}) task def get_orders(self): self.client.get(/api/orders)这里有两个关键点。第一on_start在每个虚拟用户启动时执行一次200个用户就有200次登录请求。如果你的登录接口有验证码、有风控压测前一定要处理掉否则用户全部卡在登录阶段后面接口根本没流量。第二self.client.headers.update()是直接修改当前用户HTTP会话的全局请求头后续所有请求都会自动带上这个Token。如果你要模拟不同用户有不同的Token就不能把headers写在类属性里必须写在on_start里因为每个用户实例的self是独立的。on_stop方法对应测试结束时用户实例的清理比如注销登录、释放资源。实际压测中它不是必需但如果你压测的目标系统有连接数上限之类的限制做好清理能让多轮测试之间不互相污染。4. 压测报告别只看绿色RPS、TP99、失败率怎么解读4.1 实时图表里每个数字是什么很多人打开Locust的Web界面看到一堆数字往上跳就觉得一切正常这是最危险的理解。Locust统计面板上几个核心指标的含义必须搞清楚指标名称含义怎么判断是否健康Users当前并发用户数应该等于设定的目标值RPS每秒请求数即吞吐量看是否随着用户数增加而增长avg平均响应时间反应整体体验但会被极端值拉高50%ile中位数响应时间一半请求比它快一半比它慢95%ile / 99%ile近端到近最慢的响应时间尾延迟指标是最值得关注的Failures失败请求数非零就需要排查我特别想强调百分位数的价值。平均响应时间很容易骗人假设100个请求里99个都是20毫秒只有1个是3秒平均值是50毫秒看起来完美但实际上有1%的请求用户已经明显感觉到卡了。看99%ile甚至99.9%ile才能判断极端场景下系统表现如何。4.2 怎么找到容量拐点做容量评估时不能一上来就压最大并发那样只会看到一个已经挂了的结果。正确做法是阶梯加压从低并发开始逐步增加用户数观察RPS和响应时间的变化曲线。我在实际项目里总结的拐点判断思路是这样的并发用户数从50加到100RPS跟着翻倍响应时间基本平稳——说明系统还有余量从100加到150RPS增长变缓响应时间开始抬升——接近拐点从150加到200RPS几乎不涨响应时间急剧上升——已经超过容量上限这个过程中最关键的观察对象是RPS是否还在随用户数增长。如果RPS开始持平甚至下降说明系统已经饱和背后通常是数据库连接耗尽、线程池打满、或者CPU已经跑满。此时再增加并发只会让响应时间恶化不会提升吞吐。我建议在压测时打开Locust的图表页面把RPS和响应时间两个图一起看当RPS曲线出现平台期的那一刻基本就是你要找的容量拐点。把它记录成当前系统配置下的基准数据后面做了优化再对比优化效果一目了然。4.3 从失败类型反推瓶颈Locust的失败统计里会记录失败的类型和原因这部分信息是排障的第一现场。我总结过一套常见的失败类型排查对照表失败现象常见原因排查方向ConnectionError对端不接受新连接/连接被拒绝看服务端连接数是否打满、netstat看端口队列Timeout请求等待响应超时或TimeoutError线程池/连接池耗尽SQL慢查询HTTP 5xx服务端处理异常看应用日志、检查数据库连接和中间件状态HTTP 429触发了限流确认是否有网关或接口层限流策略响应内容不符合预期业务逻辑出错可能是断言写错也可能真的返回了错误数据这里给一个排障的基本功压测的时候同时开三个窗口一个跑Locust一个top看施压机和服务器的CPU/内存一个tail -f看服务端日志。Locust的界面只是症状板真正的原因通常要结合服务端监控才能定位。头几次做压测你可能只盯着Locust面板看后来你就会明白压测是个全链路工程报告是结果服务端监控才是诊断依据。5. 压测规模上去之前参数化、分布式和稳定性5.1 测试数据准备接口压测最容易犯的错误之一是用同一份数据打全量请求。所有用户都查同一个订单、更新同一条记录首先会在数据库层面造成锁竞争其次结果不反应真实分布。在做压测前我会先准备数据池用CSV文件存一批测试账号或者业务数据脚本里随机取import csv import random from locust import HttpUser, task, between def load_users(filepathusers.csv): with open(filepath, r) as f: return list(csv.DictReader(f)) class DataUser(HttpUser): wait_time between(1, 3) def on_start(self): user random.choice(load_users()) resp self.client.post(/api/login, json{ username: user[username], password: user[password] }) self.client.headers.update({Authorization: fBearer {resp.json()[token]}}) task def get_profile(self): self.client.get(/api/profile)这里有两点值得注意。第一random.choice是从列表里随机选如果数据池里有1000个账号每个用户只会随机选择一次不会所有用户都抢同一个。但也要控制数据量的规模如果账号用完了会重复使用网络上有不少团队直接压测时把所有用户数据随机全量灌进去结果压测机器本身的CPU先被随机数生成消耗掉了这是施压侧的问题。第二有些接口对同一个数据的重复操作会触发幂等校验或者创建重复记录这种接口压测时应该用不同的随机参数比如用户名带随机后缀、订单编号带时间戳避免后端的唯一约束变成压测失败的来源。5.2 master/worker分布式部署单台施压机的并发能力有限原因是多方面的文件描述符上限、网络连接数限制、Python进程本身的资源开销。当单机无法模拟足够大的用户量时就要上分布式。Locust的分布式模型是master/worker模式# 在master节点启动 locust -f locustfile.py --master --host https://api.example.com # 在worker节点启动 locust -f locustfile.py --worker --master-host192.168.1.10master节点负责调度和汇总数据worker节点负责实际产生压力。你也可以在浏览器里直接通过master的Web界面控制workers的开始和停止。我做分布式压测时踩过一堆坑给你排几个雷master机器不需要太高配置但不要在同一台机器上同时跑master和多个worker调度进程会抢占资源worker节点的Python版本和依赖库必须和master一致脚本在master上能跑不代表worker上就能跑所有worker节点的系统时间要同步CSV历史数据的时间戳才能对齐施压机和目标服务在同一个局域网时网络延迟会小很多但也要考虑真实用户更可能是跨地域访问如果压测目的是验证公网链路需要在多个地域部署worker单机文件描述符限制这个点值得单独说。默认Linux系统的ulimit -n通常是1024压测几百并发时每个用户会建立HTTP连接加上DNS解析等系统开销很容易碰到这个上限。压测前先执行ulimit -n查看临时改大用ulimit -n 65535永久修改要改/etc/security/limits.conf。5.3 长时间稳定性测试的注意点容量压测解决的是能不能扛得住的问题稳定性压测解决的是能不能一直扛得住的问题。我做过最久的一次压测持续了24小时就是为了验证一个服务在长时间高负载下是否存在内存泄漏。长时间跑压测有三件事你会感激自己提前做了。第一用--csv定时保存结果。跑24小时之后浏览器刷新一下结果就丢了的场景我不希望你经历。--csv result会周期性把统计数据写到文件里压测中断也能保住大部分数据。第二注意施压机自身的资源占用。Locust虽然基于协程但每个虚拟用户依然有对象开销跑几小时后master节点统计的数据量也会越来越大。我的习惯是每2小时记录一次施压机的CPU和内存如果施压机自身开始swap那测出来的RPS就不准了因为瓶颈已经出现在施压侧。第三稳定性压测看的不只是平均指标更关键的是趋势。RPS是不是随时间缓慢下降响应时间是不是逐步爬升失败率是不是在某个时间点突然出现这些需要用--csv输出的历史数据画趋势图或者在监控系统里看服务端指标随时间的变化。6. 实测中踩过的坑文档没写清楚的细节6.1 keep-alive与连接复用Locust的HTTP客户端继承自requests.Session默认具备keep-alive能力也就是说同一个虚拟用户的多次请求会复用同一个TCP连接。这个特性一旦被忽略会出现一个很隐蔽的问题压测结果里连接相关的错误非常多但服务端CPU和内存都很低像是不接受连接的假象。我排查过一次200个并发用户压测失败率飙升服务端sar和top都显示负载不高最后用ss -s查看TCP连接状态发现大量TIME_WAIT连接。原因是压测脚本里每次请求都手动新建了client没有复用self.client导致每个请求都新建连接压测机器和服务端都被频繁建连拖垮了。这个坑的解决方案一句话就能概括所有请求都通过self.client发起不要在task方法里自己requests.get()。6.2 动态URL会污染统计记得用name聚合真实业务接口的URL往往带着动态参数比如/api/orders/10001、/api/orders/10002。如果你直接请求这些URLLocust会把每个不同路径都当成一个独立的统计项——1000个订单ID就有1000行统计指标零碎得没法看。解决办法是给请求加name参数task def get_order(self): order_id random.randint(10000, 99999) self.client.get(f/api/orders/{order_id}, name/api/orders/[id])加上name之后所有订单详情请求都会聚合到/api/orders/[id]这个统计项下。这个习惯我从第二次压测就开始用了因为它直接决定压测报告能不能看懂。无论任何路径参数、查询参数只要动态变化就统一聚合。6.3 单机压不上去不一定是对端不行我遇到过不止一次这种情况目标服务明明没到瓶颈但RPS就是上不去。后来在施压机上查了资源发现CPU已经打满了。Locust虽然用协程做并发但你的任务函数本身还是Python代码如果每个请求的响应体特别大解析JSON和字符串编码都会消耗CPU。施压侧优化可以从几个方向做能不解析响应体就不解析需要校验的时候用resp.status_code做状态判断不要在task里做复杂的业务计算压测脚本越轻量越好响应体很大的场景用streamTrue逐块读取并关闭连接释放资源单机确实压不动时用--processes 8让Locust在多进程下运行榨干多核CPU这里也提醒一下大响应体不仅仅影响施压机也会影响服务端的带宽和序列化开销。压测前先确认响应大小是否符合预期别让一个几MB的响应把整个压力测试带偏。6.4 断言、catch_response与失败统计Locust对HTTP状态码的处理逻辑是默认情况下4xx和5xx响应都会被记为失败。但真实业务里业务校验失败返回200或者返回400都是正常业务逻辑它不代表系统故障。如果你不做处理失败率统计里全是业务校验导致的假失败真实故障反而被淹没。正确的做法是使用catch_responseTrue主动控制from locust import HttpUser, task, between, constant_pacing class CheckUser(HttpUser): wait_time constant_pacing(2) task def biz_query(self): with self.client.post( /api/query, json{k: v}, catch_responseTrue ) as resp: if resp.status_code 400 and invalid_param in resp.text: resp.success() elif resp.status_code ! 200: resp.failure(funexpected status: {resp.status_code})这段逻辑解释一下catch_responseTrue会让请求不自动判断成败响应状态码由你在with块里手动判定。业务参数校验失败返回400时我主动调用resp.success()把它记为成功只有非预期状态码才调用resp.failure()记录为失败。还有一点容易被忽略resp.failure()只影响统计面板里的失败计数它不会让压测任务停止。Locust的设计哲学是压测仿真真实用户行为即使接口失败也继续操作而不是像测试框架那样fail-fast。如果你希望出现失败时快速终止压测可以在代码里手动抛异常但我更建议让压测跑完拿到完整体数据后再分析。另一个容易误解的点是Web界面和headless模式对失败的处理不一致。Web模式下你可以在界面上暂停、停止、修改并发数headless模式下如果设置了-t时间到了就会自动结束。正常结束前的统计汇总里会明确报告RPS、失败率、响应时间分布这些才是报告的核心素材。最后再分享一个个人习惯每次压测结束我会把命令、脚本、CSV结果和关键结论四件套一起归档到压测文档里。下次压测或排查线上问题时翻一翻历史记录就能快速定位之前这个接口在这个并发量下表现如何。压测不是一次性工作它是随着系统演进不断重复的日常事务把过程记录下来比任何一次惊艳的结果都更有价值。