1. 为什么是Locust:Python协程并发模型与JMeter线程池的差异
做性能测试的人,大部分入门用的都是JMeter。我一开始也是,界面操作直观,录制脚本也方便,跑起来之后看聚合报告,响应时间、吞吐量、错误率一目了然。但用久了会碰到几个让人不太舒服的地方:脚本维护越来越重、参数化逻辑写起来很绕、要跟开发团队协作时脚本没法放到Git里做版本管理,更别提用Python写复杂业务逻辑。后来我因为在项目里反复试Locust写压测脚本,才真正意识到它跟JMeter是两种完全不同的物种。
之所以说“不同物种”,核心在于并发模型的差异。JMeter的并发是基于线程池的,每一个虚拟用户就是一条Java线程,线程有栈、有内存开销,跑几千个并发时,线程上下文切换的成本会非常明显。Locust完全换了一条路:它用的是gevent协程,底层基于Greenlet实现,在一个操作系统线程里可以挂成千上万个协程。协程切换是用户态的,不需要操作系统参与调度,所以单机就能模拟出很高的并发数,内存占用和CPU开销都更可控。打个比方,JMeter像是一家餐厅开了很多个包间,每个包间配一个服务员,客人多了就得不停加人;Locust更像一个大食堂,一个服务员同时照看多桌,靠记性好来切换注意力,桌子再多也不会把人力成本线性拉上去。
这引出了一个很重要的实际含义:并发数不等于线程数。你在Locust里写--users 5000,并不意味着你的压测机要开5000条系统线程。它只是创建了5000个协程任务在事件循环里轮转,机器的资源消耗要小得多。我做过一个对比实验,同一台4核8G的云主机,JMeter跑到2000线程时CPU已经快被打满,而Locust用同样的机器跑5000协程,CPU占用也就60%上下,这还是在脚本里写了业务断言的情况下。
还有一个隐性优势是脚本的可读性和可维护性。Locust的测试脚本就是纯Python代码,定义HttpUser、定义task列表、用@task装饰器标注任务方法,逻辑一目了然。对于团队里同时会Python的开发和测试同学来说,压测脚本可以直接review,直接改,直接跑CI。做接口自动化那套的断言、数据驱动、日志打印,全都能复用Python生态。相比之下,JMeter的.jmx文件是一大坨XML,人工维护和代码审查都极其痛苦。
当然,Locust也不是万能药。它默认是HTTP层面的压测,如果你想做JDBC压测、JMS消息压测这类协议,Locust支持得不如JMeter开箱即用。虽然也能写自定义client,但成本不低。所以我在团队里的建议很简单:只要被测服务的入站协议是HTTP或基于HTTP的REST接口,并且团队有Python基础,直接用Locust。否则老老实实用JMeter,别跟工具较劲。
这一篇重点面向第一次接触Locust的读者。我会从安装开始,带你写第一个压测脚本,把命令行和Web UI的常用操作讲透,再补充几个高频的进阶写法,最后把我实际踩过的坑一并列出来。读完这篇,你应该能自己独立跑一个像样的压测任务。
2. 环境准备与核心对象:安装、HttpUser、任务、等待时间
2.1 安装
Locust的安装非常无脑,它是个纯Python库,pip装就行。我建议在任何新项目里都用虚拟环境隔离依赖,别一股脑装到系统Python里。具体步骤:
python3 -m venv locust-env source locust-env/bin/activate pip install locust装完之后直接在命令行验证:
locust --version如果能看到类似locust 2.x.x from ...的输出,说明安装成功了。需要注意的一点是,Locust这个项目的迭代速度不慢,主版本从1.0到2.0变化挺大,很多教程是很多年前的写法。如果你看到网上老教程里的from locust import Locust这种写法,在2.x里已经废弃了,统一用HttpUser。你装的时候直接装最新稳定版就行,我这篇的内容基于2.x版本。
如果你是那种连Python环境都不太熟的人,也别慌。装Python的时候记得勾选"Add Python to PATH",虚拟环境激活之后输入locust命令有效,就算过了环境这道关。
2.2 三个核心概念:HttpUser、task、wait_time
如果把Locust脚本拆到最简,你只需要理解三样东西:
第一是HttpUser,它是虚拟用户的基类。你写一个类继承HttpUser,就相当于定义了一种"压测用户"。这个用户身上带着一个client,它的类型是HttpSession,底层基于requests库。所以你可以在任务里直接写self.client.get("/api/users"),这跟用Python requests发请求没有本质区别,只是它自动帮你挂上了统计和上报逻辑。
第二是任务,用@task装饰器标注。一个HttpUser类里可以定义多个任务方法,Locust会按权重随机抽取方法去执行,这就模拟了真实用户掺杂着做多种操作的行为。比如一个电商项目,用户有的在浏览首页,有的在搜索商品,有的在下单,那你就定义三个任务,按实际业务占比配权重。
第三是wait_time,它模拟用户思考时间。真实用户不可能每秒钟都发一个请求,他总要看一会儿页面、想一下要点哪里。wait_time就是用来控制两次任务之间停顿多久的。常用的写法是:
wait_time = between(1, 5)表示每次任务执行完后,随机等1到5秒再进行下一次。这一条特别重要,我见过不少新手不写wait_time,结果压测机疯狂发请求,把被测服务直接打挂,最后还把锅甩到服务头上。没有等待时间的压测,更像是一种DDoS攻击,而不是模拟真实用户。
2.3 一个最小可运行的Demo
下面这十几行代码,就是Locust压测脚本的最小骨架。把它存成locustfile.py,放在当前目录,然后直接命令行执行locust,它就能跑起来。
from locust import HttpUser, task, between class WebsiteUser(HttpUser): wait_time = between(1, 5) @task(3) def view_homepage(self): self.client.get("/") @task(1) def view_item(self): self.client.get("/item/10001")这里@task(3)和@task(1)是权重。意思是压测过程中,每执行3次首页浏览,才执行1次商品页浏览。如果你希望两个操作发生的频率差不多,权重就写一样;如果想让某个操作占比高,就把它的权重调大。权重这个机制非常好用,你公司的业务后台如果能看到真实的接口调用比例,完全可以按比例映射到任务权重上。
self.client.get("/")里面的路径是相对路径,这是Locust一个很方便的设计。域名和端口不用写在每个请求里,而是在启动命令里用--host指定,或者直接看Web UI里的输入框。脚本和运行环境解耦,同一个脚本测测试环境、预发环境,只需要换host参数,非常方便。
2.4 本地快速验证脚本
写完脚本别急着上并发,先单用户跑一下确认请求路径没问题。有两种方式:一种是在命令行里跑:
locust --host https://your-api.example.com --users 1 --spawn-rate 1 --run-time 10s --headless另一种是直接在Python环境里手动调用一下task方法做冒烟验证。我在实战中更推荐后者,因为某些接口有鉴权、有签名,如果脚本里写错了,headless跑起来后会刷出一大片失败请求,其实问题出在脚本本身而不是服务。手动验证的方式很简单:
user = WebsiteUser() user.client.get("/")不过直接实例化HttpUser在某些版本里会有点小问题,更稳妥的方式是先启动环境再执行短时间压测。我通常是这么干的:起一个头部无界面模式,10秒跑一个并发,然后立刻看失败率。失败率是0,再放开跑。
3. 命令行启动与Web UI监控:完整操作链路
3.1 命令行参数逐个拆解
Locust有两种运行模式:无界面模式和Web模式。无界面模式适合在CI或服务器上跑,一条命令搞定,跑完自动退出并输出结果。我来拆几个最常用的参数。
locust -f locustfile.py --host https://api.example.com --users 100 --spawn-rate 10 --run-time 5m --headless-f指定脚本文件,默认就会找当前目录下的locustfile.py,所以如果文件名就叫这个,-f都可以省略。--host指定被测目标的基础地址,脚本里的相对URL会拼在这里。--users是最终要达到的并发用户数,--spawn-rate是每秒启动多少用户。最需要注意的就是--spawn-rate,它的作用不是瞬间拉到几百并发,而是让用户数逐步增加。为什么要有这个参数?因为压测最忌讳一上来就把全部压力砸过去,一方面会对服务造成不必要的冲击,另一方面你也看不到系统在爬坡过程中的表现。真实的线上流量是逐渐涨起来的,压测也应该模拟这个过程。
--run-time指定运行多久,格式可以是10s、5m、1h。不加这个参数,headless模式会一直跑到你手动Ctrl+C为止。在CI里跑一定要加,否则流水线永远结束不了。
还有一对参数容易被忽略:--csv和--csv-full-history。它们能把压测中的指标定时写入CSV文件,方便事后画图或做报告。比如:
locust -f locustfile.py --headless --users 200 --spawn-rate 20 --run-time 5m --csv=result --csv-full-history--csv=result的意思是生成一个前缀为result的CSV文件,--csv-full-history会在运行过程中每隔几秒保存一次完整快照,最终你会得到result_stats.csv、result_stats_history.csv、result_failures.csv等好几个文件。在没有配套监控面板的团队里,这组参数是生成压测报告的重要数据来源。
3.2 Web UI 到底看什么
不传--headless直接跑locust,启动后命令行会提示你访问http://localhost:8089。这个内置Web UI是Locust最让人喜欢的地方之一,因为压测过程中你能实时看到数据,而且可以随时在界面上调整并发数,不用重启任务。
Web UI里有几个核心区域,我建议你重点关注四个指标:
第一个是"Number of Users",当前已启动的虚拟用户数。配合曲线图,你可以直观看到爬坡过程是否平滑。
第二个是"Requests per Second(RPS)",即每秒请求数。这个是吞吐量的核心指标,反应服务在处理能力层面的实际表现。很多新手会有个误区,以为并发数高RPS就一定高。实际不一定,如果单请求耗时很长,高并发下RPS反而可能上不去,因为请求都堵在排队了。RPS = 并发数 / 平均响应时间,这个关系比学性能测试的人一定要刻在脑子里。
第三个是"Response Time"曲线,重点看95%分位和99%分位的响应时间。算术平均值很容易被极值拉偏,一个超时5秒的请求能把整体平均值拉高一大截,但95分位能更真实反映"大多数用户"的感受。接口服务的SLA如果签了"95%请求在200ms以内",你重点盯的就是这个数。
第四个是"Failures"区域。走上线流程的压测我要求失败率必须为0,哪怕0.1%的失败也不能忽略。但要注意,失败并不一定都是被测服务的问题。我在后面会专门讲怎么区分网络层失败、脚本层失败和服务层失败。
在Web UI上还有个很实用的功能:设置界面里可以直接修改Host、Users和Spawn Rate,然后点击"Start swarming"重新开始压测。这意味着你不需要因为调整并发数就去改脚本重启进程,这在上线前的压测窗口期非常省时间。
3.3 分布式运行基础
单机跑Locust虽然已经很省资源,但极端情况下还是会有瓶颈。什么情况?比如压测单接口的RPS目标要上十万,或者被测接口需要很大的测试数据集,一台压测机的带宽、CPU、内存不够用。这时候就需要分布式模式。
分布式模式的架构很简单:一个master节点负责调度和汇总,多个worker节点负责实际发请求。启动方式如下:
master节点:
locust -f locustfile.py --masterworker节点(可以起多台):
locust -f locustfile.py --worker --master-host=192.168.1.10需要注意几点:
第一,所有节点的locustfile.py必须完全一致。这个坑我踩过,某个worker节点的脚本版本旧了一点,结果跑出来的数据和其他节点不一致,整个压测结果直接作废。建议所有机器从同一个Git仓库拉取并固定版本号。
第二,worker节点不需要你手动指定并发数,并发数只定义在master的启动命令里,master负责分发任务。
第三,master和worker之间走的是带外消息通道,如果你在云上部署,记得安全组把这些端口放开,否则worker报一堆Connection to master timed out错误。
我在项目里用分布式最多的时候是在做全链路压测,20台worker模拟数万用户,比单机跑出来的数据可信得多。因为单机撑死能模拟几千到一万用户,再多就容易出现压测机本身成为瓶颈的情况,那测出来的根本是网络和机器上限,不是被测服务的上限。
4. 脚本进阶写法:权重、参数化、断言与初始化,让压测更贴近真实业务
4.1 任务权重的真实业务映射
前面那节说过@task(3)、@task(1)的用法,但权重设计的合理性比这个语法本身重要得多。我之前做过一个电商项目,一开始运营给的日常访问比例是首页:搜索:详情:下单 = 4:3:2:1。我们按这个权重直接跑了,结果压测报告里发现下单接口的QPS远高于真实的日峰值。为什么?因为真实用户看十次商品也未必会下单一次,而且下单流程里还包含一堆前置校验和跳转,不是说点一下提交订单按钮就完事了。
后来我们把下单任务里加入了前置请求、加购物车、填写地址等多步操作,同时把权重调成20:10:5:1,数据才贴近线上。所以权重不是拍脑袋写的,最好基于线上监控的接口调用比例来做映射。你的APM平台如果记录了各接口的每分钟调用次数,直接拿过去按比例算就行。
4.2 参数化:别让所有用户都请求同一个URL
新手最常见的错误是压测脚本里写死一个ID,所有虚拟用户都在请求同一个资源。压测结果看着漂亮,但服务端如果做了缓存,那测出来的数据全是缓存命中率,真实性能要打个大折扣。正确做法是让不同的请求带上不同的参数。
最简单的参数化方式是随机从列表里取:
from locust import HttpUser, task, between import random USER_IDS = [10001, 10002, 10003, 10004, 10005] class ApiUser(HttpUser): wait_time = between(1, 2) @task def get_user_profile(self): uid = random.choice(USER_IDS) self.client.get(f"/api/user/{uid}")稍微复杂一点的场景,是压测注册登录类接口。新建用户的账号密码不能全都一样,否则数据库唯一索引直接报错。这时可以用循环队列或自增计数器来保证取到不同的值。在协程环境下,你得注意多协程并发取同一份数据时的指向问题。我常用的做法是用itertools.count:
from itertools import count import string import random counter = count(1) def gen_username(): idx = next(counter) return f"loadtest_user_{idx}"因为在同一份脚本里,counter是全局对象,gevent协程之间切换时next(counter)是线程安全的,能保证每个用户拿到不同的用户名。这个设计简单可靠,我大量使用于注册接口压测。
再进阶一点,如果参数需要按顺序循环使用(比如测试一批预置的优惠券),可以用一个共享队列,每次从队尾弹出一个,用完再塞回队首:
from collections import deque coupons = deque(["COUPON_A", "COUPON_B", "COUPON_C"]) def next_coupon(): coupon = coupons.popleft() coupons.append(coupon) return coupon这里有个细节:deque的popleft和append在单进程内是原子的,不用担心多个协程同时操作导致数据错乱,但如果你用了多worker模式,每个worker各自维护一份队列,就会出现参数重复。这种情况我建议把测试数据提前写到Redis里,用一个RPOPLPUSH命令实现跨进程的循环取参数,或者直接用--csv参数传入一个外部CSV让每个worker独立读取各自的切片。总之,压测数据的一致性设计需要从一开始就考虑进去。
4.3 断言:不是写个assert就完事了
Locust允许你在任务方法里写Python断言,失败时会记录成Failure。这个能力很重要,因为压测不仅要看接口通不通,还要看返回结果对不对。最常用的是检查HTTP状态码和关键字段:
@task def create_order(self): with self.client.post("/api/order", json={"goods_id": 101}, catch_response=True) as resp: if resp.status_code != 200: resp.failure(f"HTTP {resp.status_code}") elif resp.json().get("code") != 0: resp.failure(f"business error: {resp.json().get('msg')}") else: resp.success()这里有几个关键点需要解释。
第一,catch_response=True表示不让请求库抛异常,而是把响应交给你自己判断。用with包裹后,你可以手动调用resp.failure()和resp.success()来标记本次事务成功还是失败。不写这个参数的话,Locust只按HTTP状态码来判断,2xx和3xx都算成功。但实际业务中接口返回200但业务code是5001的情况非常常见,如果没有业务断言,这种错误会被当成功记录,压测报告的失败率接近于零,但业务上已经挂了。
第二,assert关键字本身也可以写,但不推荐在catch_response模式下直接assert,因为异常会导致这个事务被标红,但错误信息不够明确。我更习惯用resp.failure("具体原因")的方式,这样在Failure列表里能看到具体是HTTP层失败还是业务code不匹配。
第三,响应体解析尽量做一次缓存。如果同一个请求的响应体被多处使用,只在第一次解析时存一下,不要每次断言都重新resp.json(),因为性能测试脚本本身也消耗资源,脚本写得拖泥带水,压测机容易成为瓶颈。
4.4 on_start与on_stop:用户生命周期钩子
on_start方法是虚拟用户启动时的初始化逻辑,最常见的用途是登录。想象一下真实场景:用户访问系统之前肯定要先登录,拿到token之后才能访问后面的接口。在Locust里,你不能在全局写一次登录然后让所有用户共享这个token,因为每个虚拟用户都是独立个体,Locust会为每个虚拟用户调用一次on_start。
from locust import HttpUser, task, between class PlatformUser(HttpUser): wait_time = between(1, 3) def on_start(self): resp = self.client.post("/api/login", json={ "username": f"user_{random.randint(10000, 99999)}", "password": "test123" }) data = resp.json() self.token = data["token"] self.client.headers.update({"Authorization": f"Bearer {self.token}"})这段代码的运行顺序是:每个虚拟用户被创建时,先执行on_start完成登录,接着才开始循环执行任务方法。所以后续所有任务里的请求都会自动带上请求头里的Authorization,特别方便。
on_stop则相反,在虚拟用户生命周期结束时调用,一般用来做清理工作,比如删除测试数据、退出登录、关闭连接等。不过我在实际项目里用得很少,因为压测结束后服务端的数据清理大多靠测试环境定期重置,或者靠压测脚本本身在on_start里使用数据工厂。你可以把它想成一个"善后"出口,用不用看具体场景,不必强求。
但这里也有个性能陷阱:如果你的登录接口本身需要几十毫秒到几百毫秒,那么在大规模压测时,on_start的执行时间会显著拖慢爬坡速度。我遇到过一次,spawn-rate设为20,理论上1分钟能启动1200个用户,但实际只启动了400个,原因就是登录接口耗时300ms,触发协程间阻塞。解决办法是把登录改成更轻量的换取token接口,或者减小spawn-rate让用户缓慢建立,又或者直接把登录依赖降到最低,用预设token代替真实登录。
5. 我踩过的坑:Locust实战中的六个高频问题与解法
5.1 客户端端口耗尽:明明服务没挂,压测机先扛不住了
做高并发压测时最容易忽略的一个瓶颈是压测机自身的TCP端口耗尽。每个TCP连接的源端口是有限的,默认范围大约在28000到61000之间,大约三万多个。如果你压测的并发数高、每个请求的保持时间又长,压测机自己的端口就会耗尽,新连接直接失败。这时的现象是:服务端负载并不高,但失败率突然飙升,错误信息显示Connection reset by peer或Address already in use。
解法有几种:一种是在代码里使用HTTP keep-alive,Locust的HttpSession实际上默认就是复用连接池的,所以正常使用问题不大,但如果你在脚本里手动创建了新的Session、关闭了连接,或者每次请求都用headers={"Connection": "close"},就会疯狂建连接。还有一种更实用的解法是修改系统内核参数,扩大本地端口范围:
sudo sysctl -w net.ipv4.ip_local_port_range="1024 65535"同时缩短TIME_WAIT的连接回收时间:
sudo sysctl -w net.ipv4.tcp_fin_timeout=15这两个参数在压测机上执行一次,端口可用量就从三万多提升到六万左右。但对十万级RPS的压测需求来说,这点还不够,最稳妥的方案就是上分布式集群,让多台worker分摊端口资源。
5.2 数据量不足导致缓存覆盖:把压力变成读缓存压力
有一次我们测一个读多写少的服务,脚本里随机读取的总共也就几十条商品记录,服务端把热门商品缓存住了,Redis命中率直接冲到95%以上,压测结果好看到爆。但上线前最后一天,真实流量一进来,缓存全部穿透,数据库被打到CPU 100%。问题根源就是压测数据量太小,跟真实数据规模和分布差太远。
做压测前先看看线上数据量级。如果线上有十万个商品ID,压测脚本至少要准备几千到上万个不同的ID。宁可ID是假的(只要数据库查不到就走正常兜底逻辑),也好过所有请求都挤在同一个缓存key上。压测脚本里参数化的数据池大小,我一般按"可见数据集的10%以上"来取。比如详情页可能被访问的商品有2万个,压测池就预留2000个以上。这样Redis的分片和淘汰策略才能被真实触发。
5.3 只看平均值:被好看的平均响应时间骗了
压测报告里最容易迷惑人的是平均响应时间。假设一个接口100个请求里,99个返回20ms,1个返回8秒,平均响应时间大约是99.8ms,看起来非常健康。但实际上有1%的用户经历了8秒的等待,已经处于崩溃边缘,如果遇上双十一流量,那1%绝对用户量也很惊人。
我习惯看两个指标:95分位和99分位,以及最长响应时间。Locust的Web UI里有一个响应时间分布图,能直观看到分段延迟。脚本里也可以通过self.client.get(..., name="api_query")给请求打标签,这样每个接口的响应时间分布能分开统计,而不是全部堆在一个池子里。
另外,压测过程中要留意响应时间曲线是否呈锯齿状。如果曲线每隔一段时间就出现一个尖峰,很可能是定时任务、GC暂停、日志刷盘等行为在挤占资源。这时候光看并发数是发现不了的,得结合响应时间分布和具体时间段来分析。遇到这种情况,我的做法是把压测结果按10秒粒度导出CSV,然后画折线图,看尖峰出现的周期,再去服务端找对应时间的日志和监控。
5.4 脚本报错信息不明确:被泛化的ConnectionError误导
Locust报错时,Failure列表里经常出现ConnectionError(Connection refused by the server)。看到这个错误,我第一反应不是服务挂了,而是先区分这个连接是被对端拒绝的,还是被本机防火墙拦截的,或者是连接池里清理了旧连接。最简单的方法是先用curl手动请求同一个接口:
curl -v https://api.example.com/api/health如果curl正常,再检查压测机的安全组和IP白名单是否放通了压测机的出口IP。我在有一次穿云环境压测时,服务端安全组只放行了办公网的IP,压测机在另一个VPC里发请求,结果全部被拒。排查了很久才发现不是服务问题。后来我养成了一个习惯:压测任务开始前,先跑一个curl做连通性验证,而不是直接跑Locust然后看一堆ConnectionError。
5.5 headless模式下忘记带--html:压测报告没留存
在CI体系里跑压测,最后一定要生成一份报告文件留档。我见过不止一个同事跑完压测不保存报告,结果需求方问"上次压测结果是多少",只能再花一晚上重新跑一遍。Locust支持直接导出HTML报告,命令很简单:
locust -f locustfile.py --headless --users 100 --spawn-rate 10 --run-time 5m --host https://api.example.com --html=report.html--html会生成一个独立的HTML文件,包含总览、图表、统计表,可以直接发给团队或归档到流水线制品库。如果还需要原始数据,前面说过的--csv参数一并加上,报告和数据都要留。
5.6 忘了固定Locust版本:上次能跑,这次全报错
Locust版本更新比较快,接口变动也不是完全没有。今天跑得好好的脚本,过三个月再跑,可能因为某个API废弃直接报错。所以我强烈建议在项目里锁定版本范围,并且把依赖写进requirements.txt:
pip freeze | grep locust >> requirements.txt这样整个团队乃至CI环境都用同一套Locust版本,避免"在我机器上能跑,在CI上报错"这类尴尬。升级Locust时走代码审查流程,升级完先跑一小轮冒烟测试,再上正式压测。
6. 压测结果怎么解读:RPS、响应时间、失败率的三点经验
6.1 以目标为导向反推压测参数
拿到压测需求时,第一条要问的不是"测哪个接口",而是"我们要验证什么目标"。目标不同,压测参数完全不同。性能验收类的目标是"在300并发下,接口P95响应时间低于500ms,无错误",那压测命令就按300并发跑。容量测试的目标是"这台服务器最多能支撑多少QPS",那就得用阶梯加压,从50并发慢慢加到1000,观察RPS和响应时间的拐点。
拐点的判断角度很关键。我一般会观察RPS曲线:当并发数继续增加但RPS几乎不再增长,甚至开始下降,同时响应时间急剧抬升时,这就是系统的容量上限。继续加并发只会让排队更严重,压出来的数字已经不能反映系统的"正常能力"了。这类结论要写进压测报告里,让运维人员知道该在哪里扩容、扩容多少。
6.2 响应体大小对RPS的影响被很多人忽略
同样的后端逻辑,如果接口返回的JSON从50KB变成500KB,RPS会差出一个数量级,因为网络传输时间在请求总耗时中占比会上升。所以压测报告里一定要记录响应包大小的基线。我在脚本里用了一步记录响应体长度,简单粗暴:
with self.client.get("/api/big_response", catch_response=True) as resp: content_length = len(resp.content)然后压测前先确认这个长度与线上一致。如果测试环境造数的数据量比线上小很多,导致响应体格外小,那压测结果会严重偏乐观,是个典型的"测试环境与生产环境数据差异"问题。造数的时候尽量按线上数据量级来,别拿几条测试数据做容量测试。
6.3 失败分类:先分清哪层失败,再谈定位
看到压测脚本里有失败,第一件事不是改服务端,而是打开Failures列表看具体错误类型。我通常把失败分成三层:
第一层是网络层失败,表现为ConnectionError、Timeout、ConnectionResetError。这类失败可能出在压测机本身、带宽、网关、负载均衡,不一定是对端服务不可用。
第二层是HTTP层失败,比如4xx、5xx。5xx基本可以判定是服务端问题,4xx则要看是不是压测脚本里的参数没传对、签名过期、鉴权失败。后者是脚本问题,不算服务缺陷。
第三层是业务层失败,就是HTTP返回200但业务code不是0。这类最隐蔽,但往往是最有价值的信息,因为它能发现状态码监控发现不了的逻辑错误。
我的做法是给任务里的每次请求在catch_response=True模式下完整分类,HTTP状态码不是2xx的走HTTP失败分支,业务code不为0的走业务失败分支。这样最终压测报告里的每个失败都能直接对应到具体原因,而不是让它含糊地汇总成一个总数。
6.4 压测结束后的数据清理和复盘
压测结束后,我还会做两件不太显眼但很有必要的事。一是检查压测产生的脏数据有没有留在被测环境,尤其是注册、下单、支付类接口。很多项目压测完不清理,导致测试环境数据库里堆积了一堆测试账号和订单,后续功能测试跑起来各种撞数据。二是在团队内部花十五分钟复盘压测曲线和失败记录,把压测过程中发现的异常点记到wiki或流水线记录中,形成历史基线。下次再压测时,还能对比这次和上次的曲线差异,判断系统是变好了还是变差了。
我个人的习惯是压测报告不只贴一张截图,而是把命令、参数、脚本版本、环境信息、数据基线都完整记录。因为数据要能和下次压测做横向对比,光有截图没有上下文是没法对比的。等到系统上线出了问题时,再看压测报告里记录的容量上限和拐点,往往能直接辅助定位问题方向。