HTTP 缓存和代理

509 阅读17分钟

一、HTTP 缓存(HTTP Caching)

HTTP 通常用于分布式信息系统,其中可以通过使用响应缓存来提高性能。缓存是一种保存资源副本并在下次请求时直接使用该副本的技术。当 web 缓存发现请求的资源已经被存储,它会拦截请求,返回该资源的拷贝,而不会去源服务器重新下载。适当使用缓存能缓解服务器端压力,提升性能(即获取资源的耗时更短)。但是不正确的缓存可能会导致用户看到过时的内容并且难以调试问题,所以需要对缓存进行合理控制。

HTTP/1.1 中缓存的目标是在许多情况下消除发送请求的需要,并在许多其他情况下消除发送完整响应的需要。

1.1 概述

当浏览器将 Web 资源存储在本地缓存中以便在下次需要资源时更快地检索时,会发生 HTTP 缓存。

image.png

当所需要的资源被完全缓存时,浏览器不需要向服务器请求,而只是使用自己的缓存副本:

image.png

1.2 缓存控制

HTTP/1.1定义的 Cache-Control 头用来区分对缓存机制的支持情况, 请求头和响应头都支持这个属性。通过它提供的不同的值来定义缓存策略。

  • 客户端可以在其 HTTP 请求中使用以下缓存请求指令
缓存请求指令描述
no-cache未经源服务器成功重新验证,缓存不得使用响应来满足后续请求
no-store缓存中不得存储任何关于客户端请求和服务端响应的内容。每次由客户端发起的请求都会下载完整的响应内容。
max-age = seconds表示资源能够被缓存(保持新鲜)的最大时间。
max-stale [ = seconds ]表示客户端愿意接受已超过其过期时间的响应。如果给出秒数,则它的过期时间不得超过该时间
min-fresh = seconds表示客户端愿意接受其新鲜度不小于其当前存在时间加上指定时间(以秒为单位)的响应
no-transform不转换实体主体
only-if-cached不检索新数据。仅当文档在缓存中时,缓存才能发送文档,并且不应联系原始服务器以查看是否存在较新的副本
  • 服务器可以在其 HTTP 响应中使用以下缓存响应指令
缓存响应指令描述
public表示响应可能被任何缓存缓存。即不仅可以被最终用户的浏览器缓存,还可以被任何可能为许多其他用户提供服务的中间代理缓存。
private表示响应消息的全部或部分是针对单个用户的,不得由共享缓存缓存
no-cache未经源服务器成功重新验证,缓存不得使用响应来满足后续请求
no-store缓存不应存储有关客户端请求或服务器响应的任何内容
no-transform不转换实体主体
must-revalidate表示缓存在考虑使用一个陈旧的资源时,必须先验证它的状态,已过期的缓存将不被使用
proxy-revalidateproxy-revalidate 指令与 must-revalidate 指令具有相同的含义,只是它不适用于非共享用户代理缓存
max-age = seconds表示资源能够被缓存(保持新鲜)的最大时间。
s-maxage = seconds此指令指定的最长期限将覆盖 max-age 指令或 Expires 标头指定的最长期限。 s-maxage 指令总是被私有缓存忽略

(1)使用缓存的方式

  • no-store不允许缓存,缓存中不得存储任何关于客户端请求和服务端响应的内容。每次由客户端发起的请求都会下载完整的响应内容。用于某些变化非常频繁的数据,例如秒杀页面;
Cache-Control: no-store
  • no-cache允许缓存但需重新验证,每次有请求发出时,缓存会将此请求发到服务器,服务器端会验证请求中所描述的缓存是否过期,是否有最新的版本。
Cache-Control: no-cache
  • must-revalidate:使用一个陈旧的资源时,必须先验证它的状态,当缓存的资源过期后,需要进行缓存验证或者重新获取资源。
Cache-Control: must-revalidate

有篇博客以买西瓜来类比三种缓存控制方式如下:

  • no_store:买来的西瓜不允许放进冰箱,要么立刻吃,要么立刻扔掉;
  • no_cache:可以放进冰箱,但吃之前必须问超市有没有更新鲜的,有就吃超市里的;
  • must-revalidate:可以放进冰箱,保鲜期内可以吃,过期了就要问超市让不让吃。

(2)过期

  • max-age=<seconds>:表示资源能够被缓存(保持新鲜)的最大时间(秒)。
Cache-Control: max-age=31536000

