先说个容易踩的坑——搜「脉冲流量」的人,可能有一半想问的不是同一件事。
我在工厂做过两年设备对接,那会儿这个词指的是电磁流量计吐出来的方波信号:管子里过去多少水,它就蹦多少个脉冲,配 0.5L/脉冲的当量,满量程能到 3000Hz,PLC 那边拿高速计数口去读。那套语境里讨论的是脉冲当量、频率上限、屏蔽层单端接地,跟服务器一点关系没有。而在机房这一侧,同一个词说的是短时间内突然涌上来、又很快退下去的一股访问量。公众号推文发出去的头 30 秒、App 开屏广告同时触发,都算。
两拨人用同一个词,搜出来的东西驴唇不对马嘴。下面只聊后面这种,工业那部分我放在最后收个尾。
脉冲流量至少有四种形状,处理方式完全不一样
我以前也以为脉冲就是「突然来一波」,做过几次活动保障才发现,不分形状就上削峰,跟闭着眼睛吃药差不多。
单次尖刺。 2 秒内从 500 QPS 冲到 20000 再掉回来。典型场景是定时任务整点触发、App 推送同步到达。这种最好防,也最容易把人骗了——因为它真的太短了。
阶梯上浮。 5 分钟涨到 3 倍,然后稳住不降。严格说这不是脉冲,是爬坡,通常意味着你被挂到哪个平台上了。应对方式完全不同,削峰对它没用,得从业务侧拦。
周期性脉冲。 每天 8:00、12:00、18:00 各来一个鼓包,打卡类、外卖类 App 特别明显。这种最省事,因为可预测,提前扩容就行。
持续高水位。 峰值不高但一直压着,比如 8000 QPS 顶 8 小时。这种最阴,它不炸你,它慢慢把内存泄漏、连接泄漏、日志写满这些平时看不出来的东西全部放大。
看清是哪种形状,再决定削峰、扩容还是降级,这一步省不掉。
大部分人盯的指标是错的:先崩的通常不是 QPS
监控面板上放 QPS 和带宽,几乎成了默认动作。但脉冲场景里,这两个往往不是最先崩的。
先崩的是 PPS 和并发连接数。 短连接场景下,一个 HTTP 请求大约等于一次三次握手,加上请求包、响应包、四次挥手,一个请求打底 8-12 个网络包。10000 QPS 就是 8 万到 12 万 PPS。云主机的 PPS 是有硬上限的,规格越小上限越低,QPS 离预警线还有一大截的时候,网卡队列和内核软中断可能先堆满了,sar -n DEV 里的 drop 开始涨,但你的 QPS 曲线看起来还很健康。
第二个隐形杀手是 TIME_WAIT。 主动关闭方会占着端口 60 秒(2MSL,这个是写死在协议里的,改不了)。net.ipv4.ip_local_port_range 默认是 32768 到 60999,两万八千来个端口。脉冲过去之后那 60 秒里,如果短连接打了两三轮,端口就没了,新连接直接报 Cannot assign requested address。
能调的其实就这么几行:
net.ipv4.tcp_tw_reuse = 1
net.ipv4.ip_local_port_range = 10000 65000
net.core.somaxconn = 65535
net.ipv4.tcp_max_syn_backlog = 65535
net.core.netdev_max_backlog = 65535
tcp_tw_reuse 需要 tcp_timestamps 开着才生效,现在内核默认都是开的。注意它只对出方向的连接起作用,别人连你留下的 TIME_WAIT 它管不了。
nginx 那边对应的是:
listen 80 backlog=65535;
worker_connections 65535;
keepalive_timeout 65;
这里有个很多人搞混的点:worker_connections 不是「能扛多少 QPS」,它是单个 worker 同时能打开的连接数。默认 512,4 个 worker 也就 2048 个并发连接。而且你要是开了反向代理,一个客户端连接会同时占掉前端和后端两个名额,实际能力直接砍半。
limit_req 的默认参数基本不能直接用
nginx 自带的 limit_req 默认是 rate=1r/s burst=0,等于没有。真要配的话大概长这样:
limit_req_zone $binary_remote_addr zone=perip:10m rate=20r/s;
limit_req_zone $server_name zone=global:10m rate=3000r/s;

