大促和秒杀当晚流量翻十几倍是正常的,但它和 CC 攻击的表现极其相似:QPS 猛涨、动态接口被打、CPU 上来。照搬平日的限流阈值会误伤真实用户,完全放开又会让刷单脚本混在里面把库存和优惠券刷走。本文用 9 个问答讲清怎么把正常洪峰和 CC 攻击区分开,以及分级阈值、削峰排队与灰度上线的具体配置。
Q:大促流量涨十倍,怎么判断是正常洪峰还是 CC 攻击?
- 只看 QPS 判断不了,要看四个维度:
- 请求路径分布:正常用户的路径是首页或活动页 → 列表 → 详情 → 下单,比例自然衰减;CC 则死磕单一动态接口,路径分布畸形。
- 是否拉取静态资源:真实浏览器会请求 css、js、图片和字体;脚本通常只打接口,静态资源占比异常低。
- 来源与指纹分布:正常流量 IP 分散,与地域和时段相符;CC 的 IP 集中在 IDC 或云段,User-Agent 高度雷同。
- 时间形态:正常洪峰是秒杀时刻的尖峰后快速回落;CC 是持续平稳或阶梯式抬升,攻击者不需要等你开抢。
- 一句话总结:洪峰是「行为像人但数量变多」,CC 是「数量不小但行为不像人」。
Q:具体该看日志里的哪些字段?
- 四条命令基本能定性,先用它们区分洪峰和攻击:
# 1) 请求路径 TOP:看是否死磕单一接口
awk -F'"' '{print $2}' access.log | awk '{print $2}' | sed 's/?.*//' | sort | uniq -c | sort -rn | head -20
# 2) 静态资源请求量:正常应占大头,脚本极低
grep -Ec '\.(js|css|png|jpe?g|gif|svg|woff2?|ico)' access.log
# 3) 单 IP 请求量 TOP:看是否集中
awk '{print $1}' access.log | sort | uniq -c | sort -rn | head -20
# 4) UA 种类数:脚本往往高度雷同
awk -F'"' '{print $6}' access.log | sort | uniq -c | sort -rn | head -10
- 读法:路径高度集中、静态资源占比极低、少数 IP 段占了大部分请求、UA 种类数很少,四条同时出现,基本可以按攻击处理。
Q:只看转化率能不能判断?
- 不够,而且有两个硬伤:
- 转化率是滞后指标:等它掉下来,用户已经流失,损失已经发生。
- 秒杀场景天然低转化:大部分人本来也抢不到,转化率下滑不一定说明被攻击。
- 建议做成双条件告警:技术指标(QPS、连接数、CPU、429 比例)和业务指标(下单成功率、支付成功率、库存扣减一致性)同时异常才升级为攻击事件,避免半夜被单个指标叫起来。
Q:Nginx 上怎么给不同接口设不同限流档位?
- 原则是分档而非一刀切:静态资源放宽、普通接口适中、写接口最严。
# 三档分开建:静态 / 普通动态 / 秒杀写接口
limit_req_zone $binary_remote_addr zone=static:20m rate=30r/s;
limit_req_zone $binary_remote_addr zone=api:20m rate=5r/s;
limit_req_zone $binary_remote_addr zone=seckill:10m rate=1r/s;
limit_conn_zone $binary_remote_addr zone=connip:10m;
server {
location ~* \.(js|css|png|jpe?g|gif|svg|woff2?|ico)$ {
limit_req zone=static burst=60 nodelay;
}
location ^~ /api/seckill/ {
limit_req zone=seckill burst=3 nodelay;
limit_conn connip 5;
limit_req_status 429;
add_header Retry-After 1 always;
}
location ^~ /api/ {
limit_req zone=api burst=10 nodelay;
limit_req_status 429;
add_header Retry-After 1 always;
}
}
- 两个要点:burst 要留出真实的连点余量(用户手快会连点两次),返回 429 而不是 502,前端才能识别并做倒计时重试。
Q:秒杀接口被刷怎么防,只靠 IP 限流够吗?
- 不够。刷单脚本普遍用代理 IP 池,换 IP 的成本极低,纯 IP 限流只能挡住最粗糙的那一批。
- 要在入口做闸门,三层叠加:
- 令牌制放行:只有先拿到令牌的请求才允许进入下单逻辑,令牌按用户维度发放并限时。
- 账号维度去重:一人一单按用户 ID、设备指纹去重,而不是按 IP。
- 分级挑战:只在秒杀开始的几分钟内启用验证码或滑块,平时不打扰正常用户。
- 库存扣减必须在服务端原子完成,避免超卖:
# 用 Lua 保证「查库存 + 扣减」的原子性
# KEYS[1]=stock:sku:1001 ARGV[1]=数量
EVAL "local s=tonumber(redis.call('GET',KEYS[1]) or '-1');
if s < tonumber(ARGV[1]) then return -1 end;
return redis.call('DECRBY',KEYS[1],ARGV[1])" 1 stock:sku:1001 1
- 另一个高频漏洞是把库存判断放在前端,或只靠页面刷新控制。脚本可以绕过页面直接打接口,务必确认后端有独立校验。
Q:削峰和排队会不会让用户觉得更慢?
- 排队确实会增加等待,但比无声超时和 502 体验好得多。三种实用手段:
- 明确反馈:返回 429 加 Retry-After,前端展示「正在排队,请稍候」,用户愿意等。
- 改变玩法:秒杀改成预约加抽签,或分批错峰放量,把瞬时尖峰摊平。
- 静态化兜底:活动页和详情页尽量走缓存,后端扛不住时直接返回静态降级页,保住首屏,别让整站跟着挂。
Q:阈值定多少才不误伤?怎么灰度上线?
- 阈值从基线来:取近 14 天同一时段、同一接口的峰值乘以 1.5 到 2 倍作为初始值,别凭感觉拍。
- 先观察再拦截:Nginx 支持只记录不拦截,跑一两天看会打到谁。
# 灰度:只记日志,不真正拒绝请求
limit_req_dry_run on;
- 压测出口、监控探针这些合法高频来源要做白名单,常见做法是让它们不参与限流计数:
geo $trusted {
default 0;
10.0.0.0/8 1;
203.0.113.0/24 1; # 压测出口
198.51.100.7 1; # 监控探针
}
map $trusted $limit_key {
1 "";
0 $binary_remote_addr;
}
limit_req_zone $limit_key zone=api:20m rate=5r/s;
- 注意:空 key 的请求通常不被计入限流,这正是白名单的常见写法,上线前请在你们使用的 Nginx 版本上验证一次。
Q:攻击和洪峰混在一起时,应急顺序是什么?
- 顺序错了会先把真实用户挡在门外,建议按这个优先级:
- 先让流量走防护层:开启 CDN 或高防的挑战模式,不要让源站直接抗。
- 再保核心链路:写接口(下单、支付、领券)优先保住,非核心入口可以临时收紧或关闭。
- 然后收严限流档位:把阈值调到只放行携带令牌或已登录的请求。
- 最后才做封禁:以多维度同时满足为准(单 IP 高频、无静态资源请求、路径单一),避免误伤整片 NAT 出口。
- 每一小时复盘一次,攻击过去后逐条回滚临时规则,不要留一堆没人记得的配置。
Q:还有什么补充建议?
- 大促前做一次全链路压测,提前把压测出口加入白名单,否则压测当天会和限流规则互相打架。
- 把预案写成可执行清单:阈值改成多少、先关哪个入口、谁负责通知,而不是写「视情况处理」。
- 留档每次大促的真实流量曲线,它就是下一次的阈值基线,比任何经验值都准。
- 如果长期被盯上,把秒杀入口挂到具备行为风控能力的 WAF 后面,与 Nginx 限流形成两层,比只靠一层限流稳得多。