该时间的计算起点是响应报文的创建时刻(即 Date 字段,也就是离开服务器的时刻),而不是客户端收到报文的时刻,也就是说包含了在链路传输过程中所有节点所停留的时间。

比如,服务器设定“max-age=5”,但因为网络质量很糟糕,等浏览器收到响应报文已经过去了 4 秒,那么这个资源在客户端就最多能够再存 1 秒钟,之后就会失效。

也可通过Expires 响应头设置过期的日期/时间。

Expires: <http-date>

如果同时设置了Cache-Control: max-age=secondExpiresmax-age优先级更高。

以上几种缓存控制策略如下流程图所示: image.png

1.3 条件请求(conditional requests)

理论上来讲,当一个资源被缓存存储后,该资源应该可以被永久存储在缓存中。由于缓存只有有限的空间用于存储资源副本,所以缓存会定期地将一些副本删除,这个过程叫做缓存驱逐。另一方面,当服务器上面的资源进行了更新,那么缓存中的对应资源也应该被更新,由于HTTP是C/S模式的协议,服务器更新一个资源时,不可能直接通知客户端更新缓存,所以双方必须为该资源约定一个过期时间,在该过期时间之前,该资源(缓存副本)就是新鲜的,当过了过期时间后,该资源(缓存副本)则变为陈旧的 驱逐算法用于将陈旧的资源(缓存副本)替换为新鲜的。

为了保证缓存中资源的新鲜度,那么应该怎么去做呢?

浏览器可以用两个连续的请求组成“验证动作”:先是一个 HEAD,获取资源的修改时间等元信息,然后与缓存数据比较,如果没有改动就使用缓存,节省网络流量,否则就再发一个 GET 请求,获取最新的版本。

但这样的两个请求网络成本太高了,所以 HTTP 协议就定义了一系列“If”开头的“条件请求”字段,专门用来检查验证资源是否过期,把两个请求才能完成的工作合并在一个请求里做。

条件请求就是浏览器可以询问服务器是否有更新的资源副本的请求。浏览器将发送一些关于它所持有的缓存资源的信息,服务器将确定是否应该返回更新的内容或者浏览器的副本是最新的。在后者的情况下,返回 304(未修改)的 HTTP 状态。

有条件的请求

尽管条件请求确实会通过网络调用调用,但未修改的资源会导致响应主体为空——节省了将资源传输回最终客户端的成本。后端服务通常还能够非常快速地确定资源的最后修改日期,而无需访问资源,这本身可以节省大量的处理时间。

(1)缓存验证

所有的条件请求首部都是试图去检测服务器上存储的资源是否与某一特定版本相匹配。为了达到这个目的,条件请求需要指明资源的版本。由于逐个字节去比较完整资源是不切实际的,况且这也并非总是想要的结果,所以在请求中会传递一个描述资源版本的值。这些值称为“验证器”,并且分为两大类:

  • Last-Modified:文件的最后修改时间
  • ETag:一个意义模糊的字符串,指代一个独一无二的版本,称为“实体标签”。

比较同一份资源的不同版本有一定的技巧性:取决于上下文环境的不同,有两种不同的等值检查(equality checks)类型:

  • 强验证类型

强验证类型的作用在于确保要比较的资源与其相比较的对象之间每一个字节都相同。对于有些首部来说需要明确指定该验证类型,而对于另外一些来说则是默认值就是强验证类型。强验证类型的要求相当严格,在服务器层面来说可能较难保证。但是它确保了数据在任何时候都没有缺损,有时候则需要以牺牲性能为代价。

使用 Last-Modified首部很难为强验证类型提供一个唯一标识。通常这是由 ETag首部来完成的,该首部可以提供使用 MD5 算法获取的资源(或其衍生品)的散列值。

  • 弱验证类型

弱验证类型与强验证类型不同,因为它会把内容相同的两份文件看做是一样的。例如,使用弱验证类型,一个页面与另外一个页面只是在页脚显示的时间上有所不同,或者是展示的广告不相同,那么就会被认为是相同的。但是在使用强验证的情况下,二者是不同的。构建应用于弱验证类型的标签(etag)体系可能会比较复杂,因为这会涉及到对页面上不同的元素的重要性进行排序,但是会对缓存性能优化相当有帮助。

验证类型与验证器的类型是相互独立的。 Last-Modified 和 ETag首部均可应用于两种验证类型,尽管在服务器端实现的复杂程度可能会有所不同。HTTP 协议默认使用强验证类型,可以指定何时使用弱验证类型。

