CC攻击怎么把缓存打穿?击穿与雪崩防护实战

CC 攻击常专挑「缓存不命中」的请求下手,等于人为制造一场持续的缓存击穿。本文用 9 个问答讲清击穿、雪崩、穿透的区别,怎么区分「被攻击」与「配置问题」,以及缓存键归一化、过期抖动、热点互斥锁等可直接落地的防护配置。

Q:CC 攻击和缓存击穿、缓存雪崩到底什么关系?

  • 缓存击穿:某个热点 key 刚过期的一瞬间,大量请求同时穿过缓存直接打到数据库。
  • 缓存雪崩:大量 key 在同一时刻集中过期,缓存层整体失效,请求全部涌向数据库。
  • 缓存穿透:请求查一个根本不存在的 key,缓存与数据库都没有,于是每次都直查数据库。
  • CC 攻击与三者的关系:攻击者不需要真的把缓存打坏,只要持续请求「缓存不命中」的那部分,就能人为制造一场持久的击穿。
  • 打动态接口、打带随机参数的 URL、打缓存本就未覆盖的路径,效果与缓存击穿完全一样:后端每个请求都要真实计算一次。

Q:为什么 CC 攻击专挑没有缓存的请求打?

  • 静态资源有 CDN 与浏览器缓存,打它性价比极低;攻击者的目标是让每个请求都真实到达后端。
  • 于是攻击特征里常见三条:请求集中在动态接口(搜索、列表、详情、下单)、URL 带随机或无意义参数(让缓存键永远不同)、几乎不请求任何静态资源。
  • 落到指标上就是:缓存命中率断崖式下降、回源率上升、后端 CPU 与数据库连接被吃满。

Q:怎么区分「缓存配错了」还是「被 CC 打了」?

# 1) 请求 URL 分布:看是否高度集中在少数动态接口
awk '{print $7}' /var/log/nginx/access.log | sed 's/?.*//' \
  | sort | uniq -c | sort -rn | head -20

# 2) 带 query 参数的比例(随机参数绕缓存的典型特征)
echo "with_args: $(grep -c '?' /var/log/nginx/access.log)"
echo "total:     $(wc -l < /var/log/nginx/access.log)"

# 3) 静态资源请求占比(攻击流量里这个比例极低)
grep -Ec '\.(js|css|png|jpg|jpeg|gif|webp|svg|woff2?)' /var/log/nginx/access.log
  • 判据:请求集中、带参数比例高、静态资源比例低,基本就是 CC 在绕缓存。
  • 反过来,如果请求分散、静态资源占大头,更像是缓存配置本身出了问题(缓存键不合理、TTL 太短、回源头被透传)。
  • 再看同一时刻的缓存命中率与回源率曲线:正常波动是平滑的,被打时是断崖,这个差值比绝对值更有说服力。

Q:攻击者用随机参数绕缓存,Nginx 层怎么接?

# 缓存键只使用白名单参数,随机参数被忽略,避免被刷出大量无效缓存对象
fastcgi_cache_path /var/cache/nginx levels=1:2 keys_zone=page:100m inactive=2h max_size=4g;
fastcgi_cache_key "$scheme$request_method$host$uri$arg_id$arg_page";

server {
    location ~ \.php$ {
        fastcgi_cache page;
        fastcgi_cache_valid 200 301 302 10m;
        fastcgi_cache_lock on;          # 同一 URL 并发只回源一次,防击穿
        fastcgi_cache_use_stale updating error timeout;
    }
}
  • 核心思路:缓存键归一化,让 ?id=1&rand=abc 与 ?id=1&rand=xyz 命中同一个缓存对象。
  • 如果用的是反向代理模式,把上面的 fastcgi_cache 换成 proxy_cache、fastcgi_cache_key 换成 proxy_cache_key 即可,思路一致。
  • 不要无脑「带参数的请求全部不缓存」:那等于把所有带参数的流量都送给后端,反而帮了攻击者。
  • 白名单里只放业务真正需要的分页、分类、ID 这类参数,其余一律不进缓存键。

Q:怎么防缓存雪崩(大批 key 同时过期)?

  • 三个抓手:过期时间加随机抖动、热点数据永不主动过期加后台定时刷新、缓存预热(发布或重启后先把热点数据打进缓存,别让第一个请求去硬扛)。
  • 没有把握时宁可把 TTL 设长,也不要设一个所有 key 同时到期的整点值。
  • 更进一步:把缓存分层(CDN 静态缓存、Nginx 页面缓存、应用层热点缓存),单层失效时还有下一层兜底。
# 后端示例(PHP):过期时间 = 基础时间 + 随机抖动,避免集体失效
$ttl = 3600 + random_int(0, 600);   // 60~70 分钟随机
apcu_store($key, $data, $ttl);

# Redis 侧:批量写入时给每个 key 单独设置带抖动的 TTL
for ($i = 0; $i < count($keys); $i++) {
    $redis->setex($keys[$i], 3600 + random_int(0, 600), $values[$i]);
}

Q:怎么防缓存击穿(热点 key 失效瞬间被打穿)?

  • Nginx 层的 fastcgi_cache_lock on(或 proxy_cache_lock on)能把同一 URL 的并发回源收敛成一个,是成本最低的一招。
  • 后端层用互斥锁或逻辑过期:热点 key 到期时不立刻重建,而是先返回旧值,同时只放一个请求去后台刷新。
  • 再补一层静态降级:热点接口命中失败时返回静态降级页,而不是把压力直接甩给数据库。
  • 最省心的做法:热点 key 设置成「不过期加后台定时更新」,从根上避开击穿窗口。

Q:缓存服务本身被 CC 打挂怎么办?

  • CC 也能直接打缓存服务(Redis / Memcached):连接数被打满、大 key 阻塞、网络带宽被吃光。
  • 先自检,再谈防护:
redis-cli info clients | grep connected_clients
redis-cli info stats  | grep -E 'rejected_connections|expired_keys'
redis-cli --hotkeys
redis-cli slowlog get 10
  • 防护要点:给缓存服务设最大连接数与超时、对访问缓存的入口做限速、热点 key 打散到多个副本、把大 key 拆小。
  • 别忘了缓存服务自身的认证与网络隔离——未授权访问等于把缓存直接交到攻击者手里。

Q:这些规则上线会不会误伤正常用户?

  • 参数白名单是最容易踩的坑:新增业务参数忘了加白,会导致该接口拿不到正确缓存甚至返回异常,上线前必须对照接口清单逐项核对。
  • TTL 拉长会带来数据延迟,涉及库存、余额、状态类的页面不能用长缓存,这类接口只做限速不做缓存。
  • 建议先在观察模式跑一周:只记录命中与未命中、统计参数分布,确认无误杀后再切换成生效状态。
  • 所有限流与缓存规则都要能一键回滚,别让性能优化变成新的故障源。

Q:还有什么补充建议?

  • 把缓存当成第一道防线,而不是单纯的性能优化:命中率高的站点,CC 的攻击成本会被大幅抬升。
  • 监控盯四个量:缓存命中率、回源率、动态接口响应时间、单个 URL 请求占比,四项一起看才能区分攻击与抖动。
  • 分层部署:CDN 缓存静态资源、Nginx 缓存可缓存的动态页、应用层缓存热点数据,别指望单层扛住。
  • 一句话总结:CC 攻击的本质是「逼你每次请求都真算一遍」,而缓存正是把这次计算省下来的东西;守住缓存键归一化、过期时间加抖动、热点 key 互斥锁这三件事,就等于把攻击者最想打开的那个缺口重新焊上。