脉冲流量扛不住怎么办?12000 QPS 那一晚,我记了 4 套方案的真实响应时间

🔑 关键词:脉冲流量,突发流量应对,QPS削峰,自动扩容延迟,限流降级

📖 摘要:脉冲流量和持续高负载是两回事:机器够用也会挂。本文用一次 11840 QPS 的实战数据,拆解 HPA 扩容的时间账、四种削峰方案的延迟与成本对比,以及 Nginx、Tomcat、HikariCP 的具体参数怎么调。

脉冲流量不是容量问题,是时间问题

图片

去年 11 月 10 号晚上 11 点 40 分,我蹲在值班室啃一碗泡面,Grafana 上那条入口 QPS 曲线平得像心电图停了。23:59:56 还是 217 QPS,0:00:00 整点一过,四秒内窜到 11840。我们的集群是按 20000 QPS 压测过的,机器绰绰有余,但 502 还是来了——集中在 00:00:06 到 00:00:51,错误率峰值 7.3%。

事后翻监控我有点懵:CPU 最高 62%,内存用了不到一半,MySQL 连接数 480(max_connections 是 2000),Redis 单分片打到 7 万 QPS 离瓶颈还远。所有指标看起来都很健康,但用户就是在报错。

这件事让我把「脉冲流量」这四个字重新理解了一遍。它跟「大流量」根本不是同一个问题。大流量考验的是容量上限,你加机器、加分片、加带宽就行;脉冲流量考验的是响应速度,你的系统在 45 秒内能不能完成从「闲」到「满载」的状态切换。前者是钱的问题,后者是时间的问题,而时间用钱买不到。

先算清 HPA 那笔时间账,你会死心

很多人的第一反应是:上 K8s,配 HPA,QPS 高了自动扩容,不就完了?我当年也是这么想的,直到我把这条链路的耗时一项项列出来。

图片

metrics-server 默认 15 秒抓一次指标(--metric-resolution=15s),HPA 控制器默认 15 秒同步一次(--horizontal-pod-autoscaler-sync-period=15s),这两步最坏情况就是 30 秒。触发扩容后,调度器分配 Pod 通常 1-3 秒,然后镜像拉取——如果你的镜像 800MB 且节点没缓存,30 到 60 秒是常态。容器起来之后 JVM 冷启动,一个 Spring Boot 应用从 main 到健康检查通过,实测 18 到 35 秒。再注册到 Service Endpoints、被 kube-proxy 的 iptables 规则认到,又是几秒。

乐观算 60 秒,悲观算 120 秒。而绝大多数脉冲流量的上升沿只有 5 到 30 秒。也就是说,等你扩容完,脉冲已经过去了。更坑的是缩容:HPA 的 scaleDown.stabilizationWindowSeconds 默认 300 秒,意思是它宁可让机器多空转 5 分钟也不愿意来回抖。这个设计对长期负载很友好,对脉冲流量就是纯粹的浪费——你花了钱扩容,结果机器在业务低谷期空跑。

我的结论很直接:HPA 处理的是「趋势」,不是「脉冲」。指望它接住秒级尖峰,等于指望电梯去追子弹。

四种方案,我把它们的真实延迟和成本摆一起

那次事故之后我们试了四套方案,前后跑了三个月,数据是我自己拿 wrk 和 tc 在预发环境复现出来的,不是抄的 PPT。

图片

方案 生效延迟 增量成本 能扛的脉冲量级 副作用
HPA 自动扩容 60-120 秒 按量付费,脉冲结束还空跑 5 分钟 只适合持续 10 分钟以上的上涨 缩容窗口期资源浪费
提前定时扩容 0 秒(但要提前 15-30 分钟操作) 固定成本,可预估 可预测的脉冲(大促、直播、定点发券) 预测错了就白花钱
入口限流 + 排队 即时,毫秒级 几乎为零 任意量级,代价是丢请求 用户体验受损,要做好降级页
消息队列削峰 0.5-3 秒(Kafka 端到端) 中间件成本 可异步化的写流量 只解决写,读还是直接打

具体讲两个细节。定时扩容看着土,但它是那天唯一真正救命的:我们在 23:30 手动把订单服务的副本从 24 个拉到 80 个,虽然多花了大概两千块机器费,但 00:00 那一分钟一个 502 都没有。代价是如果当晚活动取消,这两千块就打水漂了。

限流这块我踩的坑最多。一开始只在 Nginx 层配了 limit_req,rate=8000r/s、burst=2000、nodelay。结果发现 Nginx 确实挡住了,但挡下来的请求返回的是 503,前端页面直接白屏。后来改成把超额请求 302 跳到一个静态等待页,页面里塞了个 3 秒自动重试的 JS,用户感知从「崩了」变成「排队中」。同样的丢弃量,客诉从 400 多单降到个位数。限流的关键不在限,在你怎么告诉用户他被限了。

参数怎么调,我直接贴能跑的配置

