透视HTTP协议-HTTP的缓存机制、代理服务及代理缓存

394 阅读15分钟

HTTP的缓存机制

缓存(Cache)是计算机领域里的一个重要概念,是优化系统性能的利器。

服务器的缓存控制

  1. 浏览器发现缓存无数据,于是发送请求,向服务器获取资源;
  2. 服务器响应请求,返回资源,同时标记资源的有效期;
  3. 浏览器缓存资源,等待下次重用。 服务器标记资源有效期使用的头字段是“Cache-Control”,里面的值“max-age=30”就是资源的有效期,相当于告诉浏览器:“这个页面只能缓存30s,之后就算过期不能用”。

注意,这里的max-age是“生存时间”,时间的计算起点是响应报文的创建时刻,而不是客户端收到报文的时刻,也就是说包含了在链路传输过程中所有节点所停留的时间。

除了“max-age”,在响应报文里还可以用其他的属性来更精确地指示浏览器应该如何使用缓存:

  • no-store:不允许缓存;
  • no-cache:可以缓存,但在使用之前必须要去服务器验证是否过期,是否有最新的版本;
  • must-revalidate:如果缓存不过期就可以继续使用,但过期了如果还想用就必须去服务器验证

服务器的缓存控制策略流程图如下:

客户端的缓存控制

其实不止服务器可以发“Cache-Control”头,浏览器也可以发“Cache-Control”,也就是说请求-应答的双方都可以用这个字段进行缓存控制,互相协商缓存的使用策略。

当你点“刷新”按钮的时候,浏览器会在请求头里加一个“Cache-Control:max-age=0”,max-age=0的意思就是我需要一个最新的数据,而本地缓存里的数据至少保存了几秒钟,所以浏览器不会使用缓存,而是向服务器发请求。服务器看到max-age=0,也就会用一个最新生成的报文回应浏览器。

Ctrl+F5的“强制刷新”其实就是发了一个“Cache-Control:no-cache”,含义跟“max-age=0”基本一样。

条件请求

HTTP协议定义了一系列“If ”开头的“条件请求”字段,专门用来检查验证资源是否过期,把两个请求才能完成的工作合并在一个请求里做。而且,验证的责任也交给服务器,浏览器只需“坐享其成”。

条件请求一共有5个头字段,我们最常用的是“If-Modified-Since”和“If-None-Match”这两个。需要第一次的响应报文预先提供“Last-modified”(文件的最后修改时间)和“ETag”(Entity Tag的缩写,是资源的一个唯一标识,主要是用来解决修改时间无法准确区分文件变化的问题),然后第二次请求时就可以带上缓存里的原值,验证资源是否是最新的。

如果资源没有变,服务器就回应一个“304 Not Modified”,表示缓存依然有效,浏览器就可以更新一下有效期,然后放心大胆地使用缓存了。

条件请求里其他的三个头字段是“If-Unmodified-Since”“If-Match”“If-Range”。

HTTP的代理服务

链条的起点还是客户端(也就是浏览器),中间的角色被称为代理服务器(proxy server),链条的终点被称为源服务器(origin server),意思是数据的“源头”“起源”。

代理服务

所谓的“代理服务”就是指服务本身不生成内容,而是出于中间位置转发上下游的请求和响应,具有双重身份:面对下游的用户时,表现为服务器,代表源服务器响应客户端的请求;而面对上游的源服务器时,又表现为客户端,代表客户端发送请求。

代理有很多的种类,例如匿名代理、透明代理、正向代理和反向代理。实际工作中最常见的是反向代理,它在传输链路中更靠近源服务器,为源服务器提供代理服务。以下主要说明的也是反向代理。

代理的作用

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

代理中常用的负载均衡算法有轮询、一致性哈希等,这些算法的目标都是尽量把外部的流量合理地分散到多台源服务器,提高系统的整体资源利用率和性能。

在负载均衡的同时,代理服务还可以执行更多的功能,如:

  • 健康检查:使用“心跳”等机制监控后端服务器,发现有故障就及时“踢出”集群,保证服务高可用;
  • 安全防护:保护被代理的后端服务器,限制IP地址或流量,抵御网络攻击和过载;
  • 加密卸载:对外网使用SSL/TLS加密通信认证,而在安全的内网不加密,消除加解密成本;
  • 内容缓存:暂存、复用服务器响应

代理相关头字段

代理隐藏了真实客户端和服务器,如果双方想要获得这些“丢失”的原始信息,首先,代理服务器需要用字段“Via”标明代理的身份。

Via是一个通用字段,请求头或响应头里都可以出现。每当报文经过了一个代理节点,代理服务器就会把自身的信息追加到字段的末尾,就像是经手人改了一个章。

如果通信链路中有很多中间代理,就会在Via里形成一个链表,这样就可以知道报文究竟走过了多少个环节才到达了目的地。

