做性能测试这些年我大部分时间都在跟 JMeter 打交道但真正让我觉得压测工具还能这么玩的反而是 Python 生态里的 Locust。当时接手一个内部 API 网关的压测任务要求模拟几千个并发用户做混合场景JMeter 的脚本越写越臃肿后来换成 Locust直接用 Python 写压测逻辑代码即脚本整个团队的维护成本都降下来了。这篇就聊聊 Locust 的基本使用适合刚接触性能测试、或者想从 JMeter 转到代码化压测的同学。1. 性能测试里为什么会有 Locust 的位置1.1 先搞清楚 Locust 到底解决什么问题性能测试的工具不少JMeter、LoadRunner、Gatling 各有各的拥趸但 Locust 的核心定位很明确用代码来定义用户行为用 Python 来描述业务场景。它不像 JMeter 那样靠 GUI 拖拽组件而是把虚拟用户直接映射成 Python 类——每个虚拟用户就是一个继承自User的类实例它要执行什么操作就去类里定义对应的方法也就是任务。我第一次用 Locust 时最大的感受是它不像工具更像框架。你可以用requests库的思路去写 HTTP 请求也可以用wait_time控制模拟用户的操作间隔还可以用between、constant这些策略模拟真实的人类行为节奏。相比 JMeter 的线程组 采样器模型Locust 的模型更接近一群用户在操作你的系统这个物理直觉。1.2 和 JMeter 对比各自的取舍在哪里很多团队在选型时纠结 JMeter 还是 Locust我的看法是不是谁取代谁而是看场景。JMeter 的优势是生态成熟、插件多、非技术人员也能上手Locust 的优势是贴近代码、支持复杂业务逻辑、分布式压测配置更简单而且报告里的响应时间分布、百分位数数据非常直观。对比维度JMeterLocust脚本方式GUI 组件 XMLPython 代码学习门槛低但复杂场景脚本繁琐需要 Python 基础扩展性插件体系二次开发成本高直接写 Python 类扩展自由分布式压测需要配置 Agent 和调度一条命令启动 master/slave实时监控有但界面偏传统内置 Web UI图表丰富适用场景接口级、流程级回归压测业务建模、自定义协议、数据驱动压测如果你做的是一次性的、流程固定的压力测试JMeter 没问题但如果你们的业务是快速迭代的压测脚本要跟代码一起进 Git 仓库、要支持 CI 里跑回归Locust 这种脚本即代码的方式明显更契合。再说个实际体验Locust 的 Web 界面里你能看到每个任务的吞吐量、响应时间、失败率还能动态调整并发数不需要重启压测——这点在做长时间稳定性测试时特别有用。2. 核心概念拆解读懂 Locust 的四个关键词2.1 User 类虚拟用户是怎么来的Locust 里最基本的类是User。你可以把它理解成一个人这个人的身份特征比如请求的 headers、token、走的协议都定义在类属性里。实际操作中更常见的做法是继承HttpUser它内置了一个 HTTP 客户端基于requests实现会自动帮你处理连接池、Cookie 这类事情。from locust import HttpUser, task, between class ApiUser(HttpUser): wait_time between(1, 5) task def get_homepage(self): self.client.get(/)这里的self.client就是HttpUser给你准备好的请求对象直接调用get、post就能发起请求。注意wait_time是类属性它决定了每个虚拟用户在执行完一个任务之后、执行下一个任务之前要等待多久。2.2 task 装饰器任务权重和多任务组合一个用户通常不止做一件事情。比如电商场景里用户可能先浏览商品再加入购物车最后下单。Locust 用task装饰器来标记任务而且可以传一个权重参数控制每个任务被执行的频率。task(3) def view_item(self): self.client.get(/item/10001) task(1) def add_to_cart(self): self.client.post(/cart, json{item_id: 10001})权重为 3 的view_item被选中的概率是权重为 1 的add_to_cart的三倍。这种写法非常直观想调场景比例就改数字一点都不折腾。我后来做混合场景压测基本都用这种权重方式比 JMeter 里配吞吐量控制器要清爽得多。2.3 wait_time 策略模拟真实思考时间不加wait_time的话虚拟用户会毫不停歇地发请求这会造成疯狂打接口的效果压测出来的数据往往比真实场景要狠。真实用户操作之间是有思考时间的所以要根据业务特点选等待策略。from locust import between, constant, constant_pacing wait_time between(1, 3) # 每次随机等 1~3 秒 wait_time constant(2) # 固定等 2 秒 wait_time constant_pacing(1) # 保证每秒发起约 1 个任务含执行时间我个人的建议读接口压测用between写接口压测用constant_pacing。原因后面讲参数计算时会提到——constant_pacing能更稳定地控制请求速率避免一窝蜂式的瞬时流量。2.4 任务顺序与用户生命周期HttpUser的生命周期有三个钩子on_start用户启动时执行一次、on_stop用户停止时执行一次以及每个任务前后的task方法。on_start里通常放登录、拿 token 这类一次性的准备工作class ShopUser(HttpUser): def on_start(self): resp self.client.post(/login, json{username: test, password: test123}) token resp.json()[token] self.client.headers.update({Authorization: fBearer {token}})注意on_start执行的耗时会计入单个任务的等待循环之外。如果登录耗时长大量用户同时启动时会造成启动阶段的请求堆积这个在压测前要心里有数。3. 环境准备与安装从零跑起第一个脚本3.1 安装和版本选择Locust 是一个纯 Python 包安装命令很简单pip install locust但有几个点要提前注意建议 Python 3.8 及以上版本官方持续支持的新版本都在 3.9 上测试过。Locust 2.x 和 1.x 的 API 差异不小网上很多老教程写的task和TaskSet用法在 2.x 里已经不推荐了。安装时尽量用最新稳定版。如果公司环境有 Python 版本管理工具建议用venv或poetry做隔离避免和项目依赖冲突。装完验证一下版本locust --version能正常输出版本号就说明环境没问题。3.2 启动一个最小可用的压测文件写一个最简单的locustfile.pyLocust 默认读取的文件名内容如下from locust import HttpUser, task, between class QuickUser(HttpUser): wait_time between(0.5, 2) task def index(self): self.client.get(https://example.com/)然后在终端执行locust启动后Locust 默认在 8089 端口起一个 Web 界面。浏览器打开http://localhost:8089填上要模拟的用户数、每秒启动速率和测试目标地址点开始就能跑。这是 Locust 最友好的地方——不需要写任何命令行参数也能完成一次基础的压测。3.3 文件命名和自定义文件路径很多人会忽略 locustfile 的命名约定。默认情况下 Locust 在当前目录找locustfile.py如果文件名不一样启动时要用-f指定locust -f my_load_test.py我习惯把压测脚本按模块拆分比如locustfile.py只放用户类和任务data.py放测试数据settings.py放公共配置。这样团队多人协作时每个人负责自己的文件冲突少复用也方便。4. 实操写一个完整的压测脚本4.1 覆盖登录、业务操作、数据校验的完整示例纸上谈兵没意思我拿一个典型的用户登录后查询订单、提交反馈场景来做完整示例。这个脚本覆盖了登录拿 token、接口依赖、断言和数据校验基本能应付日常 80% 的 HTTP 压测需求。import random from locust import HttpUser, task, between class OrderUser(HttpUser): wait_time between(1, 3) def on_start(self): # 登录并获取 token resp self.client.post(/api/login, json{ username: perf_user, password: perf_pass }) if resp.status_code ! 200: self.environment.runner.quit() # 登录失败就别测了 token resp.json().get(token) self.client.headers[Authorization] fBearer {token} def get_order_id_list(self): resp self.client.get(/api/orders?page1) data resp.json() if not data.get(items): return [] return [item[order_id] for item in data[items]] task(3) def query_orders(self): self.get_order_id_list() task(2) def submit_feedback(self): order_ids self.get_order_id_list() if not order_ids: return order_id random.choice(order_ids) resp self.client.post(/api/feedback, json{ order_id: order_id, content: perf test feedback }) if resp.status_code ! 200: resp.failure(ffeedback failed: {resp.status_code}) task(1) def check_profile(self): with self.client.get(/api/profile, catch_responseTrue) as resp: if resp.status_code 200 and resp.json().get(user_id) is None: resp.failure(user_id missing in response)这个脚本里有两个细节值得注意一是submit_feedback依赖query_orders返回的数据说明 Locust 任务之间可以做数据传递二是catch_responseTrue配合resp.failure()可以自定义失败判定——不是所有 200 都代表成功响应内容不符合预期也要标记为失败这个在压测里太重要了。4.2 压测数据准备与参数化真实压测不能所有用户都打相同的订单号否则命中缓存太严重读接口的压测结果会虚高。参数化的思路很简单从 CSV 或数据库批量读取用户凭证用itertools.cycle或随机选择器给每个用户分配不同数据结合on_start只在用户启动时加载一次数据避免每个任务都读文件拖慢性能import itertools from locust import HttpUser, task, between users [perf_user_{}.format(i) for i in range(500)] user_cycle itertools.cycle(users) class ParamUser(HttpUser): wait_time between(0.5, 1) def on_start(self): self.username next(user_cycle) resp self.client.post(/api/login, json{ username: self.username, password: same_password }) self.token resp.json().get(token)一个容易踩的坑是如果你在on_start里给每个虚拟用户分配了唯一用户名但模拟上千个用户时用户池只有 500 个itertools.cycle会循环复用。这本身没问题但它意味着不同时间段的用户可能其实是同一批。如果压测目标系统有登录频率限制这个要提前评估。4.3 自定义客户端请求的处理Locust 不止能压 HTTP。如果要做 WebSocket、gRPC 或其他协议可以继承User而不是HttpUser自己定义客户端。下面是一个简化的思路from locust import User, task, between import websocket class SocketUser(User): wait_time between(1, 2) def on_start(self): self.ws websocket.create_connection(ws://example.com/socket) task def send_message(self): self.ws.send(ping) resp self.ws.recv() # 自定义统计Locust 的统计机制是围绕Environment和事件来做的所以即使走的是自定义协议只要你会发射请求事件照样能出统计报表。这部分展开讲可以单独写一篇这里先提个概念后续有机会再细说。5. 运行方式与关键参数命令行和 Web 界面都得会5.1 Web 界面模式的实战操作locust直接启动后打开http://localhost:8089能看到一个简洁的配置页Number of users模拟的峰值用户总数。Spawn rate每秒启动多少个用户也就是爬坡速度。Host压测目标地址脚本里如果用了相对路径就靠这个参数补全。实际压测时我一般这么填如果目标峰值是 1000 并发Spawn rate填 50也就是 20 秒内全部启动完。这个爬坡过程很重要——不要一上来就怼满并发让系统有时间逐步建立连接池、加载缓存否则前期的错误率数据会误导你对系统真实能力的判断。Web 界面里还有几个值得用的功能菜单里的Download data可以把压测结果导出成 CSV方便做后续分析。Charts页能看到响应时间中位数、95%、99%的实时曲线。在压测过程中你可以直接修改Number of users和Spawn rate并点Apply不需要重启压测进程。这个功能对摸底测试特别方便先 100 并发跑 2 分钟再改成 500 并发跑 2 分钟一条曲线就拿出一份容量曲线。5.2 命令行模式无头模式和 CI 集成如果压测机是远程服务器或者在 CI 里跑回归Web 界面就用不上了。Locust 提供了 headless 模式一条命令行就能完成整轮压测locust -f locustfile.py --headless -u 1000 -r 50 --run-time 10m --host https://example.com参数含义-u并发用户总数-r每秒启动用户数--run-time压测总时长单位可以是10m、1h或300s-t是--run-time的简写压测跑完进程会自动退出。如果想让数据落盘加--csvresultLocust 会生成result_stats.csv、result_history.csv等文件供后续分析。这个功能在 CI 里非常好用GitLab CI 或 Jenkins 里跑一条命令然后解析 CSV 里的 99% 响应时间超过阈值就让流水线失败。5.3 主从模式分布式压测单机压测时每个虚拟用户的协程开销其实很小但生成器和网络栈的吞吐还是有上限的。当并发量很大比如超过一万时可以考虑主从模式# master 节点 locust -f locustfile.py --master --expect-workers4 # 每个 worker 节点 locust -f locustfile.py --worker --master-host192.168.1.10我自己实测下来单机跑 5000 并发以内的 HTTP 压测基本没问题超过这个量就需要分布式了。分布式压测的注意事项有几个worker 的locustfile.py必须和 master 保持一致否则任务定义对不上跑出来的结果无效。用--expect-workers明确告诉 master 期待多少 worker可以避免漏了一个 worker 还在傻等。如果压测目标服务和 master/worker 在同一个局域网网络延迟影响不大如果跨机房要先单独测一下控制链路的稳定性。5.4 常用参数速查参数作用示例-f指定压测文件-f my_test.py-u用户数-u 1000-r每秒启动速率-r 50--run-time运行时长--run-time 10m--headless无 UI 模式--headless--csv导出 CSV 前缀--csvresult--master/--worker主从模式见上文--host覆盖 Host 配置--host https://api.example.com6. 关键参数的计算与调优思路6.1 并发用户数和启动速率怎么定很多新手拿到需求就问并发数设多少其实答案不在工具里而在业务里。最基础的计算方式是用业务量反推假如目标系统一天有 100 万次请求集中在 4 个小时内那每秒平均请求数大约是 1000000 / 14400 ≈ 69。但峰值往往是平均的 3~5 倍所以峰值 TPS 可能到 300 左右。然后要考虑并发用户数和请求速率的关系。每个用户每秒能发多少请求取决于wait_time。比如between(1, 3)的平均等待时间是 2 秒加上请求本身耗时约 0.2 秒单用户平均每秒产生约 0.45 个请求。如果想达到 300 TPS并发用户数大约是 300 / 0.45 ≈ 667。这就是为什么constant_pacing(1)这种策略在写接口压测里好用——它让每个用户稳定地每秒产生一个任务计算并发数和 TPS 的对应关系就非常直接。6.2 压测前的容量估算清单我每次压测前会先填一个表项目说明目标业务量日请求量、峰值时段平均/峰值 TPS由业务量反推单用户请求频率由 wait_time 决定预估并发用户数峰值 TPS / 单用户请求频率目标响应时间如 99% 500ms资源监控方案CPU、内存、连接数、数据库有了这些前提再动手写脚本才不会被问题问住你这个压测代表了什么场景。6.3 运行时动态调参的实战价值Locust Web 界面里我最常做的事不是看报表而是做阶梯加压。流程是先以 200 并发跑 2 分钟观察响应时间再提到 400 并发跑 2 分钟继续提到 600、800…… 直到发现 99% 响应时间突然恶化或者错误率开始抬头。那个临界点就是系统当前架构下的大致容量上限。这种跑法和一次性-u 1000怼满最大的区别在于你能区分系统扛不住和系统瞬间被打蒙两种情况。后者往往是由于连接池、缓存、线程池的冷启动造成的并不代表真实水平。逐步加压才能找到稳定的容量拐点。7. 常见问题与排查技巧实录7.1 压测时报连接错误或响应超时怎么办Locust 报错最常见的是ConnectionError和Timeout。这两个错误要分开看ConnectionError通常是目标服务没起来、防火墙拦截、DNS 解析失败。先用curl或者浏览器验证目标接口能否访问。Timeout分两种客户端等待时间太长或服务端处理不过来。Locust 的 HTTP 客户端默认没有设置超时建议在请求里显式指定self.client.get(/slow-api, timeout30)如果服务端正常但单请求耗时超过 30 秒就说明业务处理逻辑有瓶颈这本身也是压测要发现的问题。7.2 日志里的NoneType错误有段时间我的脚本偶尔报NoneType object has no attribute run排查后发现是on_start里登录失败返回了空响应后续代码取token时直接解引用空对象。这就是为什么我在 4.1 节的示例里加了登录失败后的退出逻辑。严格执行失败就停别带病压测的原则能省去大量无效跑批。7.3 单机并发数上不去怎么排查如果你发现-u 5000跑起来后本机 CPU 已经 100%但目标服务的压力并不大大概率是客户端侧的资源瓶颈。排查思路依次是看 Python 进程的 CPU 占用——过高说明请求构造或解析逻辑太重检查是不是在任务里做了大量字符串拼接、正则匹配。看连接数——ss -s或netstat检查本机 TIME_WAIT 状态是否堆积。如果过多调整系统内核参数或者使用连接复用的压测客户端。看任务里是否有耗时的同步操作——比如每个任务都查一次数据库、读一次文件这些操作会严重拖慢虚拟用户的执行速度压测结果会被拉低但不是服务端的问题是脚本的问题。7.4 压测结果里错误率高先看是不是脚本问题很多压测发现的问题最后定位出来是脚本问题。我经历过几次压测接口需要先拿到一个动态参数但脚本里写死了旧值导致大量 400 错误。压测数据没做好隔离多个虚拟用户同时操作同一条订单出现竞态。wait_time没设置虚拟用户疯狂发请求把服务端的限流策略打出来了页面大量 429。所以拿到错误率高的结果第一步不是找开发而是先检查脚本逻辑、检查数据准备确认这些错误不是压测工具自身制造出来的。有一个实用习惯每次压测前先拿 1 个用户、跑 10 秒观察基本链路是否通。这个冒烟压测能过滤掉大部分低级错误。7.5 在 Web UI 里看不到数据如果压测跑起来了但图表一直没数据先检查三件事脚本里是否真的调用了self.client发请求目标地址是否正确host是否被命令行参数覆盖。我遇到过一次最隐蔽的情况HttpUser的子类里写了task方法但方法名拼写错误导致 Locust 没有把它识别成任务用户启动了却什么都不干自然没数据。8. 从基本使用到进阶的几点体会最后说点我个人在实际操作中的感受。Locust 这套东西上手门槛确实低但要真正用好功夫在脚本之外你要会估算并发、会设计场景比例、会判断脚本问题还是系统问题。我见过很多团队用 Locust 压测脚本写得花团锦簇结果压测报告里的指标解释得含含糊糊——指标是工具给的但指标背后的业务含义得人去看。如果这篇文章对你有帮助建议你按这个顺序动手做一遍先写一个只有一个task的最小脚本跑通 Web 界面然后加上on_start和权重多任务跑一次 headless 模式接着把 CSV 导出来写一个简单的脚本算 95% 和 99% 响应时间。这一套走完Locust 的基本使用就算真正拿下了。后续如果想深入可以研究LoadTestShape自定义加压模型、HttpUser之外的自定义客户端、以及结合 Prometheus Grafana 把压测指标做成看板——这些是踩过坑之后最值得投入的方向。