客户端缓存如何帮助服务器分担流量?

缓存 | 透明多级分流系统

Posted by Booogu on April 17, 2021
4679 字 14 分钟

前言:HTTP协议针对人们要求服务端与客户端之间“无状态”的交互原则所带来的请求需携带额外数据、网络性能降低的问题,采用了客户端缓存的解决方案,从HTTP1.0/1.1/2.0的演进过程中,逐步形成了现在被称为“状态缓存”、“强制缓存”和“协商缓存”三种HTTP缓存机制,而利用好客户端的缓存,能够节省大量网络流量,这是为后端系统分流,以实现更高并发的第一步。

状态缓存

所谓状态缓存,是指不经过服务器,客户端直接根据缓存信息来判断目标网站的状态

以前,状态缓存只有301/Moved Permanently(永久重定向)这一种;后来增加了HSTS(HTTP Script Transport Security)机制,用来避免依赖301/302跳转HTTPS时,可能产生的降级中间人劫持问题,这也属于一种状态缓存。

强制缓存

根据名字,强制缓存对于一致性问题的处理策略十分直接:假设在某个时间点,比如服务器收到响应后的10分钟内,资源的内容和状态一定不会被改变,因此客户端可以不需要经过任何请求,在该时间点到来之前一直持有和使用该资源的本地缓存副本。

强制缓存生效与失效场景: 在浏览器的地址输入、页面链接跳转、新开窗口、前进和后退中,强制缓存都可以生效,但在用户主动刷新页面时应当自动失效

实现强制缓存机制的两类Headers

第一类 Expires

Expires从HTTP1.0开始提供,带有一个时间截止参数,意味着服务器承诺在该时间之前,资源不会变动,浏览器可以直接缓存该数据,示例如下:

1
2
HTTP/1.1 200 OK
Expires: Wed, 8 Apr 2020 07:28:00 GMT

Expires存在的问题

  • 受限于客户端时钟
  • 无法处理涉及到用户身份的私有资源,比如已认证用户,可能缓存某些资源到自己的浏览器上,但如果被代理服务器或内容分发网络CDN缓存起来,就可能被其他未认证的用户获取。
  • 无法描述“不缓存”语义,Expires没有考虑如何强制浏览器不缓存某个资源,所以为了实现强制不缓存,通常需要使用脚本、或者手动在资源后增加时间戳(如“xx.js?t=1586359920”“xx.jpg?t=1586359350”)来保证每次资源都会重新获取

第二类 Cache-Control

Cache-Control在HTTP1.1协议中被定义,且优先级高于Expires(同时存在,以Cache-Control为主),示例如下:

1
2
HTTP/1.1 200 OK
Cache-Control: max-age=600

Cache-Control标准参数

  • max-age:秒为单位,表示相对于请求时间多少秒内缓存有效,避免了Expires种的绝对时间受客户时钟影响问题 。
  • s-maxage:s是“share”缩写,意味着“共享缓存”有效时间,即允许被CDN、代理等持有缓存的有效时间,主要用于提示CDN这类服务器如何对缓存进行失效。
  • public和private:表明是否是涉及用户身份的私有资源,public意味着可以被CDN、代理等缓存,private意味着只能由用户客户端缓存
  • no-cache:表明资源不应该被缓存,哪怕是同一个会话中对于同一个URL地址的请求,也必须从服务端获取,从而令强制缓存完全失效(但此时缓存协商机制是仍然生效的)
  • no-store:不强制会话中是否重复获取相同URL的资源,但它禁止浏览器、CDN等以任何形式保存该资源(直接禁止保存,no-cache不禁止缓存,侧重点在于直接打到服务器上)
  • no-transform:禁止资源被任何形式修改。比如,某些 CDN、透明代理支持自动 GZip 压缩图片或文本,以提升网络性能,而 no-transform 就禁止了这样的行为,它要求 Content-Encoding、Content-Range、Content-Type 均不允许进行任何形式的修改。
  • no-fresh和only-if-cached:这两个参数是仅用于客户端的请求 Header。min-fresh 后续跟随了一个以秒为单位的数字,用于建议服务器能返回一个不少于该时间的缓存资源(即包含 max-age 且不少于 min-fresh 的数字);only-if-cached 表示服务器希望客户端不要发送请求,只使用缓存来进行响应,若缓存不能命中,就直接返回 503/Service Unavailable 错误。
  • must-revalidate和proxy-revalidate:must-revalidate 表示在资源过期后,一定要从服务器中进行获取,即超过了 max-age 的时间后,就等同于 no-cache 的行为;proxy-revalidate 用于提示代理、CDN 等设备资源过期后的缓存行为,除对象不同外,语义与 must-revalidate 完全一致。

注意:强制缓存是基于时效性的,无论是人还是服务器,在大多数情况下,其实都是没有把握去承诺某项资源不会发生变化(也是其一项过于僵硬的缺点)

协商缓存

协商缓存,是一种基于变化检测的缓存机制,在处理一致性的问题上,它比强制缓存会有更好的表现,不过它需要一次变化检测的交互开销,在性能上会略差一些。

注意,协商缓存和强制缓存是没有互斥性的,可以并行工作。比如说,当强制缓存存在时,客户端可以直接从强制缓存中返回资源,无需进行变动检查;而当强制缓存超过时效,或者被禁止(no-cache / must-revalidate),协商缓存也仍然可以正常工作。

协商缓存的两种变动检查机制

