3. 压测环境搭建:生产环境 vs 压测环境、流量隔离与数据隔离、影子库与Mock服务、压测数据准备

好,咱们进入第三章。这一章我打算聊聊压测环境怎么搭。

说实话,很多团队在压测环境上栽过跟头。我见过最夸张的一次,有人直接在线上生产环境压测,结果把数据库连接池打满了,线上业务挂了半小时。嗯,那场面,真是惨不忍睹。

所以,环境搭建这件事,看着简单,其实门道不少。咱们一个一个说。

3.1 生产环境 vs 压测环境:到底用哪个?

先回答一个经典问题:压测到底用生产环境还是单独搭一套环境?

我的答案是:能用生产环境,就别用测试环境。但前提是——你得做好隔离。

为什么这么说?

测试环境跟生产环境差距太大了。硬件配置不同、网络拓扑不同、数据量不同、并发用户数不同。你在测试环境压出来的结果,拿到生产环境基本就是废纸一张。我遇到过好几次,测试环境压测一切正常,一上线就崩。说白了,环境差异导致的。

但生产环境压测,风险也大。你想想看,一个不小心,就把线上用户给影响了。

所以,我的建议是:

  • 核心链路、关键接口:尽量在生产环境做压测,但必须做流量隔离和数据隔离
  • 非核心、新功能:可以在预发环境或灰度环境做,但预发环境要尽量跟生产环境保持一致
  • 纯功能验证:测试环境就够了,别折腾生产环境

核心原则:压测环境越接近生产环境,压测结果越可信。但接近不等于完全一样,安全第一。

3.2 流量隔离与数据隔离:压测的护身符

好,既然决定在生产环境压测,那隔离就是头等大事。

流量隔离和数据隔离,这两个东西是压测的护身符。没有它们,你就是在玩火。

3.2.1 流量隔离

流量隔离,说白了就是让压测流量跟真实用户流量互不干扰。

怎么做?我常用的方法有几种:

  • 染色标记:在请求头里加一个特殊标记,比如 X-Env: stress-test。网关、中间件、业务代码都识别这个标记,对压测流量做特殊处理
  • 独立网关:压测流量走单独的网关入口,跟用户流量物理隔离
  • 时间窗口:选在业务低峰期压测,比如凌晨2点到4点。但这个方法只能降低风险,不能完全隔离

我个人习惯用染色标记的方式。为什么呢?因为它灵活。你可以在代码里针对染色流量做各种特殊处理,比如跳过某些非核心逻辑、使用独立的连接池、记录更详细的日志等等。

小技巧:染色标记最好在网关层统一注入,业务代码无感知。这样业务团队不需要改代码,运维团队就能控制压测流量。

3.2.2 数据隔离

数据隔离比流量隔离更复杂。为什么?因为数据是有状态的。

你压测产生的数据,不能跟真实用户数据混在一起。否则,报表乱了、订单乱了、用户看到奇怪的数据了……后果很严重。

数据隔离的常见方案:

方案 原理 优点 缺点
影子库 压测数据写入独立的数据库或表 完全隔离,不影响真实数据 需要额外维护一套存储
逻辑隔离 在同一个数据库里,通过字段区分压测数据 成本低,不需要额外存储 容易遗漏,清理麻烦
Mock服务 压测请求不写入真实存储,而是返回模拟数据 完全无副作用 无法验证存储层的性能

我个人推荐影子库方案。虽然成本高一点,但安全可靠。我曾经在一个金融项目中,因为用了逻辑隔离,结果压测数据没清理干净,导致第二天的对账报表多出来几百万的虚拟交易……嗯,从那以后,我再也不敢用逻辑隔离了。

3.3 影子库与Mock服务:两种隔离利器

这两个东西,我单独拿出来说说。

3.3.1 影子库

影子库,就是给压测数据准备一个「影子」存储空间。

具体怎么做?

  • 数据库层面:创建一套跟生产库结构完全一样的影子库,表名、索引、存储过程都一样。压测流量写入影子库
  • 缓存层面:Redis、Memcached 等缓存,也准备独立的影子实例或影子key前缀
  • 消息队列:压测消息发到独立的影子Topic或影子队列

代码层面,通常通过中间件或AOP来实现路由切换。比如:

