1. 项目概述为什么是Locust如果你正在寻找一个能让你用代码思维来驾驭性能测试的工具而不是在复杂的图形界面里点来点去那么Locust很可能就是你的菜。我最早接触性能测试是从LoadRunner和JMeter开始的它们功能强大但有时候那种“配置驱动”的模式在面对需要高度定制化、逻辑复杂的压测场景时会显得有些笨重。直到遇到Locust我才发现性能测试可以如此“Pythonic”——用你熟悉的Python代码来定义用户行为这不仅仅是工具的改变更是思维模式的切换。Locust是一个开源的、可编程的负载测试框架。它的核心设计理念是“用代码定义用户行为”。这意味着你不再需要学习特定工具的脚本语言如JMeter的BeanShell或LoadRunner的C/Vuser直接用Python写逻辑。这对于开发人员和测试人员来说门槛大大降低尤其是当你的压测场景需要包含复杂的业务逻辑、动态参数化或者依赖外部服务时Locust的灵活性优势就非常明显了。从网络热词中频繁出现的“locust性能测试框架搭建”、“jmeter性能测试步骤”对比就能看出大家既关心传统工具的流程也关注这种代码化新工具的实践。简单来说Locust解决了这样一个痛点如何快速、灵活地模拟出最贴近真实用户的复杂行为并观察系统在压力下的表现。它适合那些对Python有一定了解希望压测脚本能像项目代码一样易于版本管理、复用和集成的团队。接下来我们就从最基础的名词和用法开始彻底搞懂这个工具。2. 核心概念拆解Locust世界里的“行话”要玩转Locust首先得理解它构建世界的几个核心概念。这些概念共同协作描绘出完整的压测场景。2.1 用户User与用户类User Class这是Locust里最核心的抽象。一个User类通常继承自HttpUser或User代表了一类虚拟用户的行为模式。你可以把它想象成一个“演员的剧本”。HttpUser: 最常用的类用于模拟HTTP/HTTPS请求的用户。它内置了一个client属性实际上是一个requests.Session的封装所以你几乎可以用requests库的所有方式来发起请求。User: 更通用的基类如果你要测试的不是HTTP协议比如WebSocket, MQTT等你需要继承这个类并自己实现client。在每个User类中你会定义一系列任务Task来告诉Locust这个“用户”应该做什么。2.2 任务Task与权重weight任务是User类中定义的具体行为通常是一个Python方法并用task装饰器标记。一个用户类可以有多个任务。任务权重weight:task装饰器可以接收一个整数参数比如task(3)。这个权重决定了该任务被执行的相对频率。如果任务A权重为3任务B权重为1那么在长时间运行中用户执行任务A的次数大约是任务B的3倍。这非常利于模拟真实场景中不同业务操作的比例。注意权重是比例关系不是绝对次数。Locust会按照权重比例随机选择下一个要执行的任务。2.3 等待时间wait_time用户不会像机器一样毫不停歇地执行任务。wait_time用于定义用户在两次任务执行之间需要等待多长时间这使负载更接近真实用户思考、阅读的间隔。最常用的有两种方式between(min_wait, max_wait): 等待一个在min_wait和max_wait之间的随机时间单位秒。例如wait_time between(1, 5)表示每次执行完任务后等待1到5秒再执行下一个。constant(wait_time): 每次等待固定的时间。你可以在User类中直接设置wait_time属性。2.4 客户端client对于HttpUser来说self.client就是发起请求的利器。它的用法和requests几乎一致支持get,post,put,delete等方法。Locust会自动捕获通过这些方法发起的请求并记录响应时间、成功率等指标。from locust import HttpUser, task, between class QuickstartUser(HttpUser): wait_time between(1, 2.5) task def hello_world(self): # 使用 self.client 发起请求 self.client.get(/hello) self.client.post(/login, json{username:foo, password:bar})2.5 事件Events事件钩子Hooks是Locust提供的一种在测试生命周期特定时刻注入自定义逻辑的机制。这是实现高级功能的关键比如init: 当Locust进程启动时分布式模式下每个Worker都会触发。test_start/test_stop: 整个压测开始和结束时触发。request: 每次请求发出后触发可以用于自定义统计或处理响应。例如你可以在test_start时从数据库读取测试数据在request时对特定的失败响应进行额外处理。2.6 形态Shape这是Locust用于控制负载曲线的强大功能。通过定义stages你可以模拟出复杂的用户增长模式如“逐渐增加用户数保持一段时间峰值再逐渐下降”这比简单的“固定并发数”要真实得多。from locust import LoadTestShape class StagesShape(LoadTestShape): stages [ {duration: 60, users: 10, spawn_rate: 10}, # 第1分钟10个用户每秒生成10个 {duration: 120, users: 50, spawn_rate: 5}, # 第2-3分钟增加到50用户每秒生成5个 {duration: 180, users: 50, spawn_rate: 50}, # 第3-4分钟保持50用户 {duration: 240, users: 0, spawn_rate: 10}, # 第4-5分钟降回0用户 ] def tick(self): # Locust会周期性调用此方法来确定当前的用户数和生成速率 run_time self.get_run_time() for stage in self.stages: if run_time stage[duration]: return (stage[users], stage[spawn_rate]) return None # 返回None表示测试结束3. 从零开始编写你的第一个Locust脚本理论说得再多不如动手写一个。我们来创建一个最简单的压测脚本目标是测试一个假设的API服务。3.1 环境准备与安装首先确保你安装了Python3.6及以上版本。然后通过pip安装Locustpip install locust验证安装是否成功locust -V3.2 基础脚本结构剖析创建一个名为locustfile.py的文件这是Locust默认寻找的入口文件并输入以下内容from locust import HttpUser, task, between class WebsiteUser(HttpUser): 模拟一个访问网站的用户行为。 # 用户在执行任务之间等待1到3秒 wait_time between(1, 3) # 权重为3的任务访问首页 task(3) def view_homepage(self): # 使用client发起GET请求name参数用于在统计报告中标识这个请求 self.client.get(/, name首页) # 权重为1的任务登录操作 task(1) def login(self): # 发起一个POST请求携带JSON数据 response self.client.post(/api/login, json{username: test_user, password: 123456}, name用户登录) # 你可以对响应进行断言虽然Locust主要关注性能但功能验证有时也有必要 if response.status_code ! 200: response.failure(f登录失败! 状态码: {response.status_code}) else: # 假设成功返回的JSON里包含token self.token response.json().get(token) # 每个虚拟用户启动时执行一次用于初始化如获取认证信息 def on_start(self): # 这里可以调用登录确保用户有session或token # self.login() # 注意如果调用登录操作会被记录两次一次on_start一次task pass # 每个虚拟用户停止时执行一次 def on_stop(self): # 清理工作如登出 pass代码解读与实操要点继承HttpUser因为我们要测试的是HTTP服务。wait_time定义了用户行为间隔让负载更真实。task装饰器view_homepage权重为3login权重为1。这意味着在长时间运行中访问首页的请求量大约是登录请求的3倍。self.client这是发起请求的核心对象。name参数至关重要它用于在Locust的统计报告中聚合相同“名称”的请求。如果你不用name那么每个不同的URL都会被单独统计/user/1和/user/2会被算作两个不同的请求这不利于分析。最佳实践是将同一类API操作命名为同一个name。响应验证使用response.failure()可以手动标记一个请求为失败。Locust默认将HTTP状态码400的请求记为失败但有时业务失败返回的可能是200状态码错误信息这时就需要手动标记。on_start与on_stop这是用户级别的生命周期钩子。on_start在每个虚拟用户开始运行任务前执行一次非常适合用来做登录认证获取后续请求所需的token或session。on_stop则在用户停止时执行。3.3 运行Locust测试保存好locustfile.py后打开终端进入该文件所在目录执行locust默认情况下Locust会使用locustfile.py作为脚本并在http://localhost:8089启动一个Web UI。通过Web UI运行打开浏览器访问http://localhost:8089。你会看到Locust的启动页面。Number of users (peak concurrency): 设置模拟的最大总用户数。Spawn rate (users started/second): 设置每秒启动多少个用户直到达到最大用户数。Host: 填写被测试系统的根地址如http://your-api-server.com。注意在locustfile.py中我们写的请求路径是/或/api/login它们会与这里设置的Host拼接成完整的URL。点击“Start swarming”按钮测试开始。无头模式命令行运行如果你不需要UI或者想在CI/CD流水线中运行可以使用命令行模式locust --headless --users 100 --spawn-rate 10 --run-time 1m30s --hosthttp://your-api-server.com--headless: 启用无头模式。--users: 最大用户数。--spawn-rate: 每秒生成用户数。--run-time: 测试运行时间例如1m30s表示1分30秒。--host: 目标主机。4. 结果分析与核心指标解读测试运行后无论是在Web UI还是命令行输出中你都会看到一系列指标。理解这些指标是性能测试分析的关键。4.1 Locust Web UI 核心面板Statistics统计: 这是最重要的标签页。以表格形式展示了所有被标记通过name参数的请求的聚合数据。Type: 请求名称。Requests: 总请求数。Fails: 失败请求数。Median, 90%, 95%, 99%: 响应时间的百分位数。例如“90%”为200ms意味着90%的请求响应时间在200ms以内。95%和99%值对于评估长尾效应慢请求至关重要它们能告诉你最慢的那部分用户的体验。Average: 平均响应时间。Min/Max: 最小/最大响应时间。Average Size: 平均响应体大小。Current RPS: 当前每秒请求数。Current Failures/s: 当前每秒失败数。Charts图表: 以实时图表展示总RPS、响应时间、用户数随时间的变化趋势。这对于观察测试过程中的性能波动非常直观。Failures失败: 列出所有失败的请求包括URL、异常信息、发生时间等便于快速定位问题。Exceptions异常: 显示测试运行过程中抛出的Python异常。Download Data下载数据: 允许你将统计数据、请求数据、异常数据以CSV格式下载用于进一步分析或生成报告。4.2 关键性能指标KPIs理解吞吐量Throughput/RPS: 系统每秒处理的请求数。这是衡量系统处理能力的核心指标。在负载增加时观察RPS是上升、持平还是下降可以判断系统瓶颈。响应时间Response Time: 从发送请求到接收完整响应所花费的时间。重点关注中位数Median、90分位和95/99分位值。平均响应时间容易受极值影响而分位值更能代表大多数用户的体验。例如一个API平均响应时间50ms看起来不错但如果99%值是2000ms说明有1%的用户遭遇了严重延迟。错误率Error Rate: 失败请求数占总请求数的比例。通常要求错误率低于0.1%或0.01%取决于业务关键性。高错误率往往意味着系统已过载或存在功能缺陷。并发用户数Number of Users: Locust中模拟的活跃用户数。注意这个“用户”是行为意义上的不等于系统连接数或线程数。实操心得不要只盯着“平均响应时间”和“总RPS”。一次合格的性能测试分析必须结合趋势图和分位值。比如当用户数达到某个临界点后如果95%响应时间曲线开始陡增而RPS曲线走平甚至下降这就是系统达到性能瓶颈的典型信号。此时即使平均响应时间看起来还行系统体验也已经恶化了。5. 进阶技巧与常见问题排查掌握了基础用法后我们来看看如何让Locust脚本更强大、更健壮以及如何解决常见问题。5.1 参数化与数据驱动真实的用户不会都用同样的数据。我们需要从外部文件如CSV、JSON或数据库中读取数据供虚拟用户使用。示例从CSV读取用户数据进行登录创建一个users.csv文件username,password user1,pass1 user2,pass2 user3,pass3修改locustfile.pyfrom locust import HttpUser, task, between import csv import random class DataDrivenUser(HttpUser): wait_time between(2, 5) # 在类级别读取CSV数据所有用户实例共享注意线程安全 user_data [] with open(users.csv, r) as f: reader csv.DictReader(f) for row in reader: user_data.append(row) def on_start(self): # 每个用户实例启动时从数据池中随机取一条或按顺序取作为该用户的身份 self.current_user random.choice(self.user_data) # 或者使用队列实现更精确的分配避免重复这里用随机简单演示 task def login_with_data(self): # 使用分配到的用户数据发起请求 response self.client.post(/api/login, json{username: self.current_user[username], password: self.current_user[password]}, name参数化登录) if response.status_code 200: self.token response.json().get(token) # 登录成功后可以携带token访问其他需要认证的接口 self.client.get(/api/profile, headers{Authorization: fBearer {self.token}}, name获取用户资料)注意上述简单随机选择在长时间运行下可能导致数据使用不均。对于严格的“每条数据只用一次”的需求需要使用队列queue.Queue来管理数据并注意多进程/分布式运行时的数据共享问题。5.2 处理关联与动态数据很多场景下一个请求的响应数据是下一个请求的参数。例如先创建订单返回订单ID再用这个ID查询订单。from locust import HttpUser, task, between import re class CorrelatedUser(HttpUser): wait_time between(1, 3) task def create_and_check_order(self): # 1. 创建订单 create_resp self.client.post(/api/orders, json{product_id: 123, quantity: 2}, name创建订单) if create_resp.status_code 201: order_id create_resp.json().get(id) # 2. 使用上一步返回的order_id查询订单详情 self.client.get(f/api/orders/{order_id}, name查询订单详情) # 3. 甚至可以继续基于此操作比如支付 # self.client.post(f/api/orders/{order_id}/pay, name支付订单)技巧对于返回HTML页面的场景如果需要从页面中提取csrf_token、next_id等动态值可以使用BeautifulSoup或lxml进行解析或者用正则表达式匹配。5.3 自定义客户端与测试非HTTP协议虽然HttpUser是主流但Locust也能测试其他协议。你需要继承基础的User类并实现自己的客户端。from locust import User, task, between import socket import json class SocketUser(User): wait_time between(0.1, 0.5) # 对于socket等待时间可能更短 def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.client None def on_start(self): 连接Socket服务器 self.client socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.client.connect((self.host.split(://)[1], 9999)) # 简单处理host task def send_message(self): message json.dumps({action: ping, data: hello}) start_time time.time() try: self.client.send(message.encode()) response self.client.recv(1024) # 记录成功 self.environment.events.request.fire( request_typesocket, nameping, response_timeint((time.time() - start_time) * 1000), # 毫秒 response_lengthlen(response), exceptionNone, ) except Exception as e: # 记录失败 self.environment.events.request.fire( request_typesocket, nameping, response_timeint((time.time() - start_time) * 1000), response_length0, exceptione, ) def on_stop(self): 关闭连接 if self.client: self.client.close()关键点在于使用self.environment.events.request.fire()方法手动触发请求事件这样Locust才能统计到这次操作的响应时间和成功与否。5.4 常见问题与排查技巧实录问题1Locust Web UI无法访问或“Connection refused”检查默认端口8089是否被占用。可以用locust --web-port8090指定其他端口。检查防火墙或安全组是否阻止了该端口。问题2测试时RPS很低但CPU/内存占用不高可能原因1wait_time设置过长。检查脚本中wait_time between(...)的值如果等待时间远大于任务执行时间虚拟用户大部分时间在“睡觉”自然无法产生高压力。可能原因2被测试服务响应极快而单机Locust达到了性能瓶颈。Locust本身是单进程异步IO基于gevent但在单机模式下如果目标服务响应极快1ms单个Locust进程可能无法产生足够压力。考虑使用--master和--worker进行分布式压测启动多个Worker进程。检查运行Locust的机器本身CPU是否已跑满。可能原因3网络延迟或客户端限制。检查网络状况以及操作系统对单个进程打开文件描述符的限制ulimit -n。问题3出现大量“ConnectionError”或“Timeout”失败排查首先确认被测试服务是否存活且网络可达。排查被测试服务是否已达到其连接数上限或处理极限。查看服务端日志和监控。调整可以尝试增加Locust的HTTP客户端超时时间虽然不推荐在压测中设置过长但可用于验证。class CustomUser(HttpUser): # 设置连接超时和读取超时单位秒 connection_timeout 10.0 network_timeout 10.0问题4如何模拟更复杂的用户思考时间或业务流程使用wait_time的constant_pacingwait_time constant_pacing(2)可以确保任务执行间隔包括任务执行时间至少为2秒。如果你想严格控制一个循环如“浏览商品-加入购物车-下单”的总时间这很有用。在任务方法内使用self.wait()你可以在一个任务内部进行复杂的等待逻辑例如根据上一个请求的响应内容决定等待时长。问题5分布式运行下数据文件如何共享方案A推荐使用共享存储如NFS、云存储或者将数据文件打包到Docker镜像中。确保所有Worker节点都能以相同路径访问到文件。方案B使用中央数据库如Redis、MySQL存储测试数据所有Worker从其中读取。这需要在on_start或任务中建立数据库连接。方案C在Master节点启动时通过init事件将数据加载到内存然后利用Locust的消息机制自定义消息分发给各个Worker。这种方式更复杂但性能最好。个人踩坑心得在编写复杂的、有状态关联的脚本时务必为每个请求设置清晰、唯一的name。我曾经因为多个不同参数的POST请求都叫“创建订单”导致在统计页面里所有数据混在一起根本无法区分哪个接口性能有问题。后来强制自己养成习惯name创建订单-产品A、name创建订单-产品B或者用更通用的name创建订单 [POST /api/orders]问题迎刃而解。另外对于分布式测试脚本的依赖如外部数据文件、Python包必须在所有Worker节点上保持一致否则会出现难以预料的错误。