先说 Nginx。这里最容易忽略的是 limit_conn,很多人只配了 limit_req。脉冲流量里大量是同一批 IP 的并发连接,只限速率不限连接数,后端照样被拖死。

图片

limit_req_zone $binary_remote_addr zone=perip:10m rate=20r/s;
limit_req_zone $server_name zone=perserver:10m rate=8000r/s;
limit_conn_zone $binary_remote_addr zone=connperip:10m;

server {
    limit_req zone=perip burst=40 nodelay;
    limit_req zone=perserver burst=2000 nodelay;
    limit_conn connperip 20;
    limit_req_status 429;
    limit_conn_status 429;
}

perip 那个 20r/s 是给普通用户的,正常人手速到不了;burst=40 允许短时突发,nodelay 表示突发额度用完立刻拒,不排队。perserver 的 8000 是全站兜底,比实际能承载的峰值低 20%,留缓冲。

Tomcat 侧改三个数就够。默认 maxThreads=200、acceptCount=100、max-connections=8192,这套默认值是给「稳定流量」设计的。脉冲场景下我把 threads.max 提到 800,accept-count 提到 2000,max-connections 提到 10000。注意 maxThreads 不是越大越好,它受限于你的下游——如果数据库连接池只有 32,开 800 个线程只会让 768 个线程在那儿阻塞等连接。

server:
  tomcat:
    threads:
      max: 800
      min-spare: 100
    accept-count: 2000
    max-connections: 10000

图片

数据库这边,HikariCP 默认 maximumPoolSize=10,这个值在脉冲流量下是灾难。我按 4 核 8G 的 MySQL 实例算,经验值是 CPU 核数 × 2 + 有效磁盘数,4 核 SSD 大概给到 32。但更关键的是 connectionTimeout 从默认 30000ms 砍到 800ms——脉冲来时大量线程卡在等连接上,30 秒的超时会让 Tomcat 线程池瞬间占满然后雪崩。800ms 拿不到连接就快速失败走降级,反而稳。

我踩过的四个坑,都是血

第一个是连接池预热。我们改了 maximumPoolSize=32,但 HikariCP 是懒加载的,第一次用才建连接。脉冲来的头 3 秒要建 22 个 MySQL 连接,每个 TCP 握手 + 鉴权大概 15-40ms,这 3 秒里所有请求都在超时。后来在启动时加了 initializationFailTimeout 和一段预热 SQL,问题消失。

第二个是 Redis 热 Key。我们的秒杀库存 key 单分片打到 7 万 QPS,虽然 Redis 单实例理论上能到 10 万,但那是纯 GET 的理想值。实际带着 pipeline 和序列化开销,6 万就开始抖。解法是在应用层加本地缓存,库存数每 200ms 从 Redis 刷一次,单机扛住 90% 的读。代价是有 200ms 的数据延迟,秒杀场景能接受,支付场景不能。

第三个是 CDN 回源。脉冲流量里如果回源率超过 15%,源站基本就废了。我们那次回源率一度到 34%,原因是活动页面的 URL 带了用户 ID 参数,CDN 认为是不同资源,全部 MISS。把参数去掉、改用 Vary 头区分之后,回源率降到 4.1%。这个坑特别隐蔽,压测的时候根本发现不了,因为压测机不会带真实参数。

图片

第四个是 DNS。听起来很扯,但我们确实栽了。活动开始前临时切了一次 SLB 地址,DNS TTL 还设着 600 秒,结果一部分用户还在解析旧 IP,新集群的流量比预期少了 30%,旧集群被打爆。后来所有核心域名的 TTL 都压到 60 秒以内。这个经验很便宜,但不知道的人会付出很贵的代价。

最后说点不那么技术的话

我现在的判断是:脉冲流量的本质是「用有限资源覆盖极短时间窗」,它是一个调度问题,不是一个扩容问题。 扩容解决的是「平均值」,调度解决的是「峰值」和「峰值出现的那 30 秒」。这个视角切换之后,你会发现很多方案的价值排序变了——定时扩容这种听起来很土的做法,在可预测的脉冲场景下性价比远超 HPA;而队列削峰的价值不在于扛住流量,在于把不可控的到达曲线变成可控的消费曲线。

但话也说回来,这套东西不是万能的。如果你的脉冲完全不可预测(比如突发热点事件带来的流量),定时扩容就没用,只能靠限流兜底;如果你的业务是强同步的(比如支付确认),队列削峰也用不上。我见过有人把所有流量都往 MQ 里塞,结果高峰期消息积压 200 万条,用户等了 40 秒才看到结果,比直接报错还难受。

所以别迷信方案,先搞清楚你的脉冲是什么形状:是 5 秒的尖刺还是 3 分钟的方波?是可预测的还是随机的?能异步还是必须同步?这三个问题想清楚,方案基本就定了。想不清楚,配再多参数也是碰运气。

🏷️ 标签: