使用Locust构建高并发压力测试:从原理到电商全链路实战
2026/9/17 21:02:11 网站建设 项目流程

最近在技术社区看到一个很有意思的项目名称——“法兰西的处刑拍手”。初看这个名字,很多人可能会一头雾水,这和技术有什么关系?是某种游戏模组,还是一个历史梗的代码实现?

实际上,这个名字背后指向的是一个非常具体且实用的技术场景:自动化、高并发的网络请求压力测试工具。它并非一个官方框架,而是开发者社区中流传的对一类工具或脚本的戏称,其核心任务是模拟海量用户请求,对Web服务、API接口进行“处刑”般的压力测试,并通过“拍手”(即日志和报告)来宣告结果。

如果你正在开发后端服务、维护API网关,或者需要对系统容量进行摸底,那么手动测试和简单的单线程脚本已经完全不够用了。你需要的是一个能轻松模拟真实用户行为、产生可控压力、并给出清晰性能报告的方案。本文将为你彻底拆解“处刑拍手”类工具的核心思想,并手把手带你用主流的开源工具Locust,构建一个属于你自己的、功能强大的“压力测试执行官”。

本文能帮你解决什么问题?

  1. 理解压力测试的核心逻辑:为什么单纯的“多发请求”不是好的压测?如何模拟真实场景?
  2. 快速搭建可编程的压测环境:使用基于Python的Locust,告别复杂的配置。
  3. 设计有效的压测场景:不仅测试首页,还要测试登录、查询、下单等连贯业务流。
  4. 解读压测报告:看懂RPS、响应时间、百分位数等关键指标,定位性能瓶颈。
  5. 避开常见陷阱:比如测试机自身成为瓶颈、参数化数据不足、结果误判等。

我们将从零开始,最终实现一个能对典型电商API进行全链路压力测试的完整示例。

1. 压力测试:从“拍手”到“外科手术”

在深入代码之前,我们先厘清一个关键概念:“法兰西的处刑拍手”这个戏称,恰恰点出了低效压测与高效压测的天壤之别。

  • 低效压测(“乱棍拍手”):写个循环,拼命向某个URL发送GET请求。它只关心“服务器死没死”,无法告诉你瓶颈在哪(数据库、CPU、内存还是网络?),也无法模拟真实用户思考时间、操作流程的差异。结果往往具有误导性。
  • 高效压测(“外科手术式处刑”):像手术一样精准。它能定义不同的用户角色(浏览者、购买者、管理员),编排复杂的操作序列(访问首页->登录->搜索商品->加入购物车->下单),控制用户增长速率(每秒增加10个用户),并监控系统各级资源指标。目的是定位问题,而不仅仅是发现问题。

我们今天使用的Locust,就是一个允许你用Python代码定义所有用户行为的高效压测工具。它采用协程(gevent)实现,单机就能模拟数千并发用户,并且自带Web UI实时监控。

2. 环境准备:安装Locust

Locust的安装非常简单。确保你的系统已安装Python(3.7及以上版本)。

步骤1:使用pip安装

pip install locust

安装完成后,可以通过以下命令验证:

locust -V

这将输出Locust的版本号。

步骤2:理解核心概念在写代码前,先了解Locust脚本的三个核心组成部分:

  1. HttpUser:代表一类虚拟用户。你可以创建多个类来模拟不同角色的用户。
  2. task装饰器:用于标记一个方法是用户要执行的任务。可以设置任务的权重。
  3. client属性:是HttpUser实例内部的HttpSession对象,用于发起HTTP请求,用法和requests库非常相似。

3. 第一个“处刑拍手”脚本:测试简单API

