一个响应头,就能把同一资源拆成几十份:Cloudflare 出手治理缓存抖动
背景:Vary 为什么一直被 CDN 冷落
按标准 HTTP 语义,Vary 响应头的作用是告知下游缓存:源站在生成响应前检查了哪些请求头,缓存必须按这些头的取值分别存储变体。问题出在"不经处理便直接遵循"——客户端请求头中细微的语法差异,例如空格、大小写、客户端首选语言优先级顺序不同,都可能把同一资源拆分成数十个变体。
Cloudflare 的一项实证审计覆盖了约 5 万个热门域名、超过 1.2 亿条 HTTP 响应,发现近 3000 个源站会根据四个或更多请求头生成不同响应,极端情况下涉及数十个不同字段。不受控制的请求头差异直接造成缓存抖动:命中率暴跌、源站负载上升。
两阶段架构:协商与缓存键计算解耦
Cloudflare 的思路是把内容协商拆成两个运行阶段:
- 阶段一(源站):源站服务器继续通过
Vary声明哪些请求头会影响响应生成,语义不变。 - 阶段二(边缘):Cache Rules 决定边缘节点在计算变体键之前,如何处理、规范化或忽略这些请求头的值。
底层实现上,Cloudflare 将源站声明的 Vary 字段与基础缓存键一同记录。基础缓存键由协议、主机、路径组成。后续请求先查询基础缓存键,若该键已登记响应变体,边缘节点就提取相应请求头,按指定 Cache Rule 计算并返回匹配变体;若无变体符合规范化后的条件,请求回源,源站响应随后被编入变体池索引。
Cache Rules 的三种操作
在 Cache Rules 中为 Vary 配置处理方式时,可为每个请求头分别定义行为,并为未列出的字段设置 default_action 兜底。引擎提供三种操作:
- 规范化(normalize):将
Accept、Accept-Language等复杂协商头转换为标准化的等价类别,消除客户端之间无关紧要的差异。这是治理碎片化的主力操作。 - 原样传递(pass_through):存储精确且区分大小写的字符串,适用于源站应用依赖精确 token 匹配的场景。
- 绕过缓存:对匹配易变或高基数请求头的请求完全跳过边缘缓存,确保不确定的响应变体不占用边缘容量。
配置示例
源站通过标准响应头声明动态依赖:
HTTP/1.1 200 OK Content-Type: application/json Cache-Control: public, max-age=3600 Vary: Accept, Accept-Language
对应的 Cache Rule(Ruleset Engine JSON)对协商头做规范化,未知变体走原样传递兜底:
{ "expression": "(http.request.uri.path eq \"/api/catalog\")", "action": "set_cache_settings", "action_parameters": { "cache": true, "vary": { "headers": [ { "name": "accept", "action": "normalize" }, { "name": "accept-language", "action": "normalize" } ], "default_action": "pass_through" } } }
与旧方案的对比
产品经理 Alex Krivit 和系统工程师 Zaidoon Abd Al Hadi 强调,两阶段架构解决了手动变通方案的长期局限。此前的选项包括:禁用缓存、依赖专用 Cloudflare Workers、维护脆弱的自定义缓存键(需在看到源站响应前预判相关请求头)、或使用 Vary for Images 等专用扩展。
关键差异在于触发时机:自定义缓存键会预先针对传入请求计算规则,即使源站返回的是静态且不变的响应,这些规则仍然生效;而 Cache Rules 中的 Vary 规则只在源站响应包含 Vary 头时动态触发,避免为不变的响应无谓扩张缓存键空间。
使用注意
- 对高熵请求头启用完全原样传递前,必须评估缓存键基数:若原样传递的头包含任意 token 或会话标识符,每个不同的客户端字符串都会生成一条独立的边缘缓存记录,缩短缓存生命周期并加剧淘汰抖动。
- 该能力目前已面向所有 Free、Pro、Business 和 Enterprise 区域开放。
Zero 的看法
- 这是把 RFC 7231 的 Vary 语义做工业化补强。标准只规定"按请求头分变体",从不关心规范化,等于把协商的最后一段留给下游实现——Cloudflare 的方案让 CDN 从"被动遵循"变成"主动治理",键空间控制权第一次交还到运营方手里。
- 两阶段设计真正漂亮的是"响应驱动"(live rule)替代"请求预判"(dead rule):旧自定义缓存键的痛点是静态响应也被迫走规则,现在只有真正协商的响应才占用变体空间,工程上干净得多。
- normalize 的本质是把语法差异折叠成语义等价类,这和构建工程里把 browserslist 查询折叠成构建 target 的思路同源——用有损耗但可控的归约换缓存效率。但代价是等价类别是平台黑盒,多 CDN 架构下行为未必一致,需要自行对齐。
- pass_through + 高基数头仍是 footgun:工具给了,基数爆炸的责任回到运营方。文章提醒到位,但实际踩坑的案例大概率还会出现。
- 对前端架构的现实意义:内容协商(Accept-Language、客户端协商等)可以重新作为一等公民进入设计,而不是被迫退回 URL 版本化或 cookie 分发的 workaround。React 背景的工程师在处理 i18n / 主题 / API 表示协商时,值得重新评估这套原生路径。