真实流量,本地回放:一种轻量的性能测试思路

性能测试最大的谎言是:你的模拟数据能代表生产环境。

我们正在做一个数据库查询中间件的版本迁移。新版本用了完全不同的查询协议,需要验证性能不会退化。

之前同事做了第一个版本的性能测试方案:在生产环境里开一个 shadow 线程池,用独立线程回放查询。这个方案能跑,但有几个问题。第一,任何改动(修 bug、加功能、调参数)都需要部署生产环境才能生效,迭代速度很慢。第二,虽然是在独立线程池里跑,但多少会对生产流量产生影响——CPU、内存、数据库连接都是共享的。第三,指定 TPS 进行压测时,需要人工构造请求之间的时间间隔来模拟真实的流量波动,这既不准确也增加了复杂度。

我想要的是:直接用生产流量来测,但完全不需要部署任何东西到生产环境。

设计思路

核心思路是把性能测试拆成4个完全解耦的步骤,每一步都有独立的输入和输出,像管道一样串起来。

1
2
3
Step 1 (Extract)  →  Step 2 (Prepare)  →  Step 3 (Execute)  →  Step 4 (Analyze)
生产日志 转换为查询语句 按真实速率回放 聚合分析结果
输出: JSONL 输出: JSONL+SQL 输出: 延迟数据 输出: P50/P90/P99

每一步的中间结果都存在对象存储上。你可以在任何一步停下来看看数据长什么样,发现问题了也只需要从那一步开始重跑,而不是从头来过。

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 反馈循环的应用场景),人类负责设计和验证。