秒杀洪峰和CC攻击怎么区分?分级防护实战

大促和秒杀当晚流量翻十几倍是正常的,但它和 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 限流形成两层,比只靠一层限流稳得多。