← 文章 / 云原生与基础设施
Hacker News 1小时前 · 2026-09-24 12:24:12 · 0 阅读

我们刚刚上线支持 HTTP 中最丑陋的部分:Vary

响应头 Vary 被称为“HTTP 中至今仍未得到改进的最丑陋的部分”。同一篇文章还把它形容为一套“糟糕、笨拙的机制”,在各类中间节点之间“互操作性相当差”。通常,明智的工程师看到这里就会举起双手、慢慢后退了。

这算不上对 Vary 的好评,但丑不代表没用。

同一个 URL 可能存在多个正确的响应。比如服务器可能根据浏览器不同,返回不同的图片格式。如果缓存忽略 Vary,就可能把错误的字节返回给请求;但如果把每个原始头部的值都当成不同的变体来处理,几个相似的请求就可能膨胀成成千上万个几乎无法复用的缓存条目。Vary 告诉缓存哪些请求字段可能影响响应,却不会告诉缓存哪些差异真正重要

现在,所有套餐的 Cache Rules 都已支持 Vary。源站依然负责声明哪些请求头可能影响响应,但如何处理每个头部由你决定。你可以对已知的协商头部做规范化处理,在细微差异确实重要时按精确值透传,或者在变化过于不可预测时绕过缓存。源站声明哪些内容可能变化,而你决定缓存中真正有意义的差异程度。

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: Accept

API 客户端可以用不同的偏好请求同一个 URL:

GET /catalog HTTP/1.1 
Host: example.com 
Accept: application/json

这次,正确的响应是 JSON。Vary: Accept 头部告诉缓存,仅凭 URL 不足以确定该返回哪个响应。请求的 Accept 值也必须纳入考量。

如果没有 Vary,缓存中先进入的响应可能会被同时发给这两种客户端。如果 HTML 响应先到,API 客户端就会收到标记语言,其 JSON 解析器会失败。如果 JSON 响应先到,期望网页的浏览器就会收到 API 响应。

Vary 防止缓存向客户端返回错误的响应。但它引出了一个更难的问题:当两个请求包含不同的头部值时,它们是否真的需要不同的响应?

正确的缓存变得无用

Vary 可以告诉缓存哪些请求字段可能影响响应。它并不告诉缓存响应代表什么。例如,假设源站只提供英语、法语和德语内容。一个客户端可能发送:

Accept-Language: en-US, fr;q=0.8

而另一个客户端可能请求:

Accept-Language: fr;q=0.8, en-GB

这两个请求都偏好英语。源站的响应可能会将两者映射到完全相同的英语响应。但缓存在比对原始值时,无法安全地假设它们是等价的。它们的顺序不同,语言标签 也不同(尽管源站并不区分这些差异)。因此,即使响应体内容完全相同,缓存也可能将它们存储为不同的变体。

这是 Vary 的核心问题。应用程序通常能从海量可能的请求值中生成一套较小且有限的响应表示。源站知道成千上万的语言偏好可以归并为三种受支持的语言,而缓存通常做不到这一点。

当响应依据多个字段变化时,问题会进一步加剧。一个字段有 10 种取值会产生 10 个变体;三个字段各 10 种取值则可能产生 1,000 种组合。实际请求头中的变量往往更复杂:User-Agent 取值众多,cookie 可能因访客而异,偏好请求头在顺序、格式(空格和制表符都会产生影响!)以及质量值上也存在差异。

结果就是缓存虽然逻辑上完全正确,却几乎永远处于冷状态(条目从未被复用)。相同的响应分散在不同条目中,每个条目流量太低,无法保持热点并留在缓存中。这些条目会占用容量,相互驱逐,降低缓存命中率,并把更多请求发回源站。虽然驱逐机制可以清除冷条目,但它无法仅仅因为响应内容相同就将条目合并。

一项针对近 50,000 个热门站点中超过 1.2 亿个响应的分析发现,近 3,000 个站点的响应依据 4 个或更多字段变化。有些站点依据 10、23 甚至 47 个字段变化。我们希望确保客户在合适时使用 Vary,但也希望他们不会因滥用而让缓存变得毫无用处。

某些高基数变化是有意为之。CDN 或反向代理可能会注入数值(如地理区域),以便可预测地拆分内容。这仅在数值可控且所有组件对含义达成一致时有效。若缺乏这些约束,缓存会碎片化为一系列可能永远无法复用的变体。

