基于Locust的高并发压力测试实战:从原理到性能瓶颈分析

发布时间:2026/6/30 4:57:15

基于Locust的高并发压力测试实战:从原理到性能瓶颈分析 1. 项目概述当PROJECT MOGFACE遇上高并发洪峰最近在负责一个代号为“PROJECT MOGFACE”的内部业务系统优化这个系统承载着核心的用户交互与数据处理功能。随着业务量的快速增长我们越来越担心它在流量洪峰下的表现页面会不会卡死接口响应会不会飙升到秒级数据库连接池会不会被打爆这些疑问不能只靠“我觉得应该没问题”来回答必须用数据说话。于是一场针对PROJECT MOGFACE的压力测试被提上了日程。压力测试说白了就是模拟一群“暴躁”的用户在短时间内对系统发起海量请求看看系统在极限负载下的表现。工具选型上我们没有选择老牌的JMeter而是选择了Locust。原因很简单JMeter的图形化界面虽然友好但在编写复杂、灵活的测试逻辑时不如代码来得直接而Locust基于Python可以用纯代码定义用户行为对于开发背景的我们来说学习成本和定制化成本都更低。它模拟的是真实用户的行为序列更贴近我们的测试场景——模拟高并发用户请求。这次实战的目标很明确找出PROJECT MOGFACE在当前架构下的性能瓶颈量化其承载能力并为后续的扩容、缓存优化、代码重构提供数据支撑。2. 压力测试核心思路与Locust框架解析2.1 为什么是压力测试而不仅仅是功能测试功能测试确保系统“做得对”压力测试则确保系统“撑得住”。对于PROJECT MOGFACE这类有状态的服务压力测试能暴露出在单用户、低并发场景下永远发现不了的问题。例如资源竞争大量用户同时抢购同一商品或同时修改同一份资料可能引发超卖、数据覆盖或死锁。连接池耗尽数据库、Redis等中间件的连接数是有限的高并发下如果连接获取后未及时释放会导致后续请求排队甚至失败。内存泄漏与GC压力短时间内创建大量对象如果存在未被正确释放的引用会导致内存缓慢增长最终触发Full GC造成服务暂停。第三方依赖拖垮某个非核心接口调用了外部慢服务在高并发下可能耗尽工作线程导致整个服务不可用。我们的测试思路是渐进式施压。不是一上来就用最大并发数猛冲而是像“温水煮青蛙”一样逐步增加并发用户数Ramp-up观察系统各项指标响应时间、错误率、吞吐量的变化曲线找到性能拐点。这个拐点可能就是系统的最大稳定负载点。2.2 Locust框架的优势与工作原理解析Locust是一个开源、分布式的负载测试工具。它的核心哲学是“用代码定义用户行为”这带来了几个显著优势灵活性强所有测试场景Task都是普通的Python代码你可以轻松实现复杂的业务逻辑如先登录、再查询、后下单的完整链路甚至可以根据响应内容动态决定下一步操作。资源消耗低Locust是事件驱动的基于gevent协程。一个进程可以轻松模拟数千个并发用户对压测机本身的资源消耗远小于传统的多线程模型如JMeter这意味着我们可以用更少的机器产生更大的压力。分布式支持友好通过一个主节点Master协调多个从节点Worker可以轻松实现横向扩展将压力产生能力提升数个量级非常适合模拟超大规模并发。实时Web UI它提供了一个简洁的Web界面可以实时查看RPS每秒请求数、响应时间、并发用户数等关键指标并动态调整负载非常直观。它的工作原理可以简单理解为Master节点负责分发测试任务和收集数据Worker节点启动大量greenlet微线程每个greenlet代表一个虚拟用户User。这些用户按照你定义的wait_time等待时间和tasks任务集顺序执行HTTP请求。Locust会统计所有请求的成功失败、响应时间并汇总展示。注意Locust本身不产生压力压力来自于你编写的Python代码发出的请求。代码的效率直接影响压测能力。避免在Task中执行复杂的同步阻塞操作如大量CPU计算、同步文件IO否则会成为瓶颈。3. 针对PROJECT MOGFACE的测试场景设计与脚本编写3.1 定义核心测试场景与用户行为模型PROJECT MOGFACE系统包含多个模块我们不可能也没必要一次性全压。根据二八原则我们选取了最核心、访问最频繁的3个场景作为压测目标用户登录鉴权接口 (/api/v1/login)这是一个高频的POST请求涉及密码验证、Token生成和缓存写入。我们关注其在并发下的响应速度和Token生成服务的稳定性。核心数据查询接口 (/api/v1/data/query)这是一个带复杂查询条件的GET请求涉及数据库查询和结果组装。我们关注数据库连接池、SQL索引效率以及结果缓存命中率。业务提交接口 (/api/v1/action/submit)这是一个PUT请求涉及业务逻辑校验、数据库写入和消息队列发送。我们关注事务处理能力、数据库写入锁竞争以及消息队列的堆积情况。用户行为模型我们设计为“混合场景”70%的用户行为是查询20%是登录10%是提交。这样更贴近真实的生产流量分布。每个虚拟用户在任务执行间会随机等待1-3秒模拟用户思考时间。3.2 Locust测试脚本实战编写下面是我们为PROJECT MOGFACE编写的一个基础但完整的Locust测试脚本示例 (mogface_test.py)from locust import HttpUser, TaskSet, task, between import random import json class MogfaceUserBehavior(TaskSet): # 每个用户开始运行时执行一次用于初始化 def on_start(self): 模拟用户登录获取鉴权token self.client.headers {Content-Type: application/json} # 可以使用一个用户池避免所有用户用同一账号 username ftest_user_{random.randint(1, 10000)} login_payload {username: username, password: default_password_123} with self.client.post(/api/v1/login, jsonlogin_payload, catch_responseTrue) as response: if response.status_code 200: resp_json response.json() self.token resp_json.get(data, {}).get(token) if self.token: # 将token设置到后续请求的header中 self.client.headers.update({Authorization: fBearer {self.token}}) response.success() else: response.failure(Login succeeded but no token returned.) else: response.failure(fLogin failed with status code: {response.status_code}) task(3) # 权重为3执行频率高 def query_data(self): 模拟查询核心数据 # 构造随机查询参数模拟不同用户的查询条件 query_params { page: random.randint(1, 5), size: 20, keyword: random.choice([urgent, normal, review, ]), status: random.randint(0, 3) } # 使用catch_response来自定义成功/失败判断逻辑 with self.client.get(/api/v1/data/query, paramsquery_params, name/data/query, catch_responseTrue) as response: if response.status_code 200: resp_json response.json() # 业务层面判断code为0表示成功 if resp_json.get(code) 0: response.success() else: response.failure(fBusiness logic error: {resp_json.get(msg)}) else: response.failure(fHTTP error: {response.status_code}) task(1) # 权重为1执行频率低 def submit_action(self): 模拟提交业务动作 submit_payload { actionId: fact_{random.randint(1000, 9999)}, data: {field1: value1, count: random.randint(1, 10)} } with self.client.put(/api/v1/action/submit, jsonsubmit_payload, name/action/submit, catch_responseTrue) as response: if response.status_code 200: resp_json response.json() if resp_json.get(code) 0: response.success() else: response.failure(fSubmit failed: {resp_json.get(msg)}) else: response.failure(fHTTP error: {response.status_code}) class MogfacePressureUser(HttpUser): tasks [MogfaceUserBehavior] # 指定任务集 wait_time between(1, 3) # 每个任务执行后等待1-3秒 host http://your-mogface-server.com # 被测系统地址脚本编写要点解析on_start方法每个虚拟用户实例只会执行一次常用于登录等初始化操作。这里我们动态生成用户名避免所有用户使用同一账号触发服务端的频控或锁。task装饰器其中的权重参数weight决定了该任务被执行的相对频率。query_data权重为3submit_action权重为1结合70%查询、10%提交的模型权重比大致为7:1这里用3:1是一个简化。catch_response参数这是关键它允许我们不仅根据HTTP状态码如200还根据响应体的业务内容来判断请求成功与否。比如接口返回{“code”: 500, “msg”: “系统繁忙”}HTTP状态码仍是200但业务上是失败的我们必须将其标记为失败否则测试数据将严重失真。name参数在Locust的统计报告中会按name对请求进行分组。如果我们不给client.get/put指定name那么不同参数的同一接口会被视为不同请求导致统计分散。指定name后所有对/api/v1/data/query的请求都会被合并统计。4. 分布式压测环境搭建与执行策略4.1 单机与分布式部署模式选择对于轻量级测试单机运行Locust足够。命令很简单locust -f mogface_test.py --hosthttp://your-mogface-server.com然后访问http://localhost:8089即可启动测试。但当我们需要模拟上万甚至更高的并发时单机资源CPU、网络、端口会成为瓶颈。这时就需要采用分布式模式。分布式模式由一个Master节点和多个Worker节点组成Master节点负责分发测试任务、协调Worker、收集并展示汇总数据。它本身不模拟用户。Worker节点负责执行测试脚本模拟虚拟用户向目标系统发送请求。部署时确保Master和Worker都能访问到测试脚本(mogface_test.py)和被测系统。4.2 分布式压测执行流程启动Master(在一台控制机上)locust -f mogface_test.py --master --hosthttp://your-mogface-server.com --web-host0.0.0.0 --web-port8089--master指定为主节点。--web-host0.0.0.0允许从其他机器访问Web UI。启动Worker(在多个压力机上可以和多台)locust -f mogface_test.py --worker --master-hostMASTER_IP_ADDRESS将MASTER_IP_ADDRESS替换为Master节点的实际IP。每个Worker会连接到Master。在Web UI上配置并启动测试浏览器打开http://MASTER_IP:8089。输入目标总用户数Number of users、每秒启动用户数Spawn rate。例如想最终达到5000用户每秒启动100个。点击Start swarmingMaster会指挥所有Worker开始按比例产生负载。实操心得Worker数量估算需要多少台Worker一个粗略估算方法是一个普通的Worker进程单核大约能模拟1000-2000个并发用户取决于任务复杂度。如果你需要模拟1万用户建议准备5-8个Worker进程可以分布在2-4台物理机或虚拟机上。启动后务必观察Worker机器的CPU和网络使用率如果接近饱和就需要增加Worker节点。5. 关键监控指标解读与性能瓶颈分析压测过程中不能只盯着Locust的汇总数据。必须结合系统链路的全方位监控才能准确定位瓶颈。我们主要关注以下几类指标5.1 Locust核心监控指标RPS (Requests per Second)每秒请求数。这是系统吞吐量的直接体现。随着并发用户数增加RPS会增长但在达到系统瓶颈后RPS会持平甚至下降。响应时间Response Times重点关注平均响应时间、中位数50%分位和95%/99%分位响应时间。95%响应时间P95尤为重要它表示95%的请求在这个时间内完成能反映绝大多数用户的体验。如果P95时间随着并发增加而急剧上升说明系统存在瓶颈。失败率Failures任何非成功的请求HTTP错误或业务逻辑错误都会计入失败率。在压力测试中失败率应接近0%。一旦出现失败需立即查看错误类型常见的有连接超时、连接被拒绝、5xx服务器错误等。并发用户数Number of Users当前活跃的虚拟用户数。5.2 系统资源与中间件监控Locust告诉我们系统“表现如何”我们还需要知道系统“为什么这样”。这就需要监控服务器资源被测服务器PROJECT MOGFACE应用层CPU使用率持续高于80%可能意味着计算密集型瓶颈或代码效率问题。内存使用率持续增长可能暗示内存泄漏。GC日志频繁的Full GC会导致应用暂停Stop-The-World观察GC频率和耗时。线程池状态查看业务线程池如Tomcat的HTTP线程池是否活跃线程数打满出现任务队列堆积。数据库QPS/TPS每秒查询/事务数。连接数是否达到max_connections上限。慢查询日志压测期间产生的慢SQL是首要优化目标。锁等待高并发写入场景下行锁、表锁等待情况。缓存如Redis连接数同样关注是否打满。内存使用率是否接近上限。QPS和命令耗时avg_ttl等指标是否异常。网络观察压测机与被测机之间的网络带宽是否成为瓶颈iftop,nethogs。5.3 常见性能瓶颈模式与初步判断根据监控指标可以快速定位瓶颈方向响应时间增加但CPU/内存使用率很低瓶颈很可能不在应用本身而在下游依赖。可能是数据库慢查询、外部接口调用超时、或网络延迟。检查应用日志中的超时错误并监控数据库和外部服务。CPU使用率飙升到接近100%应用层存在计算瓶颈。可能是代码中存在低效算法如多层嵌套循环、序列化/反序列化JSON/XML开销过大、或日志打印过于频繁。使用jstack或Arthas等工具抓取线程栈查看哪些线程在消耗CPU。内存使用率持续增长且不回落伴随频繁Full GC存在内存泄漏。使用jmap生成堆转储Heap Dump然后用MAT或JProfiler分析找出疑似泄漏的对象和引用链。RPS上不去失败率增高连接超时/拒绝应用服务器或中间件的连接池耗尽。检查Tomcat的maxThreads数据库的max_connectionsRedis的maxclients等配置。也可能是服务器文件描述符File Descriptor用尽。TPS数据库事务数上不去数据库写入瓶颈。可能是磁盘IOPS不足、表锁/行锁竞争激烈、或事务过长。需要优化SQL、增加索引、考虑分库分表或引入队列削峰。6. PROJECT MOGFACE实战问题排查与优化记录在实际对PROJECT MOGFACE的压测中我们遇到了几个典型问题其排查和解决过程很有参考价值。6.1 问题一低并发下响应正常高并发下P95响应时间飙升且伴随大量错误现象当并发用户数超过200时Locust报告中的P95响应时间从200ms陡增至5s以上失败率升至15%错误类型主要是ConnectionTimeout和HTTP 500。排查过程查看应用服务器监控发现CPU使用率仅40%内存稳定初步排除应用计算瓶颈。查看应用日志发现大量数据库连接获取超时的异常日志Cannot get a connection from pool within 30 seconds。检查数据库监控数据库连接数接近配置上限100并且存在大量活跃连接和睡眠连接。分析代码检查数据库连接使用代码发现一处业务逻辑在事务中先查询数据然后调用一个外部HTTP接口进行校验最后更新数据库。这个外部HTTP接口响应很慢。根因分析数据库连接池配置为100。每个请求的事务中由于调用了慢外部接口导致数据库连接被长时间占用比如2秒。根据利特尔法则当并发请求数 * 平均连接持有时间 连接池大小时连接池就会迅速耗尽。后续请求获取不到连接等待超时后失败。解决方案短期调大数据库连接池大小如从100调到200并设置合理的连接获取超时时间如5秒避免线程长时间阻塞在等待连接上。中期将外部调用移出事务。先查询数据然后异步或同步调用外部接口校验通过后再开启事务进行更新。这样大大缩短了事务持有数据库连接的时间。长期为慢外部接口引入熔断、降级和缓存。如果外部接口不可用或超时使用本地缓存或默认规则进行降级处理保证核心流程可用。6.2 问题二RPS达到阈值后无法继续提升CPU使用率未饱和现象无论怎么增加并发用户数RPS稳定在1200左右无法突破。应用服务器CPU使用率在60%徘徊。排查过程检查压测机资源使用top和iftop发现压测机Worker的CPU和网络带宽均未打满。检查应用服务器线程池使用jstack导出线程栈发现大量业务线程处于RUNNABLE状态但堆栈显示它们卡在java.net.SocketInputStream.socketRead0这个原生方法上。检查网络和下游这提示线程可能在等待网络IO。检查发现所有慢请求都卡在同一个查询接口上该接口调用了一个核心的搜索服务Elasticsearch。检查搜索服务发现搜索服务的CPU使用率很高其监控显示索引的refresh_interval设置过短导致写入性能受影响进而影响了查询性能。根因分析瓶颈转移到了下游的搜索服务。应用服务器的线程在等待搜索服务的响应由于搜索服务处理能力达到上限导致应用服务器线程池中的线程大量处于等待状态限制了整体的吞吐量。解决方案优化搜索服务调整Elasticsearch的索引配置如适当调大refresh_interval优化分片和副本数提升其查询性能。应用层优化增加缓存对于非实时性要求极高的查询结果引入本地缓存Caffeine或分布式缓存Redis减少对搜索服务的直接调用。超时与降级为搜索服务调用设置合理的超时时间如200ms超时后返回降级结果如部分数据或默认值避免线程池被拖死。线程池隔离考虑使用Hystrix或Resilience4j等库将搜索服务调用隔离到独立的线程池中即使搜索服务变慢也不会影响其他正常接口的线程。6.3 问题三压测持续一段时间后内存缓慢增长现象进行30分钟稳定性压测固定并发用户数后应用服务器的堆内存使用率从30%缓慢增长到了70%。排查过程观察GC日志发现Young GC频率正常但Old区空间在持续增长。生成堆转储在内存使用率达到80%时使用jmap -dump:live,formatb,fileheap.hprof pid命令导出堆内存快照。使用MAT分析导入堆转储文件通过Leak Suspects报告和Dominator Tree功能发现了一批HttpClient连接管理器对象和大量的byte[]数组被长期持有无法被回收。回溯代码发现项目中使用了静态的HttpClient实例但在配置连接池时没有正确设置连接存活时间evictIdleConnections和总连接数上限。导致一些陈旧的、空闲的连接及其关联的缓冲区一直没有被释放。根因分析HttpClient连接池管理不当导致的内存泄漏。虽然使用了连接池但由于缺乏有效的空闲连接驱逐策略连接对象及其内部缓冲区byte[]会常驻老年代随着时间推移不断累积。解决方案配置合理的连接池参数设置MaxIdleTime最大空闲时间让连接管理器定期驱逐空闲过久的连接。设置MaxTotal和DefaultMaxPerRoute限制连接池总量。考虑使用更现代的HTTP客户端如OkHttp或Apache HttpClient 5.x它们通常有更健壮的连接管理机制。建立内存监控预警在监控系统中设置JVM堆内存使用率的告警阈值如80%以便及时发现问题。7. 压测报告撰写与性能优化建议落地压测的最终价值不在于“测”而在于“改”。一份清晰的压测报告和可落地的优化建议是闭环的关键。7.1 如何撰写一份有价值的压测报告报告不应只是数据的罗列而应讲述一个“性能故事”测试概述说明测试目标、测试范围哪些接口、测试环境硬件配置、软件版本、网络拓扑、测试工具与策略Locust 分布式 施压模式。性能基准给出在“正常负载”如50%的预期最大并发下的性能指标RPS P95 RT 错误率作为基准线。负载测试结果展示随着并发用户数增加如从100到1000各项关键指标RPS 响应时间分位图 错误率 服务器资源使用率的变化曲线和表格。明确指出性能拐点如当并发达到800时P95响应时间超过1秒定义为最大建议负载。稳定性测试结果在最大建议负载下持续运行30分钟或1小时观察指标是否平稳内存是否有泄漏趋势。瓶颈分析与根因这是报告的核心。详细描述发现的问题如上述的连接池耗尽、下游服务瓶颈、内存泄漏并附上证据链监控截图、日志片段、分析工具输出。优化建议与预期收益针对每个瓶颈提出具体、可实施的优化方案代码改动、配置调整、架构升级并预估优化后性能可能提升的幅度。风险与后续计划说明当前未解决的风险点以及下一步的压测/优化计划如容量规划、灾难恢复测试。7.2 从测试到优化的闭环压测报告出来后需要推动优化工作落地优先级排序根据瓶颈的严重性对用户体验和业务的影响程度和修复成本对优化建议进行排序。通常修复一个导致系统不可用的连接池问题优先级高于优化一个慢查询。小范围验证在开发或测试环境先应用优化措施如修改配置、修复代码然后进行对比压测。确保优化确实有效且没有引入新的问题。灰度上线与监控将优化后的代码灰度发布到生产环境同时加强监控。观察核心指标错误率、P95响应时间是否有改善。回归测试在大的版本迭代后应对核心链路进行定期的回归压测确保系统性能没有退化。性能优化是一个持续的过程不是一劳永逸的。将压力测试和性能监控作为研发流程的固定环节才能让PROJECT MOGFACE这类系统在业务高速增长时依然保持稳健。Locust这样的工具就是我们手中那把不可或缺的“压力标尺”帮助我们量出系统的真实高度也量出那些隐藏的裂缝。

相关新闻