做接口压测这件事很多人是从“线上突然卡了”才开始重视的。我接手过不少慢接口的排查最后基本都能追溯到压测缺失要么没做过压力测试要么做过但场景设计得太糙只拿一个登录接口跑一跑以为QPS上万就算完事。等到真实流量一来数据库连接池先被打满缓存击穿雪崩式超时各种问题全冒出来。这篇文章我就拿一套完整的实战来聊聊怎么从零开始给 FastAPI 接口做一次可落地的压力测试工具选 Locust。从搭建被测接口、设计压测场景、写脚本、跑测试、看指标到定位瓶颈一步步来目标是让你看完之后能照着在自己的项目里复现整个流程。这套内容适合刚接触性能测试的后端开发也适合团队里需要搭建压测能力的测试工程师。你会知道为什么选 FastAPI 配 LocustRPS、P95、错误率这些指标到底怎么看以及最常见的那些坑——连接数限制、同步异步混用、施压机自身瓶颈——是怎么一步步排查出来的。1. 压测之前先把需求和场景想清楚1.1 压测的本质是验证系统边界做压测容易犯的第一个错误就是一上来就写脚本、跑数据跑完看到一堆数字就收工。其实压测的核心不是“把机器打满”而是回答几个业务问题这个接口最多能扛多少请求性能什么时候开始劣化瓶颈到底在应用层、数据库还是中间件在回答这些问题之前得先想清楚被测对象是谁、请求长什么样、系统当前具备哪些资源。拿这篇文章的示例来说我会构建一个带数据库查询、业务逻辑处理和缓存判断的 FastAPI 接口模拟真实业务场景。单独压一个只返回{hello: world}的接口没多大意义那测出来的只是框架本身的吞吐量不是业务的真实承载能力。压测之前需要明确几个基础信息被测环境本地开发机、测试环境还是预发布环境。不同环境的配置差别会直接影响结果解读。样本接口选最有代表性的业务接口通常是对外核心链路里访问量最大的那个或者数据库、下游依赖比较重的那个。预期容量比如线上日常 QPS 在 200 左右大促预期翻 3 倍那压测目标至少得按 1000 QPS 去看。有了这些前提压测才有意义。不然你压了一个没人用的接口或者在满负荷运行的生产库上做压测结果既没有参考价值还可能造成事故。1.2 性能目标怎么定才不算“拍脑袋”性能指标不是越多越好但至少有四个必须盯住QPS、响应时间、错误率、资源消耗。这四个指标互相独立又互相制约单独看任何一个都可能被误导。比如 QPS 很高但 P95 响应时间已经 3 秒了这种“高吞吐、慢响应”的系统对用户体验来说是灾难。再比如错误率看着是 0%但中间件配置了超长超时所有请求都在 10 秒边缘返回超时测出来的数据依然“好看”实际上业务已经无法接受了。比较合理的做法是压测目标用“SLA 容量预估”的方式定目标吞吐量日常峰值的 3 到 5 倍。比如日常峰值 300 QPS压测目标就是 900 到 1500 QPS。目标响应时间P95 在 300ms 以内P99 在 800ms 以内具体看业务类型。登录、下单这种写操作可以适当放宽查询类接口要更苛刻一些。错误率压测过程中低于 0.1%而且必须区分“业务错误”和“超时错误”后者往往意味着系统已经过载。资源水位CPU 使用率不超过 70%内存不要持续上涨数据库连接池和线程池不要打满。定好这些数字之后压测就有了明确的“通过线”而不是跑完一堆数据不知道该怎么判断。1.3 为什么选 Locust 而不是 wrk、ab 或 JMeter压测工具选型会被很多人忽略但工具本身决定了你能压出什么维度、场景能多复杂。如果是简单的静态页面和单接口吞吐量测试wrk和ab很轻量几行命令就能跑出漂亮的并发数据。但一旦业务场景复杂起来——要登录拿 token、要根据入参组装请求体、要模拟用户思考时间——这类命令行工具就明显吃力了。JMeter功能全面有完整的 GUI 和插件体系团队协作方便但它的脚本本质是 XML 结构版本迭代以后兼容性时不时出问题而且学习成本不低。最重要的是JMeter 的线程模型是传统的线程池方案单机施压能力有限大规模压测要配分布式 Agent运维成本一下就上去了。Locust走的是完全不同的路子。它基于 Python 的gevent协程模型可以在一台普通机器上模拟数万并发用户而不需要开同样数量的线程。同时因为脚本就是普通 Python 代码可以使用完整的业务逻辑、随机参数、断言判断几乎不受限制。还有一个非常实用的 Web 控制台跑起来之后直接在浏览器里查看实时指标、动态调整并发数。这些特性让它非常适合做业务接口级别的压测——既能够模拟复杂用户行为又不需要庞大的基础设施。FastAPI 配合 Locust 还有一个天然优势FastAPI 是 ASGI 框架本身基于异步事件循环对高并发 IO 场景有很好的支撑Locust 也是协程模型两者在压测的时候不会出现“测了个寂寞”的情况——施压机制本身就是异步的不会因为施压端线程耗尽而测不出真实水平。2. 准备一个“值得压”的 FastAPI 接口2.1 FastAPI 的异步模型到底是怎么提升性能的很多时候用 FastAPI 的人觉得它“快”其实快在底层组装。FastAPI 底层是 Starlette再下面是 Uvicorn 这样的 ASGI 服务器。ASGI 是异步网关协议意味着框架层面的事件循环可以把大量等待 IO 的时间让出来处理其他请求。这里有一个非常重要的区别def定义的接口和async def定义的接口性能模型完全不同。async def接口请求直接进事件循环遇到数据库查询、外部 HTTP 调用等阻塞操作时通过await让出控制权其他请求可以继续处理。并发能力由协程数量决定理论上可以支撑很高并发。def接口FastAPI 会把请求丢给线程池去执行每个请求占用一个工作线程。线程有创建和切换成本并发上来之后线程池耗尽性能会急剧下降。所以在写高性能接口的时候能用async def就用async def配合异步 ORM 或者异步数据库驱动。但也要说明一点如果你的操作是 CPU 密集型任务比如图像处理、复杂算法计算用async def并不会让计算变快因为 GIL 的存在让 CPU 密集型任务在单进程 Python 里仍然只能跑满一个核。这个时候正确的做法是设计任务队列把计算任务丢给独立 worker接口直接返回“任务已受理”。这块我们在调优部分再展开。2.2 设计一个包含缓存、数据库和业务逻辑的真实接口为了让压测更贴近现实我构造了一个“用户订单聚合查询”接口。场景很简单移动端首页需要展示用户的订单列表附带订单里的商品信息和最新的物流状态。这个接口的特点是有数据库查询查订单表和商品表有外部服务依赖物流状态有缓存逻辑订单列表一定时间内不重复查库接口文件main.py大概是这样的import asyncio import time from contextlib import asynccontextmanager import aioredis import uvicorn from fastapi import FastAPI, HTTPException from pydantic import BaseModel from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession from sqlalchemy.orm import sessionmaker DATABASE_URL mysqlaiomysql://user:passwordlocalhost:3306/demo REDIS_URL redis://localhost:6379/0 CACHE_TTL 30 engine create_async_engine(DATABASE_URL, pool_size10, max_overflow20, pool_recycle3600) SessionLocal sessionmaker(engine, class_AsyncSession, expire_on_commitFalse) redis_client aioredis.from_url(REDIS_URL, decode_responsesTrue) asynccontextmanager async def lifespan(app: FastAPI): yield await engine.dispose() await redis_client.aclose() app FastAPI(titleOrder API, lifespanlifespan) class OrderBatchResponse(BaseModel): order_id: str amount: float status: str item_count: int express_status: str async def query_order_from_db(user_id: str) - list[dict]: # 模拟从订单库查询默认返回模拟数据 # 注意真实场景这里一定用异步 SQLAlchemy 查询 await asyncio.sleep(0.01) # 模拟0~5ms的查询耗时 return [ {order_id: 20250101001, amount: 199.0, status: paid, item_count: 3}, {order_id: 20250101002, amount: 99.0, status: shipped, item_count: 1}, ] async def query_express_status(order_ids: list[str]) - dict[str, str]: # 模拟调用物流API的耗时 await asyncio.sleep(0.005) return {oid: in_transit for oid in order_ids} app.get(/api/v1/user/{user_id}/orders, response_modellist[OrderBatchResponse]) async def get_user_orders(user_id: str): cache_key fuser_orders:{user_id} cached await redis_client.get(cache_key) if cached: import json return json.loads(cached) orders await query_order_from_db(user_id) if not orders: raise HTTPException(status_code404, detailorders not found) order_ids [o[order_id] for o in orders] express_map await query_express_status(order_ids) result [] for o in orders: result.append({ order_id: o[order_id], amount: o[amount], status: o[status], item_count: o[item_count], express_status: express_map.get(o[order_id], unknown), }) await redis_client.setex(cache_key, CACHE_TTL, json.dumps(result)) return result if __name__ __main__: uvicorn.run(main:app, host0.0.0.0, port8000, workers4)这个接口的逻辑并不复杂但它覆盖了真实业务系统中最常见的三种 IO 操作数据库查询、外部 API 调用、缓存读写。压测的时候缓存命中率和未命中的比例会直接影响最终指标这正好可以用来讨论“压测时场景参数该怎么设计”。需要注意的一个细节是query_order_from_db里我用了await asyncio.sleep(0.01)来模拟数据库耗时。真实项目中你不可能在压测环境里真的连一个完备的数据库但为了测试接口逻辑可以用这种方式模拟耗时。不过正式压测一定要用接近真实的依赖否则结果不可信。2.3 同步 def 接口的隐藏杀手线程池耗尽很多人刚开始写 FastAPI 时图省事接口函数直接写成普通def然后在里面写同步的requests.get、time.sleep、同步数据库查询。小流量下没问题但一压测就容易出现响应时间突然飙升。我做过一个实验同一个查询接口async def版本和def版本在 200 并发下的表现差距非常明显版本100并发 P95200并发 P95500并发 错误率async def95ms180ms0%def 线程池260ms1200ms5%原因就是线程池有上限。FastAPI 默认的线程池大小是 40 个左右具体取决于 Python 版本和max_workers设置也就是说同一时刻最多只有 40 个同步请求在真正执行其余的都要排队。而异步版本里同时可以存在几百上千个协程只要事件循环不忙请求就一直在推进。所以在压测之前务必检查被测接口是async def还是def。如果线上业务已经全异步化但压测脚本里用了requests同步库来发请求——这也会带来施压端的性能瓶颈。Locust 里解决这个问题有专门的方式后面第三章会提到。3. 用 Locust 写出第一版压测脚本3.1 安装和最小脚本结构Locust 的安装很简单pip install locust即可。它依赖gevent在 Linux 和 macOS 下表现最稳定Windows 下做高并发会有些问题所以施压机环境建议优先选择 Linux 或者 macOS。它的核心抽象是User类。每个并发的虚拟用户就是一个 User 实例User 的行为通过task装饰器定义。继承HttpUser后User 实例会自带一个client属性这是基于requests库封装好的 HTTP 客户端支持 GET、POST、PUT 以及其他 HTTP 方法响应对象和requests的 Response 几乎一致。一个最小可运行的locustfile.py长这样from locust import HttpUser, task, between class OrderUser(HttpUser): wait_time between(1, 3) task def order_list(self): user_id 10086 with self.client.get( f/api/v1/user/{user_id}/orders, nameuser_order_list, catch_responseTrue, ) as resp: if resp.status_code 404: resp.success() elif resp.status_code ! 200: resp.failure(bad status)这段脚本做的事情很简单每个虚拟用户循环执行order_list这个请求每次请求之间随机等待 1 到 3 秒。wait_time的作用是模拟真实用户的操作间隙避免所有虚拟用户像死循环一样连发请求否则压出来的是“极限施压”不是“业务场景”。3.2 用户数和 RPS 是什么关系很多第一次用 Locust 的人会问我设了 1000 个用户为什么 RPS 只有 200这里需要纠正一个概念虚拟用户数不等于QPS。虚拟用户只是“并发在线”的模拟每个用户发起请求的频率取决于wait_time和接口自身耗时。比如接口平均耗时 200mswait_time是 1 秒那么每个用户平均每秒大约发 0.83 个请求。1000 个虚拟用户最终的 RPS 就是 1000 × 0.83 ≈ 830。如果你不设wait_timeLocust 默认在任务完成后立即开始下一次任务相当于所有用户在极限跑这时候的请求密度会明显更高。那么如果你需要压出目标 1000 RPS需要多少用户反推公式大约是用户数 ≈ 目标RPS × (平均响应时间 平均思考时间)举个例子目标 1000 QPS接口平均响应时间 50ms思考时间 1 秒那需要的用户数大约是 1000 × 1.05 ≈ 1050。如果接口响应慢比如平均 500ms思考时间还是 1 秒同样的目标 RPS 就需要 1500 个用户。所以跑压测之前先按这个公式粗算一下避免“设了 10000 用户结果 1000 RPS 都没有”的尴尬。它也能帮你反向判断系统的真实处理能力——如果并发上去之后 RPS 没有明显增长说明瓶颈已经出现继续压只会拉高响应时间和错误率。3.3 设计多场景混合压测真实业务中一个系统不会只有一个接口在被调用。拿订单服务来说用户进首页查订单列表、点进订单详情、发起售后、查看物流状态是同时发生的。如果只压查询接口漏掉写接口和查询接口之间的资源争抢效果会大打折扣。Locust 支持在一个 User 类里通过权重配置多任务。比如列表查询占 60%详情占 30%售后占 10%from locust import HttpUser, task, between class MixedTrafficUser(HttpUser): wait_time between(0.5, 2) task(6) def order_list(self): self.client.get(/api/v1/user/10086/orders, nameorder_list) task(3) def order_detail(self): self.client.get(/api/v1/order/20250101001, nameorder_detail) task(1) def refund_apply(self): self.client.post(/api/v1/order/20250101001/refund, json{reason: 压测}, namerefund_apply)任务权重task(6)、task(3)、task(1)表示所有任务按 6:3:1 的比例被随机选择。跑一段时间之后三种请求的比例就会收敛到设定值。这种方式比跑完接口 A 再跑接口 B 更能模拟真实流量混杂的情况。写接口在压测时需要特别小心。它会产生实际的数据变更压完之后要把测试数据清理掉或者针对压测流量单独打标识比如 user_id 统一用loadtest_前缀方便后续批量清理。这是一个很容易被忽略但一旦出事就很麻烦的细节。4. 完整运行压测并看懂核心指标4.1 三种运行方式我推荐无头模式跑回归Locust 提供了三种运行方式Web 模式、无头模式、自定义 Python 模式。日常调试用 Web 模式自动化和回归用无头模式。Web 模式是最直观的使用方式启动后访问http://localhost:8089就能看到控制台。你可以随时修改在线用户数观察 RPS、响应时间、异常率的变化曲线。调试脚本、验证场景设计的时候Web 模式效率很高。但真正跑正式压测的时候我建议用无头模式。原因很简单Web 模式本身会占用一定资源用于实时统计和 WebSocket 推送同时容易在压测过程中误操作——比如不小心点了“停止”。无头模式一行命令启动、固定参数、可复现、可接入 CI这才是工程化的用法。无头模式的启动命令大概是locust -f locustfile.py --host http://localhost:8000 \ --headless \ -u 500 \ -r 50 \ -t 10m \ --csvloadtest_result \ --csv-full-history \ --logfilelocust.log参数非常直观-u指定用户总数-r指定每秒启动的用户数也就是爬坡速度-t指定运行时长--csv指定统计结果落盘文件前缀--csv-full-history会记录每个时间点的完整统计画图的时候非常好用爬坡速度-r不能设得太快。如果一下子涌入几千用户施压机自身先会被击穿报ConnectionResetError。我一般会在总用户数除以 10 到 20 左右作为每秒爬坡数让接口在用户逐步增多的情况下暴露性能退化曲线——这往往比直接打满更有价值。4.2 核心指标到底该怎么读压测结束之后最怕的就是看着一堆数据不知道哪个重要。Locust 的报告会给出大量指标我一般只看这几项RPS是最先看的。它代表系统实际处理的请求速率不是用户总数也不是你预期值。如果目标 1000实际只有 300说明系统吞吐量远达不到预期要往下排查。响应时间要区分平均值和分位数来看。平均值最容易被极端值拉偏比如 99% 的请求只要 50ms但 1% 的请求拖到 5 秒平均下来可能就是 100ms看着还挺健康。但真实用户可能成为那不幸的 1%。P95 和 P99 更能代表系统在压力下的尾巴延迟。P95 指的是 95% 的请求在多少毫秒内完成压测中这个值一旦开始指数级增长说明系统已经进入过载状态。错误率不是越低越好而是要结合“错误类型”看。HTTP 5xx 是系统级错误必须清零超时错误说明请求根本没完成4xx 一般是压测脚本本身的问题比如参数错误、认证失败。常见坑是压测脚本里的 token 过期了导致大量 401、403而你在监控面板上只看到一片红色第一反应是系统挂了实际上是被压方根本没在实际执行业务逻辑。资源消耗单独看意义不大要结合 RPS 和响应时间一起看。CPU 用满但 RPS 还在涨那可能只是没有触发新的瓶颈如果 CPU 没跑满RPS 就上不去了大概率瓶颈在下游比如数据库、Redis 或者外部 API。我建议压测过程中用top或者pidstat记录应用进程的 CPU 和内存数据再用mysqladmin status或redis-cli INFO监控数据库和缓存的状态。压测不只是压接口本身整个链路都值得观察。4.3 跑一轮完整压测并复盘数据下面是我跑这个示例接口时的一组真实数据。环境是两台机器施压机 4 核 8G被压机 8 核 16G数据库和 Redis 同在被压机本机带宽千兆。接口逻辑就是上面那种带缓存和订单查询的场景。第一轮压测参数500 用户每秒启动 20运行 10 分钟无思考时间。跑完看 CSV 里的关键数据指标数值平均 RPS1216峰值 RPS1350平均响应时间178msP50160msP95312msP99780ms异常率0.05%接口整体表现不错但 P99 有明显上涨说明有少量请求承受了远高于平均的等待时间。进一步看日志后发现这些慢请求全部集中在缓存未命中的场景里也就是说前 30 秒和每隔 30 秒的瞬间流量打到 MySQL 上去单次查询耗时会明显增加。这其实是正常的缓存天然就是牺牲一部分请求的延迟换取整体性能但如果 P99 超标就需要考虑是不是缓存 TTL 设置得太短或者缓存击穿后 MySQL 扛不住那一下流量。我把缓存 TTL 从 30 秒调成 60 秒再跑一轮P99 降到了 540ms平均 RPS 略有上升到 1280。为了压出数据库本身的瓶颈我还做了一轮“缓存全绕过”的测试直接用不存在的用户 ID 压接口相当于每个请求都穿透到数据库。那一轮 RPS 直接掉到 420P95 到了 2 秒。这个数据说明没有缓存保护的数据库是本系统最大的短板与网络层、应用层无关。压测讲究的就是这种“控制变量-观察对比-解决问题”的循环。每一轮压测都带着一个假设去验证而不是无脑拉高并发数看数字变化。5. 常见问题与排查技巧实录5.1 压着压着突然大量连接超时这是我最常遇到的问题。表象很吓人刚开始跑一切正常几分钟后错误率飙升全部是ConnectionError、TimeoutError。排查思路明确之后会发现绝大多数情况和应用代码无关而是系统层配置踩了坑。首先是文件描述符限制。Linux 默认的ulimit -n通常是 1024压测时单进程建立上千连接后就会触发Too many open files表现就是连接失败。这个必须提前调大ulimit -n 65535其次是端口耗尽。施压机为了发起请求会占用本地端口每个 TCP 连接占用一个。默认的ip_local_port_range是 32768 到 60999大约最多 28000 个端口。如果压测过程中大量连接处于TIME_WAIT状态新连接就拿不到端口了。这时候需要调大本地端口范围并开启端口复用sysctl -w net.ipv4.ip_local_port_range1024 65535 sysctl -w net.ipv4.tcp_tw_reuse1 sysctl -w net.ipv4.tcp_fin_timeout30最后才轮到应用层排查。FastAPI Uvicorn 本身有连接限制如果用了 Gunicorn 管理 worker 进程还需要检查worker_connections参数是否够用。高并发场景下Uvicorn 单 worker 建议至少配置 1000 以上的连接数否则请求只能在 socket 队列里排队表现就是连接建立了但迟迟没有响应。5.2 响应时间一直很稳但 QPS 上不去这个现象也很典型压测时错误率是 0P95 也很好看但 RPS 就是卡在一个固定值上不涨把用户数从 500 加到 3000 也没用。这说明瓶颈根本不在应用层而在下游或者网络链路。举个真实例子我压过一个 FastAPI 接口无论怎么加并发RPS 永远停在 800 左右。起初以为是应用代码的问题各种加 worker、调数据库连接池都没用。后来在数据库上用show processlist一看数据库连接数一直稳定在 30 左右说明数据库侧的处理能力就那么多应用层再怎么加并发也只能排队等待。这种情况下看三个地方数据库连接池是否够用慢查询是否积压锁等待是否严重。Redis缓存命中率是否在预期范围内Redis 自身 CPU 和内存是否打满。下游 HTTP 服务被测接口依赖的外部服务是否能扛住同等压力。解决方式也比较明确如果是数据库慢查询优化索引和 SQL如果是连接池不够动态调大pool_size如果是外部依赖到了瓶颈那就得考虑异步化或者降级策略让接口不依赖外部接口也能返回结果。5.3 施压机自己先挂了这是一个很容易被忽视的点。Locust 基于协程确实能模拟大量用户但施压机不是无限资源。当用户数很高、请求体很大、响应体也很大时施压机自身的内存、CPU、带宽可能成为瓶颈。最典型的坑是使用了同步的requests库逻辑。虽然 Locust 的HttpUser.client封装已经做了异步化处理但如果你在任务函数里自己调了requests.get比如去请求额外的数据、做登录鉴权这类同步调用会阻塞整个协程直接拖垮施压效率。应该始终使用self.client而不是requests裸调。另外如果是单台机器压到了几十万用户量级那就要上多台施压机分布式压测了。Locust 提供 master-worker 模式通过--master和--worker参数启动可以横向扩展施压能力。不过对大多数业务接口单台 8 核施压机已经足够打出几万 RPS在需要上集群之前先确认你的接口是否真能抗住这么大流量。5.4 压测结果波动特别大怎么判断是不是“有效数据”有时候同一场景跑两轮RPS 数据波动很大。这种波动需要用更长的时间窗口去吸收。Locust 的统计是滚动窗口运行时间越短窗口内的随机因素影响越大。正式压测时间我建议不少于 10 分钟至少要让每个接口累积几百个请求样本否则 P95 的置信度是很低的。还有一点就是“预热期”。应用启动后JIT 编译、线程池创建、Redis 连接池建立都需要时间。直接在压测开始阶段的数据就没必要分析等运行 2 到 3 分钟后再看数据更代表系统稳定态。这也是为什么无头模式要设置-t 10m而不是跑 1 分钟就停的原因。5.5 别忘了压测完的清场工作压测会往目标系统灌入大量数据比如本地缓存的 key、数据库的测试记录、日志文件等。写操作型接口压测完如果不清理轻则浪费存储空间重则影响后续开发联调和数据统计。我的习惯是压测用的用户 ID 统一加上loadtest_前缀脚本里固定一个测试用户池。压测结束立即用脚本清理插入的数据或者用 Redis 的SCAN加DEL把测试 key 全部清掉。数据库如果有测试产生的数据按user_id批量删除不要手动一条条删。将压测结果 CSV 归档保存标注场景参数和代码版本。这些历史数据可以作为后续性能回归的基线。这样下一轮压测才有可比性。6. 压测实践中的几个经验总结聊了这么多我觉得最值得记住的不是某一项工具的使用技巧而是压测这件事本身的工作方式。第一压测一定要带着“要验证什么”的路线图。是验证容量规划是定位某个慢接口的瓶颈还是验证新架构的性能表现不同的目标决定了场景设计、施压配置和指标解析方式。目标不明确跑出来的数据只是一堆没有意义、没有行动价值的数字。第二性能问题永远要从链路角度去排查。压测中发现自己写的接口慢了第一反应应该是看数据库查询慢不慢、Redis 延迟高不高、外部依赖有没有超时而不是急着改应用层的中间件配置。大多数性能瓶颈都藏在你自己看不到的地方——那些你以为不会成为瓶颈的环节。第三把压测变成一个可重复执行的工程过程而不是一次性行为。把 Locust 脚本、施压参数、结果解析代码都提交到代码仓库接入 CI/CD 流水线。这样每次代码改动都能跑一遍回归压测性能问题在发布之前就能暴露出来而不是等到线上出事故才后悔没有做压测。最后再分享一个我在实际项目中养成的小习惯每次压测结束我会把 CSV 统计文件里的 RPS、P95、P99、错误率这几个核心指标提取出来附上代码版本号和压测环境配置贴到项目的发布文档里。时间长了之后这个表就是整个服务的性能演进史也是最有说服力的技术资产。下次团队里有人问“这个接口最多能扛多少并发”你就可以直接甩出这张表而不是拍脑袋说“我觉得两三千没问题”。