30 分钟 HTTP 查漏补缺之 Vary

30 分钟 HTTP 查漏补缺之 Vary

写在前面

最近抽空参加了几场大厂的面试,突然发现一个现象,就是不论面试偏服务端的职位还是偏客户端的职位,不论面试的 5 年以上的高级职位,还是 3 年左右的中级职位,面试官开头所问问题必然是关于 HTTP 的。

我记得之前找工作的时候,似乎都是先考察一些职位所需技能领域的基础知识,之后再考察关于 HTTP 的东西,现在大家都将 HTTP 的问题放到面试的开头来问,我觉的应该是越来越多的招聘者意识到,作为一个 Web 开发者,HTTP 真的是太重要了,必须要先考察。

回想起来,这几年我自己对于 HTTP 的学习大多是碎片化的,很多东西无法系统地在脑海中组织起来。虽然感觉 HTTP 整体的学习难度是比较低的,但是各个知识点交杂在一起又变得很复杂很难,相信大家都会有同感。同时有些知识点,如果在实际工作中没有采坑或者刻意深挖的话,很自然地就被忽略了。

由于在之前一次面试中,被狠狠地问了若干关于 Vary 的问题,所以想抽一些时间整理一下那些比较容易让人忽略的知识点,算是查漏补缺吧。

内容协商

首先需要了解的是内容协商这个术语。当我们通过某个 URI 来访问其指向的资源时,HTTP 协议可以通过内容协商机制提供资源的不同的展示形式。

如果缺少服务端开发经验话,对于这个概念可能会感到陌生,但其实我们在工作中几乎都会遇到它,比如在调用接口时,经常会用到 Accept: application/json 这个头部,有时可能会用到 Accept: application/xml,这就是内容协商,前者期望接口返回 json 格式的数据,而后者期望返回 xml 格式的数据。

一般客户端涉及的常见头部有以下几个:

  • Accept: 声明客户端可以处理的资源格式
  • Accept-Charset: 声明客户端可以处理的字符集类型
  • Accept-Language: 声明客户端可以理解的自然语言
  • Accept-Encoding: 声明客户端支持的编码格式

而服务端涉及的常见头部包括:

  • Content-Type: 指示资源的 MIME 类型
  • Content-Language: 指示该资源所期望的自然语言
  • Content-Encoding: 指示资源使用该编码格式进行内容转换

仔细观察的话,会发现它们其实存在着一定程度的对应关系。原因也很简单,既然是协商,那必然就会和两个人在进行说话一样,如果两者之间的对话内容没有关联,他们还怎么沟通呢?客户端和服务端进行沟通同理。

如果想详细了解该机制,可以参考MDN的文档,很详细,这里就不多说了。

这里顺带说明一下,对于内容协商机制中涉及的头部,从 web 发展历史上来看已经没有什么实质的用途了,原因如下(有兴趣的话可以阅读这篇wiki):

  • Accept-Charset: 由于 utf-8 成为主流的字符集类型,所以使用其他字符集类型的服务可以将其转换为 utf-8 类型
  • Accept-Language: 大体包含以下几点
    • 提供多种语言服务的网站往往是基于某种特定语言构建,再提供其他语言支持的,这样每种语言类型的内容在质量上层次不齐,而访问者可能会更倾向于内容质量更高的那一种语言,而内容协商机制无法替代用户的主观判断
    • 实践中,对于切换网站语言的功能,切换方式往往更倾向于主动切换(比如提供一个切换的按钮)而非自动切换
    • 浏览器在用户不提供语言相关配置的情况下,很难猜测用户的自然语言倾向(一般可能会根据地理定位、ip等因素猜测),打个比方,比如我会经常出差去日本,但这不代表我会说日语,同时虽然我挂了加拿大的 vps,但是提供中文内容的网站,我还是倾向于看中文


  • Accept: 与 Accept-Language 类似,同样因为内容的格式会因用户的主观意识而不同,还有诸多其他因素制约内容协商机制,所以最终失败了。

唯一有些用途的是 Accept-Encoding,但鉴于如今大部分现代浏览器都已支持多种压缩方式(常见的如 gzip、br),因此一定程度上已经不需要额外声明这个头部了,虽然大部分浏览器都会自动发送这个头部,但其实这会造成额外 23 字节的浪费。

Vary 头部

在理解(或者巩固)了内容协商的概念后,就可以介绍 Vary 这个头部了。直接引用 MDN 对于它的描述:

The Vary HTTP response header determines how to match future request headers to decide whether a cached response can be used rather than requesting a fresh one from the origin server.Vary 是一个HTTP响应头部信息,它决定了对于未来的一个请求头,应该使用一个缓存作为响应还是向源服务器请求一个新的响应。

单纯靠文档对于 Vary 的描述来理解它其实是有些困难的,最起码我会有这种感觉。

这个头部的语法和其他的 HTTP 头部类似,如下:

Vary: <header-name>, <header-name>, ...

不同的头部之间使用逗号进行分割,同时可以指定 * 为它的值,这样等价于将资源视为唯一,并不进行缓存,但这并不是最佳实践,因此不建议这么做。

Vary 的工作原理

一句话概括它的工作原理就是,就是它表示某个响应因某个响应头部而不同。举个例子,比如 Vary: Accept 的意思即为,响应因请求资源格式头部而不同,那么通过相同 URI 访问的资源就可以根据这个头上知道其内容格式不同。

