【架构实战】全链路压测:双11前的系统容量摸底
【架构实战】全链路压测双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_HEADERX-Pressure-Test;OverridepublicbooleanpreHandle(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler){StringptFlagrequest.getHeader(PT_HEADER);if(true.equals(ptFlag)){PressureTestContext.set(true);// 写入ThreadLocal}returntrue;}}// 数据隔离压测流量写入影子表AspectComponentpublicclassDataIsolationAspect{Around(annotation(Transactional))publicObjectisolateData(ProceedingJoinPointpjp)throwsThrowable{if(PressureTestContext.isPressureTest()){// 切换数据源到影子表DynamicDataSource.set(shadow_db);}try{returnpjp.proceed();}finally{DynamicDataSource.clear();}}}隔离策略资源类型隔离方式实现细节数据库影子表原表orders→ 影子表orders_ptRedisKey前缀user:123→pt:user:123消息队列影子Topicorder_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-filerequests.gor|200%\--output-httphttp://pt-env:8080\--http-set-headerX-Pressure-Test: true方案二自定义流量录制// AOP切面按比例录制线上流量AspectComponentpublicclassTrafficRecorder{Value(${traffic.record.rate:0.01})// 录制1%线上流量privatedoublerecordRate;Around(annotation(RecordTraffic))publicObjectrecord(ProceedingJoinPointpjp)throwsThrowable{LongstartSystem.currentTimeMillis();Objectresultpjp.proceed();// 按比例录制if(ThreadLocalRandom.current().nextDouble()recordRate){TrafficRecordrecordTrafficRecord.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_timebetween(0.1,0.5)# 模拟用户思考时间defon_start(self):用户登录self.client.post(/api/login,json{username:test_user,password:test123})task(3)# 权重3defbrowse_products(self):浏览商品product_idrandom.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 Id128 cpu24% 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热Keysku:12345单节点QPS 8万上限 建议本地缓存 Redis集群分片 2. API网关Nginx worker_connections 不够 建议增加worker_connections到65535 【扩容方案】 库存服务4C8G × 20 → 4C8G × 30 网关2C4G × 5 → 2C4G × 8六、总结全链路压测的三个核心原则压测即生产压测环境和生产环境配置必须一致否则压测结果毫无意义流量要染色压测流量必须全程携带标识数据必须隔离不能污染真实数据找最短板全链路压测的目的不是证明系统多能扛而是找到最薄弱的一环关键经验单服务压测是自欺欺人全链路压测才能体现真实容量压测不是一次性活动每次大版本发布前都要压压力要逐渐加不要一下子上去否则没法定位是哪层的瓶颈压测数据隔离方案要提前测试别等压测当天才发现影子表没建做好紧急停止预案压测失控时能一键停止双11不压测就像闭着眼睛上高速。技术自信源于真实的压测数据而不是直觉。个人观点仅供参考
