日 PV 1500 万、CDN 不可用:一套四级漏斗在源站消化全部流量

我们电子书产品的模板中心有个海外站点,模板页日访问约 1500 万,峰值 QPS 一千出头。这个页面最特殊的地方在于:CDN 基本帮不上忙,所有流量都必须源站自己消化。这篇文章讲讲为什么,以及我们怎么做的。

为什么 CDN 缓存不了

模板列表页的 URL 由 7 个筛选维度组合而成:

维度 取值数
语言 28
等级 4
分类 30
排序 3
主题 120
颜色 65
风格 70

排列组合约 55 亿 URL。其中有效 URL(有实际模板数据的组合)约 100 来万,剩下的是海量长尾组合。

CDN 的命中模型建立在"热门资源集中"的前提上,55 亿的组合空间让命中率趋近于零——缓存里永远存不住下一个请求要的 URL。加上页面数据是动态的(浏览数/使用数/排序实时变)还支持模糊搜索,连"缓存住一个版本"都不可行。

另外还有个数据点很有意思:1500 万日访问里 90% 集中在 4-5 个小时,归因是爬虫不规范爬取——大量请求根本不是真实用户。

所以架构命题变成:全部流量回源,源站要扛住,还要快

四级漏斗

请求 → [L1 内存路由字典校验] --非法拼接→ 直接 404(不碰任何存储,~1ms)
     → [L2 ES 查询 10-20ms](match + function_score)
     → [L3 本地 Guava 多语言标签缓存]
     → [L4 stale-while-revalidate:先返旧值 + 子线程抢分布式锁异步刷新]
     → 响应(服务端共 80-120ms,总响应 300-400ms,其余是网络耗时)

L1:内存路由字典——被爬虫教育出来的一层

各维度所有合法取值组成路由字典存内存。请求先解析路径中的过滤规则,任何维度取值不在字典内的,直接 404。

这一层是整个架构里"性价比"最高的一层:

  • 校验成本只有一次内存查找,约 1ms
  • 爬虫拼凑的非法链接在碰任何存储之前就被挡掉
  • 高峰期大量请求是不规范路由,这一层消化了它们

它是被逼出来的:上线前压测发现大量随机拼接的 URL 打进来,ES 首当其冲。加了这个纯内存的合法值校验后,存储层压力断崖式下降。

L2:ES 查询(10-20ms)

汇总分类条件查 ES。搜索是混合方案:维度条件 match 查询 + function_score 自定义评分脚本——运营可以对指定模板加减分调排序权重,实现推荐位调控。数据量 3000+,评分脚本无性能问题。

L3:本地 Guava 多语言标签缓存

ES 结果不能直接返回——28 种语言下同一维度表达不同("商务"在每种语言里文案不同),需要查每个模板的多语言标签。

基于 Guava Cache 做:最大缓存数量 + 过期时间均可配,框架级淘汰。数据结构上每个模板一个 Map(key=语言标识,value=该语言下维度信息),另一个 Map 记录缓存时间——数据与元数据职责分离

选本地缓存而不是 Redis 的原因:标签数据读多写少、量级可控(3000 模板 × 28 语言),本地缓存省一次网络往返,这 1-2ms 在每层都要抠的总预算里值得省。

L4:stale-while-revalidate——防击穿的关键

读取时发现缓存超过 10 分钟 → 先返回旧数据(用户无感),同时子线程抢 Redis 分布式锁、读最新数据回填并更新缓存时间。

两个设计点:

  • 锁保证多实例下同一模板不被并发重复刷新(防 dogpile):3 个实例同时发现同一份缓存过期,只有一个能抢到锁去刷新,其他实例的请求直接拿旧值返回——避免了"过期瞬间大量请求同步打 DB"的经典缓存击穿
  • 可用性与新鲜度解耦:用户命中的哪怕是过期缓存,响应依然是毫秒级,刷新在背后完成

TTL 10 分钟替代主动失效

多语言标签缓存用 10 分钟短 TTL,后台改动最长 10 分钟生效——省掉了整条"主动失效"链路

主动失效意味着:后台改文案 → 发消息(或广播)→ 各实例逐份失效缓存。要引入消息组件、要处理失效消息丢失、要考虑实例重启后的状态。对一个"模板文案改一下"级别的需求,这条链路的复杂度不成比例。

10 分钟 TTL 的代价是数据最长旧 10 分钟——对模板标签这种非关键数据,完全可接受。

动态计数与数据同步

两个配套设计:

浏览数/使用数:每次浏览/套用 Redis INCR,不做 DB 实时写;每小时定时脚本算增量(对比上次入库值的差值)写回。非清零方式——天然避免"清零瞬间新自增丢失"的竞态,脚本延迟也不丢数据。

ES 同步双保险:修改时实时推送 + 每小时兜底扫描(每条记录带"最后推送时间"字段对账)。实时链路漏掉的由兜底补齐,最终一致。

结果

  • 服务端处理 80-120ms(总响应 300-400ms,大头是网络)
  • 1 实例扛住上线,流量上来后扩到 3 实例,零代码改动——全链路按多实例设计:分布式锁、无本地强状态
  • 3 台 8 核 16G 机器支撑日 PV 1500 万

回头看,这个架构没有一层是"高科技",每一层都在解决一个具体问题:路由字典挡非法流量、本地缓存省网络往返、SWR 防击穿。高并发架构往往是把简单的东西放对位置,而不是堆复杂组件