【架构实战】全链路压测:上线前的最后一道关卡

一、没压测上线,10分钟就挂了

2019年618,我们预估QPS 5万。

信心满满,没有做全链路压测,只是单接口压了一下,觉得没问题。

结果618当天:

指标 预估 实际 状态
QPS 5万 8万 超预期60%
系统状态 正常 雪崩 10分钟挂掉
故障时长 0 3小时 全站不可用

问题出在哪?

  • 数据库连接池耗尽(预估200,实际需要500+)
  • Redis超时(预估QPS 10万,实际20万)
  • 服务雪崩(某个服务挂了,级联影响)
  • 没有熔断限流(流量全量打入)

直接损失:

  • 订单损失:预估500万
  • 用户流失:无法统计
  • 品牌受损:客户投诉

从那以后,大促前必须全链路压测,用压测数据指导容量规划。


二、全链路压测流程

2.1 完整流程图

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                    全链路压测完整流程                              │
│                                                                  │
│  阶段1:压测方案设计(1周)                                       │
│  ├── 确定压测场景(核心业务链路)                                 │
│  ├── 确定目标QPS(根据历史数据+增长预估)                         │
│  ├── 设计压测用例(模拟真实用户行为)                             │
│  ├── 准备压测数据(脱敏、影子库)                                 │
│  └── 制定应急预案                                                │
│                                                                  │
│  阶段2:环境准备(1周)                                           │
│  ├── 环境检查(和生产配置一致)                                   │
│  ├── 影子库/影子表准备                                           │
│  ├── 压测标记机制                                                │
│  ├── 监控大盘配置                                                │
│  └── 告警规则配置                                                │
│                                                                  │
│  阶段3:执行压测(1-3天)                                         │
│  ├── 预热阶段(10%流量)                                         │
│  ├── 阶梯加压(20% → 50% → 80% → 100% → 120%)                  │
│  ├── 持续压测(稳定运行30分钟)                                   │
│  ├── 破坏性测试(找到极限)                                       │
│  └── 实时观察监控                                                │
│                                                                  │
│  阶段4:分析报告(2-3天)                                         │
│  ├── 系统容量上限                                                │
│  ├── 瓶颈分析                                                    │
│  ├── 性能基线                                                    │
│  └── 优化建议                                                    │
│                                                                  │
│  阶段5:优化验证(1-2周)                                         │
│  ├── 修复瓶颈                                                    │
│  ├── 扩容调整                                                    │
│  └── 再次压测验证                                                │
│                                                                  │
└──────────────────────────────────────────────────────────────────┘

2.2 压测场景设计

复制代码
核心业务链路压测场景:

场景1:用户下单链路
- 浏览商品详情 → 加入购物车 → 提交订单 → 支付
- 占比:40%
- 目标QPS:2万

场景2:商品搜索链路
- 搜索关键词 → 筛选 → 排序 → 详情
- 占比:30%
- 目标QPS:1.5万

场景3:用户中心链路
- 登录 → 个人中心 → 订单列表
- 占比:20%
- 目标QPS:1万

场景4:其他链路
- 首页、活动页、优惠券等
- 占比:10%
- 目标QPS:0.5万

总计目标QPS:5万

2.3 压测数据准备

markdown 复制代码
# 压测数据准备清单

## 用户数据
- 压测账号:10万个(脱敏处理)
- 账号余额:模拟真实分布
- 收货地址:每个账号2-3个

## 商品数据
- 商品数量:10万个
- 库存:模拟真实库存(影子库)
- 价格:模拟真实价格分布

## 订单数据
- 历史订单:用于查询
- 影子订单:压测产生的订单单独存储

## 数据隔离
- 影子库:pressure_test_db
- 影子表:order_shadow、product_shadow
- 标记字段:is_pressure_test = 1

三、压测流量隔离

3.1 流量标记机制

java 复制代码
/**
 * 压测流量标记
 */
public class PressureTestContext {
    
    private static final ThreadLocal<Boolean> FLAG = new ThreadLocal<>();
    
    public static void set(boolean isPressureTest) {
        FLAG.set(isPressureTest);
    }
    
    public static boolean isPressureTest() {
        return Boolean.TRUE.equals(FLAG.get());
    }
    
    public static void clear() {
        FLAG.remove();
    }
}

/**
 * 压测流量过滤器
 */
@Component
public class PressureTestFilter implements Filter {
    
    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
                          FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        
        // 从Header识别压测流量
        String pressureTestFlag = httpRequest.getHeader("X-Pressure-Test");
        