这就是支持 Vary 所需解决的设计难题。我们既要保留足够的变化以提供正确的响应,又要避免请求之间的细微差异破坏缓存效率。

Cache Rules 如何控制 Vary

Cloudflare 客户此前已有几种方式来处理类似 Vary 的内容协商。他们可以绕过缓存、让源站自行处理,也可以在自定义缓存键或其他规则中复刻源站的协商逻辑,或者使用 Worker,以及Vary for images 这类功能。

这些方案依然有用,但它们要么放弃缓存,要么重复实现应用逻辑,要么需要额外写代码,要么只覆盖较窄的场景。Vary in Cache Rules 或许能填补现有功能之间的空缺,它把支持拆分成两个决策:

  1. 源站通过 Vary 标识可能影响响应的请求头。
  2. Cache Rule 决定 Cloudflare 如何处理每个请求头的值。

Cache Rule 并不会强制每个响应都 vary。如果源站没有返回 Vary,Cloudflare 会照常缓存响应,不过规则仍然可能在把请求转发给源站之前改写 AcceptAccept-Language

当源站返回 Vary 时,Cloudflare 会对其指定的每个请求头采用配置好的处理方式,未单独设置的请求头则使用该规则的默认处理方式。共有三种可选的处理方式:

处理方式

Cloudflare 做什么

适用场景

normalize

在选择缓存变体之前对请求头做规范化处理,让等价的请求命中同一份缓存响应。对 AcceptAccept-LanguageAccept-Encoding 应用针对性的规则;对其他请求头,则去除可选的空白字符,并按原始顺序合并重复的头字段,同时保留大小写和内部空白。

推荐作为协商头的起始点,适用于众多请求值映射到少量响应值的场景。

passthrough

使用请求头的原始字节进行缓存匹配,保留大小写、空白字符、顺序及重复值。如果头出现在多行中,Cloudflare 会按顺序使用逗号将这些行组合起来进行缓存匹配。Passthrough 模式下,发出的头行保持不变。即使 Respect Strong ETags 被禁用,Cloudflare 仍可能重写 Accept-Encoding。

具有受控值集合的头,其中精确的值会改变响应。

bypass

当源站将某头命名在 Vary 中时,不存储响应。现有缓存条目不会被删除,如需清除需手动 purge。

适用于个性化、高基数或意料之外的头,例如 CookieUser-Agent

我们推荐将 normalize 作为默认选项。对于包含个人或非限定值的特定头,使用 bypass。当精确值会改变响应时,使用 passthrough

例如,passthrough 会保留大小写、空白、顺序及重复值的区别,即使源站视它们为等效。在 Vary: X-View 配合 passthrough 的情况下,以下三个值会产生不同的缓存键:

X-View: compact,full

X-View: Compact,full

X-View: compact, full

过多的细微差异会将可复用的响应转化为缓存中许多单次使用的变体。

无论配置何种动作,Vary: * 始终绕过缓存。这意味着请求的任何方面,甚至 HTTP 消息之外的信息(如客户端 IP 地址),都可能影响源站选择的响应。因此,Cloudflare 无法在不联系源站的情况下为后续请求复用该响应。

响应在缓存中的流转

让我们跟踪上述 /catalog 请求之一在 Cloudflare 中的处理过程。

首次请求时,Cloudflare 没有该资源已存储的 Vary 数据,因此缓存查找未命中。在联系源站之前,匹配的 Cache Rule(缓存规则)可以对配置的字段进行标准化处理。

这种情况可能发生在 Cloudflare 尚不知道最终响应是否包含 Vary 之前。Cache Rule 定义了允许的标准化方式,而响应则决定了这些字段最终是否成为缓存变体的一部分。

顺序很关键。如果 Cloudflare 将多个原始值归类到一个标准化缓存键下,但源站收到的仍是原始值,源站可能会生成不同的响应,而缓存后续会认为这些响应是可以互换的。转发标准化值能确保源站选择与缓存匹配保持一致。

源站返回:

Vary: Accept, Accept-Language

Cloudflare 记录这些头部名称,并将响应作为缓存变体存储。根据 Cache Rule 处理后的头部值,用于区分同一资源的不同变体。

当另一个针对 /catalog 的请求到达时,Cloudflare 从该资源的 基础缓存键 开始:通常是 URL 加上其他配置的键字段。然后读取已存储的 Vary 字段,并对新请求中的这些头部应用 Cache Rule,以识别匹配的缓存变体。

