从一次深夜压测事故说起,我才意识到自己对工具的理解浅了。当时线上系统被一条热点推送打崩,接口响应时间从50毫秒直接飙到4秒,数据库连接池被打满,运维半夜打电话让我起来看。我第一反应是拿之前的老脚本跑一遍JMeter,结果发现脚本里的参数还是半年前的老数据,断言逻辑写得也乱,光修脚本就花了两个多小时。那次之后我就决定换一条路,把压测工具的研究提上日程,而最终在对比了一圈之后,真正让我留下来并用到今天的,就是基于Python的开源负载测试工具Locust。
Locust最打动我的地方,是它把压测脚本变成了一段普通的Python代码。不需要托拽控件、不需要维护一堆XML结构的jmx文件,用户行为和业务场景直接用代码描述,版本管理、代码复用、多人协作都变得顺理成章。这篇文章不打算做那种官网文档的翻译,而是从一个实际使用者的角度,讲清楚Locust的核心概念、安装过程中的关键选择、第一个真实脚本怎么写、以及从单机到分布式的完整路径。不管你是刚接触压测的测试新人,还是带着团队想做性能基准的开发者,这篇文章应该都能给你一些可以直接用起来的经验。
1. 为什么压测工具我最后选了Locust
1.1 从一次线上事故说起
前面提到的那次事故,起因其实不复杂,就是运营在一个大促群里发了一个链接,流量在几秒钟之内涌进来,网关层率先扛不住,紧接着后面的订单服务、会员服务、优惠券服务全部雪崩。事后复盘来看,问题并不是代码层面某个函数写得慢,而是我们对系统的容量上限完全没有底。
这种"没有底"才是最可怕的。你连自己系统在什么并发量级下会开始劣化都不知道,线上出了事就只能靠加机器硬扛,扛不住就是宕机。所以性能压测这件事,不是上线之前走个过场,而是应该长期做、持续做、每次改动之后都跑一遍的事情。但长期做就意味着压测脚本本身需要维护,如果脚本不好维护,人就会偷懒,就会拖,最后又回到"不压测直接上线的老路"上去。
我用JMeter做过一段时间的压测,说实话功能是真的强大,插件生态也丰富,但它太重了。每次打开图形界面要加载半天,脚本里塞满了各种Sampler、断言、监听器节点,改一个接口的地址都得左翻右找。更麻烦的是JMeter脚本的diff体验极差,两个人同时改一个jmx文件,合并冲突能让人崩溃。团队里面做代码评审的时候,JMeter脚本根本没法评审,只能靠跑一遍看结果说话。
1.2 Locust的差异化定位
Locust走了一条完全不同的路。它的核心设计哲学是"用代码描述行为,用协程模拟高并发"。
先讲协程,这是Locust性能底气的来源。传统工具比如JMeter默认基于线程,一个虚拟用户就是一个线程,而一个进程里能开的线程数是有限的,即使你设置几百上千个并发用户,线程切换的系统开销也会拖垮压测机本身。Locust则不同,它在Python3.7之后使用asyncio事件循环和Greenlet协程,一台压测机上能够轻松跑几千、上万甚至更多的虚拟用户,而且单机的内存和CPU开销比线程模型小了一个数量级。
再讲代码描述行为。在Locust里,每一个虚拟用户都是一个Python对象,它要做什么操作、按什么顺序做、每个操作之间停顿多久,全部可以用代码精确控制。这意味着:
- 脚本可以写进Git仓库,走完整的代码评审流程
- 支持面向对象设计,同一个基础用户行为可以派生出不同角色
- 可以直接复用项目里的API客户端库、数据构造工具
- 压测逻辑和业务语言之间的翻译成本极低
1.3 什么样的项目最适合Locust
当然我也不会说Locust在所有场景下都无敌,它有自己的舒适区。基于我的使用经验,下面这几类情况特别适合上Locust:
- HTTP/HTTPS接口的压测:这是Locust最成熟的场景,RESTful API、WebSocket部分支持,绝大多数业务系统的压测都能覆盖
- 需要复杂业务串联的场景:比如"登录→领取优惠券→下订单→支付"这种链路,用代码写最直观,用JMeter反而绕
- 团队本身是Python背景:如果你们后端或QA团队本来就会Python,那么Locust的学习成本几乎为零
- 需要把压测嵌入CI/CD流水线的场景:Locust可以无头模式运行并输出CSV报告,也可以把结果推送到InfluxDB/Grafana,天然适合自动化
反过来,如果你需要压测的协议特别冷门、没有现成的Python客户端库,或者你完全不想写代码,那JMeter的插件生态可能更合适。工具没有绝对的好坏,只有匹配不匹配。
2. 安装前的冷静期:版本、依赖和运行环境
2.1 Locust版本演进对安装选择的影响
很多新手上来就执行pip install locust,装完却发现自己看到的界面和网上教程不一样,于是开始怀疑自己装错了。其实不是装错了,是Locust的版本跨度有点大,不同版本之间的使用方式有差异。
Locust在1.0版本之前,类名、命令行参数和Web界面都和现在差别很大。比如老版本里用的是locust --host=http://xxx这种方式指定目标地址,HttpUser这个类是在1.0之后才命名为HttpUser的,之前叫HttpLocust。如果你在网上搜到了一篇2019年的文章,照着写大概率跑不起来。
所以我的建议是,新项目一律装最新稳定版,至少是2.x以上的版本。以目前的情况来说,Locust最新稳定版已经是2.x系列,安装时会自动依赖pyzmq、gevent、flask、psutil这些核心库。如果你的项目里已经有这些依赖的旧版本,pip在安装的时候会自动帮你升级或调整,这一般不会出问题,但如果你在公司的内部源上装,可能因为源同步不及时拿到过时的版本。
安装前先确认你的Python环境:
- Python版本:推荐3.8及以上,2.24版本后官方要求Python3.9+,建议直接装最新版Python3.11或3.12,没必要在这个问题上省事
- 操作系统:Windows、macOS、Linux都支持,Linux下压测机性能最好,macOS上本地调试也完全没问题
- 虚拟环境:强烈建议用虚拟环境装,不要直接往系统Python里塞
2.2 pip安装与验证
安装过程没有什么玄学,但我会建议按下面的顺序来操作。
第一步,创建并激活虚拟环境:
python -m venv locust_env source locust_env/bin/activate # Windows下是 locust_env\Scripts\activate第二步,安装Locust:
pip install locust第三步,验证安装结果:
locust --version正常情况下你会看到类似locust 2.x.x from ...的输出。看到这个就说明装好了,可以开始写脚本。如果你想在压测过程中把实时指标推到InfluxDB或InfluxDB 2.x,可以顺手装一个locust[influxdb]扩展,不过这不是必须的,后面用到的时候再装也来得及。
2.3 安装时最容易踩的坑
我在帮同事排查安装问题的时候,发现坑主要集中在这几个方面。
第一个坑是Windows下没有装Microsoft C++ Build Tools。gevent在Windows上需要编译某些C扩展,如果你从网上下载到的包没有对应的wheel文件,pip就会尝试现场编译,然后报出一堆红色错误。解决办法有两个:要么安装Visual Studio Build Tools,要么直接换用Python 3.8以上版本,因为新版本通常都有预编译的wheel,不太需要本地编译。
第二个坑是Python版本太老。有的公司服务器上还是Python 3.6甚至3.5,这时候跑最新版Locust会直接报语法错误。建议在自己的机器上用皮环境准备好脚本,压测机上只需要装一个干净环境即可。
第三个坑是和已有依赖冲突。Locust依赖gevent、geventhttpclient、psutil、flask等库,如果你的环境里有旧版本的requests或urllib3,可能会遇到ImportError。解决的办法是在虚拟环境里先升级pip install -U pip setuptools wheel,再安装Locust,顺序别搞反。
3. 第一次编写压测脚本:吃透核心概念
3.1 核心概念先过一遍
在动手写代码之前,有几个概念我觉得值得花几分钟理解清楚。这些概念就是Locust的全部,理解了它们,脚本怎么写都是顺理成章的事。
第一个概念是User(用户)。这个类代表一个虚拟用户,它定义了用户"是谁"以及"怎么做"。在代码里,我们通常不直接用User,而是用它的子类HttpUser,后者已经帮我们封装好了client属性,可以直接发起HTTP请求。
第二个概念是Task(任务)。任务就是虚拟用户执行的某个具体操作,可以是一个普通函数,上面加装饰器@task来标记权重。虚拟用户启动后,会从任务列表中随机挑一个执行,权重决定了它被选中的概率。
第三个概念是Wait Time(等待时间)。用户执行完一个任务之后,不会立刻执行下一个任务,而是会等待一段时间。这个等待时间模拟的就是现实中用户的思考时间,可以用wait_time = between(1, 5)来让任务之间随机停顿1到5秒。
第四个概念是Host(目标主机)。这是被压测系统的地址,可以在启动时通过--host参数指定,也可以直接在脚本里用host = "http://..."写死。
用一句话串起来就是:User定义什么样的人,Task定义做什么事,WaitTime定义做事的节奏,Host定义去哪做。这四个概念组合起来,就能模拟出千变万化的用户行为。
3.2 最小可运行脚本
下面这个脚本是我每次做新项目时都会先跑一遍的"冒烟脚本",目的是确认环境没问题、目标服务通不通、以及基本压测链路是否正常。
from locust import HttpUser, task, between class WebsiteUser(HttpUser): host = "http://localhost:8080" wait_time = between(1, 5) @task(weight=3) def index_page(self): self.client.get("/") @task(weight=1) def view_item(self): self.client.get("/api/items/1")这里@task(weight=3)和@task(weight=1)的意思是,用户执行"访问首页"和"查看商品"这两件事的概率比为3:1。权重越高,被随机选中的概率越大。self.client.get()就是发起HTTP GET请求,self.client是HttpUser内置的HTTP客户端,它会自动处理请求头、Cookie、连接池等细节,你不需要自己构造Session。
把这段代码保存成locustfile.py,然后切换到虚拟环境,在终端执行:
locust -f locustfile.py如果你没有在脚本里写host属性,启动时就必须加上-H参数指定目标地址:
locust -f locustfile.py --host=http://localhost:8080启动成功后,终端会出现一个地址http://0.0.0.0:8089,浏览器打开这个地址,就是Locust自带的Web控制台。
3.3 启动模式与Web UI
看到Web界面之后,你会注意到一个细节:Locust默认有Web模式和**无头模式(headless)**两种运行方式。
Web模式下,界面里有两个输入框,一个是"Number of users to simulate"(模拟用户总数),一个是"Spawn rate"(每秒启动用户数)。打个比方,如果有100个用户同时在线,但你希望用户不是瞬间全部涌入系统,而是像真实场景那样逐渐上线,那么可以设置总用户数为100、生成速率为10,这样10秒钟内用户会均匀地出现。这个特性在做阶梯加压测试时特别好用,你可以先跑10个用户看曲线,再加到50个,再加大到100个,实时观察系统在不同并发下的表现。
无头模式主要用在自动化场景,比如CI流水线里。举个实际命令:
locust -f locustfile.py --headless -u 100 -r 10 -t 5m --csv=result/load_test这条命令的意思是用100个虚拟用户、每秒增加10个的方式跑5分钟,最后把结果写到load_test开头的CSV文件里。-u是用户数,-r是生成速率,-t是运行时间,--csv是报告输出前缀。
如果你不确定有哪些参数,随时可以运行locust --help查看完整选项列表。说实话这个命令参数列表我到现在也背不全,每次都是查help,这很正常。
4. 让脚本像真实业务:参数化、关联与集合点
4.1 参数化:让每个用户都是不一样的烟火
刚开始接触Locust的时候,我踩过一个大坑:用同一个账号、同一份订单数据反复压测,结果后端缓存命中率越来越高,压测出来的数据越来越好看。这种数据其实没有任何参考价值,因为它没有模拟出真实世界中不同用户带来的数据差异。
真实场景下的参数化,就是让每个虚拟用户拥有自己的数据集。最简单的实现方式是用itertools.cycle或random.choice。
看一个登录接口的参数化例子:
import random from locust import HttpUser, task, between USERS = [ {"username": "alice", "password": "123456"}, {"username": "bob", "password": "abcdef"}, {"username": "carol", "password": "qwerty"}, ] class ApiUser(HttpUser): wait_time = between(1, 3) def on_start(self): cred = random.choice(USERS) resp = self.client.post("/login", json=cred) self.token = resp.json().get("token")这里on_start是Locust提供给用户的一个生命周期钩子,当虚拟用户启动时会调用一次,适合放登录、获取Token这类前置操作。每个用户随机取一个账号登录,登录成功后把Token保存在self.token中,后续任务就可以复用这个Token。这样每个虚拟用户就有了不同的身份和数据访问权限。
4.2 关联:从上一个响应里捞下一个请求要用的值
关联其实就是在接口返回的响应里提取出后续依赖的数据,比如创建订单后会返回一个order_id,然后你拿着这个order_id去查询订单详情或发起支付。
最直观的做法是把响应转成JSON,然后从里面取字段:
from locust import HttpUser, task, between class OrderUser(HttpUser): wait_time = between(1, 3) @task def create_order_and_check(self): create_resp = self.client.post("/orders", json={ "item_id": 1001, "quantity": 2 }) if create_resp.status_code != 201: return order_id = create_resp.json()["order_id"] detail_resp = self.client.get(f"/orders/{order_id}") if detail_resp.status_code == 200: print(f"Order {order_id} query ok")这里有一个容易被新手忽略的点:if create_resp.status_code != 201: return。创建订单如果失败,就没有order_id可以拿,硬着头皮继续请求就只是在测试错误处理逻辑,而不是压测正常业务流程。所以关联操作之前,一定先判断上游请求是否成功,失败就直接结束当前用户的这轮任务,让用户转去做别的操作。
4.3 集合点:让并发来得更猛烈一些
集合点的场景很典型:秒杀开场前,几十万用户同时卡在等待页,时间一到,所有人一起点抢购按钮。这种"同时爆发"的行为如果不用特殊手段,模拟不出来,因为前面提到的wait_time会让用户的任务自然错开。
Locust里模拟集合点的常用手段,是使用一个全局栅栏,比如threading.Barrier或者gevent.event.Event。由于Locust的虚拟用户运行在协程中,使用gevent的同步原语和Locust是兼容的,下面这个写法可以直接用:
import gevent from locust import HttpUser, task BARRIER_COUNT = 200 barrier = gevent.event.AsyncResult() class SpikeUser(HttpUser): wait_time = lambda self: 0 @task def rush_buy(self): barrier.wait(timeout=10) self.client.post("/seckill", json={"sku_id": "S001"})这里我用的是gevent.event.Event的变通方式:每个虚拟用户启动后会在barrier.wait()处阻塞,直到有足够多的用户到达后一起放行。在实际项目中,集合点要配合用户总数和启动速率来设计,如果用户总数远小于集合点阈值,那么这一轮压测等于没有集合效果,测试作废。
不过也要泼一盆冷水:集合点在Locust里不是一等公民的支持功能,实现起来多少有点"自己动手"的味道。如果你的业务对同时爆发的要求不是特别严格,我更推荐用"快速spawn大量用户"的方式来逼近,比如设置-u 1000 -r 1000,让所有用户在1秒内生成完毕,效果上也接近同时爆发。
4.4 从单机到分布式:一个命令的事
单机压测有两个天然瓶颈:一是单台压测机的端口、文件句柄、CPU、内存有限,二是你的Python GIL虽然对协程影响较小,但到了几万并发的时候,单机的资源调度还是会成为天花板。所以当你需要模拟更大规模用户量时,就要上分布式压测。
Locust的分布式模型很简洁:一个Master节点负责调度和汇总结果,多个Worker节点负责真正施压。
节点内通信靠pyzmq,所以安装时就自动装了,不需要额外配置中间件。启动方式如下:
# Master节点,Web界面跑在8099端口 locust -f locustfile.py --master --master-port=5557 --web-port=8099 # Worker节点,可以开多台机器或多进程 locust -f locustfile.py --worker --master-host=192.168.1.10 --master-port=5557Worker节点不需要-u参数,真正设置用户总数和生成速率的动作只在Master的Web界面或命令行里做。启动Master之后打开Web控制台,设置用户数量和生成速率,Master会把压力分摊到各个Worker上,实时指标则从每个Worker汇总回Master,最终统一展示在一个界面上。
实际使用分布式时有几个容易踩的坑,我建议你注意:
- Worker机器的时间要校准,偏差太大会导致数据汇总时的统计误差
- Master所在机器不要跑Worker,原因很简单,Master要承担汇总和调度的职责,压力已经不小了
- 分布式模式下
-t运行时间必须在Master上指定,Worker只需要关注连接master和加载脚本 - 如果使用了自定义模块需要在Worker和Master端保持一致,或者保证能通过
PYTHONPATH共享
5. 结果指标解读与压测过程中的常见坑
5.1 核心指标:先看RPS,再看响应时间,最后看失败率
Web界面跑起来之后,满屏数字很容易让人眼花。我的习惯是固定看几个核心指标,按优先级排列。
第一优先级是RPS(Requests Per Second,每秒请求数)。这个数字直接反映系统当前处理的吞吐量。如果你看到RPS不再随并发用户数的增加而上升,而是进入平台期,那基本可以判定系统某处到瓶颈了。瓶颈可能是应用本身,也可能是数据库、带宽或者某个下游服务,这需要结合后端监控进一步定位。
第二优先级是响应时间。Locust界面里给出的是平均值和分位值,其中95%和99%分位最值得关注。平均响应时间容易被长尾拖高,但99分位基本能代表系统在压力下的最差体验。如果99分位已经达到3秒以上,那说明有不少用户正在经历明显卡顿。
第三优先级才是失败率。不是所有失败都代表系统挂了,比如我自己就经常遇到超时和连接拒绝,这时候要看失败的具体原因。Locust在失败统计里会详细展示异常类型和错误信息,如果是因为压测机本身没有文件句柄导致连不上,那是压测机的配置问题,可不能甩锅给被测系统。
5.2 常见坑:从连接数到超时设置
前面讲的都是"怎么用",这里来讲讲我实测下来真正会咬人的坑。
第一个坑是文件句柄不够。你在Linux压测机上跑高并发,可能会看到大量的Cannot assign requested address错误。这是压测机自己的临时端口不够用了,需要用root权限改一下:
# 查看当前限制 ulimit -n # 临时提高 ulimit -n 65535端口范围也在/etc/sysctl.conf里调整,比如net.ipv4.ip_local_port_range = 1024 65535。说实话我当初第一次看到这报错的时候还以为是被测系统崩了,折腾半天才发现是自己机器的问题。
第二个坑是超时设置太短。self.client.get()默认会等待服务器响应,但不同系统对超时的预期差很多。如果一个接口本来就需要5秒才能返回,而你压测脚本里没有设超时,那并发一高,请求排队积累,就会出现大量超时失败,压测结果完全失真。建议在HttpUser里统一配置超时:
from locust import HttpUser, task, between from locust.env import Environment class MyUser(HttpUser): wait_time = between(1, 5) @task def slow_request(self): with self.client.get("/slow-api", catch_response=True, timeout=10) as resp: if resp.elapsed.total_seconds() > 8: resp.failure("Response too slow")catch_response=True能让你手动控制一个请求算成功还是失败。这里如果发现响应耗时超过8秒,就标记为失败,这样统计出来的失败率才真正有业务意义。
第三个坑是Think Time设成0。很多人为了让压测压力最大化,把wait_time设为0甚至直接不写。这样其实会让压力失真,因为真实用户不可能一秒钟发几十个请求。在常规容量评估时,我会设置between(1, 5)来模拟真实思考时间。只有在做极限压力测试或集合点秒杀场景时,才会刻意去掉等待,让压力拉满。
第四个坑是压测机性能不足。这一点最容易被忽视。分布式压测不是万能的,单台Worker能承载的虚拟用户数依然有上限。当你发现RPS和响应时间曲线反复抖动、没有一个稳定趋势,同时压测机的CPU已经跑满,那就不是被测系统在抖动,而是压测机在拖后腿。这条经验我付出过不少无效加班的代价才真正领悟,建议你把压测机的资源监控一起纳入压测过程。
5.3 把压测变成团队基础设施
最后分享一个我的个人体会。很多人把压测当作一次性的任务,项目上线前压一下,完事。但真正有价值的方式,是把压测脚本和流程沉淀成团队的基础设施。
我这边目前的做法是:每个核心服务的压测脚本都放在对应的Git仓库里,与代码一同演进。接口字段变了,压测脚本就在同一个Merge Request里更新,天然同步,不会出现压测脚本落后于线上代码的情况。配合无头模式和--csv报告输出,再接到CI流水线里,每次发布前自动跑一个冒烟压测,生成基线报告。如果这次跑出的RPS比上次下降超过20%,就直接阻断发布流程,让开发先去查性能回退的原因。
就是这一个小小的闭环,让我们后来再遇到大流量活动的时候,心里有了底。不是靠赌运气,而是靠提前量化的数据来判断系统的容量边界。Locust在其中扮演的角色,就是一个轻盈、可维护、能融入开发工作流的压测引擎。如果你也受够了重工具带来的维护成本,我想你一旦用上Locust,就很难再回头了。