(2)条件请求头部

  • If-Match:如果远端资源的实体标签与在 ETag 这个首部中列出的值相同的话,表示条件匹配成功。默认地,除非实体标签带有 'W/' 前缀,否者它将会执行强验证。

  • If-None-Match:如果远端资源的实体标签与在 ETag这个首部中列出的值都不相同的话,表示条件匹配成功。默认地,除非实体标签带有 'W/' 前缀,否者它将会执行强验证。

  • If-Modified-Since如果远端资源的 Last-Modified首部标识的日期比在该首部中列出的值要更晚,表示条件匹配成功。

  • If-Unmodified-Since:如果远端资源的 Last-Modified 首部标识的日期比在该首部中列出的值要更早或相同,表示条件匹配成功。

  • If-Range:与 If-Match或  If-Unmodified-Since相似,但是只能含有一个实体标签或者日期值。如果匹配失败,则条件请求宣告失败,此时将不会返回 206 Partial Content 响应码,而是返回 200 OK 响应码,以及完整的资源。

例如下图,当我们第一次请求资源时,服务器响应报文中带有Last-Modified 和 ETag,当第二次请求该资源时,可以带上If-None-Match头部,如果资源没有变,服务器就回应一个“304 Not Modified”,表示缓存依然有效,浏览器就可以更新一下有效期,并从缓存中获取资源。 image.png

1.4 Vary 响应

Vary) HTTP 响应头决定了对于后续的请求头,如何判断是请求一个新的资源还是使用缓存的文件

当缓存服务器收到一个请求,只有当前的请求和原始(缓存)的请求头跟缓存的响应头里的Vary都匹配,才能使用缓存的响应。

image.png 使用vary头有利于内容服务的动态多样性。例如,使用Vary: User-Agent头,缓存服务器需要通过UA判断是否使用缓存的页面。如果需要区分移动端和桌面端的展示内容,利用这种方式就能避免在不同的终端展示错误的布局

二、HTTP 代理(HTTP Proxy)

HTTP 代理是介于客户端宇服务器之间一个类似于中间人的角色,其功能就是代理网络用户去取得网络信息。代理服务器(Proxy Server)是介于浏览器 和 Web 服务器之间的一台服务器,有了它之后,浏览器不是直接到 Web 服务器去取回网页而是向代理服务器发出请求,Request信号会先送到代理服务器,由代理服务器来取回浏览器所需要的信息并传送给你的浏览器。

image.png

2.1 代理的作用

负载均衡:因为在面向客户端时屏蔽了源服务器,客户端看到的只是代理服务器,源服务器究竟有多少台、是哪些 IP 地址都不知道。于是代理服务器就可以掌握请求分发的“大权”,决定由后面的哪台服务器来响应请求。

反向代理(Surrogate):代理服务器可以作为web server使用,他们也因此被称作surrogates或者反向代理(reverse procies)。它们可以接受真正的web server请求,但是它们与web servers不同的是,它们可能会再与其他服务器建立另一个communication,以获取客户请求所需的资源。反向代理可以给web servers提速,因为公共的资源都放在反向代理里面了(无需再询问web servers)。在这样的环境(configuration)下,反向代理也被称作服务器加速器(server accelerators)。反向代理也可以与content-routing functionality协同使用,来创造一个分布式的、on-demand的replicated content网络。

网络缓存(Web cache): 代理服务器可以保存热门文件的副本,然后在客户需要的时候直接使用它们。这样一来,就减轻了原服务器(origin server)的负担:加快了交流速度、减少不必要的以太网通话(Internet communication)。这个会在第三节具体介绍。

代理的作用远不止这些,其他作用可参见 什么是HTTP代理服务器(HTTP Proxy)?

2.2 代理相关的 HTTP 头部

(1)Via 字段

Via 是一个通用首部,是由代理服务器添加的,适用于正向和反向代理,在请求和响应首部中均可出现。这个消息首部可以用来追踪消息转发情况,防止循环请求,以及识别在请求或响应传递链中消息发送者对于协议的支持能力。(即:解决了客户端和源服务器判断是否存在代理的问题)

Via: [ <protocol-name> "/" ] <protocol-version> <host> [ ":" <port> ]
or
Via: [ <protocol-name> "/" ] <protocol-version> <pseudonym>
  • <protocol-name>:可选。所使用的协议名称,如 "HTTP"。
  • <protocol-version>:所使用的协议版本号, 例如 "1.1"。
  • <host> and <port>公共代理的URL端口号
  • <pseudonym>:内部代理的名称或别名

