ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

微服务API压测实战:用Locust从脚本入门到分布式集群优化

微服务API压测实战:用Locust从脚本入门到分布式集群优化 前阵子我们组要对订单服务做上线前的容量评估我这次没有继续用 JMeter而是选了 Locust 来搭建整套 API 压测流程。微服务架构越拆越细接口数量翻着跟头涨压测脚本的复杂度也在涨。JMeter 不是不能做但脚本一到登录鉴权、动态参数、接口关联这些环节维护成本就像在一张巨大的表单里绣花。换成用 Python 写压测逻辑的 Locust 之后我两天就跑通了原来计划一周才能搞定的场景。这篇就记录一下我怎么把 Locust 从入门用到实战优化给正在折腾微服务压测的测试开发和后端同学做个参考。1. 为什么微服务架构下的API压测我最终选了Locust1.1 微服务压测的真实痛点在微服务架构下面接口不是孤立存在的。一次用户请求背后可能是网关、鉴权服务、业务服务、缓存、消息队列、数据库一路调用下去。压测的时候你面对的不是一个静态 HTML 页面而是带鉴权头、带签名、带动态参数的复合请求链路。具体到脚本层面你会发现这么几个问题接口需要先登录拿 tokentoken 还有有效期请求体里有大量动态数据比如订单号、手机号、随机字符串部分接口的返回会作为下一个接口的入参存在数据关联压测数据不能重复否则会触发幂等校验、唯一索引冲突线上真实的流量是有比例的比如 70% 查详情、20% 加购物车、10% 下单。这些问题在 JMeter 里都能解决但解决的过程很繁琐。JMeter 的图形化界面在配置几十个接口时候还勉强能忍一旦需要写复杂逻辑比如 AES 加密、签名算法、根据响应动态跳转你就得写 BeanShell 或 JSR223 脚本最终维护的其实是夹在图形界面里的代码既难读又难做版本管理。我换到 Locust 之后最大的感受是压测脚本本身就是一份普通的 Python 文件。它可以直接放进 Git 仓库可以做代码评审可以用 IDE 调试可以调任何 Python 库。对一个团队来说这个优势在微服务场景下几乎是决定性的。1.2 Locust、JMeter、k6 的选型对比这里放一张我当时对比用的表参数基于我个人实测和官方文档整理维度LocustJMeterk6脚本形式Python 代码XML 图形界面JavaScript 脚本并发模型基于 gevent 协程单进程可支持数千并发线程模型单机受线程数限制基于 Go 的协程单机性能强分布式压测原生支持 master/worker主节点统一控制通过远程启动 agent配置较繁琐原生支持 k6 cloud / 手动编排调试体验可断点调试、可在 REPL 里试请求主要是界面调试本地跑脚本 日志输出学习门槛会 Python 上手很快图形界面入门快复杂逻辑门槛高会 JS 语法即可生态较年轻与 CI/CD 集成命令行 headless 模式十分友好可通过命令行 csv 输出集成略笨重为云原生和 CI 而生我并没有说 JMeter 不行。如果你的场景就是单接口固定参数压测JMeter 依然是成熟稳定的选择。但如果你的压测目标是真实业务链路需要频繁调整逻辑我会优先推荐 Locust。k6 我也试过性能很猛适合做比较纯粹的协议层压测但在复杂业务逻辑的编写自由度上还是 Python 更舒服。1.3 必须理解的几个核心概念Locust 的模型一句话就能说清每个虚拟用户是一个协程协程按你写好的 wait_time 等待一段时间然后随机执行 User 类上标记为 task 的方法。核心概念就这几个User模拟一个用户。它本身只负责行为定义不会真正对应一条连接。HttpUser继承 User额外带一个 client 属性这个 client 是对 requests 库的封装用来发 HTTP 请求。task装饰在方法上的task(weight)weight 表示权重比如task(3)和task(1)前者被执行的概率是后者的 3 倍。wait_time用户执行完一个任务之后休息多久常写between(1, 5)表示随机等待 1 到 5 秒。这个参数直接影响压测速率。FastHttpUser基于 geventhttpclient 实现的 HttpUser请求效率更高后面分布式章节我会再展开。这五个概念已经覆盖绝大多数日常压测场景剩下的是 Web UI、事件钩子、csv 输出这些外围能力。别一开始就被TaskSetSequentialTaskSet这些老概念劝退新版本里直接在 User 类上写 task 就够了代码反而更清晰。我见过不少刚接触 Locust 的人把它当成性能测试工具其实它更像一个可编程压测框架。它不会替你做压力模型但给了你做任意压力模型的自由区别就在这。2. 从零写一个能跑的Locust脚本环境准备与第一个压测用例2.1 环境准备与安装我用的是 Python 3.10建议你至少在 3.9 以上。新版本 Locust 对 Python 版本有要求太老的版本装不上。安装前先建一个虚拟环境这是 Python 项目的常规操作python3 -m venv .venv source .venv/bin/activate pip install locust装完确认版本locust --version如果你看到locust 2.x.x就说明装好了。我这边当时装的是 2.24 左右不同小版本在 Web UI 样式和参数细节上会有差异但核心用法没有大变化。需要注意一点如果你是 Windows 环境gevent 依赖的 C 扩展在安装时偶尔会出问题。官方文档建议直接装预编译的 wheel 包一般pip install locust会自动选对版本。真的遇到编译错误把 pip 升到最新再装一次基本能解决。2.2 第一个最简脚本在项目目录创建一个locustfile.py这是 Locust 的默认文件名不带-f参数时会自动找它from locust import HttpUser, task, between class ApiUser(HttpUser): wait_time between(1, 3) task def get_health(self): self.client.get(/health)这个脚本的意思很直白每个虚拟用户协程在每次请求后随机等待 1 到 3 秒每次执行时随机挑一个标记为 task 的方法这里只有一个任务就是 GET /health。如果你要压测的是一个 POST 接口改成这样task def create_order(self): self.client.post( /orders, json{product_id: 10001, count: 2}, )self.client的用法和 requests 几乎一致get、post、put、delete、head、patch 都有headers、params、json、data、files 这些参数也都能传。这一点对从 requests 转过来的人来说几乎不需要学习成本。2.3 启动方式Web UI 模式与命令行模式开发调试阶段我习惯直接跑 Web UIlocust -f locustfile.py --host https://api.example.com启动后浏览器打开http://localhost:8089填上并发用户数、每秒启动数、压测地址点开始就行。Web UI 可以实时看请求数、响应时间、异常数还能在线下载报告 CSV对临时摸底非常方便。真正到了自动化压测和 CI 阶段用 headless 命令行模式locust -f locustfile.py --host https://api.example.com \ --headless \ -u 500 \ -r 50 \ -t 10m \ --csvreport/load_test \ --htmlreport/load_test.html参数含义我列一下-u目标虚拟用户数-r每秒生成多少个用户速率-t压测持续时间支持10m、30s、1h--csv前缀每 3 秒写一版 CSV 快照压测结束生成完整数据文件--html导出带图表 HTML 报告。第一次跑的时候别贪多先用-u 10 -r 2 -t 1m热个身确认脚本没有返回异常再往上加。我很长一段时间的习惯都是先跑 10 个用户打开报告看有没有 401、500然后再谈加压。3. 把真实业务逻辑塞进脚本鉴权、参数关联与数据驱动3.1 为什么必须处理鉴权如果你压测的接口需要鉴权而你的脚本里没写 token 逻辑那么压测一开始你就会看到满屏的 401RPS 再高也没有意义——你测的其实是未授权请求被拒绝的速度。在 Locust 里做鉴权有两种常见策略。策略一全局登录一次token 复用class ApiUser(HttpUser): wait_time between(1, 3) def on_start(self): resp self.client.post(/auth/login, json{ username: load_test_user, password: xxxxx, }) token resp.json()[data][token] self.client.headers[Authorization] fBearer {token}on_start在每个虚拟用户启动时执行一次。几百个用户会各登录一次这样做的好处是每个协程都有独立 token符合真实用户分布。如果担心登录接口本身成为压测瓶颈可以用test_start钩子全局登录一次再把 token 作为类属性共享from locust import events events.test_start.add_listener def on_test_start(environment, **kwargs): resp requests.post(...) ApiUser.token resp.json()[data][token]两种策略没有绝对优劣。压测目标是业务接口时我倾向策略一因为登录流量本身也是真实调用的一部分更贴近线上。如果业务要求 token 有效期很长、登录接口资源有限就选策略二。3.2 动态参数与接口关联微服务接口最麻烦的不是鉴权而是参数动态化。比如下单接口要求订单号不能重复创建订单后还要用返回的订单号去查支付状态。这就是接口关联。拿一个典型的电商链路为例登录拿 token - 创建订单拿 order_id - 查订单状态。脚本可以这样写import json from locust import HttpUser, task, between class OrderUser(HttpUser): wait_time between(1, 2) def on_start(self): login_resp self.client.post(/auth/login, json{ username: user_001, password: xxxx, }) data login_resp.json() token data[data][token] self.client.headers[Authorization] fBearer {token} task def create_and_query_order(self): order_resp self.client.post(/orders, json{ sku_id: 10086, count: 1, }) if not order_resp.ok: return order_id order_resp.json()[data][order_id] self.client.get(f/orders/{order_id}/status)这里的核心动作是order_id order_resp.json()[data][order_id]把上一个接口的响应变成下一个接口的路径参数。如果响应体不是 JSON可以用正则提取import re order_id re.search(rorder_id:\s*([^]), order_resp.text).group(1)动态参数的另一类常见需求是从数据池里随机取。比如压测不同区域维度的订单接口import random regions [cn-east, cn-north, cn-south, cn-west] task def regional_order(self): region random.choice(regions) self.client.post(/orders, json{ region: region, user_id: random.randint(10000, 99999), })数据量更大时从 CSV 读入内存再循环使用或者连测试库随机取一行原理都一样压测脚本要尽量复用真实数据的分布特征而不是每个请求都用同一个固定参数。3.3 压测中必现的401问题别让鉴权错误污染压测结果我做过不少接口压测发现一上高并发就出现一堆 401是个高频现象而且很多时候不是被测服务的问题是压测脚本自己的问题。常见原因有三个。第一个是 token 过期。登录接口发的 token 有效期可能只有 30 分钟压测跑到第 40 分钟时全局复用的 token 已经失效后面的请求全部 401。解决办法是缩短单轮压测时长或者在脚本里捕获 401 并重新登录。Locust 支持在任务里判断响应码resp self.client.get(/orders/123/status) if resp.status_code 401: self.on_start()第二个是 Authorization 头没传对。不少平台要求Authorization: Bearer token或者自定义头X-API-Key少传一个空格、大小写写错都会 401。压测前建议先用真实请求单独调试一遍别直接上百并发。第三个是使用了错误的 API Key。我遇到好几次同事从配置中心拿错了 key或者 key 被轮换后测试环境没同步压测一开始就是一片 401。判断方式很简单单独用一条真实请求打目标接口看返回体里的错误信息。如果返回的是incorrect api key provided、unexpected status 401 unauthorized大概率是配置本身的问题不是被测服务的问题。这里有个非常实用的建议压测开始时先看前 1 分钟的异常分布。如果异常率从第一秒就很高那基本是脚本或配置问题如果前 5 分钟正常、后面突然 401那优先怀疑 token 过期或服务端限流策略。这两种问题的处理路径完全不一样。3.4 数据驱动与加载策略最后说一下数据驱动。市面上很多压测工具都强调数据文件参数化Locust 的做法更加朴素——在 Python 里怎么读数据脚本里就怎么写。我常用的是把测试账号放在 CSV 里在模块加载时读入一次import csv USERS [] with open(users.csv, r) as f: USERS list(csv.DictReader(f)) class DataDrivenUser(HttpUser): wait_time between(0.5, 1) task def use_account(self): user USERS.pop(0) if USERS else None if user: self.client.post(/some/api, json{username: user[username]})读取数据的开销记住一个原则能加载一次就不要加载一千次。如果把 CSV 读取放在每个用户的on_start里1000 个并发就是 1000 次文件 IO完全没必要。4. 从单机到分布式千级并发下压测集群的搭建与调优4.1 单机瓶颈在哪里用 Locust 单机跑几百并发很轻松跑到几千时通常会遇到几类问题CPU 占满、文件描述符不够、TCP 端口耗尽。先解释一下原理Locust 的并发模型是 gevent 协程单进程的协程吃 CPU 很少但 Python 进程有 GIL当请求响应体很大、需要做大量 JSON 解析或正则匹配时CPU 会成为瓶颈。另一个瓶颈是 TCP 连接压测机作为客户端每条连接要占用一个本地端口默认的临时端口范围有限。你可以先看系统限制ulimit -n如果输出只有 1024那并发到 1000 就很容易出现Cannot assign requested address这类连接错误可以临时调大ulimit -n 65535单机 Locust 能扛多少并发很大程度上取决于压测机自身配置和响应体大小。一个 2C4G 的机器压简单 JSON 接口跑到 3000 并发是可能的但压大响应体接口可能 1000 就吃满了。如果你的目标是 5000 甚至 10000 并发别继续压榨单机了直接上分布式集群。4.2 主从模式的工作原理Locust 的分布式模式很清爽一个 master 节点负责分发任务、汇总统计数据、提供 Web UI若干个 worker 节点实际执行压测任务。启动方式# master 节点 locust -f locustfile.py --master # worker 节点 locust -f locustfile.py --worker --master-host192.168.1.10master 和 worker 都加载同一个locustfile.py。worker 连上 master 后你在 master 的 Web UI 上看到的并发用户总数为所有 worker 的累加。这里有个容易踩的坑master 和 worker 的 Locust 版本必须一致。我遇到过版本不一致导致 worker 反复重连 master、压测跑不起来的诡异问题排查了半天才发现是两台机器上pip install locust的版本不一样。master 本身不执行压测任务配置不用太高worker 才是真正的压力发生源CPU 和内存要给足。4.3 用 Docker Compose 快速拉起压测集群Docker 方式部署最省心官方镜像直接可用。我在项目里的docker-compose.yml大概是这样的services: master: image: locustio/locust:latest ports: - 8089:8089 volumes: - ./:/mnt/locust command: -f /mnt/locust/locustfile.py --master worker: image: locustio/locust:latest volumes: - ./:/mnt/locust command: -f /mnt/locust/locustfile.py --worker --master-hostmaster然后执行docker compose up --scale worker4就能拉起一个 master 加 4 个 worker 的集群。整个压测脚本目录挂载进容器代码更新后重启容器即可。用--scale worker4只改变 worker 数量master 始终只有一个这点和普通服务扩容不太一样。需要调整并发规模时要么改-u参数要么调整 worker 数量两者都可以。4.4 性能调优让压测机自身别成为瓶颈分布式不是万能的还要注意压测侧的性能优化。我的经验排序是第一能换FastHttpUser就换。底层用的是 geventhttpclient比默认HttpUser的 requests 实现轻量很多。官方和一些社区测试中FastHttpUser 的请求吞吐比 HttpUser 高出一个数量级。改法就一行from locust import FastHttpUser class ApiUser(FastHttpUser): pass大部分情况下原有的self.client写法都不用改。第二控制日志输出。压测时不要无脑print更不要在每个请求里记录 debug 日志。日志本身会占 CPU 和磁盘 IO高并发时对结果的影响不可忽视。要输出只在捕获到异常时打印关键信息。第三监控压测机自身。压测时同时打开htop和vmstat如果 worker 节点的 CPU 已经 100%说明瓶颈在压测侧此时加用户数只会让结果更失真正确做法是加 worker 或优化脚本。5. 压测报告的读法哪些指标值得盯哪些指标容易骗人5.1 Locust 聚合报告的关键指标在 Web UI 或--csv导出的数据里核心指标其实就那么几项RPS、平均响应时间、中位数响应时间、P95、P99、异常率、当前用户数。我给个表格帮新手快速对号入座指标看什么常见误读RPS系统吞吐能力把单机 RPS 当整体能力忽略 worker 数量平均响应时间整体水平被少量慢请求拉高看不出长尾P50典型用户感受只代表多数用户正常不代表高负载P95 / P99长尾与抖动不看这两个值等于没发现性能隐患异常率错误请求占比401 和 500 都算异常但原因完全不同我特别想说 P95 和 P99。很多接口平均响应时间 80ms看起来很美但 P99 可能是 800ms说明有 1% 的请求慢了一个数量级。在线服务最怕的就是这种隐性问题它可能来自 GC 停顿、缓存击穿、连接池争抢。只盯着平均值做优化很容易得出系统没问题的错误结论。5.2 从异常状态码倒推问题根因压测报告里的异常率不是用一个数字概括的我习惯按状态码拆开看401鉴权问题。先查脚本里的 token、Header、API Key再查服务端是否有会话失效机制。429限流。服务端限流一般有策略比如按 IP、按用户、按接口。压测时出现 429要确认是业务预期行为还是限流阈值配置太低。500服务端异常。这时候要看服务日志、数据库慢查询、异常堆栈。502 / 504网关或代理层面的问题。常见于压测并发加大后网关连接后端超时。我遇到过一个印象很深的案例某接口压测到 800 并发时异常率突然飙到 15%大部分是 429。当时第一反应是限流阈值太低后来查日志发现是压测机出口 IP 被 WAF 自动封了。这种问题不会直接显示在业务代码里必须把压测机的出口 IP 加白名单或者反过来用真实线上入口做压测。这类问题只能靠经验判断。5.3 从响应时间分布定位瓶颈的方法响应时间变长不一定就是目标服务变慢。我按调用链路的顺序梳理了一套排查顺序先确认压测机自身没有被打满CPU、带宽、端口。观察目标服务的 CPU、内存、GC、连接数。如果 CPU 已经打满优先考虑扩容或优化业务代码。看数据库指标。慢 SQL、锁等待、连接池打满都是压测时最常暴露的问题。看外部依赖。如果接口内部调了第三方 API 或另一个微服务要看下游返回耗时和错误率。第三方服务一旦抖动上游接口的 P99 会非常难看。我这么排序的原因很简单响应时间是从客户端视角统计的它只能告诉你慢不能告诉你哪里慢。要回答哪里慢必须分层看各链路节点的指标。Locust 负责证明慢存在具体根因要靠 APM 和基础设施监控去挖。6. 我踩过的坑和沉淀下来的Locust压测习惯6.1 六个最值得记住的坑第一个坑wait_time 写成 0。有人为了提高并发把等待时间设为 0结果每个协程都在疯狂发请求压测机 CPU 先被打满测出来的数据完全不可信。wait_time 是模拟用户思考时间的你不想要可以适当调小但最好别设成 0。第二个坑在压测脚本里引用不存在的测试数据。比如用一个固定手机号反复下单结果因为订单号重复导致接口报错。解决办法是让脚本自己生成随机数据或者用前置脚本灌一批可用的测试数据。第三个坑token 全局复用但没考虑有效期。前面已经讲过不再重复。第四个坑master 和 worker 版本不一致。分布式压测跑起来像个哑弹看不到任何请求多半是这个问题。第五个坑压测机和被测服务部署在同一台机器上。这个属于资源竞争压测机一高并发被测服务的 CPU 被抢走结果自然不准。尽量分开机器至少分开物理资源。第六个坑只看最终汇总不看过程趋势。接口可能在压测第 5 分钟出现内存泄漏但你没看过程数据只盯着最后的汇总 RPS根本发现不了。Locust 生成的 CSV 数据分很多时间片建议留档下来做趋势分析。6.2 我沉淀下来的压测动作清单现在我在项目里做一次正式压测基本按这个流程走写脚本前先用手工方式把接口调通确认鉴权和参数规则。写一个locustfile.py先用 10 个用户跑 1 分钟检查异常码。确认没有 401、500 之后按目标并发数 50%、100%、150% 三档逐步加压。压测过程中同时记录服务端关键指标包括 CPU、内存、DB 慢查询、下游调用耗时。压测结束后导出 CSV 和 HTML 报告按 P95、P99、异常率、时间趋势三个维度写结论。把locustfile.py和报告提交到 Git作为这个接口的历史基线。这个清单看起来朴素但真的能避免很多压了个寂寞的情况。最有价值的是第 1 步和第 4 步它们决定了压测结果的真实性。6.3 把 Locust 融入持续压测最后聊一下自动化。Locust 的 headless 模式特别适合放进 CI 流程。在 Jenkins 或 GitHub Actions 里把压测做成一个 job每天晚上跑一次回归或者每次上线前跑一次容量评估都能自动留报告。我用的比较顺手的写法是这样locust -f locustfile.py --host https://staging.example.com \ --headless \ -u 200 -r 20 -t 5m \ --csvreport/staging \ --htmlreport/staging.html \ --expect-workers4--expect-workers4用于分布式模式下master 会等待 4 个 worker 全部连接后才开始任务避免出现master 已开始worker 还没连上的竞态。这个参数在 CI 场景下几乎是必加的。另外可以利用事件钩子做自定义扩展比如压测结束时把结论推到企业微信或邮件。Locust 的扩展点很多真正用到的时候再查文档即可。总之先把自己的压测动作标准化自动化只是水到渠成的事。从我自己的经验看Locust 最大的价值不是压测工具这四个字而是把一个复杂的性能验证问题简化成了一个写 Python 脚本的问题。你把业务逻辑理清楚脚本写规范剩下的事情 Locust 都会帮你处理得明明白白。这套实践我用到现在已经帮组里提前发现了三次数据库慢查询和两次第三方接口抖动希望这篇内容也能让你少走一段弯路。
返回列表