// 伪代码示例:基于染色标记的路由切换
public class DataSourceRouter {
    public String getDataSource() {
        String traceId = MDC.get("traceId");
        if (traceId != null && traceId.startsWith("STRESS_")) {
            return "shadowDataSource"; // 压测流量走影子库
        }
        return "primaryDataSource"; // 正常流量走主库
    }
}

这里要注意:影子库的数据要定期清理。不然压测数据越积越多,磁盘空间就爆了。我一般会在压测结束后,自动触发一个清理任务。

3.3.2 Mock服务

Mock服务,就是模拟外部依赖的行为。

什么时候用Mock?

  • 外部依赖不可控,比如第三方支付接口、短信网关
  • 外部依赖性能太差,会成为压测瓶颈,但你不想测它
  • 外部依赖有调用次数限制或费用

Mock服务的实现方式:

  • Mock Server:用 WireMock、MockServer 等工具,启动一个假的HTTP服务,返回预设的响应
  • 接口Mock:在代码层面,通过依赖注入或AOP,把真实接口替换成Mock实现
  • 流量录制回放:先录制真实请求的响应,压测时直接回放

警告:Mock服务虽然方便,但不要滥用。如果你Mock了太多服务,压测结果就失去了意义。你想想看,你Mock了数据库、Mock了缓存、Mock了外部接口,那你到底在测什么?测的是你的代码在「真空」中的性能吗?

我的建议是:只Mock那些你不想测、或者不能测的外部依赖。核心的内部服务,比如数据库、缓存、消息队列,尽量用真实的。

3.4 压测数据准备:别让数据成为瓶颈

好,环境搭好了,隔离做好了,接下来就是压测数据。

很多人忽略这一步,觉得随便造点数据就行了。结果压测的时候,数据量不够,缓存命中率异常高,压出来的结果虚高。或者数据分布不合理,导致某些索引失效,压出来的结果又偏低。

数据准备,其实是个技术活。

3.4.1 数据量要匹配

压测数据量,要跟生产环境的数据量级一致。

比如生产环境有1亿条订单数据,你压测环境只有1万条,那压出来的结果肯定不准。为什么?因为数据量小的时候,数据库的B+树索引深度浅,查询快。数据量大了,索引深度增加,IO次数增加,性能就下来了。

我一般会按生产环境数据量的 80%~120% 来准备压测数据。少了不行,多了也没必要。

3.4.2 数据分布要真实

数据分布也很重要。比如用户ID的分布、订单时间的分布、商品类别的分布,都要跟生产环境一致。

举个例子:生产环境的热门商品占了80%的访问量,冷门商品只占20%。如果你压测数据是均匀分布的,那压出来的结果就会偏乐观。因为热点数据集中时,数据库的缓存、连接池、锁竞争都会更激烈。

怎么生成真实分布的数据?

  • 脱敏导出:从生产环境导出数据,脱敏后导入压测环境。这是最真实的方法
  • 数据采样:从生产环境采样一部分数据,然后按比例放大
  • 数据生成工具:用 DataFactory、JMeter 的 CSV Data Set Config 等工具,按指定分布生成数据

3.4.3 数据准备脚本

我习惯写一个数据准备脚本,每次压测前自动执行。脚本大概长这样:

#!/bin/bash
# 压测数据准备脚本

echo "开始准备压测数据..."

# 1. 清理旧数据
mysql -h shadow_db -u stress -p'password' -e "TRUNCATE TABLE orders;"

# 2. 生成基础数据
python3 generate_data.py \
  --table orders \
  --count 10000000 \
  --distribution hotspot \
  --output /tmp/orders.sql

# 3. 导入数据
mysql -h shadow_db -u stress -p'password' orders < /tmp/orders.sql

# 4. 重建索引
mysql -h shadow_db -u stress -p'password' -e "ALTER TABLE orders ENGINE=InnoDB;"

# 5. 预热缓存
python3 warm_cache.py --table orders --cache redis_shadow

echo "数据准备完成!"

注意最后一步:预热缓存。如果不预热,压测刚开始的时候,缓存是空的,所有请求都打到数据库,那压出来的结果会偏悲观。预热之后,缓存命中率跟生产环境一致,结果才真实。

总结一下:压测环境搭建,核心就三件事——隔离、模拟、准备。隔离做不好,压测就是破坏;模拟做不好,压测就是自欺欺人;准备做不好,压测就是浪费时间。

好,这一章就到这里。下一章咱们聊聊压测工具的选择和脚本编写,那又是另一番天地了。


公众号:蓝海资料掘金营,微信deep3321