如果通信链路中有很多中间代理,就会在 Via 里形成一个链表,这样就可以知道报文究竟走过了多少个环节才到达了目的地。例如下图中有两个代理:proxy1 和 proxy2,客户端发送请求会经过这两个代理,依次添加就是“Via: proxy1, proxy2”,等到服务器返回响应报文的时候就要反过来走,头字段就是“Via: proxy2, proxy1”。

image.png

(2)X-Forwarded-For 字段

X-Forwarded-For (XFF) 在客户端访问服务器的过程中如果需要经过HTTP代理或者负载均衡服务器,可以被用来获取最初发起请求的客户端的IP地址,这个消息首部成为事实上的标准。即:为谁而转发

在消息流从客户端流向服务器的过程中被拦截的情况下,服务器端的访问日志只能记录代理服务器或者负载均衡服务器的IP地址。如果想要获得最初发起请求的客户端的IP地址的话,那么 X-Forwarded-For 就派上了用场。

此外还有两个与之相关的头部:

  • X-Forwarded-Host (XFH) 是一个事实上的标准首部,用来确定客户端发起的请求中使用  Host  指定的初始域名。

  • X-Forwarded-Proto (XFP) 是一个事实上的标准首部,用来确定客户端与代理服务器或者负载均衡服务器之间的连接所采用的传输协议(HTTP 或 HTTPS)。

三、HTTP 缓存代理

第一节中说到,我们可以在请求到一些常用的网络资源后,可以将其存储在浏览器缓存中以便下次需要资源时可以不用到源服务器去重新下载,一次提升性能。但是,浏览器的缓存大小有限,并不能完全减轻服务器的压力。那么对于类似于热点新闻、爆款商品的详情页等 “读多写少” 的资源,一秒钟内可能有成千上万次的请求,有没有什么其他的缓存能够帮助解决服务器的并发压力呢。

这就要说到我们第二节中提到的HTTP代理了,我们可以把一些经常请求的资源存放在代理服务器上,那么客户端请求这些资源的时候,就可以在代理服务器上获取,代理服务器就不需要再去向源服务器请求了。

image.png

3.1 代理缓存控制

之前第一节中提到的Cache-Control头部不仅可以用于控制浏览器的缓存策略,也可以用于代理缓存策略。例如:max-age、no_store、no_cache 和 must-revalidate,这 4 种缓存属性可以约束客户端,也可以约束代理。但是也会有一些新的属性专门用于控制代理缓存。

(1)私有和公共缓存

  • public:指令表示该响应可以被任何中间人(比如中间代理、CDN等)缓存。若指定了"public",则一些通常不被中间人缓存的页面(比如 带有HTTP验证信息(帐号密码)的页面 或 某些特定状态码的页面),将会被其缓存。

  • private:则表示该响应是专用于某单个用户的,中间人不能缓存此响应,该响应只能应用于浏览器私有缓存中。

Cache-Control: private
Cache-Control: public

(2)proxy-revalidate

与must-revalidate作用相同,但它仅适用于共享缓存(例如代理),并被私有缓存忽略。

(3)s-maxage

覆盖max-age或者Expires头,但是仅适用于共享缓存(比如各个代理),私有缓存会忽略它。即:只限定在代理上能够存多久,而客户端仍然使用“max_age”

(4)no-transform

不得对资源进行转换或转变。Content-EncodingContent-RangeContent-Type等HTTP头不能由代理修改。例如,非透明代理或者如 Google's Light Mode 可能对图像格式进行转换,以便节省缓存空间或者减少缓慢链路上的流量。no-transform指令不允许这样做。

image.png

(5)max-stale

表明客户端愿意接收一个已经过期的资源。可以设置一个可选的秒数,表示响应不能已经过时超过该给定的时间

(6)min-fresh

表示客户端希望获取一个能在指定的秒数内保持其最新状态的响应

(7)only-if-cached

表示只接受代理缓存的数据,不接受源服务器的响应。如果代理上没有缓存或者缓存过期,就应该给客户端返回一个 504(Gateway Timeout)。

image.png

HTTP 缓存和代理的出现都是为了减轻服务器的压力,以空间换时间,也正是这些技术的的出现,是的网络请求更加迅速。

参考:
HTTP
透视HTTP协议