【K8S 运维实战】36-电商大促容量保障
案例:电商大促容量保障全过程一句话定位:从容量评估到事后复盘,还原一次 2000 节点集群扛住 5 倍流量的全过程,把大促保障从玄学变成可复用的工程动作。写在前面做过电商大促运维的人都知道,大促当晚最怕的不是流量来了,而是流量来了你不知道瓶颈在哪。2024 年双 11 是我接手这家公司交易链路 SRE 的第三个年头,前两次大促我们都扛过去了,但扛得很狼狈——2022 年双 11 零点抢购,支付网关 CPU 飙到 95%,靠紧急扩容 200 个 Pod 才稳住;2023 年因为缓存集群打满,商品详情页大面积超时,事后排查发现是某个秒杀活动的热点 Key 没做本地缓存。今年老板拍了目标:GMV 冲 50 亿,峰值 QPS 预估是日常的 5 倍,核心交易链路 30 微服务,生产集群 2000 节点,容不得半点闪失。我们提前 6 周启动容量保障专项,从压测、评估、预案到当晚值班,做了一整套体系化动作。这篇文章就是这次专项的完整复盘,不是理论文章,是踩过坑、改过方案、最后真扛住了的实战记录。大促容量保障的本质不是扩容,而是在不确定的流量下,用确定性的工程动作把风险收敛到可控范围。下面我把整个流程拆开讲,希望能给同样在做大促保障的同学一个可参考的框架。案例概览维度内容场景2024 年双 11 全球狂欢节,核心交易链路容量保障集群规模生产 K8s 集群 2000 节点(32C128G * 1500 64C256G * 500)业务规模30 核心微服务,日常峰值 QPS 8 万,大促预估 40 万时间跨度专项启动 9 月 25 日 → 大促 11 月 11 日 → 复盘 11 月 18 日团队投入SRE 8 人 业务研发 20 人 DBA 3 人 中间件 4 人关键产出容量评估模型、全链路压测方案、弹性预案、值班 SOP最终结果大促当晚峰值 QPS 38.7 万,核心链路 SLA 99.99%,无 P0 故障时间线:09-2910-0610-1310-2010-2711-0311-1011-17容量基线摸底全链路压测瓶颈定位与评估弹性预案落地限流降级开关演练值班手册定稿红蓝对抗演练预案回滚验证预热扩容(11/10)零点抢购(11/11)数据汇总复盘会议评估阶段准备阶段演练阶段大促复盘双11容量保障专项时间线一、背景与挑战1.1 业务背景公司是综合电商平台,核心交易链路包含:用户中心、商品中心、购物车、订单、支付、库存、营销、优惠券、风控、消息中心等 30 微服务,全部跑在 K8s 上。日常峰值 QPS 约 8 万,大促预估 5 倍即 40 万 QPS。架构上采用接入层(Nginx Ingress) 服务层(Spring Cloud) 数据层(MySQL/TiDB/Redis/Kafka)的三层结构。1.2 历史问题前两次大促暴露的核心问题:容量评估靠拍脑袋:2022 年凭经验给每个服务扩容 3 倍,结果支付服务扩容不足、商品服务扩容过剩,资源浪费 30%。压测覆盖不全:只压了单接口,没做全链路压测,链路上游扩容了下游没扩,瓶颈转移。弹性能力不足:HPA 响应慢,抢购瞬间流量 30 秒涨 10 倍,HPA 还没扩出来服务就挂了。预案不可执行:值班手册写在 wiki 里,真出事的时候翻不到,2023 年缓存故障花了 8 分钟才找到降级开关。1.3 本次挑战流量预估 5 倍,但实际可能 4-6 倍波动,需要弹性能力兜底30 服务链路复杂,瓶颈点难以预判大促当晚 0 点抢购是生死时刻,任何 P0 故障都不能超过 5 分钟成本控制:老板要求大促资源成本不超过日常的 2 倍二、方案设计2.1 整体架构容量保障不是单点动作,而是一个闭环体系。我们设计了评估-压测-预案-演练-保障-复盘六步闭环:沉淀经验容量评估全链路压测瓶颈定位弹性预案限流降级红蓝演练大促保障事后复盘2.2 弹性预案分层架构针对抢购场景瞬间峰值、持续时间短的特点,我们设计了三层弹性防御:┌─────────────────────────────────────────────────────────┐ │ 第一层:预热池(提前扩好,流量来了直接接) │ │ - 核心服务预热 200% 副本数 │ │ - 11/10 22:00 前完成预热 │ ├─────────────────────────────────────────────────────────┤ │ 第二层:HPA KEDA(基于自定义指标弹性) │ │ - HPA 基于 CPU/内存 │ │ - KEDA 基于消息队列堆积、QPS │ │ - 响应时间 60 秒 │ ├─────────────────────────────────────────────────────────┤ │ 第三层:Cluster Autoscaler(节点池扩容) │ │ - 预留 300 节点 Buffer │ │ - 节点启动 3 分钟(预热镜像) │ └─────────────────────────────────────────────────────────┘2.3 容量评估模型这是本次专项最核心的产出之一。我们把容量评估从经验驱动升级为数据驱动,建立了一个量化模型:指标计算公式数据来源单 Pod 承载 QPS压测得出(95 分位 RT 200ms 时的 QPS)全链路压测所需 Pod 数目标 QPS / 单 Pod QPS × 安全系数(1.3)评估所需节点数Σ(各服务所需 Pod 数 × Pod 资源) / 节点可用资源评估资源利用率阈值CPU 70%,内存 80%监控基线弹性 Buffer峰值 Pod 数 × 20%预留以支付服务为例:压测得出单 Pod(2C4G)在 95 分位 RT 200ms 时承载 1200 QPS,大促目标 QPS 8 万(支付峰值占总 QPS 的 20%),安全系数 1.3,则所需 Pod 数 80000 / 1200 × 1.3 ≈ 87 个,实际按 100 个准备。三、实施过程3.1 第一阶段:容量基线摸底(9/25 - 10/2)第一步是搞清楚现状。我们用 Prometheus 导出了过去 90 天所有核心服务的资源使用率和 QPS 数据,建立容量基线:# 导出各服务最近 90 天的 P95 CPU/内存使用率# 用 promtool 查询并导出 CSVcat/tmp/capacity_query.pyEOF import requests import csv import time PROM_URL http://prometheus:9090 SERVICES [user-svc, product-svc, cart-svc, order-svc, pay-svc, inventory-svc, coupon-svc, risk-svc] queries { cpu_p95: quantile_over_time(0.95, rate(container_cpu_usage_seconds_total{namespaceprod}[5m])[90d:1h]), mem_p95: quantile_over_time(0.95, container_memory_working_set_bytes{namespaceprod}[90d:1h]), qps_p95: quantile_over_time(0.95, sum(rate(nginx_ingress_controller_requests{namespaceprod}[5m])) by (service)[90d:1h]), } with open(capacity_baseline.csv, w, newline) as f: writer csv.writer(f) writer.writerow([service, cpu_p95_cores, mem_p95_gb, qps_p95]) for svc in SERVICES: row [svc] for key, query in queries.items(): resp requests.get(f{PROM_URL}/api/v1/query, params{query: query.replace(prod, fprod,service{svc})}) data resp.json() val float(data[data][result][0][value][1]) if data[data][result] else 0 row.append(round(val, 2)) writer.writerow(row) EOFpython3 /tmp/capacity_query.py摸底结果发现三个问题:商品服务 CPU 利用率日常就到 65%(接近红线)、支付服务内存使用率 78%(有泄漏嫌疑)、库存服务 QPS 日常波动大(1-3 万),这些都需要在压测中重点验证。3.2 第二阶段:全链路压测(10/3 - 10/12)全链路压测是大促保障的核心。我们用了自研压测平台 JMeter 影子库方案,核心原则是压测流量不打脏生产数据。3.2.1 压测架构┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ 压测发压机 │───▶│ 流量染色网关 │───▶│ 生产 K8s │ │ (JMeter集群) │ │ (打标 X-Pt) │ │ (影子链路) │ └──────────────┘ └──────────────┘ └──────┬───────┘ │ ┌───────────────────────┼───────────────┐ ▼ ▼ ▼ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ 影子 Redis│ │ 影子 MySQL│ │ 影子 Kafka│ │ (独立实例)│ │ (脱敏数据)│ │ (独立Topic)│ └──────────┘ └──────────┘ └──────────┘压测流量通过 HTTP HeaderX-Pt: 1染色,全链路透传,各中间件识别染色流量后路由到影子库。3.2.2 压测数据准备# 1. 生产数据脱敏导出到影子库mysqldump-hprod-mysql-ureadonly-p*** order_db\--wherecreate_time 2024-09-01\|seds/手机号正则/***替换***/g\|mysql-hshadow-mysql-uroot -p*** shadow_order_db# 2. 构造压测用户(100万影子用户)python3 gen_pressure_users.py--count1000000--prefixpt_user_# 3. 影子 Redis 预热(从生产同步热 Key)redis-cli-hprod-redis--rdbdump.rdb# 修改 rdb 后恢复到影子 Redisredis-cli-hshadow-redis-a*** FLUSHALLcatdump.rdb|redis-cli-hshadow-redis-xrestore shadow_key03.2.3 发压方案发压采用阶梯式加压,每个阶梯保持 10 分钟观察:# 压测计划配置pressure_plan:target_svc:pay-svcstages:-qps:20000# 日常峰值的 1 倍duration:10m-qps:40000# 2 倍duration:10m-qps:60000# 3 倍duration:10m-qps:80000# 4 倍duration:10m-qps:100000# 5 倍(目标)duration:30m# 持续 30 分钟验证稳定性sla:p95_rt:200mserror_rate:0.1%JMeter 发压脚本核心部分:# 启动分布式发压(5 台发压机)jmeter-n-tpay_svc_pressure.jmx\-R10.0.1.11,10.0.1.12,10.0.1.13,10.0.1.14,10.0.1.15\-X-lresult.jtl-e-oreport/3.2.4 压测结果(关键瓶颈)压测三轮,发现 4 个瓶颈:服务瓶颈现象根因处理pay-svcQPS 到 6 万时 P95 RT 从 80ms 飙到 800msDB 连接池打满(50→200)调连接池 加读写分离inventory-svcQPS 到 5 万时开始报错Redis 热点 Key 单分片打满本地缓存 Key 分片order-svcQPS 到 8 万时 OOMJVM 堆 4G 不够,GC 停顿 2s升到 8G G1GCproduct-svcQPS 到 10 万时 CPU 100%图片处理计算密集拆独立服务 CDN 卸载3.3 第三阶段:弹性预案落地(10/13 - 10/22)3.3.1 HPA 配置# pay-svc HPA - 基于 CPU 和内存apiVersion:autoscaling/v2kind:HorizontalPodAutoscalermetadata:name:pay-svc-hpanamespace:prodspec:scaleTargetRef:apiVersion:apps/v1kind:Deploymentname:pay-svcminReplicas:20# 日常副本数maxReplicas:150# 大促扩容上限behavior:scaleUp:stabilizationWindowSeconds:0# 扩容立即响应policies:-type:Percentvalue:100# 每次最多翻倍periodSeconds:30-type:Podsvalue:20periodSeconds:30selectPolicy:MaxscaleDown:stabilizationWindowSeconds:300# 缩容延迟 5 分钟policies:-type:Percentvalue:10periodSeconds:60metrics:-type:Resourceresource:name:cputarget:type:UtilizationaverageUtilization:60-type:Resourceresource:name:memorytarget:type:UtilizationaverageUtilization:703.3.2 KEDA 基于 QPS 弹性HPA 基于 CPU/内存有滞后性,我们用 KEDA 监控更前置的指标——QPS 和消息队列堆积:# KEDA ScaledObject - 基于自定义 QPS 指标apiVersion:keda.sh/v1alpha1kind:ScaledObjectmetadata:name:order-svc-kedanamespace:prodspec:scaleTargetRef:name:order-svcminReplicaCount:30maxReplicaCount:200pollingInterval:15cooldownPeriod:300triggers:-type:prometheusmetadata:serverAddress:http://prometheus:9090metricName:order_svc_qpsthreshold:1500# 单 Pod 承载 1500 QPSquery:|sum(rate(nginx_ingress_controller_requests {namespaceprod,serviceorder-svc,status!~5..}[1m]))-type:kafkametadata:bootstrapServers:kafka:9092consumerGroup:order-consumertopic:order-eventslagThreshold:1000# 堆积超 1000 触发扩容offsetResetPolicy:latest3.3.3 Cluster Autoscaler 节点预热节点扩容慢是抢购场景的致命伤。我们做了两件事:预留 Buffer 节点池:11/10 22:00 前预先扩出 300 个节点(平时空闲),CA 不回收。节点镜像预热:把核心服务镜像提前 pull 到所有节点,Pod 启动时间从 90 秒降到 15 秒。# DaemonSet 预热镜像apiVersion:apps/v1kind:DaemonSetmetadata:name:image-pullernamespace:kube-systemspec:selector:matchLabels:app:image-pullertemplate:metadata:labels:app:image-pullerspec:tolerations:-operator:ExistsinitContainers:-name:pull-payimage:registry.example.com/pay-svc:v2.3.0command:[true]-name:pull-orderimage:registry.example.com/order-svc:v3.1.0command:[true]-name:pull-inventoryimage:registry.example.com/inventory-svc:v2.0.0command:[true]containers:-name:pauseimage:registry.k8s.io/pause:3.9resources:requests:cpu:10mmemory:16Mi3.4 第四阶段:限流降级演练(10/23 - 10/27)弹性扩容是加法,限流降级是减法,两者必须配合。我们梳理了每个核心服务的降级开关,并做了真实演练。3.4.1 限流配置(Sentinel 网关)# Ingress 限流配置 - 按服务粒度apiVersion:networking.k8s.io/v1kind:Ingressmetadata:name:pay-svc-ingressnamespace:prodannotations:nginx.ingress.kubernetes.io/limit-connections:1000nginx.ingress.kubernetes.io/limit-rps:50000nginx.ingress.kubernetes.io/limit-burst:10000nginx.ingress.kubernetes.io/custom-headers:rate-limit-headersspec:rules:-host:pay.example.comhttp:paths:-path:/pathType:Prefixbackend:service:name:pay-svcport:number:80803.4.2 降级开关矩阵每个服务定义了 3 级降级开关,值班同学可一键执行:服务L1 降级(轻)L2 降级(中)L3 降级(重)product-svc关闭推荐关闭评论返回静态详情coupon-svc关闭叠加关闭领取全部返回可用risk-svc关闭规则引擎仅黑名单全部放行pay-svc关闭积分抵扣限流 50%排队模式降级开关通过 Apollo 配置中心下发,1 秒内生效,并接入值班控制台。3.5 第五阶段:红蓝对抗演练(10/28 - 11/1)预案写出来不算数,演练过才算数。我们组织了 5 天红蓝对抗,红队注入故障,蓝队值班响应:Day1:模拟 pay-svc DB 主库宕机,验证主从切换(目标 RTO 30s)Day2:模拟 Redis 集群 3 节点同时挂,验证降级到本地缓存Day3:模拟某可用区整体故障,验证跨 AZ 切流Day4:模拟 Kafka 消费堆积 100 万,验证 KEDA 扩容Day5:模拟 etcd 不可用,验证 apiserver 降级演练暴露的关键问题:DB 主从切换脚本在 K8s 环境下依赖 Pod IP,Pod 重建后 IP 变化导致切换失败,改为基于 Service 名 Readiness 探针后解决。四、踩坑与应急4.1 大促前夜:预热扩容 Pod 起不来(11/10 22:30)现象:按预案给 order-svc 扩容到 200 副本,90 个 Pod 一直 ContainerCreating。定位:# 查看异常 Podkubectl describe pod order-svc-xxx-nprod|grep-A5Events# 输出:Failed to pull image order-svc:v3.1.0: rpc error: code Unknown进一步查节点:image 大小 1.8G,300 个节点同时拉,把 Harbor 打满了。修复:# 1. 紧急给 Harbor 横向扩容kubectl scale deploy harbor-core-nharbor--replicas5kubectl scale deploy harbor-registry-nharbor--replicas8# 2. 已有镜像的节点打标签,调度优先kubectl labelnodenode-xxxpreloaddone--overwrite# 3. 用 DaemonSet 预热镜像到剩余节点kubectl apply-fimage-puller-ds.yaml教训:预热动作必须提前做,不能等到大促前夜才开始。后续我们把镜像预热写进了 SOP,要求 T-2 天完成。4.2 零点抢购:支付 P95 RT 突增(11/11 00:03)现象:00:03 支付 P95 RT 从 80ms 涨到 350ms,告警炸了。时间线:00:03:12 告警:pay-svc P95 RT 200ms 持续 1 分钟 00:03:15 值班 SRE 确认非误报,拉群 00:03:20 查看 Grafana:CPU 65%,内存 70%,无异常 00:03:25 查链路追踪:发现下游 pay-gateway DB 慢查询 00:03:30 DBA 反馈:主库连接数打满(200/200) 00:03:35 执行预案:扩容 pay-gateway Pod 50→100,连接池分摊 00:03:50 连接数降到 120,P95 RT 回落到 110ms 00:04:10 全部恢复正常根因:压测时 pay-gateway 是 100 副本,大促前为了省资源降到 50,连接池按 100 副本配置的单 Pod 连接数 4,结果 50 副本 × 4 200 正好顶满 DB 连接上限。教训:缩容时必须同步调整连接池配置,资源调整要有联动检查清单。4.3 凌晨 1 点:库存服务 5xx 飙升(11/11 01:15)现象:inventory-svc 5xx 错误率从 0.01% 飙到 3%。定位:查日志发现是 Redis 超时,进一步查 Redis 集群,某个分片 CPU 100%。根因:秒杀商品库存 Key 全落在一个分片(Hash 环倾斜)。应急修复:一键降级到本地缓存(Caffeine),牺牲一致性保可用,大促结束后恢复。# 触发降级开关curl-XPOST http://apollo.example.com/apps/inventory-svc\-d{key:degrade.local_cache,value:true}5 秒内 5xx 降到 0。事后复盘增加了热点 Key 自动分片能力。五、复盘与改进5.1 实际 vs 预估对比指标预估实际偏差峰值 QPS40 万38.7 万-3.25%峰值 Pod 数18501720-7%节点数峰值19001830-3.7%CPU 平均利用率65%58%-7%大促成本(相对日常)2.0x1.8x-10%容量预估整体偏保守,资源利用率有提升空间。5.2 经验教训清单压测要全链路:单接口压测会漏掉下游瓶颈,必须全链路。预热要提前:镜像预热、DB 连接预热、JIT 预热,都要在大促前 2 小时完成。弹性要分层:HPA KEDA CA 预热池,四层兜底才稳。降级要演练:开关不演练,出事就用不上,必须红蓝对抗。值班要扁平:大促当晚值班指挥群只留决策人,执行人单独拉群,避免信息噪音。缩容要联动:任何资源调整都要检查依赖项(连接池、队列、缓存)。数据要闭环:大促后 3 天内出复盘报告,经验沉淀到下次评估模型。5.3 长期改进容量评估模型自动化:把模型做成平台,每周自动产出容量报告。全链路压测常态化:每月一次小压测,每季度一次全链路。弹性能力下沉:把 HPA/KEDA 配置做成默认模板,新服务自动接入。降级开关治理:开关统一收口到控制台,废弃开关定期清理。六、可复用产出6.1 大促保障 SOP 框架T-42 天:启动专项,确定目标 QPS、SLA、成本预算 T-35 天:容量基线摸底,输出当前容量报告 T-28 天:全链路压测第一轮,定位瓶颈 T-21 天:瓶颈整改 弹性预案落地 T-14 天:全链路压测第二轮(验证整改) T-10 天:限流降级开关演练 T-7 天:红蓝对抗演练 T-3 天:值班手册定稿,值班人员确认 T-2 天:镜像预热、节点 Buffer 扩好 T-1 天 22:00:核心服务预热扩容到目标副本数 T-0 大促当晚:值班保障,零点抢购重点盯防 T1 天:数据汇总 T7 天:复盘会议,经验沉淀6.2 容量评估模型(表格模板)服务日常QPS大促QPS单Pod QPS安全系数所需Pod所需节点备注user-svc5k25k8001.3413product-svc15k75k20001.3494order-svc3k15k5001.3396CPU 密集pay-svc2k10k12001.3112inventory-svc4k20k10001.32646.3 值班手册框架# 大促值班手册 ## 1. 值班编排 - 指挥:XXX(决策) - 核心链路 SRE:XXX/XXX(执行) - 中间件 SRE:XXX(DB/Redis/MQ) - 业务研发:各服务 oncall ## 2. 告警分级与响应 - P0:核心链路 5xx 1%,5 分钟内响应,10 分钟内恢复或降级 - P1:非核心服务异常,15 分钟响应 - P2:资源告警,30 分钟响应 ## 3. 应急预案索引 - 支付故障 → 预案 P-001 - 缓存故障 → 预案 P-002 - DB 故障 → 预案 P-003 ## 4. 降级开关速查表(同 4.2.2) ## 5. 升级路径 - P0 故障 5 分钟未恢复 → 升级到技术 VP - 10 分钟未恢复 → 升级到 CTO6.4 弹性预案 YAML 示例(见 3.3.1 HPA、3.3.2 KEDA、3.3.3 镜像预热 DaemonSet,可直接复用)思考题如果大促流量预估错了(实际来了 8 倍而不是 5 倍),你的预案如何兜底?全链路压测中,如何保证压测流量不污染生产数据?你的染色方案如何覆盖消息队列异步链路?HPA 和 KEDA 在你的场景下应该如何选择?能否共存?共存的指标冲突如何解决?延伸阅读KEDA 官方文档:https://keda.sh/docs/K8s Cluster Autoscaler 最佳实践《SRE:Google 运维解密》第 6 章监控分布式系统阿里双 11 全链路压测实践公开分享Prometheus 自定义指标与 HPA v2 集成