跳到主内容

大促全链路压测:影子库搭建与流量回放避坑实录 ​

问题背景:为什么单机压测无法回答大促容量 ​

大促前最常见的问题不是“接口 QPS 能到多少”,而是“整条链路在真实业务模型下还能不能扛住”。订单、库存、优惠券、支付、消息队列、缓存和数据库彼此影响,单机接口压测往往只能发现 CPU 或慢 SQL,却无法暴露连接池耗尽、缓存击穿、下游限流、消息堆积和跨服务资源竞争。

全链路压测的目标,是在接近生产的部署形态下,用可识别、可隔离、可回滚的方式模拟大促流量,完成容量评估、瓶颈定位和限流预案验证。核心难点有两个:一是压测数据不能污染生产库表、缓存和消息;二是压测流量要尽量还原真实用户路径,而不是只打一个热点接口。

方案对比:独立环境、数据标记与影子库怎么选 ​

常见方案有三类。

第一类是完全独立的压测环境。优点是隔离彻底,缺点也明显:服务规格、数据库参数、网络拓扑、缓存命中率和生产差异很大,压出来的容量难以换算;每次同步版本和基础数据成本高。

第二类是生产环境加流量标记,复用生产表,通过字段或用户标识区分压测数据。实现简单,但侵入业务代码,清理数据容易遗漏唯一索引、库存扣减、审计日志和异步消息,风险较高。

第三类是生产环境全链路压测:入口请求携带压测标识,微服务、缓存 Key、消息、定时任务和数据库连接均识别该标识,SQL 自动路由到影子库。它最贴近真实容量,也最适合大促容量评估,但要求中间件和数据访问层具备统一隔离能力。

压测流量携带标识网关与服务透传上下文影子缓存/消息Key 与 Topic 隔离动态数据源SQL 路由生产库真实流量影子库压测流量

建议选型原则是:核心交易链路使用影子库;只读查询或报表类接口可以使用影子表或压测账号;强依赖外部第三方的支付、短信、物流接口使用 Mock 或沙箱,不能向外部发送真实请求。

实操步骤一:搭建影子库与动态数据源 ​

影子库不是简单复制一份数据库即可。它的表结构、索引、参数、字符集、时区和连接串模板必须与生产一致,数据量也要按大促模型预热。可以使用逻辑备份或快照创建影子库,但用户手机号、收货地址、支付令牌等敏感字段需要脱敏。

下面示例基于 Spring Boot、MyBatis 和动态数据源实现。请求头携带 X-Load-Test: 1 时,当前线程切换到影子数据源。

java
// 压测上下文:只保存是否为压测流量,避免把业务参数塞入 ThreadLocal
public final class LoadTestContext {
    private static final ThreadLocal<Boolean> FLAG =
            ThreadLocal.withInitial(() -> false);

    public static void mark() { FLAG.set(true); }
    public static boolean isLoadTest() { return FLAG.get(); }
    public static void clear() { FLAG.remove(); }
}

// 动态数据源:根据上下文选择生产库或影子库
public class ShadowRoutingDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return LoadTestContext.isLoadTest() ? "shadow" : "master";
    }
}

数据源配置如下:

yaml
spring:
  datasource:
    master:
      jdbc-url: jdbc:mysql://prod-db:3306/trade?useSSL=true&serverTimezone=Asia/Shanghai
      username: trade_app
      password: ${MASTER_DB_PASSWORD}
      maximum-pool-size: 50
    shadow:
      jdbc-url: jdbc:mysql://shadow-db:3306/trade_shadow?useSSL=true&serverTimezone=Asia/Shanghai
      username: trade_shadow
      password: ${SHADOW_DB_PASSWORD}
      maximum-pool-size: 50

注册动态数据源:

java
@Configuration
public class DataSourceConfig {
    @Bean
    @ConfigurationProperties("spring.datasource.master")
    public DataSource masterDataSource() {
        return DataSourceBuilder.create().type(HikariDataSource.class).build();
    }

    @Bean
    @ConfigurationProperties("spring.datasource.shadow")
    public DataSource shadowDataSource() {
        return DataSourceBuilder.create().type(HikariDataSource.class).build();
    }

    @Bean
    public DataSource dataSource() {
        var router = new ShadowRoutingDataSource();
        var targets = Map.of(
                "master", masterDataSource(),
                "shadow", shadowDataSource());
        router.setTargetDataSources(new HashMap<>(targets));
        router.setDefaultTargetDataSource(masterDataSource());
        return router;
    }
}

过滤器负责识别压测标识,并在请求结束时清理 ThreadLocal:

java
@Component
public class LoadTestFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain chain) throws ServletException, IOException {
        try {
            if ("1".equals(request.getHeader("X-Load-Test"))) {
                LoadTestContext.mark();
            }
            chain.doFilter(request, response);
        } finally {
            // 必须清理,避免 Tomcat 工作线程复用导致串流量
            LoadTestContext.clear();
        }
    }
}