假设它们标准化为:

Accept: text/html

Accept-Language: en,fr

Cloudflare 直接使用这些值查找匹配的缓存变体,而不是将请求与每个已存储的变体逐一比较。

如果存在匹配的变体且处于新鲜状态,则该请求为缓存命中。否则,Cloudflare 将请求发送到源站,并可能将返回的响应存储为另一个变体。

源站响应完成了闭环。对于 Vary 中列出的每个头部,Cloudflare 使用该头部配置的操作,或如果该头部未单独列出,则使用规则的默认操作:

  • 如果它不包含 Vary,Cloudflare 正常缓存。
  • 如果所有命名的头部都解析为 normalize(标准化)或 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 模式会把它们当作不同的变体。如果 Cache Rule 允许 enfrdenormalize 模式会把两者都归约为 enfr,从而共享同一个缓存响应。

为此,Cloudflare 会将 AcceptAccept-LanguageAccept-Encoding 的值转为小写,再按质量值从高到低排序,质量值相同时按字母顺序排列。这样客户端的排序不会影响缓存键。排序后,Cloudflare 会去掉质量值非零条目的参数。在缩短语言标签或按配置的语言和格式过滤时,也可能会丢失 q=0("不可接受")信息。比如 en-US;q=0 可能变成 en。如果源站需要看到这些排除项,请对 AcceptAccept-Language 使用 passthrough。

你还可以在 AcceptAccept-Language 头中配置规则,仅保留指定的媒体类型或语言。地区语言标签(如 en-US)会简化为基于语言 en,除非配置了完整的标签。这让你可以将标准化流程与你源站实际提供的格式和语言对齐。

为了使源站选择与缓存匹配保持一致,Cloudflare 会将标准化后的 AcceptAccept-Language 值转发给源站。若启用了 Respect Strong ETags,它还会转发标准化后的 Accept-Encoding 值。其他头部仅用于缓存匹配进行标准化。

在 Cache Rules 中配置 Vary

在 Cloudflare 控制台中,前往缓存 > Cache Rules,创建或编辑规则,使响应符合缓存条件,并添加Vary设置。先设置默认行为,然后添加你预期源站会在 Vary 中命名的头。

相同的配置也可以通过 Rulesets API 在 http_request_cache_settings 阶段中实现。默认设置为你在 Vary 中命名但未单独配置的头选择回退操作。

此示例将 AcceptAccept-Language 标准化为配置好的格式和语言集合。默认的标准化操作也适用于 Vary 中命名的其他头:

{
  "rules": [
    {
      "ref": "vary_negotiated_content",
      "description": "Cache bounded negotiated representations",
      "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 文档

为何不使用自定义缓存键?

至此,一个显而易见的问题是:“为什么不像 自定义缓存键 那样,将 AcceptAccept-Language 添加进去?”

当这些字段始终属于资源标识的一部分时,这样做没问题。但自定义缓存键会把已配置的维度添加到规则覆盖的每一个响应中,无论源站是否实际使用了这些字段。

Vary 是由响应驱动的,但同一个基础键下的可缓存响应需要一组一致的 Vary 字段。

当某个请求属性始终定义了资源时,就用自定义缓存键;当源站在所有可缓存响应上声明了同一组请求字段时,就用 Vary。除非经过深思熟虑和充分测试,否则不要把同一个头部同时用在两者上。

现在就在 Cache Rules 中使用 Vary 吧!

Vary 解决的问题很直观:同一个 URL 可能对应多个正确的响应。但它也给缓存出了一个难题:哪些请求差异才是真正重要的?源站知道自己能提供哪些响应,而缓存需要知道哪些请求可以复用每个响应。

Cache Rules 中的 Vary 把这两种视角连接了起来。源站标明可能影响响应的请求字段,你则可以决定是对这些值做归一化、用透传保留精确差异,还是干脆不缓存该响应。

Vary 从来不是因为丑而不能用。但手动配置支持的格式和语言,未必适合每个应用。我们正在评估已过期的 Availability Hints 草案中的思路——让源站直接描述自己提供的各种表示形式——能否减少这部分工作。

Cache Rules 中的 Vary 功能现已面向 Free、Pro、Business 和 Enterprise 套餐开放,可通过 Cloudflare dashboardRulesets APITerraform 使用。

原始来源: Hacker News

评论 (0)