Via字段只解决了客户端和源服务器判断是否存在代理的问题,还不能知道对方的真实信息。

但服务器的IP地址应该是保密的,不过反过来,通常服务器需要知道客户端的真实IP地址,方便做访问控制、用户画像、统计分析。

最常用的两个头字段是“X-Forwarded-For”和“X-Real-IP”,“X-Forwarded-For”的字面意思是“为谁而转发”,形式上和“Via”差不多,也是每经过一个代理节点就会在字段里追加一个信息。但“Via”追加的是代理主机名(或者域名),而“X-Forwarded-For”追加的是请求方的 IP 地址。所以,在字段里最左边的 IP 地址就客户端的地址。“X-Real-IP”是另一种获取客户端真实 IP 的手段,它的作用很简单,就是记录客户端 IP 地址,没有中间的代理信息,相当于是“X-Forwarded-For”的简化版。如果客户端和源服务器之间只有一个代理,那么这两个字段的值就是相同的。

另外还有两个字段:“X-Forwarded-Host”和“X-Forwarded-Proto”,它们的作用与“X-Real-IP”类似,只记录客户端的信息,分别是客户端请求的原始域名和原始协议名。

代理协议

因为通过“X-Forwarded-For”操作代理信息必须解析HTTP报文头,会降低代理的转发性能。而且“X-Forwarded-For”等头必须要修改原始报文,对于有些情况,是不允许甚至不可能的(比如使用HTTPS通信被加密)

所以就出现了一个专门的“代理协议”(The PROXY protocol)它由知名的代理软件 HAProxy 所定义,也是一个“事实标准”,被广泛采用(注意并不是 RFC)。

“代理协议”有 v1 和 v2 两个版本,v1 和 HTTP 差不多,也是明文,而 v2 是二进制格式。今天只介绍比较好理解的 v1,它在 HTTP 报文前增加了一行 ASCII 码文本,相当于又多了一个头。

这一行文本其实非常简单,开头必须是“PROXY”五个大写字母,然后是“TCP4”或者“TCP6”,表示客户端的 IP 地址类型,再后面是请求方地址、应答方地址、请求方端口号、应答方端口号,最后用一个回车换行(\r\n)结束。

例如下面的这个例子,在 GET 请求行前多出了 PROXY 信息行,客户端的真实 IP 地址是“1.1.1.1”,端口号是 55555。

PROXY TCP4 1.1.1.1 2.2.2.2 55555 80\r\n
GET / HTTP/1.1\r\n
Host: www.xxx.com\r\n
\r\n

服务器看到这样的报文,只需要解析第一行就可以拿到客户端地址,不需要再去理会后面的HTTP数据。

不过代理协议并不支持“X-Forwarded-For”的链式地址形式,所以拿到客户端地址后再如何处理就需要代理服务器与后端自行约定。

HTTP的缓存代理

HTTP的服务器缓存功能主要由代理服务器来实现(即缓存代理)

缓存代理服务

代理服务器收到源服务器发来的响应数据后需要做两件事。第一件是把报文转发给客户端,而第二个就是把报文存入自己的Cache里。下一次再有相同的请求,代理服务器就可以直接发送304或者缓存数据,不必再从源服务器那里获取。

在 HTTP 的缓存体系中,缓存代理的身份十分特殊,它“既是客户端,又是服务器”,同时也“既不是客户端,又不是服务器”。说它“即是客户端又是服务器”,是因为它面向源服务器时是客户端,在面向客户端时又是服务器,所以它即可以用客户端的缓存控制策略也可以用服务器端的缓存控制策略,也就是说它可以同时使用上面 讲的各种“Cache-Control”属性。但缓存代理也“即不是客户端又不是服务器”,因为它只是一个数据的“中转站”,并不是真正的数据消费者和生产者,所以还需要有一些新的“Cache-Control”属性来对它做特别的约束。

源服务器的缓存控制

我们要区分客户端上的缓存和代理上的缓存,可以使用两个新属性“private”和“public”,“private”表示缓存只能在客户端保存,是用户“私有”的,不能放在代理上与别人共享。而“public”的意思就是缓存完全开放,谁都可以存,谁都可以用。

其次,缓存失效后的重新验证也要区分开(即使用条件请求“Last-modified”和“ETag”),“must-revalidate”是只要过期就必须回源服务器验证,而新的“proxy-revalidate”只要求代理的缓存过期后必须验证,客户端不必回源,只验证到代理这个环节就行了。

再次,缓存的生存时间可以使用新的“s-maxage”(s 是 share 的意思,注意 maxage 中间没有“-”),只限定在代理上能够存多久,而客户端仍然使用“max-age”。

还有一个代理专用的属性“no-transform”。代理有时候会对缓存下来的数据做一些优化,比如把图片生成 png、webp 等几种格式,方便今后的请求处理,而“no-transform”就会禁止这样做,不许“偷偷摸摸搞小动作”。