让我们从一个最简单的例子开始,测试一个公开的API接口(例如https://httpbin.org/get)。

创建一个名为locustfile.py的文件(这是Locust默认查找的脚本文件名)。

# locustfile.py from locust import HttpUser, task, between class QuickstartUser(HttpUser): # between 用于设置用户执行每个任务后等待的随机时间范围(单位:秒) wait_time = between(1, 5) @task def get_homepage(self): # 使用client发起GET请求 response = self.client.get("/get") # 你可以对响应进行断言 # assert response.status_code == 200 # assert "url" in response.json() @task(3) # 权重为3,意味着这个任务被执行的频率是get_homepage的3倍 def get_status(self): self.client.get("/status/200")

代码解释

  • 我们定义了一个名为QuickstartUser的用户类。
  • wait_time = between(1, 5)表示每个用户在完成一个任务后,会随机等待1到5秒,这模拟了用户的思考或阅读时间,让压测更真实。
  • @task装饰器标记了用户的任务。@task(3)表示该任务的权重是3。在默认情况下,Locust会随机选择任务执行,权重越高,被选中的概率越大。在这个例子中,get_status被调用的概率大约是get_homepage的3倍。

4. 运行并观察你的“处刑官”

在终端中,进入locustfile.py所在的目录,运行以下命令:

locust

如果脚本文件名不是locustfile.py,则需要指定:

locust -f your_script.py

启动后,控制台会输出类似以下信息:

[2024-05-XX ...] INFO/locust.main: Starting web interface at http://0.0.0.0:8089 [2024-05-XX ...] INFO/locust.main: Starting Locust X.X.X

现在,打开浏览器,访问http://localhost:8089,你将看到Locust的Web UI。

Web UI 配置界面

  1. Number of users (peak concurrency):要模拟的用户总数。
  2. Spawn rate (users started/second):每秒启动多少个用户,用于控制压力爬升速度。
  3. Host:被测试系统的根URL(例如:https://httpbin.org)。我们在代码中用的是相对路径/get,会和这里的主机名拼接成完整URL。

填写Hosthttps://httpbin.org,设置用户数为10,生成率为1,点击“Start swarming”

观察监控面板

  • Statistics:表格显示每个请求的统计信息,包括请求类型、名称、请求次数、失败次数、平均/最小/最大响应时间,以及重要的RPS(Requests per Second,每秒请求数)
  • Charts:实时图表展示总RPS和响应时间的变化。
  • Failures:显示失败的请求及其原因。
  • Exceptions:显示运行过程中抛出的异常。
  • Download Data:测试结束后,可以下载CSV格式的报告。

5. 进阶实战:模拟电商用户完整行为链

现在,我们来构建一个更真实的“处刑”场景:模拟用户在一个电商平台上的典型操作流。假设我们测试一个名为http://my-shop-api.com的待测服务。

场景设计

  1. 30%的用户只浏览商品列表和详情。
  2. 70%的用户执行完整流程:浏览 -> 登录 -> 将商品加入购物车 -> 下单。
  3. 需要参数化数据(不同的用户账号、商品ID)。

更新后的locustfile.py

# locustfile.py - 电商全链路压测示例 from locust import HttpUser, task, between, TaskSet import random # 参数化数据池 USER_CREDENTIALS = [ {"username": "user1", "password": "pass1"}, {"username": "user2", "password": "pass2"}, # ... 可以从文件读取更多用户 ] PRODUCT_IDS = [1001, 1002, 1003, 1004, 1005] class BrowseBehavior(TaskSet): """浏览行为任务集""" @task(2) def view_product_list(self): # 模拟查看商品列表,可能带分页参数 with self.client.get("/api/products?page=1&size=20", catch_response=True) as response: if response.status_code == 200: # 可以从响应中解析出商品ID,用于后续查看详情,这里简单随机选一个 self.product_id = random.choice(PRODUCT_IDS) response.success() else: response.failure(f"Failed to get product list: {response.status_code}") @task def view_product_detail(self): # 确保product_id已从列表接口“获取” pid = getattr(self, 'product_id', random.choice(PRODUCT_IDS)) with self.client.get(f"/api/products/{pid}", name="/api/products/[id]", catch_response=True) as response: if response.status_code == 200 and "name" in response.text: response.success() else: response.failure(f"Failed to get product detail for {pid}") @task(1) def stop(self): # 有概率跳出浏览行为,返回父级任务集 self.interrupt() class BuyerBehavior(TaskSet): """购买者行为任务集,继承自浏览行为,并添加购物操作""" def on_start(self): """每个用户实例开始时会执行一次,用于登录""" credentials = random.choice(USER_CREDENTIALS) self.username = credentials['username'] response = self.client.post("/api/auth/login", json=credentials) if response.status_code == 200: self.client.headers.update({'Authorization': f'Bearer {response.json().get("token")}'}) print(f"{self.username} logged in successfully.") else: print(f"Login failed for {self.username}") @task class NestedBrowseBehavior(BrowseBehavior): """嵌套,购买者首先也是一个浏览者""" pass @task(2) def add_to_cart(self): pid = random.choice(PRODUCT_IDS) payload = {"productId": pid, "quantity": 1} with self.client.post("/api/cart/items", json=payload, catch_response=True) as response: if response.status_code == 201: response.success() self.cart_item_id = response.json().get('id') # 假设返回购物车项ID else: response.failure(f"Failed to add product {pid} to cart") @task(1) def checkout(self): # 简化下单流程 with self.client.post("/api/orders/checkout", json={}, catch_response=True) as response: if response.status_code == 200 or response.status_code == 201: response.success() print(f"{self.username} placed an order.") else: response.failure(f"Checkout failed for {self.username}") class WebsiteUser(HttpUser): """主用户类""" wait_time = between(2, 8) # 电商用户操作间隔更长一些 # 70%的用户执行购买者流程,30%的用户只执行浏览者流程 tasks = {BuyerBehavior: 7, BrowseBehavior: 3} # 注意:BrowseBehavior 被直接引用,也嵌套在BuyerBehavior中,权重由Locust内部调度

脚本深度解析

  1. TaskSet:用于将任务分组。BrowseBehavior封装了所有浏览操作,BuyerBehavior封装了购买操作(其中又嵌套了BrowseBehavior)。这使得代码结构清晰,易于维护。
  2. on_start方法:每个用户实例在开始执行其任务循环前会调用一次,非常适合放置登录等初始化操作。
  3. @task权重:在WebsiteUser中,tasks = {BuyerBehavior: 7, BrowseBehavior: 3}表示有70%的概率用户会执行BuyerBehavior任务集,30%的概率执行BrowseBehavior任务集。
  4. 参数化与动态数据
    • USER_CREDENTIALSPRODUCT_IDS模拟了不同的测试数据。
    • name参数在client.get中用于聚合统计。将/api/products/1001/api/products/1002统一命名为/api/products/[id],这样在报表中它们会被合并统计,更清晰。
    • catch_response=True允许你手动控制请求的成功/失败判定,非常灵活。
  5. self.interrupt():用于从嵌套的TaskSet中跳出,返回到父级任务集。

6. 运行复杂场景与结果分析

使用同样的命令启动Locust,在Web UI中设置目标主机为http://my-shop-api.com(请替换为你的测试地址或使用Mock服务)。

关键指标解读(在Statistics标签页)

  • Type / Name:请求的接口。
  • # Requests:总请求数。
  • # Fails:失败数。失败率是压测首要关注点
  • Average (ms), Min, Max:平均、最小、最大响应时间。
  • Median (ms):中位数响应时间,50%的请求快于此值。
  • p95 (ms), p99 (ms):95%和99%分位响应时间。这是衡量用户体验和SLA的关键。即使平均响应时间很好,但p99很高,意味着有1%的用户遭遇了严重延迟。
  • Requests/s:该接口的每秒请求数。

如何定位瓶颈

  1. 看失败:如果登录接口大量失败,可能是验证服务或数据库瓶颈。
  2. 看响应时间趋势:在Charts标签页,随着并发用户数增加,如果响应时间曲线陡然上升,那个拐点可能就是系统的当前容量极限。
  3. 对比接口:如果/api/products很慢,但/api/products/[id]很快,瓶颈可能在列表查询的数据库索引或分页逻辑上。
  4. 结合系统监控:真正的“处刑官”需要内外结合。在压测同时,使用如grafana+prometheus监控被测服务器的CPU、内存、磁盘I/O、数据库连接数、慢查询等。当Locust显示响应时间变慢时,去系统监控里找哪个资源先达到了瓶颈。

7. 常见问题与排查思路

问题现象可能原因排查方式解决方案
压测机CPU先达到100%Locust单机协程数过多,压测机自身成为瓶颈。监控压测机资源。观察Locust的RPS是否上不去。1. 使用分布式模式运行Locust(一个Master,多个Worker)。
2. 使用更强大的压测机。
3. 优化测试脚本,减少不必要的计算。
“Socket”相关错误操作系统或被测服务器端口耗尽。检查压测机和服务器上的netstat连接数。1. 调整系统参数(如net.ipv4.ip_local_port_range,net.ipv4.tcp_tw_reuse)。
2. 增加服务器连接池大小。
3. 在Locust的HttpUser中设置fixed_count或使用连接池。
响应时间慢,但服务器资源很低瓶颈可能在数据库、外部API、或中间件(如Redis、MQ)。检查数据库监控(慢查询、锁等待)、外部服务调用链。1. 优化数据库查询,添加索引。
2. 检查外部依赖服务的健康状况和限流策略。
3. 引入缓存。
Locust Web UI 无法访问或卡死默认的8089端口被占用,或Web界面在高并发下资源消耗大。检查端口,查看Master节点日志。1. 启动时指定其他端口:locust --web-port=8090
2. 使用无头模式运行并输出日志:locust --headless -u 1000 -r 100 --run-time 10m --csv=report
任务权重似乎不生效TaskSet@task的权重理解有误,或使用了execute_task检查tasks属性定义和@task装饰器参数。牢记:HttpUser.tasks列表中的权重是选择哪个TaskSet@task方法的概率。嵌套TaskSet内部的任务权重是独立的。

8. 最佳实践与工程建议

要让你的“处刑拍手”专业且高效,请遵循以下实践:

  1. 环境隔离:永远在预发布环境性能测试专用环境进行压测,严禁直接对生产环境操作。
  2. 数据准备与清理
    • 使用独立的测试数据库,并通过脚本预先灌入足够多样化的数据(百万级)。
    • 压测脚本应包含“数据清理”或使用能自动回滚的测试账号,避免产生垃圾数据。
  3. 渐进式加压
    • 不要一开始就上最大并发。使用--spawn-rate参数缓慢增加用户数,观察系统指标变化,找到性能拐点。
  4. 结果可复现
    • 使用--csv参数将每次压测结果输出为CSV文件,便于对比历史数据。
    • 记录详细的测试配置(Locust版本、脚本版本、服务器配置、网络条件)。
  5. 脚本可维护性
    • 将配置(如主机名、用户池文件路径)提取到外部文件(如config.py或环境变量)。
    • 使用@events监听器,在测试开始和结束时执行自定义操作(如准备数据、生成报告)。
    from locust import events import logging @events.test_start.add_listener def on_test_start(environment, **kwargs): logging.info("压力测试即将开始,正在初始化测试数据...") # 调用数据初始化脚本 @events.test_stop.add_listener def on_test_stop(environment, **kwargs): logging.info("压力测试结束,正在清理环境...") # 调用数据清理脚本
  6. 断言与验证:善用catch_response=True和手动success()/failure(),不仅检查HTTP状态码,还要检查响应体内容,确保业务逻辑正确。

通过以上步骤,你已经掌握了使用Locust构建一个精准、高效、可编程的“压力测试执行官”的全部核心技能。从简单的接口测试到复杂的全链路业务场景模拟,你现在可以系统地评估你的服务在真实流量下的表现,并精准地找到性能瓶颈所在。记住,压测的最终目的不是击垮系统,而是了解它、加固它。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询