实操步骤二:跨服务、缓存和消息隔离 ​

微服务调用时要把压测标识放入 RPC Header 或 Trace 上下文,禁止只靠 URL 参数传递。OpenFeign 可以统一增加请求拦截器:

java
@Bean
public RequestInterceptor loadTestRequestInterceptor() {
    return template -> {
        if (LoadTestContext.isLoadTest()) {
            template.header("X-Load-Test", "1");
        }
    };
}

Redis 建议统一封装 KeyBuilder,压测流量自动增加前缀,例如 shadow:stock:sku_1001。不要在业务代码里手写 Redis Key,否则很容易漏掉某个写路径。消息队列则将 Topic 或 Exchange 路由到影子 Topic,消费者继续透传标识;无法新建 Topic 时,可在消息 Header 中打标,并由消费者判断后写入影子库,但要防止没有升级的老消费者消费压测消息。

实操步骤三:流量录制、回放与容量评估 ​

流量回放优先选择生产高峰时段的只读或脱敏后请求,保留接口比例、用户思考时间、参数分布和依赖调用顺序。不能只拿均匀流量循环回放,否则缓存命中率、锁竞争和热点分布都会失真。

一个可运行的简单回放器如下,生产中可替换为 JMeter、Gatling 或公司内部平台:

java
public class ReplayTask implements Runnable {
    private final WebClient client = WebClient.builder()
            .baseUrl("https://api.example.com")
            .build();

    private final List<String> traceSamples;

    public ReplayTask(List<String> traceSamples) {
        this.traceSamples = traceSamples;
    }

    @Override
    public void run() {
        for (String path : traceSamples) {
            client.get().uri(path)
                    .header("X-Load-Test", "1")
                    .retrieve()
                    .toBodilessEntity()
                    .doOnSuccess(resp -> Metrics.recordSuccess(path))
                    .doOnError(ex -> Metrics.recordError(path, ex))
                    .block(Duration.ofSeconds(3));
            // 保留样本中的间隔,而不是所有线程无限猛打
            sleepLikeRealUser(path);
        }
    }

    private void sleepLikeRealUser(String path) {
        long pauseMs = path.contains("detail") ? 800 : 120;
        try { Thread.sleep(pauseMs); } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

容量评估至少分三轮:基线轮验证功能正确性和隔离性;阶梯轮每 5 到 10 分钟增加 20% 流量,观察 P95、P99、错误率、CPU、连接池、慢 SQL、MQ 延迟;极限轮验证限流、熔断、降级和扩容策略。容量水位不要只按 CPU 80% 估算,还要给出数据库连接池、磁盘 IO、网络带宽、下游依赖和消息堆积的安全阈值。

避坑总结:影子库和回放最容易翻车的位置 ​

  1. 异步线程丢标识,压测数据写入生产库。 现象是压测订单在影子库能查到,但库存流水或通知记录出现在生产表。根因通常是 @Async、线程池、CompletableFuture 没有传递 ThreadLocal。修正方式是使用可传播上下文的线程池包装器,或在提交任务时显式复制压测标识,并在任务结束时清理。

  2. 影子表结构与生产不一致,压测通过但上线失败。 常见表现是压测 SQL 走了旧索引,或生产新增字段后影子库没有同步。应把影子库结构校验接入发布流程,压测前自动比对表、字段、索引、触发器和数据库参数,不一致直接阻断压测。

  3. 只隔离数据库,遗漏缓存、MQ 和第三方调用。 例如压测请求把生产商品缓存删掉,导致真实用户请求回源;压测消息被老消费者消费,触发真实短信或支付回调。必须统一封装 Redis Key、MQ 客户端和外部网关,默认拒绝未识别压测流量访问第三方。

  4. 回放数据过于理想,容量评估偏大。 所有请求使用同一个用户、同一批 SKU、零等待时间,会制造虚假热点,也无法模拟登录、购物车、凑单和支付成功率。应从真实访问日志提取接口比例与参数分布,脱敏后按时间节奏回放,并单独构造热点商品和突发流量场景。

  5. 没有设置熔断与停止条件。 压测不是把服务打挂为止。应提前约定错误率超过 1%、P99 超过预算、生产业务核心指标异常时自动断流;压测账号、影子库权限和回调地址也要二次确认。

全链路压测的价值不在于得到一个漂亮的 QPS 数字,而在于用可重复的实验回答容量风险在哪里、瓶颈先出现在哪个资源、预案是否真的生效。影子库和流量回放只是手段,严格的标识透传、资源隔离、数据校验和停止机制,才是大促容量评估可靠落地的基础。

免责声明

本工具与文章内容仅用于学习测试场景,优化建议仅供参考,请勿直接用于生产环境,由此产生的风险自行承担。