资讯动态

Locust压测实战:命令行参数详解与分布式压测高阶指南

发布时间:2026/10/20 18:07:26 来源:尧图企业网站定制
上周给一个内部服务做上线前的容量评估我直接在命令行敲了几条 locust 命令几分钟后拿到了 P95、RPS 和失败率数据。同事问我为什么不用现成的压测平台我说因为整个压测链路就藏在 Python 脚本里环境有 Python 就能跑脚本跟着代码仓库走任何一次调整都有迹可循。这种轻量级的体验是图形化工具很难替代的。上一篇文章已经把 locust 的基础 demo 跑通了这篇我们把常用参数逐个拆开再带着你做一遍从 0 到 1 的高阶实战。如果你已经会写 User 类、知道 task 是怎么回事但一到命令行就不知道 -u、-r、-t 怎么配或者压测完对着报告一脸懵这篇就是给你准备的。全文的思路是先讲清楚为什么要用 locust、它在什么场景下真的比 JMeter 或 wrk 舒服再把核心参数一个个剥开讲透接着给一套可以直接复制的实战 demo最后聊压测报告怎么看、常见问题怎么排查。内容比较长建议收藏后按章节跟着做。1. 轻量级压测为什么选 locust先想清楚工具边界1.1 同样是压测工具locust 的区别在哪里很多人一提到压测就想到 JMeter。JMeter 功能确实全图形界面里配线程组、监听器、各种 Sampler都能跑起来。但它的脚本本质上是 XML不好做代码评审也不好放进 Git 里做 diff。我见过不少项目JMeter 脚本从测试环境改到生产环境参数都是手动在界面上改改完没人知道改了什么。压测结束之后想追溯某个请求体到底是谁改的基本靠聊天记录。wrk 和 ab 这类工具又是另一个极端。单机性能很强几十行配置就能打出很高的请求量但它们只能做很简单的路径压测。想模拟登录态、想依赖上一个接口返回的动态参数、想根据业务状态码做断言这些需求 wrk 就很难优雅地承接。locust 本质是一个 Python 程序。你执行 locust -f 脚本它会读你的 User 类按配置生成一堆虚拟用户每个用户是一个轻量协程。压测脚本就是普通 Python 文件可以用函数封装请求、把响应 JSON 拿出来做断言、根据失败码刷新 Token、按条件跳过任务。正常项目里几百行就能模拟一条完整的业务链路而且这些代码可以跟着业务代码一起走 review。这里简单解释一下“协程”。你可以把 locust 的虚拟用户想象成外卖平台的骑手后台不是给每个骑手单独开一个系统进程而是一个调度系统在不断切换任务。协程就是这种轻量级的调度单元。因为不是每用户一个线程所以用户数从几百涨到几千甚至几万内存不会跟着线程数量爆炸。这也是 locust 敢把用户数开很大的底气。1.2 什么场景真正适合 locust选型这件事不是看工具名气而是看你要压的东西长什么样。我根据自己的经验把适合和不适合的场景都列一下。适合用 locust 的场景接口之间有依赖关系比如登录拿 token再用 token 查列表、建订单。这种链路过长时JMeter 里的关联正则表达式会写到你怀疑人生而 locust 里就是一个普通变量。请求参数需要动态生成比如随机用户名、随机金额、时间戳、签名串。这些逻辑本身就是 Python 代码写起来没有任何心智负担。需要做业务级别的断言。比如 HTTP 200 不代表成功只有响应体里 code0 才算成功。locust 抓到响应后可以自由判断。想接入 CI 流水线。headless 模式跑完自动退出结果以 CSV 和 HTML 输出退出码可以供流水线判断。需要长时间稳定性观测。locust 的 CSV 历史数据可以描绘出指标随时间变化的曲线方便定位内存泄漏或缓慢劣化。不太适合的场景只测单个接口的极限吞吐量wrk 更直接一个文件搞定资源占用也更低。纯协议层的压测比如 TLS 握手、UDP、gRPC 的复杂交互locust 虽然可扩展但生态和支持度不如专业工具。团队里所有人已经非常熟悉 JMeter 的流程并且有现成脚本资产。这时候硬换工具迁移成本会抵消技术上的优势。我个人建议是一个团队里至少保留一套 locust 脚本专门应对接口链路复杂、需要频繁变化的场景。简单压测交给轻量工具复杂业务链路交给 locust各干各擅长的活。2. 常用参数详解命令行背后你到底配了什么很多初学者把 locust 的启动命令当成“魔法咒语”在背其实每个参数背后都有明确的目的。下面先给一张速查表后面再逐个展开。参数示例作用--users-u 1000虚拟用户总数--spawn-rate-r 20每秒孵化的用户数--run-time-t 10m压测运行时长--headless--headless不使用 Web 界面--host--hosthttp://127.0.0.1:8000覆盖目标主机地址--csv--csvresult/xxx输出 CSV 报告前缀--html--htmlresult/xxx.html输出 HTML 报告--tags--tagsorder只执行带 order 标签的任务--exclude-tags--exclude-tagsdemo排除带 demo 标签的任务--stop-timeout--stop-timeout5停止时等待任务结束的最长秒数2.1 --users用户数量不是并发请求数这是最容易误解的参数。--users 指的是同时在线的虚拟用户数。每个虚拟用户都是一个协程它会循环执行任务发起请求、等待、发起下一个请求、再等待。假设你在代码里写了 wait_time between(1, 5)那么一个用户平均每 3 秒发起一次请求期望值 3 秒。100 个用户的情况下理论上每秒能产生大约 33 个请求。这里的计算方法是RPS 约等于用户数除以平均请求间隔也就是 100 除以 3。这个认知非常关键。很多人以为“并发 1000”就是系统同时有 1000 个请求打到服务上这是不对的。更准确的说法是“系统同时有 1000 个用户在线”他们按自己的节奏操作真正落在服务端的请求速率由等待时间和任务执行时间共同决定。所以当你发现用户数已经调到 500但目标接口的 RPS 还是上不去第一反应应该是去检查 wait_time而不是盲目继续加用户。2.2 --spawn-rate慢速孵化比秒杀式上线更有效--spawn-rate 控制用户从 0 增长到目标数量的速度。默认值是 1也就是每秒增加 1 个用户。如果你不传这个参数500 个用户要孵化 500 秒很多时候你等不到全部跑起来就已经到运行时长上限了。为什么不建议直接拉满打个比方你测试一座桥能承受多少吨重量科学做法是让货车逐渐增加而不是先派一列满载的重卡冲上去。瞬时把大量用户压上去服务可能在前几秒就拒绝连接但你根本不知道它是在哪一秒开始崩的。慢速孵化让流量按梯度上升你可以观察响应时间从哪个用户量开始爬升错误率从哪个点开始出现这样才能定位系统的容量拐点。如果你知道想要什么效果可以简单算一下。想用 10 分钟把 1000 个用户放完spawn-rate 就是 1000 除以 600 秒约等于 1.67取整为 2。想快一点5 分钟放完那就是 4。注意spawn-rate 也可以大于用户总数。这种情况下 locust 会在几秒内创建完所有用户。这个技巧在快速验证脚本是否报错的时候很省时间。2.3 --headless 和 --run-timeCI 环境下的正确姿势Web 界面模式适合本地调试。你能在浏览器里看到用户数曲线、实时 RPS、响应时间还可以手动调整用户数。但真实压测尤其是流水线里的压测不可能每次都打开浏览器点按钮。--headless 就是无界面模式。locust 启动后不开启 Web 服务直接按参数执行压测跑完自动退出。配合 --run-time 可以精确控制运行时长比如 10m 就是 10 分钟30s 就是 30 秒。一个典型的命令长这样locust -f order_demo.py --headless -u 100 -r 10 -t 3m这个命令的意思是无界面模式一共 100 个用户每秒孵化 10 个跑 3 分钟。压测结束之后进程退出退出码 0。这对 CI 来说就是一条普通命令可以直接写进流水线。如果希望压测在某个时间点提前结束可以用 --stop-timeout。它表示运行时长到了之后最多再等待多少秒让当前任务结束超时则强制关闭。默认情况下locust 会尽量让你当前正在执行的任务跑完但有些长任务可能一直卡住这时候 stop-timeout 就是保险丝。2.4 --host 与任务标签过滤--host 用来覆盖目标主机地址。它的优先级高于脚本里 HttpUser.host 属性。开发的时候你可以在代码里写死一个本地地址上压测环境的时候用 --host 指定真实地址这样不用改代码。另外一个特别实用的参数是 --tags 和 --exclude-tags。它们配合 tag 装饰器使用可以让你一个脚本文件里放多个业务场景然后按需执行。举个例子你的脚本里有登录、查询订单、创建订单三个任务但这次只关心查询接口那就可以这样locust -f order_demo.py --headless -u 100 -r 5 -t 1m --tagsquery启动后 locust 只执行带 tag(query) 标记的任务。这样省去频繁拆文件的麻烦也让脚本的可复用性提高不少。多标签用逗号分隔比如 --tagsquery,create。3. 进阶参数与分布式压测从单机走向多机3.1 master/worker 是怎么回事单机压测最大的瓶颈往往不是目标服务而是压测机自己。用户数一多每个协程都在构造请求、解析响应、维护统计CPU 很快就会被吃满。一旦压测机 CPU 打满你测出来的数据就不是服务真实水平而是压测机的加班成果。分布式压测就是为了解决这个问题。locust 把节点分成 master 和 worker。master 负责调度和汇总结果不直接产生压力worker 真正执行压测脚本模拟用户流量。master 和 worker 之间通过 ZeroMQ 通信只要网络能通跨机器部署是很自然的事情。启动方式也很直观。master 端命令locust -f order_demo.py --master --master-bind-host0.0.0.0 --master-bind-port5557 -u 1000 -r 20 --headless -t 10mworker 端命令locust -f order_demo.py --worker --master-hostmaster_ip --master-port5557关键点在于用户数和孵化速率只在 master 端指定worker 端不需要重复写。worker 会从 master 接收调度指令知道自己该跑多少个用户。如果每个 worker 自己再配一套 -u 和 -r很容易出现预期之外的总用户数。从我的实测经验来看一台普通云主机扛 1000 个以内用户基本没问题。如果目标用户数到几千甚至上万建议拆 2 到 4 台 worker既分散 CPU 压力也能提高 spawn-rate 的吞吐能力。3.2 多机部署的注意事项分布式压测真正麻烦的地方不在命令而在网络和版本管理。第一master 的端口要对 worker 开放。默认是 5557 和 5558一个用于消息接收一个用于结果上报。如果你是在云内网环境跑安全组和防火墙都得放行这两个端口。第二所有 worker 上必须是一份完全相同的压测脚本Python 依赖版本也要一致。我吃过一次亏某台 worker 上的依赖包版本旧导致脚本跑出来的请求体和别的不一样最后统计报告里的错误率失真。第三worker 需要能访问目标接口同时也要能访问 master。有时候 worker 能压测但连不上 master会被默默忽略master 日志里也没有明显报错。所以启动后最好先确认 worker 已经成功连接再开始正式压测。第四避免在 worker 端单独生成报告。因为每个 worker 只会记录自己这一份数据手工合并很痛苦。正确做法是只能在 master 端传 --csv 和 --htmlworker 只要跑任务就行。3.3 等待时间的进阶玩法between、constant、constant_pacingwait_time 是控制用户请求节奏的核心参数也是影响 RPS 的第一变量。locust 提供了三种常见策略。between(1, 5) 是最常用的写法表示每个任务执行完成后随机等待 1 到 5 秒再执行下一个任务。这种方式模拟的是普通用户的随机性但要注意它的期望值是 3 秒如果你想让用户操作更频繁就得把区间缩小。constant(2) 表示固定等待 2 秒。适合那些节奏稳定的场景比如监控探针每隔固定时间拉取一次数据。constant_pacing(3) 是一个很多人没吃透的参数。它保证的是“一个完整任务周期大约 3 秒”而不是“固定等待 3 秒”。如果任务本身耗时 0.2 秒那么等待时间会自动补到约 2.8 秒如果任务耗时已经超过 3 秒那就不再额外等待直接执行下一个任务。这很适合模拟“用户每 3 秒操作一次”的稳定速率场景。我实际用下来constant_pacing 在控制端到端请求速率方面非常稳定。如果你想让目标接口保持特定的 QPS比如每秒 200 次可以通过用户数和 pacing 搭配计算而不是让用户疯狂地空转。4. 从 0 到 1 的压测 demo 实战4.1 场景设定与目标这一节我们构造一个完整的压测脚本目标是模拟一个订单服务的混合流量。包含三个操作登录获取 token、查询订单列表、创建订单。为了贴近真实用户行为查询的频率应该远大于创建订单所以我们把权重设计成 3 比 1。被测服务地址假设是本地的 http://127.0.0.1:8000。这里用本地地址是为了避免读者运行时踩到域名解析的坑方便快速复现。脚本整体结构是每个虚拟用户启动时先登录一次拿到 token 放到请求头里之后循环执行带权重的任务。4.2 完整脚本与关键设计from locust import HttpUser, task, between, tag, events import random events.request_failure.add_listener def on_request_failure(request_type, name, response_time, exception, **kwargs): print(f[FAIL] {request_type} {name} {response_time}ms {exception}) class OrderUser(HttpUser): wait_time between(1, 3) host http://127.0.0.1:8000 def on_start(self): # 每个虚拟用户启动时先登录一次把 token 放到请求头 resp self.client.post( /api/v1/auth/login, json{ username: fload_user_{random.randint(1, 5000)}, password: passw0rd, }, ) token resp.json().get(token) self.client.headers.update({Authorization: fBearer {token}}) task(3) tag(query) def list_orders(self): with self.client.get( /api/v1/orders, params{page: 1, size: 20}, catch_responseTrue, ) as resp: if resp.status_code 401: resp.failure(token expired, need refresh) else: resp.success() task(1) tag(create) def create_order(self): payload { product_id: random.randint(1000, 9999), quantity: random.randint(1, 5), } self.client.post(/api/v1/orders, jsonpayload)这段脚本有几个值得关注的点。on_start 方法不是必须的但你可以在里面做初始化。这里是登录拿 token这样每个虚拟用户都会有一个独立的 token而不是所有用户共享同一个更贴近真实场景。但要注意on_start 本身也会消耗时间如果用户数很大孵化过程会变慢这是正常的。catch_responseTrue 并手动调用 resp.success() 或 resp.failure() 是个高级技巧。默认情况下locust 会根据 HTTP 状态码判断成功失败但很多接口的业务码和 HTTP 状态码并不是一一对应的。手动控制之后你可以把“HTTP 200 但业务失败”的响应也记成失败真正压出系统里不可见的问题。task(3) 和 task(1) 控制权重。list_orders 被选中的概率是 create_order 的三倍。短时间看任务执行的顺序可能不太均匀但请求量上去之后会趋近权重比例。监听函数用 events.request_failure.add_listener 绑定。当任何请求失败时打印一行错误信息方便实时排查。这个监听器在实际压测时很有用尤其是某些请求在 Web Summary 里已经被计入失败但你不清楚具体异常是什么。4.3 启动命令与参数组合调试阶段先用小流量验脚本locust -f order_demo.py -u 10 -r 2浏览器打开 http://127.0.0.1:8089你能看到用户数和 RPS 的实时曲线。先确认没有明显报错再正式压测。正式跑 5 分钟输出 CSV 和 HTML 报告locust -f order_demo.py --headless -u 500 -r 20 -t 5m --hosthttp://127.0.0.1:8000 --csvresult/order_test --htmlresult/order_test.html如果这次只想压查询接口不想创建订单locust -f order_demo.py --headless -u 100 -r 5 -t 1m --tagsquery这个命令组合非常实用。一个脚本多个场景用标签控制范围省得维护一堆文件。4.4 输出文件与运行细节加上 --csv 和 --html 之后压测结束时会在 result 目录下生成几个文件。后缀 _stats.csv 是汇总数据_stats_history.csv 是每隔几秒采样一次的时序数据.html 是带图表的可视化报告。CSV 文件用 Excel 或 pandas 都能打开适合进一步做分析。我建议正式压测前先花 30 秒跑一个小流量脚本确认所有接口路径、请求头、业务断言都是对的。因为压测脚本本身也是代码如果中间链路出了问题你很难分清到底是脚本 bug 还是服务瓶颈。这个习惯能在后面的问题排查中省下大量时间。5. 压测报告与指标解读别只盯着平均值5.1 控制台输出字段逐个拆headless 压测结束之后控制台会打印一个完整的 Summary。每一行表示一个请求类型和接口组合表头类似这样字段含义关注点Type请求方式可以看出是否有不必要的轮询Name接口路径或名称路径带动态参数时会按名称聚合# Requests总请求数结合运行时长可以估算 RPSFails失败数失败率往往比总百分比更有价值Median中位数50% 请求的耗时小于这个值90%ile90 分位开始暴露长尾问题的位置99%ile99 分位极端情况的表现Avg平均值容易被离群值带偏Min / Max最小 / 最大耗时Max 可以作为排查线索Requests/s每秒请求数系统真实吞吐很多人只看 Avg 和 Fails忽略分位数。这是一个大坑。假设 Median 是 30msAvg 是 300ms说明有少量请求特别慢把平均值拉高了。这时候系统的长尾问题已经很严重但你看平均值可能还在可接受范围内。真正要关注的是 99% 甚至 99.9% 的表现因为它决定了极端情况下用户会不会超时。如果 P90 和 P99 的差距很大说明系统存在明显的排队或阻塞现象可能是某个线程池被打满也可能是数据库连接不够。这时候不是单纯看 Avg 的高低而是要看长尾在哪一段开始拐弯。5.2 RPS 的计算逻辑与对比前面提到 RPS 约等于用户数除以平均请求间隔。这里放一张简单的理论对照表用户数wait_time理论 RPS500between(1,5) 期望 3s约 167500constant(1)约 500500constant_pacing(2)约 250实际压测中 RPS 会比理论值略低因为任务执行过程本身也消耗时间而这个时间不属于等待时间。你可以用这个公式反推用户数如果目标是 500 RPS平均每个请求间隔按 2 秒算那就需要约 1000 个用户。理解这个关系之后再看到压测报告里的 RPS 低于预期你就知道该往哪个方向调参数了。是用户数不够还是 wait_time 太大还是任务本身执行时间太长。5.3 CSV 报告与二次分析--csv 参数生成的两个 CSV 文件是压测后的宝藏。_stats.csv 是最终汇总_stats_history.csv 是动态采样的时序记录。长时间压测后你可以用 pandas 画一张 P90/P99 随时间的趋势图import pandas as pd import matplotlib.pyplot as plt df pd.read_csv(result/order_test_stats_history.csv) plt.plot(df[Timestamp], df[90%ile], labelP90) plt.plot(df[Timestamp], df[99%ile], labelP99) plt.legend() plt.title(Latency Trend) plt.show()趋势图比任何平均值都更能说明问题。我曾经做过一次 8 小时稳定性压测前两个小时指标很平第三个小时开始 P99 缓慢爬升第六个小时突然翻倍。如果只看汇总报告你只能看到一个偏高的 P99根本不知道它是从什么时候开始的。有了时序数据就能把劣化点和发版时间对上排查效率完全不同。6. 常见问题与避坑实录6.1 压测机 CPU 很高但目标服务负载很低这种情况我见过不止一次第一反应通常是脚本有问题。你在压测机上疯狂构造请求、解析响应、维护统计但这些请求并没有真正到达目标服务或者到达频率远低于预期。排查路径先看压测机的 TCP 连接数确认有没有大量请求处于 TIME_WAIT。再看脚本里是否执行了耗 CPU 的加密解密或字符串处理这些计算会严重拖慢压测机。然后检查 wait_time 是否设置过小导致协程空转。如果目标服务负载就是上不去先在压测机上用抓包工具确认请求是否真的发出去然后再去调参数。6.2 用户数设了 500RPS 却只有十几这也是一类高频问题。原因可能有这么几个wait_time 设置太长用户大部分时间都在等待。on_start 里有阻塞操作导致用户孵化到一半就卡住。任务内部逻辑提前 return请求根本没发出。压测机和目标服务之间网络延迟太高单请求耗时已经超过等待时间。我的建议是先用小用户数跑一遍把每个任务的响应时间、失败率都打印出来确认单请求行为正常再放大用户数。不要在用户数很大的时候才开始排查脚本逻辑那样问题会被数据量淹没。6.3 任务权重配了但任务分布总是不稳定task 的权重是概率性质的不是说创建订单权重为 1查询权重为 3就会严格按照 1:3 的顺序执行。短时间窗口内可能连续 5 次都是查询任务看起来分布很不均匀。这是正常现象请求数上去之后比例才会趋近权重。如果你希望严格按顺序执行任务那就得用顺序 TaskSet或者在一个任务里手动按顺序调用多个接口。但多数压测场景下随机权重反而更接近真实用户行为。6.4 TCP 端口耗尽与 TIME_WAIT高并发压测时压测机作为客户端每个连接都由四元组唯一标识。Linux 默认的本地端口范围通常只有几千个短连接用完一遍之后端口进入 TIME_WAIT 状态新连接无法复用就会出现大量连接失败。解决思路有两个方向。第一在脚本里复用连接开启 keep-alive减少新建连接的数量。第二调整内核参数扩大端口范围并允许复用sysctl -w net.ipv4.ip_local_port_range1024 65535 sysctl -w net.ipv4.tcp_tw_reuse1注意这些参数是在压测机上调整不是目标服务。如果你已经做了连接复用还是不够最直接的办法是增加压测机数量把连接压力分散到多台机器上。6.5 分布式压测时各 worker 重复输出报告很多人在分布式压测时给每台 worker 都传了 --csv结果每个节点都生成一份文件最后数据七零八落。正确做法是只在 master 端传 --csv 和 --htmlworker 端只运行压测任务。另外要确认所有 worker 的脚本版本一致。压测脚本的改动要同步到所有节点否则某台 worker 跑的是旧逻辑统计结果就会失真。我通常会把脚本放到共享目录或者打成包分发避免手动拷贝出现版本差异。结尾最后分享一个我自己保持了很久的习惯。每次新压测场景我都会先写一个只包含单接口的最小脚本跑 30 秒确认请求头、参数、断言都正确再逐步加上登录、依赖、多任务权重。因为压测脚本本身也是代码一旦中间链路出问题你很难分清是脚本 bug 还是服务瓶颈。locust 最大的价值不是能打出多高的并发数而是能把压测真正做进日常开发流程里。脚本跟着代码走参数可以覆盖报告可以自动生成一切都有迹可循。宁可多花半小时把脚本组织得干净也不要压完之后对着历史报告猜当时到底发生了什么。这篇就当是给上一个阶段做个总结下次有机会我再聊聊怎么基于 CSV 画出延时趋势图以及在流水线里做失败自动中断的实践。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