        if ("true".equals(pressureTestFlag)) {
            // 标记压测流量
            PressureTestContext.set(true);
            
            // 设置压测用户ID
            String userId = httpRequest.getHeader("X-Pressure-User-Id");
            PressureTestContext.setUserId(userId);
            
            // 路由到影子库
            DataSourceContextHolder.set("shadow");
            
            // 日志标记
            MDC.put("pressure_test", "true");
        }
        
        try {
            chain.doFilter(request, response);
        } finally {
            PressureTestContext.clear();
            DataSourceContextHolder.clear();
            MDC.remove("pressure_test");
        }
    }
}

3.2 影子库路由

java 复制代码
/**
 * 动态数据源路由
 */
public class DynamicDataSource extends AbstractRoutingDataSource {
    
    @Override
    protected Object determineCurrentLookupKey() {
        // 压测流量路由到影子库
        if (PressureTestContext.isPressureTest()) {
            return "shadow";
        }
        return "master";
    }
}

/**
 * MyBatis拦截器 - 影子表
 */
@Intercepts({
    @Signature(type = Executor.class, method = "update", 
               args = {MappedStatement.class, Object.class})
})
public class ShadowTableInterceptor implements Interceptor {
    
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        if (PressureTestContext.isPressureTest()) {
            MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
            // 动态替换表名为影子表
            // order → order_shadow
            String sql = ms.getBoundSql(invocation.getArgs()[1]).getSql();
            sql = sql.replaceAll("order", "order_shadow");
            // ... 修改SQL
        }
        return invocation.proceed();
    }
}

3.3 Redis隔离

java 复制代码
/**
 * Redis压测key前缀
 */
@Component
public class PressureTestRedisTemplate {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    private static final String PRESSURE_PREFIX = "pt:";
    
    public void set(String key, Object value) {
        String realKey = PressureTestContext.isPressureTest() 
            ? PRESSURE_PREFIX + key 
            : key;
        redisTemplate.opsForValue().set(realKey, value);
    }
    
    public Object get(String key) {
        String realKey = PressureTestContext.isPressureTest() 
            ? PRESSURE_PREFIX + key 
            : key;
        return redisTemplate.opsForValue().get(realKey);
    }
}

四、监控大盘

4.1 关键监控指标

复制代码
压测监控大盘:

系统层:
- CPU使用率
- 内存使用率
- 磁盘IO
- 网络IO

应用层:
- QPS(每秒请求数)
- RT(响应时间):P50/P99/Max
- 错误率
- 并发数

中间件层:
- MySQL:连接数、QPS、慢查询
- Redis:QPS、内存、命中率
- MQ:消息堆积、消费延迟

业务层:
- 下单成功率
- 支付成功率
- 订单量

4.2 压测监控截图示例

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                    压测监控大盘                                   │
│                                                                  │
│  QPS: 45000/50000 ████████████████████░░░░ 90%                  │
│  RT-P99: 180ms    ████████████████████████ 正常 (<200ms)         │
│  错误率: 0.5%     ████████████████████████ 正常 (<1%)            │
│  CPU: 75%         ████████████████████░░░░ 正常                  │
│  内存: 80%        ████████████████████░░░░ 正常                  │
│                                                                  │
│  ┌────────────────────────────────────────────────────────────┐ │
│  │                   实时QPS曲线                               │ │
│  │  5万 ┤                                                      │ │
│  │  4万 ┤        ╭─────╮                                       │ │
│  │  3万 ┤   ╭────╯     ╰────╮                                  │ │
│  │  2万 ┤───╯                ╰────                              │ │
│  │  1万 ┤                                                      │ │
│  │      └─────────────────────────────────────────────────     │ │
│  │           10:00  10:05  10:10  10:15  10:20                 │ │
│  └────────────────────────────────────────────────────────────┘ │
│                                                                  │
└──────────────────────────────────────────────────────────────────┘

五、踩坑实录

坑1:压测数据污染生产

问题:压测数据写入了生产数据库,导致数据混乱。

踩坑场景

压测产生的订单数据混入了生产订单,运营同事以为是真实订单,导致发货错误。

解决方案

markdown 复制代码
# 数据隔离方案:

1. 影子库/影子表
   - 压测流量路由到影子库
   - 生产流量走主库
   - 物理隔离,绝对不会混

2. 标记字段
   - 压测数据添加 is_pressure_test = 1 标记
   - 业务逻辑过滤掉压测数据

3. 压测后清理
   - 压测结束清理影子库数据
   - Redis清理压测key(pt:*)

坑2:压测影响真实用户

问题:压测占用了大量资源,真实用户体验下降。

踩坑场景

我们在白天高峰期压测,压测流量和真实流量混在一起,导致真实用户响应变慢。

解决方案

markdown 复制代码
# 压测时间选择:

1. 低峰期压测
   - 凌晨2-6点用户少
   - 影响最小

2. 独立压测环境
   - 完全隔离的压测环境
   - 不影响生产

3. 流量控制
   - 压测流量打标
   - 服务端优先处理真实流量

坑3:只压了接口没压全链路

问题:单个接口没问题,但完整链路有瓶颈。

踩坑场景

我们压测了下单接口,QPS能到1万。但实际用户路径是"浏览→加购→下单",串联起来QPS只有3000。

解决方案

markdown 复制代码
# 全链路压测原则:

1. 模拟真实用户路径
   - 浏览 → 搜索 → 加购 → 下单 → 支付
   - 按真实比例分配

2. 关联接口一起压
   - 下单接口依赖库存、优惠、支付
   - 这些接口也要压测

3. 数据预热
   - 真实场景有缓存
   - 冷启动和热启动性能差异大

坑4:没有监控

问题:压测时没看监控,不知道瓶颈在哪。

踩坑场景

压测发现性能不行,但不知道瓶颈在哪。CPU、内存、数据库都正常,最后发现是第三方接口慢。

解决方案

markdown 复制代码
# 监控先行:

1. 压测前配置监控
   - 应用监控:QPS、RT、错误率
   - 系统监控:CPU、内存、IO
   - 中间件监控:MySQL、Redis、MQ
   - 链路追踪:SkyWalking

2. 实时观察
   - 多屏监控
   - 有人专门盯监控
   - 发现问题立即停止

3. 记录数据
   - 每分钟记录一次指标
   - 用于后续分析

坑5:压测结果没落地

问题:压测完出了报告,但没有优化,大促时照样挂。

踩坑场景

压测发现数据库连接池不够,需要从200增加到500。但领导觉得"改动太大有风险",没有调整。结果大促时数据库连接池耗尽,服务雪崩。

解决方案

markdown 复制代码
# 压测-优化-验证闭环:

1. 压测报告必须落地
   - 列出所有问题
   - 分配责任人和截止日期
   - 定期跟进

2. 优化后必须验证
   - 优化后再次压测
   - 确认问题解决

3. 风险评估
   - 不优化的风险 > 优化的风险
   - 用数据说话

六、最佳实践

6.1 压测清单

复制代码
压测前检查清单:

□ 环境准备
  □ 环境和生产配置一致
  □ 数据量级和生产一致
  □ 影子库/影子表准备好

□ 压测方案
  □ 核心链路确定
  □ 目标QPS确定
  □ 压测脚本准备好
  □ 压测数据准备好

□ 监控告警
  □ 监控大盘配置好
  □ 告警规则配置好
  □ 有人盯监控

□ 应急预案
  □ 随时可以停止
  □ 回滚方案准备好
  □ 值班人员就位

6.2 压测报告模板

markdown 复制代码
# 全链路压测报告

## 1. 压测概况
- 压测时间:2024-06-15 02:00-06:00
- 压测环境:预发布环境
- 目标QPS:50,000
- 实际QPS:55,000(达标)

## 2. 系统容量
| 系统 | 预估容量 | 实测容量 | 结论 |
|------|---------|---------|------|
| 订单服务 | 5万QPS | 5.5万QPS | 达标 |
| 支付服务 | 3万QPS | 2.8万QPS | 未达标 |
| 库存服务 | 5万QPS | 5万QPS | 达标 |

## 3. 瓶颈分析
- 支付服务:数据库连接池不足,需要从200增加到300
- Redis:单节点QPS达到极限,需要扩容

## 4. 优化建议
- [ ] 支付服务数据库连接池增加50%
- [ ] Redis扩容到3节点
- [ ] 下单接口添加本地缓存

## 5. 结论
系统整体满足618需求,支付服务需要优化后再次压测验证。

七、血的教训

不压测就上线等于裸奔。压测不是浪费时间,是节省事故处理时间。

那次618事故:

  • 直接损失500万+
  • 用户信任受损
  • 团队加班处理事故
  • 领导质疑团队能力

如果提前压测,这些问题都能提前发现。


八、思考题

  1. 你的系统有做全链路压测吗?
  2. 你遇到过压测和实际情况不符的情况吗?
  3. 你有什么压测经验可以分享?

个人观点,仅供参考