一、没压测上线,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万+
- 用户信任受损
- 团队加班处理事故
- 领导质疑团队能力
如果提前压测,这些问题都能提前发现。
八、思考题
- 你的系统有做全链路压测吗?
- 你遇到过压测和实际情况不符的情况吗?
- 你有什么压测经验可以分享?
个人观点,仅供参考