性能测试最大的谎言是:你的模拟数据能代表生产环境。
我们正在做一个数据库查询中间件的版本迁移。新版本用了完全不同的查询协议,需要验证性能不会退化。
之前同事做了第一个版本的性能测试方案:在生产环境里开一个 shadow 线程池,用独立线程回放查询。这个方案能跑,但有几个问题。第一,任何改动(修 bug、加功能、调参数)都需要部署生产环境才能生效,迭代速度很慢。第二,虽然是在独立线程池里跑,但多少会对生产流量产生影响——CPU、内存、数据库连接都是共享的。第三,指定 TPS 进行压测时,需要人工构造请求之间的时间间隔来模拟真实的流量波动,这既不准确也增加了复杂度。
我想要的是:直接用生产流量来测,但完全不需要部署任何东西到生产环境。
设计思路
核心思路是把性能测试拆成4个完全解耦的步骤,每一步都有独立的输入和输出,像管道一样串起来。
1 | Step 1 (Extract) → Step 2 (Prepare) → Step 3 (Execute) → Step 4 (Analyze) |
每一步的中间结果都存在对象存储上。你可以在任何一步停下来看看数据长什么样,发现问题了也只需要从那一步开始重跑,而不是从头来过。
Step 1: 从生产日志提取真实流量
从生产环境的日志里抽取真实的查询请求。不是抽样,是完整还原一段时间(比如1小时)内的所有查询,保留原始的时间戳排序。
输出是一个按秒粒度排列的 JSONL 文件——每一行是一个查询请求,带着它原始发生的时间。这意味着后续回放时,自然就能还原生产的流量模式(高峰、低谷、突发),完全不需要人工构造流量模型或模拟请求间隔的随机性。
Step 2: 转换为目标查询语句
这一步把提取的原始请求转换为具体的查询语句。支持多种模式——我们的场景里是旧版查询语言和新版 SQL。
由于 Step 2 和 Step 3 是解耦的,在做版本迁移时只需要改这两步(生成不同语法的查询,用不同的客户端执行),而 Step 1 和 Step 4 完全不变。这就是解耦的好处。
Step 3: 按真实速率回放
这是核心。三种执行策略:
- 原速回放(replay):按照 Step 1 中记录的时间戳,以1:1的速率回放。10点15分23秒有3个查询,就在那一秒发3个。这是最接近生产的模式。
- 固定 TPS(fixed-tps):以指定的每秒查询数匀速发送。用于测试特定负载下的表现。
- 压力模式(beast):尽可能快地发送所有查询,不限速。用于找到系统的极限。
关键设计:回放完全不需要人工模拟流量的时间波动。 因为 Step 1 保留了生产的时间分布——这本身就是最真实的流量模式。你用假数据永远模拟不出凌晨3点和下午3点流量模式的真实差异。
多任务并行也有考虑:每个任务分到确定性的查询子集(无重复、无随机),不同任务之间有不同的时间偏移,避免所有任务同时发包。每个任务独立输出自己的性能结果子集,供 Step 4 聚合。
Step 4: 聚合分析
收集所有任务的延迟数据(Step 3 的多个任务各自输出性能结果子集),跨任务聚合后输出:
- 整体 P50、P90、P99、Max
- 错误率
- 实际 TPS
- 按 API 拆分的各项指标
有了这些,新旧版本的性能对比一目了然。
无需部署到生产环境
整个框架通过 SSH 隧道连接测试环境的数据库中间件。本地开发机或者容器化的执行环境都能跑——不需要碰生产环境的任何东西。
- 本地开发机:一条命令启动,适合快速验证和调试(比如先跑5秒看看有没有错误)
- 容器化执行(Fargate):并行多个任务做大规模压测,结果自动上传到 S3
这意味着你可以随时跑性能测试,不需要预约环境、不需要等队列、不需要申请权限部署到测试集群。
调试友好
由于每一步解耦:
- Step 1 提取出来的流量文件,你可以直接查看有没有异常请求
- Step 2 的输出是人类可读的查询语句,可以手动验证语法是否正确
- Step 3 执行过程中会记录每个失败查询到错误日志(JSONL格式),包含完整的请求内容和错误信息。你可以直接把失败的查询单独重放一次来定位问题
- Step 4 的报告直接告诉你哪个 API 有问题
传统的性能测试工具(JMeter、Gatling 等)通常是个黑盒——你看到”P99 = 3秒”但很难追溯到是哪个查询、什么参数导致的。我们的做法让每一层都是透明的。
版本迁移中的实际应用
我们在做数据库查询中间件从 V1 到 V2 的迁移。V1 用的是一种定制查询语言,V2 用标准 SQL。整个框架里:
- Step 1 完全不变(流量是同一份)
- Step 2 新增了 SQL 生成模式(一个参数切换)
- Step 3 新增了 V2 客户端(通过 PrivateLink + 认证)
- Step 4 完全不变
迁移的性能验证用的就是同一份生产流量,一份走 V1 路径,一份走 V2 路径,Step 4 直接对比。公平、真实、可复现。
实际测试结果
以下是我们对新版中间件(V2)的一次实测,目标 30 TPS,持续 5 分钟,9000 个查询全部来自生产流量:
单任务执行:
| 指标 | 值 |
|---|---|
| 总查询数 | 9000 |
| 成功 | 9000 |
| 错误率 | 0% |
| 实际 TPS | 16.8(因背压限制,未达到目标30) |
| P50 | 2398ms |
| P90 | 4427ms |
| P99 | 13401ms |
Per-API 拆分立刻暴露了问题:
| API | 调用量 | P50 | P90 | P99 |
|---|---|---|---|---|
| GetDomainData | 7782 | 2.3s | 4.0s | 7.7s |
| GetAccountLevelSummary | 981 | 3.0s | 9.3s | 26.8s |
| GetAccountProductList | 143 | 3.2s | 12.6s | 73.3s |
| GetAccountLevelTrend | 94 | 3.6s | 12.3s | 73.0s |
主力 API(占 86% 流量的 GetDomainData)表现尚可,但长尾 API 的 P99 高达 70+ 秒——这在传统的”平均 TPS”报告里是看不到的。
双任务并行对比:
| 1 task | 2 tasks | |
|---|---|---|
| 实际 TPS | 16.8 | 19.1 |
| P50 | 2398ms | 4788ms |
| P90 | 4427ms | 7041ms |
| 错误率 | 0% | 0% |
有意思的发现:两个任务并发时,延迟几乎翻倍(P50 从 2.4s 到 4.8s),但吞吐量只从 16.8 涨到 19.1。这说明测试环境的 V2 实例在 15-20 TPS 就已经饱和了——增加客户端并发只是把延迟推高,并不能线性提升吞吐。
这正是用真实流量测试的价值: 如果我们用均匀的模拟请求来测,可能会得出”V2 轻松支持 30 TPS”的结论。但真实流量里有大量长尾查询(跨账户汇总、趋势计算),它们占据数据库连接时间远超简单查询,导致整体饱和点比预期低得多。
写在最后
这个框架的核心思想很简单:把性能测试当成一个数据管道来设计,而不是一个单体工具。 每一步解耦、中间结果持久化、执行环境灵活。你获得的好处是:
- 用真实流量而非模拟数据——结果可信
- 无需部署到生产——安全且灵活
- 每一步独立观察——debug 不再是猜谜
- 支持版本对比——同一份数据两条路径
如果你也在做类似的中间件迁移或性能基线建立,可以参考这个思路。整个框架的 MVP 版本半小时就开发完了,后续的多任务并行、V2支持、分析报告等功能是在几天内逐步迭代出来的——全部代码都是 AI 写的(这恰好是L3 反馈循环的应用场景),人类负责设计和验证。