location /api/ {
limit_req zone=perip burst=40 nodelay;
limit_req zone=global burst=2000 nodelay;
limit_conn perip 50;
}
zone=perip:10m 里的 10m 是共享内存,一个 IP 大概占 64 到 128 字节,10m 差不多能记 8 万到 16 万个 IP。这个数字很容易被忽略,等日志里刷出 zone exhausted 的时候,限流其实已经悄悄失效了。
burst=40 nodelay 的意思是允许瞬时 40 个请求直接放行、不排队。加不加 nodelay 取决于你想保护谁:不加就是漏桶,请求在 nginx 里排队等着被匀速放出去,客户端 RT 会被拉长,但后端很舒服;加了就是令牌桶,放行快,后端照样吃瞬时脉冲。
漏桶和令牌桶的区别被讲烂了,但落到脉冲流量上有个很实际的结论——只有漏桶能削峰,令牌桶只是把峰切碎了,总量一点没少。 你要保护的是后端的数据库,那就老老实实用漏桶。
真正的雪崩在峰值过去之后 90 秒,这才是我想说的
这是这篇里我最想讲的部分。
那次活动,前端在 30 秒内推了大约 8 万个请求过来。后端压测出来的上限是 2000 QPS,按纸面算撑 40 秒就过去了。实际结果:服务挂了两分半。
复盘的时候把链路捋了一遍,打死服务的不是那 8 万个请求,是后面那波回声:
前端 axios 没设 timeout,默认一直等;等待的 20 秒里用户以为没反应,平均每人手动又点了 4 到 7 次;网关层还配了一次自动重试,3 秒超时后重发;后端线程池是 core 20 / max 200 / queue 1000,队列满了直接拒绝,拒绝又触发前端重试。
8 万 × 手动点击 5 次 + 自动重试 1 次,大概 48 万次请求。峰值被放大了 6 倍,而且这 48 万是压在一个已经被打残的服务上。
所以我的看法是:脉冲流量的杀伤半径不在那一秒的峰值,在峰值之后 30 到 120 秒的回声段。 你盯着 QPS 曲线看,会觉得峰早就过去了、服务该恢复了,但那个时间窗口里的真实负载可能是峰值的 2 到 6 倍,而且全是无效流量。
防回声的优先级其实比防峰值还高,具体就三件事:
第一,前端必须设超时,而且比后端短。后端 3 秒,前端设 2.5 秒,等待期间按钮直接禁用并给倒计时,别让用户有机会连点。
第二,网关层的自动重试直接关掉。重试只加在幂等接口上,并且必须走指数退避,1s / 2s / 4s / 8s,最多 4 次。同步重试 3 次等于给后端加 3 倍负载,这是最容易被忽视的放大器。
第三,拒绝要拒绝得快。等 3 秒超时再拒,比直接拒更伤人,因为那 3 秒它一直占着线程。nginx 直接 return 503 带上 Retry-After 头,比放它进后端排队强得多。
自己造脉冲来测,别只看平均延迟
想验证上面这些东西,k6 的 ramping-arrival-rate 是最好用的。它按到达率算而不是按虚拟用户算,曲线能造得很准:
import http from 'k6/http';
export const options = {
scenarios: {
pulse: {
executor: 'ramping-arrival-rate',
startRate: 100,
timeUnit: '1s',
preAllocatedVUs: 500,
maxVUs: 3000,
stages: [
{ target: 100, duration: '10s' },
{ target: 8000, duration: '5s' },
{ target: 8000, duration: '20s' },
{ target: 200, duration: '10s' },
{ target: 200, duration: '60s' },
],
},
},
};
export default function () {
http.get('http://your-host/api/hot');
}
注意最后那个 stage。很多人造完尖峰就结束了,回落后的那 60 秒不放量、只盯指标,才是回声段的验证窗口。这个窗口里要看的东西:
nginx 的 $upstream_response_time 看 P99,不是平均值,平均值在这种场景下毫无意义;后端线程池的 active 数和队列长度;JVM 的 GC pause,G1 正常是 50 到 200 毫秒,一旦出现 Full GC 就是几百毫秒到一两秒,这段时间所有请求都在等;数据库连接池的等待数;还有 netstat -s | grep retrans 里的重传率。
顺带一提,很多人一遇到扛不住的第一反应是加带宽。脉冲场景下带宽通常不是瓶颈,CPU 的上下文切换、锁竞争、GC 才是。先把 30 到 120 秒那段曲线看清楚,再决定钱花在哪。
最后给找流量计的朋友收个尾。电磁流量计、涡街流量计的输出一般是集电极开路或者电平脉冲,典型参数是低电平 0-1V、高电平 5-24V,占空比 50%,频率上限从 1Hz 到 5000Hz 不等。选型时真正要算的不是频率,是脉冲当量:4-20mA 配 0.1L/脉冲的话,PLC 高速计数口读到 100Hz,对应就是 10L/s,也就是 36m³/h,先确认这个数能不能盖住你的量程上限。信号线用屏蔽双绞,屏蔽层单端接地,跟动力线至少隔开 20cm,不然计数会莫名其妙地跳。