下图是完整的服务器端缓存控制策略,可以同时控制客户端和代理。

另外,源服务器在设置完“Cache-Control”后必须要为报文加上“Last-modified”或“ETag”字段。否则,客户端和代理后面就无法使用条件请求来验证缓存是否有效,也就不会有 304 缓存重定向。

客户端的缓存控制

客户端在HTTP缓存体系里要面对的是代理和源服务器,也必须区别对待,如下图

图中两个新属性“max-stale”和“min-fresh”,“max-stale”的意思是如果代理上的缓存过期了也可以接受,但不能过期太多,超过x秒也会不要。“min-fresh”的意思是缓存必须有效,而且必须在x秒后依然有效。

有时候客户端还会发出一个特别的“only-if-cached”属性,表示只接受代理缓存的数据,不接受源服务器的响应。如果代理上没有缓存或者缓存过期,就应该给客户端返回一个504(Gateway Timeout)。

其他问题

两个相关的问题:

第一个是“Vary”字段,它是内容协商的结果,相当于报文的一个版本标记。

同一个请求,经过内容协商后可能会有不同的字符集、编码、浏览器等版本。比如,“Vary: Accept-Encoding”“Vary: User-Agent”,缓存代理必须要存储这些不同的版本。

当再收到相同的请求时,代理就读取缓存里的“Vary”,对比请求头里相应的“ Accept-Encoding”“User-Agent”等字段,如果和上一个请求的完全匹配,比如都是“gzip”“Chrome”,就表示版本一致,可以返回缓存的数据。

另一个问题是“Purge”,也就是“缓存清理”,它对于代理也是非常重要的功能,例如:

  • 过期的数据应该及时淘汰,避免占用空间;
  • 源站的资源有更新,需要删除旧版本,主动换成最新版(即刷新);
  • 有时候会缓存了一些本不该存储的信息,例如网络谣言或者危险链接,必须尽快把它们删除。

清理缓存的方法有很多,比较常用的一种做法是使用自定义请求方法“PURGE”,发给代理服务器,要求删除URI对应的缓存数据。

Q&A

Q:Cache 和 Cookie 都是服务器发给客户端并存储的数据,你能比较一下两者的异同吗?

A:Cache 和 Cookie 的相同点是:都会保存到浏览器中,并可以设置过期时间。

不同点:

  1. Cookie 会随请求报文发送到服务器,而 Cache 不会,但可能会携带 if-Modified-Since(保存资源的最后修改时间)和 If-None-Match(保存资源唯一标识) 字段来验证资源是否过期。
  2. Cookie 在浏览器可以通过脚本获取(如果 cookie 没有设置 HttpOnly),Cache 则无法在浏览器中获取(出于安全原因)。
  3. Cookie 通过响应报文的 Set-Cookie 字段获得,Cache 是缓存完整的报文。
  4. 用途不同。Cookie 常用于身份识别,Cache 则是由浏览器管理,用于节省带宽和加快响应速度。
  5. Cookie 的 max-age 是从浏览器拿到响应报文时开始计算的,而 Cache 的 max-age 是从响应报文的生成时间(Date 头字段)开始计算。

Q:即使有“Last-modified”和“ETag”,强制刷新(Ctrl+F5)也能够从服务器获取最新数据(返回 200 而不是 304),请你在实验环境里试一下,观察请求头和响应头,解释原因。

A:强制刷新是因为请求头里的 If-Modified-Since 和 If-None-Match 会被清空所以会返回最新数据。

Q:你觉得代理有什么缺点?实际应用时如何避免?

A:a 代理服务器与上下游的通信机制也是http协议,因此增加了传输中的数据泄漏和篡改风险,可以使用https解决。b 如果代理服务器发生故障,会影响客户端的正常访问,可以增加代理服务器的数量,并配置代理服务器负载均衡算法。c 由于多了代理服务器的请求响应过程,增加了从源客户端和源服务器之间的来回时间。

Q:你知道多少反向代理中使用的负载均衡算法?它们有什么优缺点?

A:轮询,加权轮询,随机法,加权随机法,源地址哈希法,最小连接数法

Q:加入了代理后 HTTP 的缓存复杂了很多,试着用自己的语言把这些知识再整理一下,画出有缓存代理时浏览器的工作流程图,加深理解。

Q:缓存的时间策略很重要,太大太小都不好,你觉得应该如何设置呢?

A:需要根据具体情况来定: 如果缓存的内容不变,那可以把缓存时间设置为永久。 如果缓存的内容会变化,但周期较长,可以根据她的变化周期来设置,比如:一天或一周 如果缓存的内容变化频繁,那缓存的过期时间就需要更短了,比如:一分钟 如果缓存的内容随时变化,且没啥规律,那还是不用用了 总之是根据场景来的核心是在提速的愿望能实现的前提下,数据也是最新的,否则不如不用缓存。