【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 故障 |
时间线:
一、背景与挑战
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 查询并导出 CSV
cat > /tmp/capacity_query.py << 'EOF'
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{namespace="prod"}[5m])[90d:1h])',
"mem_p95": 'quantile_over_time(0.95, container_memory_working_set_bytes{namespace="prod"}[90d:1h])',
"qps_p95": 'quantile_over_time(0.95, sum(rate(nginx_ingress_controller_requests{namespace="prod"}[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", f'prod",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)
EOF
python3 /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 Header X-Pt: 1 染色,全链路透传,各中间件识别染色流量后路由到影子库。
3.2.2 压测数据准备
# 1. 生产数据脱敏导出到影子库
mysqldump -h prod-mysql -u readonly -p*** order_db \
--where="create_time > '2024-09-01'" \
| sed 's/手机号正则/***替换***/g' \
| mysql -h shadow-mysql -u root -p*** shadow_order_db
# 2. 构造压测用户(100万影子用户)
python3 gen_pressure_users.py --count 1000000 --prefix pt_user_
# 3. 影子 Redis 预热(从生产同步热 Key)
redis-cli -h prod-redis --rdb dump.rdb
# 修改 rdb 后恢复到影子 Redis
redis-cli -h shadow-redis -a *** FLUSHALL
cat dump.rdb | redis-cli -h shadow-redis -x restore shadow_key 0
3.2.3 发压方案
发压采用阶梯式加压,每个阶梯保持 10 分钟观察:
# 压测计划配置
pressure_plan:
target_svc: pay-svc
stages:
- 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: 200ms
error_rate: 0.1%
JMeter 发压脚本核心部分:
# 启动分布式发压(5 台发压机)
jmeter -n -t pay_svc_pressure.jmx \
-R 10.0.1.11,10.0.1.12,10.0.1.13,10.0.1.14,10.0.1.15 \
-X -l result.jtl -e -o report/
3.2.4 压测结果(关键瓶颈)
压测三轮,发现 4 个瓶颈:
| 服务 | 瓶颈现象 | 根因 | 处理 |
|---|---|---|---|
| pay-svc | QPS 到 6 万时 P95 RT 从 80ms 飙到 800ms | DB 连接池打满(50→200) | 调连接池 + 加读写分离 |
| inventory-svc | QPS 到 5 万时开始报错 | Redis 热点 Key 单分片打满 | 本地缓存 + Key 分片 |
| order-svc | QPS 到 8 万时 OOM | JVM 堆 4G 不够,GC 停顿 2s | 升到 8G + G1GC |
| product-svc | QPS 到 10 万时 CPU 100% | 图片处理计算密集 | 拆独立服务 + CDN 卸载 |
3.3 第三阶段:弹性预案落地(10/13 - 10/22)
3.3.1 HPA 配置
# pay-svc HPA - 基于 CPU 和内存
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: pay-svc-hpa
namespace: prod
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: pay-svc
minReplicas: 20 # 日常副本数
maxReplicas: 150 # 大促扩容上限
behavior:
scaleUp:
stabilizationWindowSeconds: 0 # 扩容立即响应
policies:
- type: Percent
value: 100 # 每次最多翻倍
periodSeconds: 30
- type: Pods
value: 20
periodSeconds: 30
selectPolicy: Max
scaleDown:
stabilizationWindowSeconds: 300 # 缩容延迟 5 分钟
policies:
- type: Percent
value: 10
periodSeconds: 60
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 60
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 70
3.3.2 KEDA 基于 QPS 弹性
HPA 基于 CPU/内存有滞后性,我们用 KEDA 监控更前置的指标——QPS 和消息队列堆积:
# KEDA ScaledObject - 基于自定义 QPS 指标
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: order-svc-keda
namespace: prod
spec:
scaleTargetRef:
name: order-svc
minReplicaCount: 30
maxReplicaCount: 200
pollingInterval: 15
cooldownPeriod: 300
triggers:
- type: prometheus
metadata:
serverAddress: http://prometheus:9090
metricName: order_svc_qps
threshold: "1500" # 单 Pod 承载 1500 QPS
query: |
sum(rate(nginx_ingress_controller_requests
{namespace="prod",service="order-svc",status!~"5.."}[1m]))
- type: kafka
metadata:
bootstrapServers: kafka:9092
consumerGroup: order-consumer
topic: order-events
lagThreshold: "1000" # 堆积超 1000 触发扩容
offsetResetPolicy: latest
3.3.3 Cluster Autoscaler + 节点预热
节点扩容慢是抢购场景的致命伤。我们做了两件事:
- 预留 Buffer 节点池:11/10 22:00 前预先扩出 300 个节点(平时空闲),CA 不回收。
- 节点镜像预热:把核心服务镜像提前 pull 到所有节点,Pod 启动时间从 90 秒降到 15 秒。
# DaemonSet 预热镜像
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: image-puller
namespace: kube-system
spec:
selector:
matchLabels:
app: image-puller
template:
metadata:
labels:
app: image-puller
spec:
tolerations:
- operator: Exists
initContainers:
- name: pull-pay
image: registry.example.com/pay-svc:v2.3.0
command: ["true"]
- name: pull-order
image: registry.example.com/order-svc:v3.1.0
command: ["true"]
- name: pull-inventory
image: registry.example.com/inventory-svc:v2.0.0
command: ["true"]
containers:
- name: pause
image: registry.k8s.io/pause:3.9
resources:
requests:
cpu: 10m
memory: 16Mi
3.4 第四阶段:限流降级演练(10/23 - 10/27)
弹性扩容是"加法",限流降级是"减法",两者必须配合。我们梳理了每个核心服务的降级开关,并做了真实演练。
3.4.1 限流配置(Sentinel + 网关)
# Ingress 限流配置 - 按服务粒度
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: pay-svc-ingress
namespace: prod
annotations:
nginx.ingress.kubernetes.io/limit-connections: "1000"
nginx.ingress.kubernetes.io/limit-rps: "50000"
nginx.ingress.kubernetes.io/limit-burst: "10000"
nginx.ingress.kubernetes.io/custom-headers: "rate-limit-headers"
spec:
rules:
- host: pay.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: pay-svc
port:
number: 8080
3.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。
定位:
# 查看异常 Pod
kubectl describe pod order-svc-xxx -n prod | grep -A 5 Events
# 输出: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 -n harbor --replicas=5
kubectl scale deploy harbor-registry -n harbor --replicas=8
# 2. 已有镜像的节点打标签,调度优先
kubectl label node node-xxx preload=done --overwrite
# 3. 用 DaemonSet 预热镜像到剩余节点
kubectl apply -f image-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 -X POST http://apollo.example.com/apps/inventory-svc \
-d '{"key":"degrade.local_cache","value":"true"}'
5 秒内 5xx 降到 0。事后复盘增加了"热点 Key 自动分片"能力。
五、复盘与改进
5.1 实际 vs 预估对比
| 指标 | 预估 | 实际 | 偏差 |
|---|---|---|---|
| 峰值 QPS | 40 万 | 38.7 万 | -3.25% |
| 峰值 Pod 数 | 1850 | 1720 | -7% |
| 节点数峰值 | 1900 | 1830 | -3.7% |
| CPU 平均利用率 | 65% | 58% | -7% |
| 大促成本(相对日常) | 2.0x | 1.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 大促当晚:值班保障,零点抢购重点盯防
T+1 天:数据汇总
T+7 天:复盘会议,经验沉淀
6.2 容量评估模型(表格模板)
| 服务 | 日常QPS | 大促QPS | 单Pod QPS | 安全系数 | 所需Pod | 所需节点 | 备注 |
|---|---|---|---|---|---|---|---|
| user-svc | 5k | 25k | 800 | 1.3 | 41 | 3 | |
| product-svc | 15k | 75k | 2000 | 1.3 | 49 | 4 | |
| order-svc | 3k | 15k | 500 | 1.3 | 39 | 6 | CPU 密集 |
| pay-svc | 2k | 10k | 1200 | 1.3 | 11 | 2 | |
| inventory-svc | 4k | 20k | 1000 | 1.3 | 26 | 4 |
6.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 分钟未恢复 → 升级到 CTO
6.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 集成
更多推荐




所有评论(0)