但我们已经知道,对于大部分内容协商机制中涉及的头部,已经被看作是失败的,那么 Vary 和这些头部搭配使用还有什么意义呢?话虽如此,但 Vary 还可以与 HTTP 中其他的头部来搭配使用,从而满足很多应用场景下的特殊需求,比如动态服务、防止缓存错乱等。

Vary 的应用场景

以下简单罗列一些常用的应用场景以及采坑指南。

Vary 与 动态服务

关于动态服务,最常见的莫过于 Vary: User-Agent。众所周知,UA 是一段特征字符串,通常包含区分客户端类型、操作系统、版本号等信息,随着移动 web 应用变得越流行,一个应用网站同时提供桌面和移动两种版本的应用是很常见的事情。通过设置 Vary: User-Agent 头部,对于搜索引擎,对于关键字的搜索结果可以提供更加准确的应用版本,对于客户端,可以使其从缓存服务器获取到相应应用类型的缓存版本,而不是错误地将桌面版缓存传递给移动版应用。

web 应用的性能在加载速度这一指标上,很大程度上取决于加载资源的大小,而图片资源是所占比例最大的一块。为了减少图片的大小,除了对常见的图片格式进行压缩以外,chrome 推出的 WebP 格式也是不错的选择。但是这里的问题是,不是所有的浏览器都支持 WebP 图片格式的,所以这里使用 Vary: Accept 来针对浏览器的支持情况返回相应的缓存副本,支持则返回 WebP 格式,不支持则返回缩略图或者原图。

还有其他关于动态服务的场景,比如要针对不同分辨率的屏幕加载不同质量的图片(Client Hints 相关的头部)、针对不同用户身份提供不同的资源(Cookie头部)等等。

Vary 与 缓存错乱

有时候我们会发现响应中存在 Vary: Accept-Encoding 头部信息,我原先按照内容协商机制中所描述的内容来理解,但到后来才发现,其实很大程度上是为了防止缓存错乱的问题。

设想一下,如果没有这个头部,当两个分别支持 gzip 和 不支持 gzip 的客户端对同一份资源进行获取时,结果会变得十分微妙。如果不支持 gzip 的客户端先访问,缓存代理会缓存未压缩的版本,那么当支持 gzip 的客户端再访问时,由于命中缓存,虽然它支持 gzip 但也只能加载未压缩的资源。反过来同样如此,支持 gzip 客户端先访问,则缓存代理会缓存压缩版本,当不支持 gzip 的客户端再访问时,缓存同样命中,但是由于它无法对压缩资源解码,所以会呈现乱码。

通过 Vary: Accept-Encoding 我们可以防止这种情况的发生,因为 Vary 在这里其实是扮演着校验器的角色,它会进一步对命中缓存的资源进行再校验,如果发现头部信息不同,则会将缓存资源视为无效,从而将请求继续转发至源服务器。这对于缓存代理服务器也有一定的益处,因为可以有有依据地针对不同的 Accept-Encoding 缓存不同的资源副本。

Vary 与 缓存命中率

Vary 虽然可以防止缓存错乱,但并不代表可以滥用,盲目的使用会适得其反,比如之前提及的 Vary: *,这样等价于将每个请求视为唯一,并且不缓存其响应资源,除非有意为之,不然没有人会牺牲缓存带来的性能提升。

同时对于一些 Header 的值是开放性的,比如之前提及的 User-Agent,如果单纯从字面量来匹配的话,众多桌面浏览器的值会因各种因素而不同的,如果仅是简单地将 UA 作为区分桌面端和移动端的依据,那么缓存命中率会达到一个很低的水平。如何解决这个问题呢?可以将这些 UA 头部的值进行标准化,比如可以通过正则匹配所有桌面浏览器的 UA 并重新更改为 Desktop,之后再转发至缓存代理和源服务器,这样有利于提高缓存命中率,关于这部分的内容,可以参考这篇文章,其中有很细致的讲解。

所以我们要时刻留意,在使用 Vary 时,一定要根据缓存命中率作出调整,在不发生缓存错乱的情况之下,尽可能的提高资源的缓存命中率。

Vary 与 CORS

对于跨域的有情况,Vary 也包含一些内容。HTTP 协议规定,当服务端响应包含 Access-Control-Allow-Origin 头部,且它的值是一个具体的域名而不是通配符 *,那么这时必须要包含 Vary: Origin 这个头部。

为什么要包含这个头部,因为请求头中的 Origin 头部代表了该请求来源的具体域名信息,那么对于不同域名网站所发起的请求,会使用仅属于它本身的缓存。一般而言,我们很少会遇到这种问题,因为一般都将 Access-Control-Allow-Origin 设置为了 *,至少我自己是这样的。如果想进一步了解 Vary 和 CORS 的内容,可以参考这篇文章。

最后

差不多就这么多内容了,如有错误,还望指正。

参考链接

  • 内容协商
  • Best Practices for Using the Vary Header
  • IE 与 Vary
  • CORS
  • Vary
  • Response with Vary Header
  • Understanding Vary Header
  • Getting the most out of Vary with Fastly
  • Why not conneg
  • 条件型 CORS 响应下因缺失 Vary: Origin 导致的缓存错乱问题
转载
原文链接:人类身份验证 - SegmentFault
发布于 2018-10-18 10:52