We just shipped support for the ugliest part of HTTP: Vary
TL;DR · AI 摘要
We just shipped support for the ugliest part of HTTP: Vary Cloudflare Blog post Pingora Cache CDN Performance September...
核心要点
- 主题聚焦:We just shipped support for the ugliest part of
- 来源:The Cloudflare Blog,建议结合原文判断细节。
- AI 分析暂不可用,本条为保底评分与摘要。
我们刚刚推出了对 HTTP 最丑陋部分的支持:Vary | Cloudflare 博客
post
Pingora
缓存
CDN
性能
2026年9月22日
我们刚刚推出了对 HTTP 最丑陋部分的支持:Vary
Alex Krivit 和 Zaidoon Abd Al Hadi
12 分钟阅读
复制链接
响应头 Vary 被称为“我们尚未改进的 HTTP 最丑陋部分”。同一篇文章将其描述为一种“可怕且笨拙的机制”,在中介设备之间具有“相当糟糕的互操作性”。通常,理性的工程师此时会举手后退。
这并不完全是对 Vary 的背书,但丑陋并不意味着无用。
一个 URL 可能有多个正确的响应。例如,服务器可能会向不同浏览器发送不同格式的图片。如果缓存忽略 Vary,它可能会向请求发送错误的数据。但如果缓存将每个原始请求头值视为唯一,那么相似的请求可能会导致成千上万条几乎无法复用的缓存条目。Vary 告诉缓存哪些请求字段可能影响响应,但它不会告诉缓存哪些差异真正重要。
现在,所有计划的缓存规则都支持 Vary。源站仍然声明可能影响响应的请求头字段,但你可以决定 Cloudflare 如何处理每个字段。你可以规范化已知的协商头字段,当细微差异重要时传递精确值,或在变化过于不可预测时绕过缓存。源站声明哪些内容可能变化,而你决定哪些变化对缓存来说真正有意义。
Vary 的工作原理
Vary 是一个标准的 HTTP 响应头,用于告诉中介缓存(如 Cloudflare)哪些请求字段可能影响源站返回的响应。网站使用 Vary 来根据同一 URL 提供不同语言、图片格式、压缩方案或区域内容。
假设有一个 URL 可以生成两个有效表示。浏览器请求一个网页:
GET /catalog HTTP/1.1
Host: example.com
Accept: text/html源站返回 HTML,并将 Accept 标识为可能影响响应的字段:
HTTP/1.1 200 OK
Content-Type: text/html
Cache-Control: public, max-age=3600
Vary: AcceptAPI 客户端可以用不同的偏好请求相同 URL:
GET /catalog HTTP/1.1
Host: example.com
Accept: application/json这次正确的响应是 JSON。Vary: Accept 头告诉缓存,仅凭 URL 无法在响应之间做出选择,还必须考虑请求的 Accept 值。
如果没有 Vary,缓存中先存储的响应可能会同时被发送给两个客户端。如果 HTML 被优先存储,API 客户端会收到 HTML 标记,其 JSON 解析器会失败。如果 JSON 被优先存储,期望网页的浏览器会收到 API 响应。
Vary 防止缓存向请求客户端发送错误的响应。但它引入了一个更难回答的问题:当两个请求包含不同的头值时,它们是否真的需要不同的响应?
正确缓存变得无用时
Vary 可以告诉缓存哪些请求字段可能影响响应,但它不会告诉缓存响应代表什么。例如,假设一个源站仅提供英文、法语和德语内容。一个客户端可能会发送:
Accept-Language: en-US, fr;q=0.8而另一个客户端可能会请求:
Accept-Language: fr;q=0.8, en-GB这两个请求在此处都偏好英文。源站的响应可能会将这两个请求映射到完全相同的英文响应。但比较原始值的缓存无法安全地假设它们是等效的。它们的顺序和语言标签不同(而源站并不区分这些差异)。因此,即使响应体包含相同的字节,缓存可能会将它们存储为不同的变体。
这就是 Vary 的核心问题。应用程序通常会从大量可能的请求值中生成少量、有限的表示形式。源站了解数千种语言偏好会合并为三种支持的语言,而缓存通常无法做到这一点。
当响应根据多个字段变化时,这个问题会进一步加剧。一个字段的十个可能值会生成十个变体。三个字段的十个值可以生成 1,000 种组合。实际的请求头可能具有更大的基数:User-Agent 值众多,Cookie 可能对每个访客都是唯一的,而偏好头可能在顺序、格式(空格和制表符有影响!)和质量值上存在差异。
结果是一个可能完全正确但几乎永久处于冷状态(从未被重用)的缓存。相同的响应可能分散在接收流量过少而无法保持热状态和缓存中的条目中。它们会消耗容量、相互驱逐、降低缓存命中率,并将更多请求发送回源站。驱逐操作可以移除冷条目,但无法仅仅因为响应相同就将它们合并。
对近 50,000 个流行网站的 1.2 亿多个响应进行分析后发现,几乎有 3,000 个网站在四个或更多字段上使用 Vary。其中一些网站在 10、23 或甚至 47 个字段上使用 Vary。我们希望确保客户拥有在适当情况下使用 Vary 的工具,但又不至于导致缓存变得毫无用处。
某些高基数的变体是故意设计的。CDN 或反向代理可能会注入值(例如地理区域),以可预测的方式对内容进行分区。当可能的值受控且每个组件都对其含义达成一致时,这种方法是有效的。没有这些限制时,缓存会碎片化为可能永远无法重用的变体。
这就是我们需要解决的设计问题,以支持 Vary。我们需要保留足够的变体以提供正确的响应,同时避免请求之间的偶然差异破坏缓存效率。
缓存规则如何控制 Vary
Cloudflare 客户之前已有几种处理类似 Vary 的协商内容的方法。他们可以绕过缓存并让源站自行处理,或在自定义缓存键或其他规则中复制源站的协商逻辑,使用 Worker,或使用图像的 Vary 等功能。
这些选项仍然有用,但它们要么放弃缓存,要么复制应用程序逻辑,要么需要编写额外代码,要么仅适用于更狭窄的使用场景。缓存规则中的 Vary 可能通过将支持拆分为两个决策,在这些现有功能之间填补空白:
- 源站使用 Vary 来标识可能影响响应的请求头。
- 缓存规则决定 Cloudflare 如何处理每个头的值。
缓存规则不会强制每个响应都使用 Vary。如果源站未返回 Vary,Cloudflare 会正常缓存响应,尽管规则可能在将请求转发给源站之前仍然重写 Accept 和 Accept-Language。
当源站返回 Vary 时,Cloudflare 会根据每个标头配置的操作进行处理。未单独设置的标头会使用规则的默认操作。可用的三种操作如下:
操作
Cloudflare 的处理方式
推荐使用场景
规范化
在选择缓存变体前对请求标头进行规范化处理,使等效请求共享缓存响应。对 Accept、Accept-Language 和 Accept-Encoding 应用特定规则。对于其他标头,会去除可选空格并按原始顺序合并重复的标头行,保留大小写和内部空格。
适用于需要协商的标头,当大量请求值映射到少量响应时作为起点。
直通模式
使用请求标头的原始字节进行缓存匹配,保留大小写、空格、顺序和重复值。如果标头出现在多行,Cloudflare 会按顺序用逗号合并这些行用于缓存匹配。直通模式不会修改传出的标头行。当禁用“尊重强ETag”时,Cloudflare 仍可能重写 Accept-Encoding。
适用于值受控的标头,精确值会影响响应。
绕过
当源站将该标头列入 Vary 时,不会存储响应。现有缓存条目不会被删除,因此需要清除时请手动清除。
适用于个性化、高基数或不可预测的标头,如 Cookie 或 User-Agent。
我们建议将规范化作为默认操作。对于包含个人或无界值的单个标头,请使用绕过。当精确值影响响应时,请使用直通模式。
例如,直通模式会保留大小写、空格、顺序和重复值的差异,即使源站将它们视为等效。使用 Vary: X-View 和直通模式时,这三个值会产生不同的缓存键:
X-View: compact,full
X-View: compact, full
过多的偶然差异会使可复用的响应变成缓存中的多个独立变体。
无论配置何种操作,Vary: * 始终会绕过缓存。这意味着请求的任何方面(甚至HTTP消息之外的信息,如客户端IP地址)都可能影响源站选择的响应。因此,Cloudflare 在不联系源站的情况下无法复用该响应。
响应如何通过缓存
让我们跟踪前面示例中的 /catalog 请求在 Cloudflare 中的处理过程。
首次请求时,Cloudflare 没有该资源的存储 Vary 数据,因此缓存查找失败。匹配的缓存规则可以在 Cloudflare 联系源站前对配置字段进行规范化处理。
这可能发生在 Cloudflare 确认最终响应是否包含 Vary 之前。缓存规则定义允许的规范化范围;响应随后决定这些字段是否成为缓存变体的一部分。
这种顺序非常重要。如果 Cloudflare 将多个原始值归类到一个规范化缓存键下,但源站仍接收这些原始值,源站可能会生成不同的响应,而缓存后续会认为这些响应是可互换的。传递规范化值可确保源站选择与缓存匹配保持一致。
源站返回:
Vary: Accept, Accept-Language
Cloudflare 会记录这些标头名称,并将响应作为缓存变体存储。根据缓存规则处理后的标头值,会将此变体与其他相同资源的变体区分开。
当另一个请求到达 /catalog 时,Cloudflare 会从资源的基础缓存键开始:通常是 URL 加上其他配置的键字段。然后它会读取存储的 Vary 字段,并将新请求的这些头部应用到缓存规则中,以识别匹配的缓存变体。
假设这些字段归一化为:
Accept: text/html Accept-Language: en,fr
Cloudflare 会使用这些值直接查找匹配的缓存变体,而不会逐个将请求与所有存储的变体进行比较。
如果存在匹配且有效的缓存变体,此次请求即为缓存命中。如果没有匹配项,Cloudflare 会将请求发送到源站,并可能将响应结果存储为另一个变体。
源站响应完成循环。对于 Vary 中列出的每个头部,Cloudflare 会使用为该头部配置的操作,或如果该头部未单独列出则使用规则的默认操作:
- 如果不包含 Vary,Cloudflare 会正常缓存该响应。
- 如果所有列出的头部都归一化为“通过”(passthrough),Cloudflare 可以将响应存储为缓存变体。
- 如果任何列出的字段使用“绕过”(bypass),Cloudflare 不会存储该响应。
- 如果响应包含 Vary: *,Cloudflare 不会存储该响应。
这给源站带来了重要责任。每个可基于请求字段产生差异的可缓存响应,都必须一致返回适当的 Vary 头,包括错误和备用响应。如果某个响应省略了该头,Cloudflare 可能会缓存该响应,而缺少必要的差异信息来保持其隔离性。
图中的缓存键是概念性的。后续请求假设缓存响应是新鲜的。
任何针对缓存资源的清除操作都会覆盖其所有 Vary 变体。清除自定义缓存键的现有要求仍然适用。
更改 Vary 配置不会自动清除现有内容。新策略可能会生成不同的缓存键:请求可能在新键下未命中并重新填充,而旧条目会持续存在直到过期或被清除。
归一化确保等效请求共享同一缓存
还记得上面请求英文和法语内容的示例吗?
Accept-Language: en-US, fr;q=0.8 Accept-Language: fr;q=0.8, en-GB
这两个请求都优先选择英文,但“通过”(passthrough)会将它们视为不同变体。如果缓存规则允许 en、fr 和 de,归一化会将两者都简化为 en、fr,从而共享同一个缓存响应。
为此,Cloudflare 会将 Accept、Accept-Language 和 Accept-Encoding 中的值转换为小写,然后按质量值(quality value)从高到低排序,质量值相同则按字母顺序排序。客户端的排序顺序因此不会影响缓存键。排序后,Cloudflare 会从质量值非零的条目中移除参数。在缩短语言标签或过滤到配置的格式和语言时,也可能丢失 q=0(“不可接受”)的条目。例如,en-US;q=0 可能会简化为 en。如果源站需要查看这些排除项,请对 Accept 或 Accept-Language 使用“通过”(passthrough)。
您还可以配置规则,仅保留 Accept 和 Accept-Language 中指定的媒体类型或语言。区域语言标签(如 en-US)会归一化为基本语言 en,除非配置了完整标签。这使您能够将归一化与源站实际提供的格式和语言对齐。
为了使源站选择与缓存匹配保持一致,Cloudflare 会将标准化后的 Accept 和 Accept-Language 值转发至源站。当启用 "Respect Strong ETags" 时,也会转发标准化后的 Accept-Encoding 值。其他头部字段仅在缓存匹配时进行标准化处理。
在缓存规则中配置 Vary
在 Cloudflare 控制台中,进入 Caching > Cache Rules,创建或编辑规则,使响应具备缓存资格,并添加 Vary 设置。设置默认行为后,添加源站预期使用的头部字段名称。
通过 Rulesets API 的 http_request_cache_settings 阶段也可使用相同配置。默认设置会对源站在 Vary 中声明但未单独配置的头部字段选择回退操作。
此示例将 Accept 和 Accept-Language 标准化为配置的格式和语言集合。默认标准化操作也适用于 Vary 中声明的其他头部字段:
{
"rules": [
{
"ref": "vary_negotiated_content",
"description": "缓存边界协商表示",
"expression": "(http.host eq \"example.com\" and http.request.uri.path eq \"/catalog\")",
"action": "set_cache_settings",
"action_parameters": {
"cache": true,
"vary": {
"default": {
"action": "normalize"
},
"headers": {
"accept": {
"action": "normalize",
"media_types": ["text/html", "application/json"]
},
"accept-language": {
"action": "normalize",
"languages": ["en", "fr", "de"]
}
}
}
}
}
]
}这是发送至 http_request_cache_settings 阶段入口点的 PUT 请求完整请求体。PUT 操作会替换该入口点中的所有规则。如果已有缓存规则,请将其包含在 rules 数组中,或改用对应的单规则创建/更新操作。
如果源站为每种媒体类型和语言组合提供单一表示,将产生六种内容组合。这不会将缓存限制为六个键。优先级顺序、缺失头部以及标准化为空值的字段都可能生成更多缓存键。请保持支持的集合规模较小,并明确定义规则边界。部署后,使用不同头部值测试相同 URL,确保它们标准化为相同缓存变体。从同一客户端发送测试请求,确认返回预期格式和语言,并检查 CF-Cache-Status。缓存填充后查找命中记录,调查持续未命中或意外绕过响应。
关于限制、更多示例以及如何在 Terraform 中设置,请参阅 Vary 文档。
为什么不使用自定义缓存键?
到目前为止,一个显而易见的问题是:「为什么不将 Accept 和 Accept-Language 添加到自定义缓存键中?」
当这些字段始终是资源标识的一部分时,这样做是可行的。但自定义缓存键会将配置的维度添加到所有受规则覆盖的响应中,无论源站是否使用这些字段。
Vary 是响应驱动的,但相同基础键下的可缓存响应需要一致的 Vary 字段集合。
当请求属性始终定义资源时使用自定义缓存键。当源站在所有可缓存响应中声明相同的请求字段集合时使用 Vary。除非有意为之并经过测试,否则避免在两者中重复使用相同头部。
今天就在缓存规则中使用 Vary!
Vary 帮助解决一个显而易见的问题:一个 URL 可能存在多个正确的响应。但它也让缓存面临更复杂的难题——哪些请求差异实际上会影响响应?源服务器知道它能提供哪些响应,而缓存需要知道哪些请求可以复用每个响应。
缓存规则中的 Vary 将这两个视角联系起来。源服务器标识可能影响响应的请求字段,你可以决定是否对值进行规范化、使用直通方式处理精确差异,或者将响应排除在缓存之外。
Vary 从未因为形式上的复杂而失去实用性。但手动配置支持的格式和语言可能并不适合所有应用。我们正在评估是否可以借鉴已过期的 Availability Hints 草案中的思路,通过让源服务器直接描述其提供的表示形式来减少相关工作。
缓存规则中的 Vary 今天已通过 Cloudflare 控制台、Rulesets API 和 Terraform 在 Free、Pro、Business 和 Enterprise 计划中提供。
目录列
讨论列
相关标签及社交媒体链接
相关标签
在社交媒体上关注
- Cloudflare
- Alex Krivit
电子邮件订阅