【架构实战】全链路压测:双11前的系统容量摸底
📅 2026/7/7 0:12:59
👁️ 阅读次数
📝 编程学习
【架构实战】全链路压测:双11前的系统容量摸底
一、背景:压测不做,大促必跪
2020年双11,我们团队经历了一次永生难忘的教训。
大促前做了压测,但只压了单服务——订单服务撑住了5万QPS,信心满满。结果双11当天流量一来,秒挂。
排查发现:
- 库存服务的连接池只有200,瞬间打满
- Redis集群的热Key把单节点CPU打到100%
- 消息队列积压了200万条消息
- 数据库的慢查询拖死了整个集群
问题根因:单服务压测不等于全链路压测。订单服务能扛5万QPS,但下游服务扛不住,全链路最短板决定系统容量。
“压测不是压单服务,是压整条链路的短板。”
二、全链路压测 vs 单服务压测
2.1 核心区别
| 维度 | 单服务压测 | 全链路压测 |
|---|---|---|
| 压测范围 | 单个服务接口 | 完整业务链路(入口到DB) |
| 流量特征 | 固定参数请求 | 真实流量回放/模拟 |
| 数据依赖 | Mock数据 | 真实数据(脱敏) |
| 瓶颈定位 | 只能发现本服务瓶颈 | 发现全链路最短板 |
| 压测目标 | 服务QPS上限 | 系统容量上限 |
| 复杂度 | 低 | 高 |
2.2 全链路压测架构
┌─────────────────────────────────────────────────────────────┐ │ 全链路压测架构 │ │ │ │ ┌──────────┐ │ │ │压测控制台 │ ← 调度压测任务、监控压测指标 │ │ └────┬─────┘ │ │ │ │ │ ┌────▼─────┐ ┌──────────┐ ┌──────────┐ │ │ │施压集群 │ │流量录制 │ │数据工厂 │ │ │ │(JMeter/ │ │(GoReplay)│ │(脱敏数据) │ │ │ │Locust) │ │ │ │ │ │ │ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ │ │ │ │ │ │ ┌────▼─────────────▼─────────────▼─────┐ │ │ │ 压测流量(带压测标识) │ │ │ └────┬─────────────┬─────────────┬─────┘ │ │ │ │ │ │ │ ┌────▼────┐ ┌────▼────┐ ┌────▼────┐ │ │ │ API网关 │──>│ 订单服务 │─>│ 库存服务 │ │ │ │(限流) │ │ │ │ │ │ │ └─────────┘ └────┬────┘ └────┬────┘ │ │ │ │ │ │ ┌──────▼────┐ ┌─────▼─────┐ │ │ │ Redis │ │ MySQL │ │ │ │ (压测隔离)│ │ (影子表) │ │ │ └──────────┘ └───────────┘ │ └─────────────────────────────────────────────────────────────┘三、全链路压测关键技术
3.1 流量染色与隔离
核心问题:压测流量和真实流量不能混在一起,否则会污染生产数据(如压测订单进入统计报表)。
解决方案:流量染色
// 压测流量拦截器:在Header中打入压测标识@ComponentpublicclassPressureTestInterceptorimplementsHandlerInterceptor{privatestaticfinalStringPT_HEADER="X-Pressure-Test";@OverridepublicbooleanpreHandle(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler){StringptFlag=request.getHeader(PT_HEADER);if("true".equals(ptFlag)){PressureTestContext.set(true);// 写入ThreadLocal}returntrue;}}// 数据隔离:压测流量写入影子表@Aspect@ComponentpublicclassDataIsolationAspect{@Around("@annotation(Transactional)")publicObjectisolateData(ProceedingJoinPointpjp)throwsThrowable{if(PressureTestContext.isPressureTest()){// 切换数据源到影子表DynamicDataSource.set("shadow_db");}try{returnpjp.proceed();}finally{DynamicDataSource.clear();}}}隔离策略:
| 资源类型 | 隔离方式 | 实现细节 |
|---|---|---|
| 数据库 | 影子表 | 原表orders→ 影子表orders_pt |
| Redis | Key前缀 | user:123→pt:user:123 |
| 消息队列 | 影子Topic | order_topic→order_topic_pt |
| 日志 | 不落盘 | 压测日志直接丢弃,不写入ES |
| 第三方 | Mock | 调用银行/短信等外部接口时Mock返回 |
3.2 流量录制与回放
方案一:GoReplay 流量录制
# 录制线上流量gor --input-raw :8080\--output-file requests.gor\--output-file-append\--http-allow-method GET\--http-allow-method POST\--http-allow-url /api/order# 回放流量到压测环境(2倍速率)gor --input-file"requests.gor|200%"\--output-http"http://pt-env:8080"\--http-set-header"X-Pressure-Test: true"方案二:自定义流量录制
// AOP切面按比例录制线上流量@Aspect@ComponentpublicclassTrafficRecorder{@Value("${traffic.record.rate:0.01}")// 录制1%线上流量privatedoublerecordRate;@Around("@annotation(RecordTraffic)")publicObjectrecord(ProceedingJoinPointpjp)throwsThrowable{Longstart=System.currentTimeMillis();Objectresult=pjp.proceed();// 按比例录制if(ThreadLocalRandom.current().nextDouble()<recordRate){TrafficRecordrecord=TrafficRecord.builder().request(pjp.getArgs()).response(result).duration(System.currentTimeMillis()-start).timestamp(start).build();recordQueue.offer(record);// 异步写入}returnresult;}}3.3 施压引擎
# Locust 压测脚本fromlocustimportHttpUser,task,betweenimportrandomclassOrderFlowUser(HttpUser):wait_time=between(0.1,0.5)# 模拟用户思考时间defon_start(self):"""用户登录"""self.client.post("/api/login",json={"username":"test_user","password":"test123"})@task(3)# 权重3defbrowse_products(self):"""浏览商品"""product_id=random.randint(1,10000)self.client.get(f"/api/product/{product_id}",headers={"X-Pressure-Test":"true"})@task(2)# 权重2defadd_to_cart(self):"""加入购物车"""self.client.post("/api/cart/add",json={"productId":random.randint(1,10000),"quantity":random.randint(1,3)},headers={"X-Pressure-Test":"true"})@task(1)# 权重1defcreate_order(self):"""下单"""self.client.post("/api/order/create",json={"productId":random.randint(1,10000),"quantity":1,"addressId":1},headers={"X-Pressure-Test":"true"})四、压测指标与瓶颈分析
4.1 核心压测指标
┌──────────────────────────────────────────────────────────┐ │ 全链路压测指标体系 │ ├──────────────┬──────────────┬──────────────┬─────────────┤ │ 层级 │ 指标 │ 健康值 │ 告警阈值 │ ├──────────────┼──────────────┼──────────────┼─────────────┤ │ 业务层 │ 下单成功率 │ > 99.9% │ < 99% │ │ │ 支付成功率 │ > 99.99% │ < 99.9% │ ├──────────────┼──────────────┼──────────────┼─────────────┤ │ 应用层 │ P99延迟 │ < 200ms │ > 500ms │ │ │ 错误率 │ < 0.1% │ > 1% │ │ │ QPS │ 目标值 │ 低于80% │ ├──────────────┼──────────────┼──────────────┼─────────────┤ │ 中间件层 │ Redis延迟 │ < 1ms │ > 5ms │ │ │ MQ积压 │ < 1000 │ > 10000 │ │ │ DB慢查询 │ 0 │ > 10/min │ ├──────────────┼──────────────┼──────────────┼─────────────┤ │ 基础设施层 │ CPU │ < 70% │ > 85% │ │ │ 内存 │ < 80% │ > 90% │ │ │ 网络带宽 │ < 60% │ > 80% │ │ │ 磁盘IO │ < 70% │ > 85% │ └──────────────┴──────────────┴──────────────┴─────────────┘4.2 瓶颈定位方法论
压测发现问题后的排查顺序: 1. 基础设施层(最快排查) ├── top: CPU是否打满? ├── free -m: 内存是否耗尽? ├── iostat: 磁盘IO是否打满? └── sar -n DEV: 网络带宽是否瓶颈? 2. 中间件层 ├── Redis: slowlog get 100 → 是否有慢查询? ├── MySQL: show full processlist → 是否有锁等待? ├── MQ: 消息积压量 → 消费者是否跟不上? └── 连接池: 连接数是否耗尽? 3. 应用层 ├── arthas: thread -n 3 → CPU最高的线程在做什么? ├── arthas: trace → 哪个方法耗时最长? ├── GC日志: 是否频繁Full GC? └── 线程池: 线程是否全部阻塞? 4. 业务层 ├── 慢接口 → 是否有未优化的SQL? ├── 热Key → Redis单节点是否成为瓶颈? └── 串行调用 → 是否可以改并行?4.3 Arthas 线上诊断
# 排查CPU使用率最高的线程thread-n3# 输出示例:# "http-nio-8080-exec-45" Id=128 cpu=24% RUNNABLE# at OrderService.calculateDiscount(OrderService.java:156)# → 发现是优惠券计算逻辑耗CPU# 追踪方法调用耗时trace com.example.OrderService createOrder-n5# 输出示例:# `---[95.23ms] com.example.OrderService:createOrder()# +---[0.05ms] validateOrder()# +---[0.10ms] checkInventory()# +---[93.80ms] calculateDiscount() ← 瓶颈在这里# +---[0.15ms] saveOrder()# `---[0.08ms] sendNotification()# 监控JVM内存和GCdashboard五、压测实战流程
5.1 压测前准备清单
□ 确认压测环境与生产环境配置一致 □ 数据隔离方案已测试通过(影子表、影子Key) □ 压测标识从网关一路传递到DB □ 告警已临时静默(避免压测告警打扰值班人员) □ 压测期间禁止任何其他发布操作 □ 准备紧急停止按钮(一键终止压测) □ 监控大盘已配置好(Grafana Dashboard) □ 下游依赖已Mock或确认可以承受压测5.2 压测执行流程
阶段一:基线压测(10%目标QPS) → 确认压测基础设施正常 → 确认流量染色和数据隔离有效 → 确认监控数据正确采集 阶段二:梯度加压 10% → 30% → 50% → 70% → 100% → 120% 每梯度持续5分钟,观察指标是否线性增长 阶段三:摸高测试 继续加压直到找到第一处瓶颈 记录瓶颈点:QPS、错误率、资源使用率 阶段四:稳定性压测 在80%极限QPS下持续运行30分钟 观察是否有内存泄漏、GC恶化、连接池泄漏 阶段五:容量报告 汇总各服务的QPS上限 给出扩容建议和限流阈值5.3 压测报告模板
=== 全链路压测报告 === 压测日期:2026-11-01 压测目标:双11大促容量摸底 【压测结果汇总】 ┌──────────┬──────────┬──────────┬──────────┬──────────┐ │ 服务 │ 目标QPS │ 极限QPS │ 达标 │ 建议扩容 │ ├──────────┼──────────┼──────────┼──────────┼──────────┤ │ 订单服务 │ 50,000 │ 62,000 │ ✓ │ - │ │ 库存服务 │ 50,000 │ 38,000 │ ✗ │ +50% │ │ 支付服务 │ 30,000 │ 45,000 │ ✓ │ - │ │ 优惠券服务│ 20,000 │ 22,000 │ ✓ │ - │ │ 网关 │ 100,000 │ 85,000 │ ✗ │ +30% │ └──────────┴──────────┴──────────┴──────────┴──────────┘ 【瓶颈分析】 1. 库存服务:Redis热Key(sku:12345)单节点QPS 8万上限 建议:本地缓存 + Redis集群分片 2. API网关:Nginx worker_connections 不够 建议:增加worker_connections到65535 【扩容方案】 库存服务:4C8G × 20 → 4C8G × 30 网关:2C4G × 5 → 2C4G × 8六、总结
全链路压测的三个核心原则:
- 压测即生产:压测环境和生产环境配置必须一致,否则压测结果毫无意义
- 流量要染色:压测流量必须全程携带标识,数据必须隔离,不能污染真实数据
- 找最短板:全链路压测的目的不是证明系统多能扛,而是找到最薄弱的一环
关键经验:
- 单服务压测是自欺欺人,全链路压测才能体现真实容量
- 压测不是一次性活动,每次大版本发布前都要压
- 压力要逐渐加,不要一下子上去,否则没法定位是哪层的瓶颈
- 压测数据隔离方案要提前测试,别等压测当天才发现影子表没建
- 做好紧急停止预案,压测失控时能一键停止
双11不压测,就像闭着眼睛上高速。技术自信源于真实的压测数据,而不是直觉。
个人观点,仅供参考
编程学习
技术分享
实战经验