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