资讯详情

Locust高并发压测实战:从脚本设计到瓶颈定位

📅 2026/9/10 1:10:28 | 华诺云谱 👁 阅读
Locust高并发压测实战:从脚本设计到瓶颈定位
如果你也经历过那种凌晨两点被监控电话叫醒打开面板看到CPU打满、数据库连接池耗尽、线上服务一个接一个雪崩的场景你应该能理解“压力测试”这四个字的分量。系统跑得好不好不是上线那一刻才确定的而是取决于你有没有在上线之前用足够真实的高并发流量把系统按在地上摩擦过。Locust就是我很喜欢用来干这件事的工具。它不像JMeter那样带着厚重的GUI也不像wrk那样只能发简单的HTTP请求它让你用纯Python代码定义用户行为用协程模拟成千上万的并发用户在微服务架构下做精准的压力测试。这篇文章我结合自己踩过的坑和实际项目经验把从脚本设计、分布式压测、结果分析到瓶颈定位的完整思路拆开讲清楚希望能给正在准备压测的同学一些可直接参考的实操经验。1. 为什么选Locust做高并发压测工具选型的一笔账1.1 Locust的核心模型不是“并发工具”而是“虚拟用户工厂”很多刚接触Locust的人会把它理解成一个“发请求的工具”这其实是个挺大的误区。Locust的核心抽象是“用户”User每个用户是一个独立的协程它在不停地执行你定义的任务——比如打开首页、登录、下单、查询订单。而“并发数”就是同时有多少个这样的用户协程在跑。这个模型和真实世界的对应关系非常直接1000个在线用户就是1000个协程在不断地执行业务流程。每个用户都有自己的独立状态可以持有自己的登录态、自己的参数数据完全模拟真实用户的行为节奏。这和JMeter里那种“线程组 采样器”的模型相比在表达业务复杂性的时候明显更自然。Locust的另一个底层优势是协程。它基于gevent实现并发单机就可以支撑几千甚至上万的并发用户而不需要像线程模型那样为每个用户开一个操作系统线程内存开销小得多。我之前在8核16G的压测机上用Locust单机跑过大概8000并发每个用户平均2秒一个请求CPU和内存都还有余量。要是用JMeter的线程模型跑到这个量级线程调度和内存占用早就把压测机自己拖垮了。1.2 与JMeter、wrk、Gatling的对比没有最好的工具只有最合适的场景不是说Locust天下无敌而是每个工具都有自己最擅长的场景。我在选型的时候一般这样判断工具核心模型擅长场景短板Locust协程 Python复杂业务逻辑、高并发分布式、持续集成内置报表较弱需配合GrafanaJMeter线程 GUI企业级测试、丰富的插件生态、非程序员团队脚本维护成本高资源开销大wrk / ab多线程 C简单接口的极限压测快速评估无法模拟业务流程无分布式GatlingActor Scala高性能压测、DSL风格脚本、专业报告学习曲线陡脚本对普通后端开发不够友好我做性能压测的原则是看场景选工具而不是看流行度选工具。如果只是验证某个接口的QPS上限wrk就够了5分钟出结论如果要模拟真实的用户行为链路并且要持续在CI里跑回归Locust是很好的选择因为脚本就是Python代码可以写断言、可以传参、可以跟pytest等测试框架结合、可以嵌入到DevOps流水线里。1.3 什么团队最适合把Locust作为压测主力根据我的经验以下几种团队最值得尝试LocustPython技术栈占主导的团队团队成员都熟悉Python写压测脚本几乎零学习成本日常的代码规范和代码评审流程可以直接复用到压测脚本上。微服务架构的团队服务多、链路长、依赖复杂需要一个既能压单接口又能编排整条业务链路的工具。Locust的TaskSet可以用嵌套的方式描述复杂的用户行为比在JMeter里拖拽逻辑控制器要直观得多。重视持续性能回归的团队性能问题最怕“上线时才知道”。如果把性能压测做成流水线里的一个环节每次发版前自动跑一轮轻量压测Locust的无头模式--headless和API接口非常适合做这件事。2. 从零搭建一个能“骗过”生产环境的压测环境2.1 环境准备几分钟跑起第一个压测脚本安装没什么好说的pip install locust一行搞定。我建议在虚拟环境里装避免污染全局Python环境。如果要用分布式压测注意控制端和所有worker节点都要装相同版本的Locust版本不一致经常会冒出一些莫名其妙的兼容性问题。写一个最简单的压测脚本先验证环境通不通from locust import HttpUser, task, between class WebsiteUser(HttpUser): wait_time between(1, 3) task def get_root(self): self.client.get(/api/v1/health)命令行启动locust -f load_test.py --hosthttps://staging.example.com --headless -u 1000 -r 100 --run-time 10m参数含义-u 1000表示模拟1000个虚拟用户-r 100表示每秒启动100个用户--run-time 10m表示跑10分钟。这种“用户递增”的启动方式很关键它能让系统有个“预热”的过程避免刚启动瞬间所有流量涌入把系统直接打死导致数据失真。注意--host压测的是staging环境不是生产环境。原则上压测前需要确认目标环境的容量和隔离情况避免影响线上真实用户。如果必须压测预发布或灰度环境务必提前协调好运维和业务团队设置好监控告警的屏蔽策略。2.2 设计能模拟真实用户行为的TaskSet有了最基本的脚本就要开始“加码”了。真实用户不会只访问一个接口而是在应用里完成一连串操作打开首页、搜索商品、查看详情、加购物车、下单、支付、查看订单。这一串行为就是Locust里的TaskSet。from locust import HttpUser, task, between, TaskSet import random class UserBehavior(TaskSet): def on_start(self): # 用户进来先登录模拟真实用户的登录态 resp self.client.post(/api/v1/auth/login, json{ username: fuser_{random.randint(1, 50000)}, password: test_password_123 }) self.token resp.json().get(token) task(1) def browse_home(self): self.client.get(/api/v1/home, headersself.auth_header) task(3) def search_product(self): keyword random.choice(KEYWORDS) self.client.get(/api/v1/search, params{q: keyword}, headersself.auth_header) task(2) def create_order(self): product_id random.choice(PRODUCT_IDS) self.client.post(/api/v1/orders, json{product_id: product_id}, headersself.auth_header) property def auth_header(self): return {Authorization: fBearer {self.token}} class WebsiteUser(HttpUser): tasks [UserBehavior] wait_time between(0.5, 5)这里有几个我认为值得注意的细节on_start方法用于模拟“用户进入系统”时的初始化动作一般用来做登录、获取token、初始化数据等。这比每个任务都重复调登录接口要高效得多也更接近真实用户行为。task(N)的权重参数表示这个任务被选中的概率比例。比如搜索任务是3下单任务是2说明真实用户里搜索商品的频次高于下单的频次。权重应该来自业务埋点数据而不是拍脑袋。wait_time是用户两次行为之间的思考时间。真实用户不可能毫秒不差地连续操作需要有一个符合实际的时间间隔。可以是固定值也可以用between(0.5, 5)表示0.5到5秒之间的随机等待。2.3 数据参数化决定压测结果可信度的关键环节很多团队做压测最容易犯的错误就是拿同一份数据反复打同一个接口。这样测出来的结果非常乐观但极不真实。真实场景下每个用户拿到的数据不同缓存命中率、数据库索引选择、分片路由结果都不一样系统承受的压力也完全不同。参数化要做的是这几件事账号池准备足够多的测试账号避免所有用户挤在同一个账号上。我之前见过一个压测1000并发用户全部用同一个token去打接口结果把Redis里那个key的读写打到了极限误判成Redis瓶颈其实真实场景根本不会这样。业务数据池商品ID、用户ID、订单号等要准备足够的量级随机抽取同时注意避开热点数据。比如搜索压测如果所有请求都搜同一个关键词那这个关键词的索引缓存会被打爆而得出的结论“搜索接口性能很好”是没有参考价值的。文件读取方式不要把10万行数据的CSV一次性读进内存然后随机选更推荐用numpy.random.choice配合分批加载的方式或者直接把数据灌进Redis里用随机key去取。简单场景下Python的random.choice在几万数据量级别下够用再大就要考虑效率了。可以额外在用户类里维护一个全局数据池import pandas as pd class WebsiteUser(HttpUser): data_pool pd.read_csv(test_data.csv) task def search(self): keyword self.data_pool.sample(1)[keyword].values[0] self.client.get(/api/v1/search, params{q: keyword})这样确保每个请求拿到的都是不同的关键词模拟出的数据库和缓存压力才接近真实。3. 精准压测的核心从“把机器打满”到“把问题问清楚”3.1 第一步先量化这次的压测目标很多人上来就定“压到服务器崩溃为止”这不是目标这是结果。精准压测要回答的是系统在什么样的并发量、什么样的事务混合比下还能保持什么样的性能水位。我一般建议在压测开始前把目标拆成三个维度容量目标系统需要支撑多少并发在线用户核心接口的峰值QPS是多少。这里的推导逻辑是日活用户 → 峰值在线率 → 平均每用户每秒请求数 → 核心接口QPS。推导过程要写出来方便后续复盘时调整参数。性能目标核心接口在目标并发下的平均响应时间ART和TP99响应时间是多少。注意TP99比平均值重要得多平均值会被大量快速请求拉低尾延迟才是影响真实用户体验的关键。稳定性目标在目标并发下持续运行多长时间比如30分钟或1小时期间不能出现错误率上升、内存溢出、连接池耗尽等稳定性问题。有了明确的目标压测才能有的放矢而不是“跑个看看”。我一般会把目标写进压测报告的开头后续所有数据都围绕这些目标来给结论。3.2 梯度加压与稳定运行压力测试的两个阶段精准压测不能一次性把并发拉到最大值而是应该用“梯度加压”的方式让系统逐步升温。我常用的策略是预热阶段2-3分钟并发从0慢慢升到目标值的20%让JIT编译、缓存预热、连接池初始化都完成。梯度爬升阶段5-10分钟并发按每1-2分钟增加20%-30%的速度向上爬持续观察各层监控。峰值保持阶段10-30分钟在目标并发下持续运行观察是否有缓慢恶化的问题比如内存泄漏、连接泄漏。梯度回落阶段3-5分钟并发逐步回落到0观察系统是否能正常回收资源。这样设计的好处是如果系统在某个并发点崩溃你可以明确知道“压测在并发数大约是多少的时候出了状况”而不是只知道“跑挂了但不知道是在什么时候挂的”。用Locust的命令行做梯度加压有两种方式一是用--step-load配合--step-users和--step-time参数二是自己在脚本里用events钩子控制。前者简单够用locust -f load_test.py --hosthttps://staging.example.com --headless \ -u 5000 -r 200 --run-time 30m \ --step-load --step-users 500 --step-time 3m这段命令表示每3分钟增加500个用户直到5000用户。注意--step-load模式下-u是最终目标并发数-r是每步的用户增长速度。每步结束后Locust会自动恢复到一个较低的状态再开始下一轮增加这个“间歇”时间会受到--step-time影响需要在设计压测方案时就考虑到。在压测过程中我习惯开着三个东西Locust的Web界面或者无头模式的CSV输出、Grafana仪表盘看系统指标、终端跑着htop和dstat看压测机自身状态。很多时候系统没挂反而是压测机先撑不住了这种情况如果没留意到会得出“被测系统性能差”的错误结论。3.3 数据解读与瓶颈定位三层定位法压测结束不等于工作结束数据解读才是重头戏。我一般遵循一个“三层定位法”来看数据第一层看Locust的响应数据。平均响应时间、TP50/TP95/TP99、错误率、每秒钟请求数。如果TP99比平均高出一个数量级说明存在明显的长尾延迟如果错误率在某个并发点突然飙高大概率是某个组件到了极限。第二层看应用的调用链和日志。慢请求的Trace都花在哪个环节了是网关层、业务逻辑层、还是数据库访问层日志里有没有超时、重试、熔断的迹象这一步需要链路追踪系统比如SkyWalking、Zipkin和结构化日志的配合没有的话压测时临时加日志也要看清楚。第三层看基础组件的指标。CPU、内存、磁盘IO、网络带宽、数据库连接数、GC频率、Redis的慢查询、消息队列的堆积量。这些指标要和第一层的请求数据对齐看响应时间变慢时数据库的哪个指标异常了我举个例子有一次压测一个订单服务QPS到200左右时响应时间开始直线上升。第一层看LocustTP99从120ms涨到了2s以上第二层看Trace发现耗时80%都在数据库查询上第三层看数据库指标连接数并没有打满但有一个SQL的扫描行数特别大——查订单表的时候索引没走对。定位到这条慢SQL之后加上联合索引重新压测后QPS直接翻了三倍。这就是三层定位法最爽的地方不是靠猜而是靠数据一层层穿透。4. 微服务架构下的分布式压测与常见问题实录4.1 单机撑不住了用主从模式做分布式压测微服务架构下的压测流量往往比较大。如果目标并发在几千的量级单机Locust通常够用但如果要压几万甚至更高的并发就需要用分布式模式。Locust的主从模式原理很简单一台master机器负责调度和汇总结果多台worker机器负责真正产生压力。# master节点启动 locust -f load_test.py --master --hosthttps://staging.example.com # 每台worker节点启动 locust -f load_test.py --worker --master-hostmaster_ip几个实操上的经验worker节点和master节点的时间要同步不然压测数据的时间戳会错乱图表上看不出趋势。用NTP强制同步一遍最省心。master节点本身不产生压力它只负责任务分发和结果聚合。但要注意如果worker数量很多master的CPU和内存消耗也会很大别把master配置得太低。所有worker节点加载的脚本要完全一致包括数据文件。很多团队踩过这个坑workerA加载了完整的数据池workerB的数据文件没更新导致压测结果不一致。规范化做法是把数据文件放在固定路径用版本控制管理worker节点从同一个仓库拉取。网络带宽是容易忽略的瓶颈。一台压测worker如果跑在千兆网卡上撑死也就能产生几百Mbps的流量。如果压测目标是看网络IO密集型应用的极限先估一下带宽上限别让网络成为压测的天花板。4.2 慢启动与用户爬坡微服务场景下的踩坑记录微服务架构有一个常见问题服务启动时需要做很多初始化工作比如加载配置中心、预热Redis缓存、连接数据库、创建线程池。如果一上来就灌入大量并发请求很多服务会在“还没准备好的时候”被压趴下但这不代表它的真实容量就是那么低。我实际遇到的典型案例一个Java微服务启动后需要从配置中心拉取规则数据并缓存在本地这个过程大概需要30秒完成。第一次压测时没有预热阶段脚本启动就灌了300个并发结果这个服务直接抛了一堆连接超时错误。后来加了预热阶段等服务把规则缓存都加载好了再逐步加压同样300并发下从错误率80%变成了0错误。所以微服务压测的脚本设计里一定要考虑服务启动预热的因素。常见做法是先用较低并发比如10-50跑1-2分钟确认服务健康后再开始梯度加压。4.3 压测数据失真的元凶连接复用与缓存命中最后一个常见问题也是我认为压测数据最容易失真的原因连接复用和缓存命中率。Locust的HttpUser默认使用requests.Session会复用TCP连接。这在真实场景下是合理的——真实用户也不会每个请求都重新建立TCP连接。但如果压测脚本对同一个URL反复请求又会造成缓存命中率异常偏高导致数据库压力被低估。对策有两个方向混合接口URL在脚本里让URL路径带上随机参数比如/api/v1/products/{random_product_id}这样能模拟不同用户访问不同资源的情况。定期清理缓存如果压测目标之一就是验证冷缓存下的性能需要在压测前或压测中安排好缓存清理策略。注意生产环境的缓存清理必须极其谨慎建议在独立压测环境里操作。还有一个容易被忽略的点Cookie和Session的处理。如果被测系统使用Session保存用户状态压测脚本里必须正确地处理Cookie传递。真实用户是带着自己的Session玩的而如果脚本里所有用户共用同一个Session那本质上是在压同一个用户的接口调用数据参考价值会大打折扣。5. 高并发压测的进阶玩法让Locust融入研发流程5.1 把压测脚本做到CI流水线里防止性能回归性能问题和功能Bug不一样很多时候不是一次代码变更引入的而是随着系统演进悄悄劣化的。每次发版前手动跑一轮完整压测不现实一个折中的方案是在CI里集成一个轻量级的“冒烟性能测试”部署完测试环境后自动跑3-5分钟的低并发压测核心接口的TP99超过阈值就阻断发布。用Locust做这件事很方便因为它是Python库可以直接在测试代码里调用import os import subprocess def test_staging_performance(): result subprocess.run([ locust, -f, smoke_test.py, --host, os.environ[STAGING_URL], --headless, -u, 200, -r, 20, --run-time, 3m, --csv, performance_report ], capture_outputTrue, textTrue) # 解析CSV结果检查关键指标 ...这样每次提交代码后都能快速发现“这次改动让下单接口的TP99从200ms变成了500ms”这样的回归而不需要等到上线后才靠用户反馈来发现。5.2 从“压接口”到“压链路”再到“压场景”微服务架构下的高并发压测最难的不是压某个接口而是模拟一条真实的业务链路。真实用户的一笔订单操作会调用API网关、订单服务、库存服务、支付服务、消息队列、数据库、Redis还会触发下游的异步任务。如果只是单独压订单服务测出来的容量和真实情况往往对不上。我建议分三步做接口级压测压每个核心接口的极限QPS摸清每个服务的单点容量。链路级压测用Locust模拟完整业务链路浏览→下单→支付→查单找到链路中的瓶颈点。场景级压测在链路级基础上混合多个业务场景的比例比如80%浏览、15%下单、5%支付——根据真实埋点数据验证整个系统在混合业务流量下的表现。链路级和场景级的压测结果才是容量规划和扩缩容决策的可靠依据。单纯接口级压测虽然快但很容易得出“每个服务都很NB串联起来就崩了”这种让人抓狂的结论——其实问题往往出在服务间的依赖调用、线程池配置和分布式事务处理上这些只有链路级压测才能暴露出来。5.3 基于压测报告做容量规划而不是拍脑袋最后一个小建议把每次压测的结果沉淀下来形成一份可对比的性能基线报告。包括但不限于环境信息、脚本版本、并发数、QPS、TP99、错误率、系统资源指标、瓶颈说明。下次压测时拿出来对比就能清晰地看到系统容量是变好了还是变差了。容量规划的时候有一个简单的参考公式生产环境的峰值并发通常是压测稳定并发数的50%-70%。留出的余量给了突发流量、全链路自愈能力和未来业务增长的空间。如果压测时系统在5000并发下已经很吃力那生产环境控制在2500-3500并发左右是比较稳妥的而不是天真地以为系统能稳稳扛住5000。写在最后我个人做压测这些年最大的体会是压测工具只是手段精确描述问题才是核心能力。Locust的价值在于它足够灵活能让你用Python像写业务代码一样去编排压测行为真正做到“把系统放到真实流量下验证”。但你最终得到的是一份可靠的数据还是只是一堆漂亮的图表取决于你有没有认真设计用户模型、做好参数化、控制好变量以及有没有能力从一串数字里定位到真正的瓶颈。如果你正准备在自己的项目里引入Locust我建议从小处开始——先写一个最简单的脚本压你自己的一个核心接口把环境、工具、流程都跑通再慢慢加业务复杂度。等这套流程成熟了再推广到全链路压测和CI性能回归。踩过几次坑之后你会发现这套体系带来的价值远比找一个测试工具跑一次压测要深远得多。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。