协商缓存有两种变动检查机制,一种是根据资源的修改时间进行检查,另一种是根据资源唯一标识是否发生变化来进行检查。它们都是靠一组成对出现的请求、响应 Header 来实现的(注意,必须成对出现,也体现出“协商”的语义)。

根据资源修改时间检查

根据资源的修改时间进行检查的协商缓存机制。它的语义中包含了两种标准参数:Last-Modified 和 If-Modified-Since

服务端->客户端:Last-Modified,用于告诉客户端资源的最后修改时间。 客户端->服务端:对于带有这个 Header 的资源,当客户端需要再次请求时,会通过 If-Modified-Since,把之前收到的资源最后修改时间发送回服务端。

协商结果体现一: 如果此时,服务端发现资源在该时间后没有被修改过,就只要返回一个 304/Not Modified 的响应即可,无需附带消息体,从而达到了节省流量的目的:

1
2
3
HTTP/1.1 304 Not Modified
Cache-Control: public, max-age=600
Last-Modified: Wed, 8 Apr 2020 15:31:30 GMT

协商结果体现二: 而如果此时,服务端发现资源在该时间之后有变动,就会返回 200/OK 的完整响应,在消息体中包含最新的资源:

1
2
3
4
5
HTTP/1.1 200 OK
Cache-Control: public, max-age=600
Last-Modified: Wed, 8 Apr 2020 15:31:30 GMT

Content

根据资源唯一标识是否变化检查

“根据资源唯一标识是否发生变化来进行检查”的协商缓存机制。它的语义中也包含了两种标准参数:Etag 和 If-None-Match

服务端->客户端:Etag,用于告诉客户端资源的唯一标识。

HTTP 服务器可以根据自己的意愿,来选择如何生成这个标识,比如 Apache 服务器的 Etag 值,就默认是对文件的索引节点(INode)、大小和最后修改时间进行哈希计算后而得到的。

客户端->服务端:对于带有这个 Header 的资源,当客户端需要再次请求时,就会通过 If-None-Match,把之前收到的资源唯一标识发送回服务端。

协商结果体现一:如果此时,服务端计算后发现资源的唯一标识与上传回来的一致,就说明资源没有被修改过,只需返回一个 304/Not Modified 的响应即可,无需附带消息体,达到节省流量的目的:

1
2
3
HTTP/1.1 304 Not Modified
Cache-Control: public, max-age=600
ETag: "28c3f612-ceb0-4ddc-ae35-791ca840c5fa"

协商结果体现二:如果此时,服务端发现资源的唯一标识有变动,会返回 200/OK 的完整响应,在消息体中包含最新的资源:

1
2
3
4
5
HTTP/1.1 200 OK
Cache-Control: public, max-age=600
ETag: "28c3f612-ceb0-4ddc-ae35-791ca840c5fa"

Content

需要强调的是:Etag是HTTP中一致性最强的缓存机制,同时,也是HTTP中性能最差的缓存机制

一致性最强:比如对于Last-Modified 参数,它标注的最后修改只能精确到秒级,而如果某些文件在一秒钟以内被修改多次的话,它就不能准确标注文件的修改时间了;又或者,如果某些文件会被定期生成,可能内容上并没有任何变化,但 Last-Modified 却改变了,导致文件无法有效使用缓存。而这些情况,Last-Modified 都有可能产生资源一致性的问题,只能使用 Etag 解决。

性能最差:体现在每次请求时,服务端都必须对资源进行哈希计算,这比起简单获取一下修改时间,开销要大了很多。所以,Etag 和 Last-Modified 是允许一起使用的,服务器会优先验证 Etag,在 Etag 一致的情况下,再去对比 Last-Modified,这是为了防止有一些 HTTP 服务器没有把文件修改日期纳入哈希范围内

什么是HTTP的内容协商机制

上面提到的协商缓存,是针对缓存的“协商”机制,而HTTP的内容协商机制,则是用来协商HTTP传输的内容本身的。

为什么需要内容协商?

在HTTP设计中,一个URL可以对应同一个资源的不同版本,比如,一段文字的不同语言版本、一个文件的不同编码格式版本、一份数据的不同压缩格式版本等,因此,针对请求的缓存机制,也必须能提供对单资源多版本的支持,如何支持呢?通过协商

内容协商的载体

HTTP的内容协商过程,也是由一对同时出现的Header来体现的,其载体为:

  • Accept*(Accept、Accept-Language、Accept-Charset、Accept-Encoding)开头的一套请求 Header
  • 与上述Header对应的以Content-*(Content-Language、Content-Type、Content-Encoding)开头的响应 Header

内容协商的过程

有了上述载体,对于一个URL能够获取同一资源多个版本的场景中,缓存同样也需要明确的表示来获知,它要根据什么内容来对同一个URL返回给用户正确的资源。这个就是Vary Header的作用,Vary之后应该跟随一组其他Header的名字,比如:

1
2
HTTP/1.1 200 OK
Vary: Accept, User-Agent

以上响应的含义是:应该根据 MINE 类型和浏览器类型来缓存资源,另外服务端在获取资源时,也需要根据请求 Header 中对应的字段,来筛选出适合的资源版本。

根据约定,协商缓存不仅可以在浏览器的地址输入、页面链接跳转、新开窗口、前进、后退中生效,而且在用户主动刷新页面(F5)时也同样是生效的。只有用户强制刷新(Ctrl+F5)或者明确禁用缓存(比如在 DevTools 中设定)时才会失效,此时客户端向服务端发出的请求会自动带有“Cache-Control: no-cache”