ITPub博客

首页 > 人工智能 > 人工智能 > 我们刚刚发布了对 HTTP 中最丑陋部分的支持:Vary

我们刚刚发布了对 HTTP 中最丑陋部分的支持:Vary

人工智能 作者:ITPUB社群 时间:1970-01-01 08:33:46 0 删除 编辑


响应头 Vary 曾被称为“HTTP 中最丑陋、我们至今尚未改进的部分”。同一篇文章还将其描述为一种“可怕的、笨拙的机制”,在中间层之间“互操作性极差”。这通常是明智的工程师举起双手慢慢后退的地方。

这算不上是对 Vary 的认可,但丑陋并不意味着无用。

一个 URL 可以有多个正确的响应。例如,服务器可能会向不同的浏览器发送不同的图像格式。如果缓存忽略 Vary,就有可能向请求返回错误的字节。但如果它将每个原始头值都视为不同的值,少量相似的请求就可能扩散成数千个几乎无法复用的缓存条目。Vary 告诉缓存哪些请求字段可能影响响应,但它并没有告诉缓存哪些差异实际上重要。

Vary 支持现已在所有套餐的 Cache Rules 中可用。源站仍然指定可能影响响应的请求头,但由您决定 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: 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 的核心问题。应用程序通常从大量可能的请求值中生成一小组有限的表示形式。源站明白数千种语言偏好会归并为三种支持的语言,而缓存通常并不了解这一点。

当响应因多个字段而变化时,这个问题会进一步加剧。一个字段有十种可能值就会产生十个变体。三个字段各有十种值则可能产生 1,000 种组合。真实标头的基数可能远高于此:User-Agent(智能体) 值数量众多,Cookie 可能因访问者而异,偏好标头可能在顺序、格式(空格和制表符都很重要!)以及质量值上有所不同。

结果是缓存可能完全正确,却几乎永远处于冷状态(条目从未被复用)。相同的响应可能分散在流量过少、无法保持热度和留在缓存中的条目里。它们会消耗容量、相互驱逐、降低缓存命中率,并将更多请求发送回源站服务器。驱逐可以移除冷条目,但无法仅因为响应相同就将它们合并。

对来自近 50,000 个热门站点的超过 1.2 亿个响应的分析发现,近 3,000 个站点在四个或更多字段上使用 Vary。有些站点甚至在 10 个、23 个甚至 47 个字段上使用 Vary。我们希望确保客户在适当的时候拥有使用 Vary 所需的工具,但又不至于过度使用而创建出无用的缓存。

一些高基数变化是有意为之的。CDN 或反向代理可能会注入值(例如地理区域),以可预测的方式对内容进行分区。当可能的值是受控的,并且每个组件对其含义达成一致时,这种方式是有效的。如果没有这些约束,缓存就会碎片化成可能永远不会被复用的变体。

这就是我们为支持 Vary 而需要解决的设计问题。我们需要保留足够的变化以提供正确的响应,同时又不允许请求之间的偶然差异破坏缓存效率。

Cache Rules 如何控制 Vary

Cloudflare 客户已经有多种方式来处理与 Vary 类似的协商内容。他们可以绕过缓存并让源站处理,在自定义缓存键或其他规则中复现源站的协商逻辑,使用 Worker,或使用 Vary for images 等功能。

这些选项仍然有用,但它们要么放弃缓存,要么重复应用逻辑,要么需要编写额外代码,要么只解决较窄的用例。Cache Rules 中的 Vary 可能通过将支持拆分为两个决策来填补这些现有功能之间的空白:

  • 源站使用 Vary 来标识可能影响响应的请求头。

  • Cache Rule 决定 Cloudflare 如何处理每个头的值。

Cache Rule 不会强制每个响应都进行 vary。如果源站不返回 Vary,Cloudflare 会正常缓存响应,不过该规则仍可能在将请求转发到源站之前重写 Accept 和 Accept-Language。

当源站确实返回 Vary 时,Cloudflare 会对其指定的每个头使用配置的操作。没有单独设置的头使用规则的默认操作。可用的三种操作是:

