案例:电商大促容量保障全过程

一句话定位:从容量评估到事后复盘,还原一次 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-29 10-06 10-13 10-20 10-27 11-03 11-10 11-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 历史问题

前两次大促暴露的核心问题:

  1. 容量评估靠拍脑袋:2022 年凭经验给每个服务扩容 3 倍,结果支付服务扩容不足、商品服务扩容过剩,资源浪费 30%。
  2. 压测覆盖不全:只压了单接口,没做全链路压测,链路上游扩容了下游没扩,瓶颈转移。
  3. 弹性能力不足:HPA 响应慢,抢购瞬间流量 30 秒涨 10 倍,HPA 还没扩出来服务就挂了。
  4. 预案不可执行:值班手册写在 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 + 节点预热

节点扩容慢是抢购场景的致命伤。我们做了两件事:

  1. 预留 Buffer 节点池:11/10 22:00 前预先扩出 300 个节点(平时空闲),CA 不回收。
  2. 节点镜像预热:把核心服务镜像提前 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 经验教训清单

  1. 压测要全链路:单接口压测会漏掉下游瓶颈,必须全链路。
  2. 预热要提前:镜像预热、DB 连接预热、JIT 预热,都要在大促前 2 小时完成。
  3. 弹性要分层:HPA + KEDA + CA + 预热池,四层兜底才稳。
  4. 降级要演练:开关不演练,出事就用不上,必须红蓝对抗。
  5. 值班要扁平:大促当晚值班指挥群只留决策人,执行人单独拉群,避免信息噪音。
  6. 缩容要联动:任何资源调整都要检查依赖项(连接池、队列、缓存)。
  7. 数据要闭环:大促后 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,可直接复用)

思考题

  1. 如果大促流量预估错了(实际来了 8 倍而不是 5 倍),你的预案如何兜底?
  2. 全链路压测中,如何保证压测流量不污染生产数据?你的染色方案如何覆盖消息队列异步链路?
  3. HPA 和 KEDA 在你的场景下应该如何选择?能否共存?共存的指标冲突如何解决?

延伸阅读

  • KEDA 官方文档:https://keda.sh/docs/
  • K8s Cluster Autoscaler 最佳实践
  • 《SRE:Google 运维解密》第 6 章"监控分布式系统"
  • 阿里双 11 全链路压测实践公开分享
  • Prometheus 自定义指标与 HPA v2 集成
Logo

电商企业物流数字化转型必备!快递鸟 API 接口,72 小时快速完成物流系统集成。全流程实战1V1指导,营造开放的API技术生态圈。

更多推荐