1. 项目概述从“压测焦虑”到“代码即脚本”的转变做后端开发或者运维的朋友估计都经历过“压测焦虑”。产品上线前老板问“咱们的系统能扛住多少用户” 你心里没底只能含糊地说“应该没问题”。或者某个大促活动前你需要验证新扩容的服务器和缓存策略是否真的有效总不能真等到流量洪峰来了再祈祷吧。这时候一个趁手的负载测试工具就成了刚需。过去你可能用过 JMeter它功能强大但那个图形化界面和复杂的配置写个稍微复杂点的逻辑就让人头大测试脚本的版本管理和协作也是个麻烦事。而今天要聊的Locust则提供了一种截然不同的思路用代码定义用户行为。Locust 的核心魅力就在于此。它不是一个有着复杂 UI 的客户端而是一个基于 Python 的开源负载测试框架。你只需要用纯 Python 代码编写你的测试脚本定义一群“蝗虫”Locust 意为蝗虫如何模拟用户去“啃食”你的系统。这种方式带来的好处是显而易见的脚本本身就是代码可以用 Git 管理可以复用函数和类可以方便地引入复杂的业务逻辑比如先登录获取 token再带着 token 去查询订单甚至可以直接使用你项目中的业务模型。对于开发人员来说这几乎是无缝衔接的体验。我最初接触 Locust 是为了测试一个异步任务队列的接口。用 JMeter 模拟那种带有等待、轮询结果的链式调用非常别扭而在 Locust 里我只需要用requests库配合几个time.sleep()和条件判断一个真实的用户场景就模拟出来了。从那时起Locust 就成了我性能测试工具箱里的首选。它不仅解决了“能不能压”的问题更能帮你回答“用户是怎么用的系统在真实场景下的瓶颈在哪里”。2. 核心设计理念与架构拆解2.1 为什么选择“代码即脚本”这得从负载测试的本质说起。我们压测的目的不是简单地用最大并发数去“冲垮”一个接口而是尽可能真实地模拟线上用户的操作流从而发现系统在特定场景下的性能瓶颈。真实的用户行为是有逻辑、有状态、有变化的。逻辑性用户不会凭空发送请求。典型的场景是“浏览商品列表 - 查看商品详情 - 加入购物车 - 下单支付”。这是一个有前后依赖关系的序列。在 Locust 里你可以用一个 Python 类的方法来清晰地定义这个序列。状态性用户登录后的 session 或 token 需要跨请求保持。在代码里这只是一个实例变量self.client.headers[“Authorization”] token那么简单。变化性用户输入的数据不会是固定的比如每次搜索的关键词、购买的商品 ID 都可能不同。利用 Python 的random、faker等库你可以轻松生成逼真的测试数据。传统的工具通过配置元件、前置处理器等图形化模块来拼凑这些逻辑当场景复杂时配置会变得极其臃肿且难以维护。而代码天生就是为描述复杂逻辑而生的。Locust 把“定义测试场景”这件事完全交给了 Python 这门强大的语言让你能用最熟悉的方式去构建最真实的压力模型。2.2 Locust 的核心运行架构理解了理念我们再看看它是怎么跑的。Locust 采用主从Master-Worker分布式架构这使它能够轻松发起大规模并发。Master 节点这是大脑。它不模拟任何用户只负责协调。它的工作包括启动测试、收集所有 Worker 节点的实时数据、汇总并展示统计信息通过 Web UI、控制测试的启动和停止。你通过命令行启动一个 Master 进程。Worker 节点这是干活的肌肉。每个 Worker 进程会启动多个协程greenlet每个协程模拟一个用户Locust去执行你编写的测试脚本。Worker 可以部署在多台机器上共同分担压力生成的任务。Master 和 Worker 之间通过网络通信。Web UI这是控制面板和仪表盘。一个简洁的本地网页你可以在这里设置模拟的用户总数和每秒启动速率Ramp-up然后启动测试。测试过程中它会实时展示总 RPS每秒请求数、响应时间平均、中位数、P95、P99、失败率等关键指标并以图表形式呈现。这种架构的好处是扩展性极强。当你需要模拟十万、百万级用户时只需要增加 Worker 机器的数量即可。Master 的 Web UI 提供了一个统一的视图让你对全局测试状态一目了然。注意Master 节点本身资源消耗很低但它是单点。如果 Master 进程崩溃整个测试就会中断。在生产级压测中需要确保 Master 所在机器的网络和运行环境稳定。3. 环境准备与第一个测试脚本3.1 安装与极简验证安装 Locust 非常简单因为它就是一个 Python 包。强烈建议使用虚拟环境venv 或 conda来管理依赖避免污染全局环境。# 创建并进入虚拟环境以 venv 为例 python -m venv locust_env source locust_env/bin/activate # Linux/macOS # locust_env\Scripts\activate # Windows # 安装 locust pip install locust安装完成后可以通过locust -V检查版本。现在我们来创建第一个脚本就叫locustfile.py这是 Locust 默认寻找的入口文件名。3.2 解剖你的第一个 Locustfile一个最基本的locustfile.py需要包含一个继承自HttpUser的用户类。HttpUser内置了一个client属性它是requests.Session的封装自动保持了 cookies并且会将其发起的请求纳入 Locust 的统计中。from locust import HttpUser, task, between class QuickstartUser(HttpUser): # wait_time 定义了用户在执行每个任务后等待的时间。 # between(1, 5) 表示等待 1 到 5 秒之间的一个随机数。 wait_time between(1, 5) # task 装饰器将一个方法标记为一个“任务”。 # 括号里的数字是权重默认是1。权重越高被选择执行的概率越大。 task(3) # 这个任务执行概率是下面那个任务的3倍 def view_items(self): # self.client 用于发起 HTTP 请求用法和 requests 几乎一样。 # 它的请求会被 Locust 自动记录和统计。 self.client.get(/api/items) self.client.get(/api/items/1) task(1) def hello_world(self): self.client.get(/hello) # on_start 方法会在每个模拟用户开始运行时被调用一次常用于登录等初始化操作。 def on_start(self): # 假设登录接口并保存 token response self.client.post(/login, json{username:test, password:test}) if response.status_code 200: self.client.headers[Authorization] fBearer {response.json()[token]} else: # 如果登录失败这个用户后续的请求可能会失败或者你可以标记它停止 print(Login failed)我们来拆解一下这个脚本QuickstartUser类代表一类用户行为。你可以定义多个这样的类来模拟不同角色的用户如浏览用户、下单用户、管理员。wait_time这是关键。它让用户行为之间有了“思考时间”使压测流量更接近真实用户“点击-等待-再点击”的模式而不是毫无间歇的疯狂轰炸。between是最常用的还有constant固定间隔等。task定义了用户具体做什么。一个类里可以有多个task。Locust 会为每个用户循环执行这些任务每次随机选择一个任务根据权重执行完后等待wait_time指定的时间再选择下一个。on_start和on_stop用于用户级别的初始化和清理工作如登录和登出。这个脚本已经定义了一个完整的用户场景用户启动后先登录然后以 3:1 的概率要么去查看商品列表和详情要么去访问/hello页面每次操作后休息 1-5 秒。3.3 启动测试并查看结果保存好locustfile.py后在终端进入该文件所在目录运行locust默认会启动 Web UI 在http://localhost:8089。打开浏览器访问这个地址你会看到启动界面。Number of users要模拟的总用户数。Spawn rate每秒启动多少个用户用于控制压力爬升的斜率。Host被测试系统的根地址比如http://your-api-server.com。填写后点击 “Start swarming”测试就开始了。Web UI 的图表和统计数字会开始实时变化。这里你能看到几个核心指标RPS当前所有用户每秒发送的请求数。这是衡量服务器吞吐量的直接指标。Response Times响应时间。尤其要关注 P9595% 的请求响应时间低于此值和 P99它们比平均值更能反映尾部延迟对用户体验影响巨大。Failures失败的请求数及比例。点击可以查看具体是哪些请求失败了错误原因是什么。4. 编写高级且真实的测试场景简单的 GET 请求只是开始。真实的业务远比这复杂。4.1 处理动态参数与数据关联很多请求需要携带前一个请求的返回结果。例如先创建一个订单拿到订单号再去查询这个订单。from locust import HttpUser, task, between import json class OrderUser(HttpUser): wait_time between(2, 5) order_id None # 实例变量用于在任务间传递数据 task def create_and_query_order(self): # 1. 创建订单 create_payload {product_id: 123, quantity: 2} with self.client.post(/api/orders, jsoncreate_payload, catch_responseTrue) as response: # 使用 catch_responseTrue 可以更精细地控制成功/失败的判断 if response.status_code 201: self.order_id response.json()[order_id] # 标记这个请求成功即使状态码不是200-299 response.success() else: response.failure(fCreate order failed: {response.text}) # 等待一下模拟用户操作间隔 self.wait() # 2. 查询刚创建的订单 (如果创建成功) if self.order_id: self.client.get(f/api/orders/{self.order_id}, name/api/orders/[id]) # 使用 name 参数将动态URL归类统计否则每个不同的order_id都会被视为独立请求关键技巧catch_responseTrue允许你自定义请求成功/失败的条件。比如接口返回{“code”: 500, “msg”: “…”}但 HTTP 状态码是 200。这时你可以根据response.json()[“code”]来判断并调用response.failure()。name参数对于动态路径的 URL一定要用name参数给它一个统一的名称。否则/api/orders/1001和/api/orders/1002在 Locust 的统计里会被当成两个不同的接口导致数据分散无法分析。这是新手常踩的坑。4.2 使用测试数据集压测经常需要不同的测试数据比如用不同的用户名登录。我们可以从 CSV 文件或列表中读取。import csv from locust import HttpUser, task, between class DataDrivenUser(HttpUser): wait_time between(1, 3) def on_start(self): # 假设我们有一个 users.csv 文件里面有 username 和 password 两列 with open(‘users.csv‘, newline‘‘) as f: reader csv.DictReader(f) self.user_list list(reader) # 读取所有用户到内存 self.current_user None task def login_and_do_something(self): if not self.user_list: self.stop(True) # 如果没有数据了停止这个用户 return # 每次任务取一个用户也可以随机取 self.current_user self.user_list.pop() username self.current_user[‘username‘] password self.current_user[‘password‘] # 登录 resp self.client.post(/login, json{username: username, password: password}) if resp.ok: token resp.json()[‘token‘] self.client.headers[“Authorization”] f“Bearer {token}” # 执行登录后的操作... self.client.get(“/profile”)注意这种方式在分布式运行时会出问题因为每个 Worker 进程都会独立读取一遍 CSV 文件。对于分布式压测更好的做法是使用共享队列如 Redis或者让 Master 统一分配数据。Locust 内置的on_locust_init事件可以在 Master 启动时加载数据然后通过消息传递给 Workers但这需要更复杂的代码。4.3 模拟更复杂的用户思考模型wait_time不仅仅是随机等待。Locust 提供了几种模式constant(3)每次固定等 3 秒。between(1, 5)1到5秒随机。constant_pacing(2)恒定步调。这是非常有用且常被忽略的一个。它会尝试让两次任务执行之间的总时间任务执行时间等待时间刚好等于你设定的值比如2秒。如果你的任务执行很快0.1秒它就等1.9秒如果任务执行慢1.5秒它就只等0.5秒。这能更精确地控制单个用户的请求频率。from locust import HttpUser, task, constant_pacing class PacingUser(HttpUser): # 无论任务执行多久都试图让每个“任务循环”的间隔为2秒 wait_time constant_pacing(2) task def my_task(self): self.client.get(“/api”)5. 分布式压测与实战配置单机运行的 Locust 受限于本机的网络和 CPU能模拟的用户数有限通常几千个。要产生更大的压力必须使用分布式模式。5.1 启动 Master 和 Worker假设你有三台压力机master-host,worker1-host,worker2-host。在 Master 节点# --master 表示以Master模式启动 # --expect-workers 2 表示期望连接2个Worker等它们都连上后才可在Web UI启动测试 locust --master --expect-workers2在每个 Worker 节点# --worker 表示以Worker模式启动 # --master-host 指定Master节点的地址 # 需要将 locustfile.py 和任何依赖的数据文件复制到Worker机器或放在共享存储上 locust --worker --master-hostmaster-host当两个 Worker 都连接成功后Master 的 Web UI 上会显示 “2 workers connected”。此时你在 Web UI 上设置的“用户数”和“孵化率”会被平均分配给所有 Worker。例如设置 10000 个用户2 个 Worker则每个 Worker 负责模拟 5000 个用户。5.2 关键配置参数与实战经验通过命令行参数可以精细控制测试行为以下是一些最常用的--headless无头模式不启动 Web UI。适用于在 CI/CD 流水线中自动运行压测。locust --headless --users 1000 --spawn-rate 50 --run-time 5m --hosthttp://target.com--users总用户数。--spawn-rate每秒启动用户数。--run-time测试运行时长如5m(5分钟)、1h30m。--csv将结果数据导出为 CSV 文件便于后续用其他工具如 Pandas、Excel进行深度分析。locust --headless --users 1000 --run-time 2m --csvresult这会生成result_stats.csv汇总、result_failures.csv失败记录等文件。-f指定自定义的 locustfile 名称或路径。locust -f load_tests/api_scenario.py--autostart与--headless配合自动开始测试无需在 UI 点击。实战心得压力机本身不能成为瓶颈。监控压力机的 CPU、内存、网络带宽和本地端口使用情况netstat。如果压力机资源耗尽产生的流量曲线会失真。对于高并发需要多台压力机。小心“连接耗尽”。单台机器对同一个目标主机端口有连接数限制。如果模拟用户数极大可能会遇到Cannot assign requested address错误。需要调整压力机的内核参数例如增加net.ipv4.ip_local_port_range和net.ipv4.tcp_tw_reuse。分布式数据同步如前面提到的如果测试需要预置的账号池且每个用户只能用一次简单的文件读取会导致数据重复使用。一个解决方案是使用 Redis 作为共享队列Master 在on_test_start事件中将所有账号推入 Redis每个 Worker 的用户在on_start时从 Redis 中弹出一个账号。6. 结果分析与性能瓶颈定位压测的最终目的是发现和定位瓶颈。Locust 的 Web UI 和 CSV 报告提供了原始数据但分析需要你的洞察。6.1 看懂关键指标响应时间Response Times中位数Median一半的请求快于此值。它受极端值影响小能较好反映“典型”体验。P95 / P9995%/99% 的请求快于此值。这是服务等级目标SLO最常关注的指标。P99 升高意味着有 1% 的用户经历了非常慢的请求这往往对应着数据库慢查询、缓存失效、Full GC 等具体问题。平均Average容易被少数极端慢的请求拉高参考价值低于中位数和百分位数。RPSRequests per Second随着并发用户数增加RPS 会上升但达到系统瓶颈后RPS 会趋于平缓甚至下降而响应时间会急剧上升。那个拐点就是系统的最大吞吐量。失败率Failure Rate任何非零的失败率都需要严肃对待。点击查看具体失败请求错误信息如 502 Bad Gateway, 504 Timeout, 连接被拒等是定位问题的第一线索。6.2 常见的性能瓶颈模式及排查方向结合 Locust 的曲线图你可以观察到一些典型模式模式一响应时间缓慢上升RPS 平稳现象随着用户数增加响应时间线性增长但 RPS 基本不变。可能原因应用服务器处理能力达到瓶颈。CPU 使用率可能已接近 100%。需要检查应用服务器的线程池/工作进程配置、代码中的同步阻塞调用、低效的算法等。模式二响应时间阶梯式跳跃RPS 波动现象响应时间在某个点突然大幅增加RPS 可能下降。可能原因依赖的外部服务或数据库达到瓶颈。例如数据库连接池耗尽、慢查询增多、第三方 API 限流。需要检查中间件MySQL、Redis、MQ的监控指标。模式三大量失败连接超时现象失败率陡增错误多为连接超时或拒绝。可能原因服务器或网络的连接数被占满。检查服务器的netstat查看连接状态检查负载均衡器或 Web 服务器如 Nginx的worker_connections配置。模式四内存使用率持续增长最终崩溃现象测试前期正常运行一段时间后响应时间变慢最终服务崩溃。可能原因内存泄漏。需要结合 JVM/应用的内存监控如 GC 日志进行分析。6.3 将 Locust 数据与系统监控关联孤立的压测数据意义有限。你必须将 Locust 报告的 RPS、响应时间曲线与服务器监控系统的 CPU、内存、磁盘 I/O、网络 I/O、数据库连接数、慢查询日志等曲线在时间轴上对齐。当 Locust 显示响应时间飙升时服务器监控的 CPU 是否也同时飙高如果是瓶颈在计算。当失败率上升时数据库的活跃连接数是否达到上限或者 Redis 的响应时间是否变长使用 APM 工具如 SkyWalking, Pinpoint可以追踪单个慢请求的完整调用链精确找到是哪个服务、哪个方法、哪条 SQL 语句慢了。一个完整的排查流程可能是Locust 报告显示 P99 响应时间从 200ms 突增至 2s。查看服务器监控发现应用服务器 CPU 正常但 MySQL 服务器 CPU 接近 100%。登录 MySQL执行SHOW PROCESSLIST;发现大量相同的慢查询处于Sending data状态。找到对应的 SQL 语句发现缺少一个关键索引。添加索引后重复压测P99 响应时间恢复正常。7. 常见问题与避坑指南在实际使用中你会遇到各种各样的问题。这里记录了一些典型坑点和解决方案。7.1 Locust 本身的问题问题现象可能原因解决方案启动报错Address already in use8089 端口被占用使用--web-port指定其他端口如locust --web-port8090Worker 连接不上 Master防火墙阻止了端口通信默认 5557, 5558确保 Master 主机上这两个端口对 Worker 开放。使用--master-bind-host和--worker-bind-host指定绑定地址。Web UI 图表不更新或卡住浏览器兼容性或网络问题尝试刷新页面或使用--web-host绑定到0.0.0.0确保可访问。对于长时间压测建议使用--headless模式并导出 CSV 分析。模拟用户数达不到设定值单机资源CPU/内存/端口不足使用分布式压测。检查压力机本身的资源使用率。调整系统最大文件描述符和本地端口范围。catch_responseTrue时手动success()后仍被统计为失败请求本身的 HTTP 状态码不在 2xx 范围内且未在catch_response块内处理在with语句块内无论 HTTP 状态码如何都必须显式调用response.success()或response.failure()。7.2 测试脚本与性能相关坑点忘记给动态 URL 设置name参数后果统计信息被分散到无数个不同的条目下无法聚合分析。解决任何时候请求的 URL 包含路径参数或查询参数都使用name。# 错误每个不同的user_id都会产生一条统计 self.client.get(f“/api/users/{user_id}“) # 正确所有/user/xxx的请求都会被归到“/api/users/[id]”下统计 self.client.get(f“/api/users/{user_id}“, name“/api/users/[id]“)坑点在任务代码中执行耗时或阻塞的操作后果Locust 的协程是单线程的如果一个任务里执行了time.sleep(10)或者一个同步的 CPU 密集型计算会阻塞整个 Worker 进程严重影响并发能力。解决Locust 的wait_time是异步等待不会阻塞。对于自定义的长时间等待如模拟用户观看视频应使用 Locust 的wait()方法。对于必须调用的同步阻塞函数考虑将其放到一个线程池中执行但这会增加复杂度。坑点测试数据准备不当后果分布式运行时数据重复使用或耗尽导致测试场景不真实或提前结束。解决对于只读数据如城市列表可以每个 Worker 都加载一份。对于消耗性数据如唯一优惠券使用中央队列Redis或提前分区为每个 Worker 分配不同的数据文件。7.3 系统与环境配置压力机优化进行大规模压测前调整 Linux 压力机的内核参数是必要的。# 增加本地端口范围 sysctl -w net.ipv4.ip_local_port_range“1024 65535“ # 启用TIME_WAIT套接字重用 sysctl -w net.ipv4.tcp_tw_reuse1 # 增加最大打开文件数 (在 /etc/security/limits.conf 中永久设置) ulimit -n 65535目标系统监控压测前务必确保目标系统服务器、数据库、缓存等的监控仪表盘已就绪。没有监控的压测就像蒙着眼睛开车。Locust 的强大在于它将性能测试编程化、工程化。它可能没有一些商业工具那样华丽的报表但它与开发流程的契合度、灵活性和可扩展性是无与伦比的。当你把 Locustfile 纳入代码仓库与 CI/CD 集成定期对关键链路进行自动化压测时你才真正将性能保障左移建立起对系统能力的持续信心。记住压测不是为了证明系统有多强而是为了发现它在哪里弱从而在用户发现之前解决它。