| 操作 | Cloudflare 的行为 | 最适合用于 |
| --- | --- | --- |
| normalize | 在选择缓存变体之前规范化请求头,帮助等效请求共享缓存响应。对 Accept、Accept-Language 和 Accept-Encoding 应用特定于头的规则。对于其他头,它会修剪可选空白,并按原始顺序合并重复的头行,保留大小写和内部空白。 | 协商头的推荐起点,其中许多请求值映射到少量响应。 |
| passthrough | 使用请求头的原始字节进行缓存匹配,保留大小写、空白、顺序和重复值。如果头出现在多行中,Cloudflare 会按顺序用逗号合并这些行以进行缓存匹配。Passthrough 保持传出的头行不变。当 Respect Strong ETags 被禁用时,Cloudflare 仍可重写 Accept-Encoding。 | 具有受控值集合的头,其中确切值会改变响应。 |
| bypass | 当源站在 Vary 中指定该头时,不存储响应。现有缓存条目不会被移除,因此如果需要清除它们,请进行 purge。 | 用于个性化、高基数或意外的头,例如 Cookie 或 User-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 联系源站之前对配置的字段进行规范化。

这可能发生在 Cloudflare 知道最终响应是否包含 Vary 之前。Cache Rule 定义了允许的规范化;响应随后决定这些字段是否成为缓存变体的一部分。

这个顺序很重要。如果 Cloudflare 将多个原始值归组到一个规范化缓存键下,但源站仍然收到那些原始值,源站可能会产生不同的响应,而缓存之后会认为这些响应可以互换。转发规范化后的值可以使源站选择与缓存匹配保持一致。

源站响应如下:

Vary: Accept, Accept-Language

Cloudflare 记录这些标头名称,并将响应存储为缓存变体。根据 Cache Rule 处理后的标头值,将该变体与同一资源的其他变体区分开来。

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

假设它们规范化为:

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

两个请求都偏好英语,但透传会将它们视为不同的变体。如果缓存规则允许 en、fr 和 de,规范化会将两者都归约为 en、fr,使它们能够共享同一个缓存响应。

为此,Cloudflare 会将 Accept、Accept-Language 和 Accept-Encoding 中的值转换为小写,然后按质量值(quality value)排序,最高值在前,相同则按字母顺序排列。因此,客户端的排序不会影响缓存键。排序后,Cloudflare 会从质量值非零的条目中剥离参数。在缩短语言标签或过滤为配置的格式和语言时,也可能丢失 q=0(“不可接受”)。例如,en-US;q=0 可能变为 en。如果源站需要看到这些排除项,请对 Accept 或 Accept-Language 使用透传。

你还可以配置规则,使其在 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 规范化为配置的格式和语言集合。默认的 normalize 操作也适用于 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。在缓存填充后查找命中(hit),并调查持续未命中(miss)响应或意外的绕过(bypass)响应。

有关限制、更多示例以及如何在 Terraform 中设置此配置,请参阅 Vary 文档。

为什么不使用自定义缓存键?

此时,一个显而易见的问题是:“为什么不把 Accept 和 Accept-Language 添加到自定义缓存键中?”

当这些字段始终是资源标识的一部分时,这种做法是可行的。但自定义缓存键会将配置的维度添加到该规则覆盖的每一个响应中,无论源站是否使用了它们。

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

当某个请求属性始终定义资源时,使用自定义缓存键。当源站在可缓存响应中声明同一组请求字段时,使用 Vary。避免将同一个标头同时放入两者,除非这种重复是有意为之并经过测试的。

立即在 Cache Rules 中使用 Vary!

Vary 有助于解决一个显而易见的问题:一个 URL 可能有多个正确的响应。但它给缓存带来了一个更棘手的问题——哪些请求差异才是真正重要的?源站知道它可以提供哪些响应。缓存需要知道哪些请求可以复用每个响应。

Cache Rules 中的 Vary 将这两种视角连接起来。源站标识可能影响响应的请求字段。您决定是规范化值、使用直通以保留精确差异,还是将响应排除在缓存之外。

Vary 从来不是因为太丑陋而无用。但手动配置支持的格式和语言可能并不适合每个应用程序。我们正在评估已过期的 Availability Hints 草案中的想法是否可以通过让源站直接描述其所提供的表示来减少这项工作。

Cache Rules 中的 Vary 现已在 Free、Pro、Business 和 Enterprise 计划中通过 Cloudflare dashboard、Rulesets API 和 Terraform 提供。


转载声明:本文为转载整理,原文版权归原作者及原站点所有。

原文来源:Cloudflare Blog

原创地址:https://blog.cloudflare.com/vary-support/

原文作者:Zaidoon Abd Al Hadi;如有侵权,请联系管理员删除。

请登录后发表评论 登录
全部评论

注册时间:2022-11-21

  • 博文量
    1090
  • 访问量
    546977