一、开场白:凌晨 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 空间,必须引入"时间窗口 + 分片"策略。
三、第一道闸:网关层的"零成本"拦截
-
正则黑名单
在 Nginx 层加入 Lua 脚本,校验 ID 时间戳是否在未来 1 天或早于 90 天前,直接返回 400。该规则零内存占用,拦截 80% 的伪造流量。
-
Token Bucket 限速
对"空结果"响应单独建桶,阈值设为正常用户均值的 5 倍,超过即滑块验证码。
-
负反馈标记
连续触发 3 次空结果的用户 Cookie 打标,后续 10 分钟所有请求降级到"只读缓存",禁止回源数据库。
四、第二道闸:Redis 侧的"分片布隆"
-
空间模型
120 亿 key 若用单层布隆,需要 14.4 Gb 内存,远超单机预算。采用"业务线分片 + 时间滚动"方案:
-
每个业务线独享一个 64 Mb 的 Bloom Filter;
-
以"天"为单位滚动,过期 90 天的 filter 直接丢弃;
-
总内存占用 = 业务线数量 × 64 Mb ≈ 1.1 Gb。
-
-
并发写入
内容发布时同步写 MySQL 与布隆过滤器,写过滤器使用 Redis 的
BF.ADD
,失败时通过 MQ 补偿,保证最终一致。 -
假阳性治理
假阳性概率 p=0.01%,每日误判 12 万请求。引入"二次确认"策略:过滤器返回存在时,先查 Redis,miss 后再查 MySQL;过滤器返回不存在时,直接返回 404。这样只有 12 万请求多一次 Redis 查询,成本可接受。
五、第三道闸:空值缓存的"动态 TTL"
-
分层 TTL
将空值缓存划分为 L1(Redis,30 秒)、L2(Caffeine 本地,5 秒)。L1 miss 后回源 DB,DB 返回空则写 L1 并携带 TTL=30 秒;L2 用于抗突发热点。
-
TTL 自适应
引入 PID 控制器:
-
输入:过去 1 分钟空结果 QPS 与数据库 CPU;
-
输出:TTL 在 5~300 秒之间动态调整;
-
目标:CPU 保持在 60% 以下,Nil Ratio 低于 2%。
实践表明,自适应后穿透峰值降低 92%,平均 TTL 收敛在 45 秒。
-
六、第四道闸:数据库侧的"最后一击"
-
空结果表
创建表
fake_id_log(id bigint primary key, gmt_create datetime)
,空结果写入该表,替代直接回主库。 -
合并写
使用 MySQL 的
INSERT IGNORE
批量 1000 条,减少行锁冲突。 -
异步回填
消费者发现空结果 ID 存在于正式表时,删除空结果记录并刷新缓存,闭环纠错。
七、演练与度量:如何证明防御有效
-
红蓝对抗
每月随机挑选 2 台云主机模拟攻击,工具可配置 QPS、ID 区间、随机度。对抗后输出三项指标:
-
拦截率 = 1 - 到达数据库的请求 / 总攻击请求;
-
误杀率 = 正常请求被 404 的比例;
-
恢复时长 = 从攻击结束到系统指标恢复常态的用时。
-
-
影子过滤器
生产环境并行运行一套"影子布隆",参数与正式完全一致,但只记录日志不拦截。对比两者指标,可量化假阳性漂移。
-
混沌工程
利用 ChaosBlade 随机下线 30% Bloom 节点,验证剩余节点能否承担流量;同时观测 TTL 自适应算法的收敛速度。
八、踩坑日记:三次血与泪的教训
-
Lua 正则回溯
早期使用
ngx.re.match
贪婪模式,遇到 128 位超长 ID 时 CPU 爆涨,改为ngx.re.find
非回溯后解决。 -
过滤器重建抖动
某次全量重建布隆时,采用
BF.LOADCHUNK
,因网络抖动导致 3 秒阻塞,引发雪崩。后改为"滚动双缓冲":新过滤器在后台构建,构建完成后原子替换。 -
PID 控制器震荡
初期 PID 参数激进,TTL 在 5 秒和 300 秒之间来回跳,造成缓存颠簸。引入一阶滞后滤波后,曲线平滑。
九、尾声:穿透的尽头是成本博弈
在 120 亿 key 面前,100% 拦截是不经济的。最终目标是把穿透概率压到"可忽略"区间,同时保证内存、CPU、人力成本线性可控。经过 6 个月迭代,平台 Nil Ratio 稳定在 0.7%,误杀率 0.05%,单条请求新增 RT 0.8 ms,全年节省数据库费用 120 万元。幽灵仍在,但已被关进笼子。