脉冲流量治理实操:3 秒内 QPS 从 1180 冲到 38600,我从内核参数改到线程池

🔑 关键词:脉冲流量, 突发流量, 限流削峰, 内核参数调优, Nginx限流

📖 摘要:一次真实的秒杀脉冲流量事故复盘。拆解脉冲流量的三个量化特征(上升沿、峰均比、脉冲宽度),给出可执行的内核参数、Nginx limit_req、线程池与 Redis 连接池配置思路,以及本地队列、MQ、令牌桶、滑动窗口四种削峰手段在脉冲场景下的实际差别。

先说这次是什么情况

图片

去年冬天帮一个做本地生活的小团队救火。他们 0 点做限时秒杀,入口是 4 台 8C16G 的 Nginx,后面挂 12 个 Java 实例,一台 4G 内存的 Redis 主从。日常 QPS 也就 1200 上下晃,压测报告上写着单机 8000 QPS 无压力。

0 点 00 分 03 秒,入口 QPS 从 1180 跳到 38600。注意是 3 秒内完成的跳变,不是 3 分钟。Nginx 的 active connections 从 900 涨到 21000,load 5 秒内干到 47。然后 12 个 Java 实例的 Tomcat 线程池(maxThreads=200)在 8 秒内全部打满,接口 P99 从 78ms 冲到 6.4s。

有点反直觉的是,数据库 CPU 只到 55%,机房出口带宽峰值都没超过 340Mbps。请求压根没走到 DB,全堵在 Nginx 回源和应用线程池排队上。

我当时只动了三处:入口 Nginx 把非核心接口按 IP 限速、Redis 连接池从 8 调到 64、Java 线程池队列从 1000 压到 200 并开快速失败。40 秒后 P99 回到 220ms 左右。下面把整个思路拆开讲。

别急着说流量大,先量三个数

判断是不是脉冲流量,看三个指标就够了,比看 QPS 曲线有用得多。

图片

上升沿时间:QPS 从基线爬到峰值用了多久。低于 5 秒的叫脉冲,5 秒到 1 分钟叫陡坡,1 分钟以上那是正常增长。这次不到 3 秒,属于典型脉冲。

峰均比:峰值 QPS 除以 10 分钟均值。38600 / 1300 ≈ 29.7。峰均比超过 10 的,靠自动扩缩容基本来不及,冷启动镜像、连接池预热、JIT 编译都要时间,只能削。

脉冲宽度:峰值维持在 80% 以上的时长。这次是 38 秒。宽度小于 1 分钟的,别考虑加机器,加完脉冲已经过去了。

三个数一套,方案基本就定了。很多人上来就喊扩容,其实 38 秒的脉冲,扩容的收益接近于零。

我当时的排查顺序和具体命令

图片

顺序很重要,先看链路再看应用,反过来会白折腾。

  1. sar -n DEV 1 30 看网卡包速率和带宽,排除物理层打满
  2. ss -s 看 TIME_WAIT 和 SYN_RECV 数量。当时 SYN_RECV 到了 3400
  3. nstat -az | grep -i Listen 看 TcpExtListenDrops。这个数不为 0,说明 accept 队列在丢连接
  4. 应用侧同时拉线程池 activeCount 和队列 size,两个一起涨就是排队型雪崩

那台机器内核是 3.10,net.core.somaxconn 还是默认的 128。这个值在 5.4 以后的内核里默认已经改成 4096,但老机器上不改就是坑。我一起调了这几个:net.core.somaxconn=4096net.ipv4.tcp_max_syn_backlog=8192net.ipv4.tcp_abort_on_overflow=1。改完 SYN_RECV 的堆积明显下来了。

第三步最容易被跳过。很多团队一看 QPS 涨就去调大线程池,其实 ListenDrops 不为 0 的话请求压根没进到应用里,调线程池是无效动作。

四种削峰手段,在脉冲场景下差别很大

本地内存队列:扛 1 到 3 秒的脉冲有用。队列长度别超 2000,再长用户等待时间和超时配置会打架。缺点是实例间不均,A 机器排队 B 机器空闲。

图片

MQ 削峰:适合能异步的写操作。这次秒杀下单要同步返回结果,用不上。如果业务能接受 800ms 延迟,RabbitMQ 单机 5000 QPS 打底是没问题的。

令牌桶(Nginx limit_req):允许短时突发,靠 rate 和 burst 两个参数控制。我配的是 rate=300r/s burst=600 nodelay。nodelay 这个词是关键,不加它,超出速率的请求会进队列排队,反而把延迟拉长,用户感知更差。

滑动窗口(Redis + Lua):精度最高,能做到 100ms 粒度,但每次请求多一次 Redis 往返,实测 0.3 到 0.8ms。QPS 到 4 万的时候,光限流本身就要吃掉一个 Redis 实例 15% 左右的 CPU。

选哪个,看脉冲宽度和能不能异步,没有通用答案。

我现在的观点:脉冲的对手不是容量,是时间对齐

图片

这次最值得复盘的不是那几行参数,是一个认知上的转弯。

38600 QPS 本身真不可怕。他们压测过 5 万 QPS 跑满 5 分钟,系统稳得很。差别在哪?压测时请求是均匀铺开的,真实脉冲是所有用户在同一个 3 秒里做同一个动作。CPU、连接数、锁竞争、缓存 miss 全部叠在同一个瞬间。

所以削峰的核心不是抗住多少量,是把时间轴打散。我在入口加限速、在线程池加快失败、在客户端 SDK 里给每个用户请求加了 0 到 800ms 的随机退避,三招其实都在做同一件事。

如果只能记一句话:脉冲流量的问题,一半出在同步,不出在容量。

两个我实际踩过的坑

第一个是二次脉冲。限流之后被拒的请求,客户端自动重试了,800ms 后又是一波。监控上两个尖峰间隔正好等于客户端重试间隔。后来 SDK 里改成指数退避,第一次 200ms,之后每次乘 2,最多重试 3 次。

图片

第二个是 DNS。3 秒内两万多个连接涌进来,DNS 走 UDP,丢包后要等 5 秒超时重试。日志里有一批请求耗时正好 5000ms 出头,就是这么来的。把 Nginx 的 resolver 超时从默认 30s 改成 2s,加了一层本地缓存,这批长尾就没了。

这两个坑都不在压测报告里,是真实流量才会暴露的东西。

什么时候该加机器

不是所有脉冲都要削。如果上升沿在 30 秒以上、脉冲宽度超过 5 分钟、峰均比低于 5,那这就是增长不是脉冲,老老实实扩容更划算。

我一般看一条线:如果从检测到流量上升到新实例能接住请求的时间,超过脉冲宽度的一半,就别扩容了,削它。反过来才有扩容的意义。

🏷️ 标签: