在百亿流量面前,让“不存在”无处遁形——Redis 缓存穿透的极限攻防实录

一、开场白:凌晨 01:47 的"幽灵峰值"

2024 年 3 月 18 日,某头部内容平台的监控大屏突然飘红:网关 502 比例从 0.01% 蹿至 17%,Redis 集群命中率仍保持 99.3%,数据库 CPU 却逼近 95%。值班同学第一反应是"有热点 key 过期",但查看 Keyspace 后发现过期事件寥寥。十分钟后,根源浮出水面:攻击者利用"内容 ID 自增 + 随机偏移"的方式,瞬时灌入 2300 万个根本不存在的 ID,导致缓存穿透。本文以这次真实事件为蓝本,拆解百亿流量场景下穿透攻防的极限实践。

二、业务画像:内容 ID 的结构化特征

平台内容 ID 为 64 位 long,高 24 位是业务线,中 24 位是时间戳(天级),低 16 位是随机序列。合法 ID 总量约 120 亿,但每日新增仅 8000 万。攻击者只需在时间戳区间外随机生成,即可 100% 命中"不存在"。该特征决定了传统布隆过滤器难以覆盖全部 key 空间,必须引入"时间窗口 + 分片"策略。

三、第一道闸:网关层的"零成本"拦截

  1. 正则黑名单

    在 Nginx 层加入 Lua 脚本,校验 ID 时间戳是否在未来 1 天或早于 90 天前,直接返回 400。该规则零内存占用,拦截 80% 的伪造流量。

  2. Token Bucket 限速

    对"空结果"响应单独建桶,阈值设为正常用户均值的 5 倍,超过即滑块验证码。

  3. 负反馈标记

    连续触发 3 次空结果的用户 Cookie 打标,后续 10 分钟所有请求降级到"只读缓存",禁止回源数据库。

四、第二道闸:Redis 侧的"分片布隆"

  1. 空间模型

    120 亿 key 若用单层布隆,需要 14.4 Gb 内存,远超单机预算。采用"业务线分片 + 时间滚动"方案:

    • 每个业务线独享一个 64 Mb 的 Bloom Filter;

    • 以"天"为单位滚动,过期 90 天的 filter 直接丢弃;

    • 总内存占用 = 业务线数量 × 64 Mb ≈ 1.1 Gb。

  2. 并发写入

    内容发布时同步写 MySQL 与布隆过滤器,写过滤器使用 Redis 的 BF.ADD,失败时通过 MQ 补偿,保证最终一致。

  3. 假阳性治理

    假阳性概率 p=0.01%,每日误判 12 万请求。引入"二次确认"策略:过滤器返回存在时,先查 Redis,miss 后再查 MySQL;过滤器返回不存在时,直接返回 404。这样只有 12 万请求多一次 Redis 查询,成本可接受。

五、第三道闸:空值缓存的"动态 TTL"

  1. 分层 TTL

    将空值缓存划分为 L1(Redis,30 秒)、L2(Caffeine 本地,5 秒)。L1 miss 后回源 DB,DB 返回空则写 L1 并携带 TTL=30 秒;L2 用于抗突发热点。

  2. TTL 自适应

    引入 PID 控制器:

    • 输入:过去 1 分钟空结果 QPS 与数据库 CPU;

    • 输出:TTL 在 5~300 秒之间动态调整;

    • 目标:CPU 保持在 60% 以下,Nil Ratio 低于 2%。

      实践表明,自适应后穿透峰值降低 92%,平均 TTL 收敛在 45 秒。

六、第四道闸:数据库侧的"最后一击"

  1. 空结果表

    创建表 fake_id_log(id bigint primary key, gmt_create datetime),空结果写入该表,替代直接回主库。

  2. 合并写

    使用 MySQL 的 INSERT IGNORE 批量 1000 条,减少行锁冲突。

  3. 异步回填

    消费者发现空结果 ID 存在于正式表时,删除空结果记录并刷新缓存,闭环纠错。

七、演练与度量:如何证明防御有效

  1. 红蓝对抗

    每月随机挑选 2 台云主机模拟攻击,工具可配置 QPS、ID 区间、随机度。对抗后输出三项指标:

    • 拦截率 = 1 - 到达数据库的请求 / 总攻击请求;

    • 误杀率 = 正常请求被 404 的比例;

    • 恢复时长 = 从攻击结束到系统指标恢复常态的用时。

  2. 影子过滤器

    生产环境并行运行一套"影子布隆",参数与正式完全一致,但只记录日志不拦截。对比两者指标,可量化假阳性漂移。

  3. 混沌工程

    利用 ChaosBlade 随机下线 30% Bloom 节点,验证剩余节点能否承担流量;同时观测 TTL 自适应算法的收敛速度。

八、踩坑日记:三次血与泪的教训

  1. Lua 正则回溯

    早期使用 ngx.re.match 贪婪模式,遇到 128 位超长 ID 时 CPU 爆涨,改为 ngx.re.find 非回溯后解决。

  2. 过滤器重建抖动

    某次全量重建布隆时,采用 BF.LOADCHUNK,因网络抖动导致 3 秒阻塞,引发雪崩。后改为"滚动双缓冲":新过滤器在后台构建,构建完成后原子替换。

  3. PID 控制器震荡

    初期 PID 参数激进,TTL 在 5 秒和 300 秒之间来回跳,造成缓存颠簸。引入一阶滞后滤波后,曲线平滑。

九、尾声:穿透的尽头是成本博弈

在 120 亿 key 面前,100% 拦截是不经济的。最终目标是把穿透概率压到"可忽略"区间,同时保证内存、CPU、人力成本线性可控。经过 6 个月迭代,平台 Nil Ratio 稳定在 0.7%,误杀率 0.05%,单条请求新增 RT 0.8 ms,全年节省数据库费用 120 万元。幽灵仍在,但已被关进笼子。

相关推荐
时光追逐者32 分钟前
一款免费、简单、高效的在线数据库设计工具
数据库·mysql·oracle·sql server
CDN36032 分钟前
CDN 缓存不生效 / 内容不更新?7 种原因 + 一键刷新方案
运维·网络安全·缓存
another heaven33 分钟前
【软考 2026 最新版 NoSQL 数据库全分类】
数据库·nosql
满天星830357734 分钟前
【MySQL】表的操作
linux·服务器·数据库·mysql
yashuk39 分钟前
Ubuntu 系统下安装 Nginx
数据库·nginx·ubuntu
F1FJJ44 分钟前
VS Code 里管理 PostgreSQL,有哪些选择?主流扩展横向对比
网络·数据库·postgresql·容器
Bdygsl1 小时前
MySQL(8)—— 事务
数据库·mysql
IvorySQL1 小时前
直播回顾| PostgreSQL 18.3 x IvorySQL 5.3:开启 AI 数据库新纪元
数据库·postgresql·开源
编程之升级打怪1 小时前
数据库的实时同步和异步同步
数据库
清水白石0081 小时前
《Python 性能优化实战:多进程并行 vs C/Rust/Cython 扩展的对比决策与团队落地指南》
python·spring·缓存