CC攻击和真实流量怎么区分?看日志时间分布

流量涨了、CPU 高了,是活动投放带来的真实流量,还是被 CC 攻击打了?只看请求总数永远分不清。本文用 9 个问答讲清怎么从 Nginx 日志的时间分布切入,配合请求间隔、URL 集中度把 CC 攻击和真实流量分开,命令与脚本可直接复制。

Q:为什么只看请求数分不清 CC 和真实流量?

因为 CC 攻击追求的是「用最少的请求消耗你最多的资源」,它会主动选择那些最贵的请求。真实流量的总量和 CC 完全可能一样,但两者的结构差异非常大。

  • 真实流量请求分散:一个访客会打开首页、列表页、详情页,还会顺带请求 CSS、JS、图片。
  • CC 攻击请求集中:同一时刻大量请求砸向同一个搜索页、同一个接口、同一个动态页。
  • 真实流量时间分散:请求间隔长短不一,没人的时段曲线自然掉落。
  • CC 攻击时间规律:请求间隔高度一致,凌晨三点照样保持高峰。

所以判别 CC 的第一原则是:不看总量,看分布。总量是放大器,分布才是身份指纹。

Q:日志时间分布怎么看?先出分钟级曲线

最省事的办法是把日志按分钟聚合,画出请求数曲线。一分钟一个点已经足够看出形状,不用上复杂工具。

# 按分钟聚合请求数,看 Top 15 的峰值分钟(以 23/Sep/2026 为例)
grep "23/Sep/2026" /var/log/nginx/access.log \
  | awk '{print substr($4,14,5)}' \
  | sort | uniq -c | sort -rn | head -15

# 按秒聚合,抓秒级突发(单秒请求数过高即为突发特征)
grep "23/Sep/2026" /var/log/nginx/access.log \
  | awk '{print substr($4,14,8)}' \
  | sort | uniq -c | sort -rn | head -20

# 按小时聚合,看整天曲线是否呈昼夜规律
grep "23/Sep/2026" /var/log/nginx/access.log \
  | awk '{print substr($4,14,2)}' \
  | sort | uniq -c

substr($4,14,5) 取的是标准日志里 [23/Sep/2026:14:05:03 这一段的小时与分钟。命令写法固定,Nginx 默认日志格式都能用,不需要改日志格式

Q:怎么算请求间隔,判断是不是机器在刷?

请求间隔是区分人和机器最硬的指标。人点页面有阅读时间,机器按固定频率发请求。间隔越小、标准差越小,越像机器

# 统计单个 IP 的请求间隔:均值、标准差、最小/最大间隔(秒)
python3 <<'PY'
import re, statistics
from datetime import datetime
MON = {'Jan':1,'Feb':2,'Mar':3,'Apr':4,'May':5,'Jun':6,
       'Jul':7,'Aug':8,'Sep':9,'Oct':10,'Nov':11,'Dec':12}
ip = '203.0.113.7'
ts = []
for line in open('/var/log/nginx/access.log', errors='ignore'):
    if not line.startswith(ip + ' '):
        continue
    m = re.search(r'\[([0-9]{2})/([A-Za-z]{3})/([0-9]{4}):([0-9]{2}):([0-9]{2}):([0-9]{2})', line)
    if m:
        d, mo, y, H, Mi, S = m.groups()
        ts.append(datetime(int(y), MON[mo], int(d), int(H), int(Mi), int(S)).timestamp())
ts.sort()
if len(ts) > 1:
    gap = [b - a for a, b in zip(ts, ts[1:])]
    print('requests=%d avg=%.2fs stdev=%.2fs min=%.2fs max=%.2fs' % (
        len(ts), sum(gap)/len(gap), statistics.pstdev(gap), min(gap), max(gap)))
PY

判读标准很直观:真实访客的平均间隔通常在几十秒到几百秒,标准差和均值同一量级;CC 攻击的平均间隔常在 1 秒以内,标准差低于 0.5 秒,也就是「像钟表一样准」。看到 stdev 远小于 avg,基本可以定性。

Q:真实流量的时间分布有什么规律?

抓住三条规律,就有了对比基线,判断异常才有参照。

  • 昼夜曲线:凌晨 2 点到 6 点通常是全天最低谷。如果这一段还在高位运行,先怀疑机器流量。电商、行业站还可能叠加工作日与周末的差异。
  • 峰谷比合理:普通站点的峰值小时请求数通常是低谷的 3 到 10 倍。峰谷比突然拉到 50 倍以上,说明峰值那一头是外来的。
  • 曲线有惯性:真实流量上涨是渐进的(活动开始、内容被推荐),从低谷到峰值一般要十几分钟以上。秒级或一两分钟内完成的拉升,几乎都是脚本。

Q:CC 攻击的曲线有哪些典型形状?

四种形状对应四种打法,形状本身就是攻击者的行为特征。

  • 阶跃型:请求数在极短时间内跳升,然后维持一个平台。最常见,直接打满后端并发。
  • 脉冲型:打一两分钟、停几分钟,反复循环。目的是让你的限速和封禁策略不停进出、掩护其他攻击。
  • 锯齿型:一小时内被削平几次又抬起来,说明攻击方在多批代理 IP 之间轮换,单 IP 封禁会被绕开。
  • 恒定型:全天保持同一条直线,深夜也不例外。这种最难缠,攻击者已经在做成本优化,把强度压在你的封禁阈值附近。

补一句:深夜高位的「恒定型」是定性 CC 最省事的证据。把同一时段的往日曲线摆在一起,形状对不对一眼就看得出。

Q:时间维度之外还要交叉看哪些指标?

三个交叉维度,能把误判率压到很低。

  • URL 集中度:真实流量的 Top 10 URL 一般只占总量 30% 上下;CC 攻击时单个 URL 占比常超过 60%。
# URL 集中度:Top 10 URL 的请求数与占比
grep "23/Sep/2026" /var/log/nginx/access.log | awk '{print $7}' \
  | sort | uniq -c | sort -rn > /tmp/url.txt
TOTAL=$(grep "23/Sep/2026" /var/log/nginx/access.log | wc -l)
TOP10=$(head -10 /tmp/url.txt | awk '{s+=$1} END{print s}')
awk -v t=$TOP10 -v a=$TOTAL 'BEGIN{printf "top10=%.1f%% of %d\n", t*100/a, a}'
head -10 /tmp/url.txt
  • 会话完整度:真实访客一定会请求 .css.js、图片。只请求 HTML、从不请求静态资源的请求占比突然抬升,是脚本访问的直接证据。
  • 响应时间:在日志里加上 rt=$request_time,真实流量平时响应时间是稳定的;被 CC 时后端被占满,P95 响应时间会明显抬升,而请求数未必同步上涨,这正是 CC 早于流量异常暴露的信号。
log_format detail '$remote_addr - $remote_user [$time_local] "$request" '
                  '$status $body_bytes_sent rt=$request_time uct="$upstream_connect_time" urt="$upstream_response_time"';
access_log /var/log/nginx/detail.log detail;

Q:能不能自动跑一个判别脚本?

可以。把三个指标固化成脚本,每天定时跑一次,输出异常等级即可。

#!/bin/bash
# cc-timing-check.sh —— 从时间分布判别 CC 迹象
LOG=/var/log/nginx/access.log
DAY=$(date -d yesterday +%d/%b/%Y)
grep "$DAY" $LOG > /tmp/day.log

TOTAL=$(wc -l < /tmp/day.log)
# 1) 深夜 02:00-05:00 的请求数
NIGHT=$(awk '{t=substr($4,14,2)+0; if(t>=2 && t<=5) n++} END{print n+0}' /tmp/day.log)
# 2) 峰值分钟与低谷分钟
PEAK=$(awk '{print substr($4,14,5)}' /tmp/day.log | sort | uniq -c | sort -rn | head -1 | awk '{print $1}')
LOW=$(awk '{print substr($4,14,5)}' /tmp/day.log | sort | uniq -c | sort -n | head -1 | awk '{print $1}')
# 3) 单 URL 最高占比
TOPURL=$(awk '{print $7}' /tmp/day.log | sort | uniq -c | sort -rn | head -1 | awk '{print $1}')

echo "total=$TOTAL night=$NIGHT peak_min=$PEAK low_min=$LOW ratio=$(awk -v p=$PEAK -v l=$LOW 'BEGIN{if(l>0) printf "%.1f", p/l}') top_url_ratio=$(awk -v t=$TOPURL -v a=$TOTAL 'BEGIN{printf "%.1f%%", t*100/a}')"

# 经验阈值:夜间占比 > 30% / 峰谷比 > 30 / 单 URL 占比 > 50% —— 命中两项即人工复核

脚本本身不封任何东西,只做判断并记录。误封的代价远高于漏过,所以第一层永远只输出结论,动作交给人确认或交给 WAF 规则执行。

Q:判成 CC 之后第一步做什么?

按「降低单次请求成本 → 抬高单位请求成本 → 切断来源」的顺序处理,别一上来就封 IP。

  1. 先给被打的 URL 上缓存或限速:把命中率提上去,攻击请求打不到后端就等于无效。限速只作用于被攻击的那个位置,整体限速会把真实用户一起限掉。
  2. 再抬高成本:对动态接口加人机验证或指纹校验,让攻击方必须投入更多资源。
  3. 最后才封来源:用 ipset 带 timeout 自动解封,避免手工维护黑名单把正常用户永久挡在外面。
# 只针对被攻击的搜索页单独限速,其他位置完全不受影响
limit_req_zone $binary_remote_addr zone=cc_search:10m rate=3r/s;

location = /search {
    limit_req  zone=cc_search burst=6 nodelay;
    limit_req_status 429;
    proxy_pass http://backend;
}

Q:还有什么补充建议?

  • 先存基线,再谈异常:把正常时段的分钟级曲线存档,最好按「工作日 / 周末」「有活动 / 无活动」分开存。没有基线,任何曲线看起来都像攻击。
  • 别把投放和活动当攻击:曲线变陡之前,先确认是否刚发了推广、上了推荐位、被大号转发。这三个动作形状和阶跃型 CC 很像。
  • 别把蜘蛛当 CC:搜索引擎蜘蛛也会集中请求同一批 URL。用 UA 加官方 IP 段双重确认,避免误伤收录。
  • 日志字段要提前加$request_time$upstream_response_time 事后无法补记,攻击发生时才发现缺字段就来不及了。
  • 把三项指标固化成看板:峰值分钟请求数、单 URL 最高占比、夜间请求占比。这三个数字比任何告警阈值都直观。