learn-tech/专栏/周志明的架构课/17_客户端缓存是如何帮助服务器分担流量的?.md
2024-10-16 06:37:41 +08:00

15 KiB
Raw Blame History

                        因收到Google相关通知网站将会择期关闭。相关通知内容
                        
                        
                        17 _ 客户端缓存是如何帮助服务器分担流量的?
                        你好,我是周志明。这节课,我们继续来讨论透明多级分流系统中,最靠近用户一侧的分流部件:浏览器的客户端缓存。

当万维网刚刚出现的时候浏览器的缓存机制差不多就已经存在了。在HTTP协议设计之初人们便确定了服务端与客户端之间“无状态”Stateless的交互原则即要求客户端的每次请求是独立的每次请求无法感知、也不能依赖另一个请求的存在这既简化了HTTP服务器的设计也为它的水平扩展能力留下了广阔的空间。

但无状态并不是只有好的一面。因为客户端的每次请求都是独立的,服务端不会保存之前请求的状态和资源,所以也不可避免地导致它会携带重复的数据,造成网络性能的降低。

那么HTTP协议针对这个问题的解决方案就是客户端缓存。从HTTP/1.0到1.1、再到2.0版本的演进中逐步形成了现在被称为“状态缓存”、“强制缓存”或简称为“强缓存”和“协商缓存”这三种HTTP缓存机制。

这其中的状态缓存是指不经过服务器客户端直接根据缓存信息来判断目标网站的状态。以前只有301/Moved Permanently永久重定向这一种后来在RFC6797中增加了HSTSHTTP Strict Transport Security机制用来避免依赖301/302跳转HTTPS时可能产生的降级中间人劫持问题在第28、29讲中我还会展开讲解这个问题这也属于另一种状态缓存。

因为状态缓存涉及的内容只有这么一点,所以后面,我们就只聚焦在强制缓存与协商缓存这两种机制的探讨上。

下面我们就先来看看强制缓存的实现机制吧。

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

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

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

在HTTP协议中设置了两类可以实现强制缓存的Headers标头Expires和Cache-Control。

第一类Expires

Expires是HTTP/1.0协议中开始提供的Header后面跟随了一个截止时间参数。当服务器返回某个资源时如果带有该Header的话就意味着服务器承诺在截止时间之前资源不会发生变动浏览器可直接缓存该数据不再重新发请求。我们直接来看一个Expires头的示例程序

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

那么你能看到Expires设计得非常直观易懂但它考虑得其实并不周全。我给你简单举几个例子。

受限于客户端的本地时间

比如,在收到响应后,客户端修改了本地时间,将时间点前后调整了几分钟,这就可能会造成缓存提前失效或超期持有。

无法处理涉及到用户身份的私有资源

比如合理的做法是某些资源被登录用户缓存在了自己的浏览器上。但如果被代理服务器或者内容分发网络CDN缓存起来就可能会被其他未认证的用户获取。

无法描述“不缓存”的语义

比如一般浏览器为了提高性能往往会自动在当次会话中缓存某些MINE类型的资源这会造成设计者不希望缓存的资源无法被及时更新。而在HTTP/1.0的设计中Expires并没有考虑这方面的需求导致无法强制浏览器不允许缓存某个资源。

所以以前为了实现这类功能我们通常不得不使用脚本或者手工在资源后面增加时间戳如“xx.js?t=1586359920”“xx.jpg?t=1586359350”来保证每次资源都会重新获取。

不过关于“不缓存”的语义在HTTP/1.0中其实是预留了“Pragma: no-cache”来表达的但在HTTP/1.0中并没有确切地描述Pragma参数的具体行为随后它就被HTTP/1.1中出现过的Cache-Control给替代了。

现在尽管主流的浏览器也通常都会支持Pragma但它的行为仍然是不确定的实际上并没有什么使用价值。而Cache-Control的出现进一步压缩了Pragma的生存空间。所以接下来我们就一起来看看它是如何支持强制缓存机制的实现的。

第二类Cache-Control

Cache-Control是HTTP/1.1协议中定义的强制缓存Header它的语义比起Expires来说就丰富了很多。而如果Cache-Control和Expires同时存在并且语义存在冲突比如Expires与max-age / s-maxage冲突的话IETF规定必须以Cache-Control为准。

同样这里我们也看看Cache-Control的使用示例

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

那么你能看到这里的示例中使用的参数是max-age。实际上在客户端的请求Header或服务器的响应Header中Cache-Control都可以存在它定义了一系列的参数并且允许自行扩展即不在标准RFC协议中由浏览器自行支持的参数。Cache-Control标准的参数主要包括6种下面我就带你一一了解下。

max-age和s-maxage

在前面的示例中你会发现max-age后面跟随了一个数字它是以秒为单位的表明相对于请求时间在Date Header中会注明请求时间多少秒以内缓存是有效的资源不需要重新从服务器中获取。这个相对时间就避免了Expires中采用的绝对时间可能受客户端时钟影响的问题。

另一个类似的参数是s-maxage其中的“s”是“Share”的缩写意味着“共享缓存”的有效时间即允许被CDN、代理等持有的缓存有效时间这个参数主要是用来提示CDN这类服务器如何对缓存进行失效。

public和private

这一类参数表明了是否涉及到用户身份的私有资源。如果是public就意味着资源可以被代理、CDN等缓存如果是private就意味着只能由用户的客户端进行私有缓存。

no-cache和no-store

no-cache表明该资源不应该被缓存哪怕是同一个会话中对同一个URL地址的请求也必须从服务端获取从而令强制缓存完全失效但此时的协商缓存机制依然是生效的no-store不强制会话中是否重复获取相同的URL资源但它禁止浏览器、CDN等以任何形式保存该资源。

no-transform

no-transform禁止资源以任何形式被修改。比如某些CDN、透明代理支持自动GZip压缩图片或文本以提升网络性能而no-transform就禁止了这样的行为它要求Content-Encoding、Content-Range、Content-Type均不允许进行任何形式的修改。

min-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完全一致。

好了,现在你应该就已经理解了强制缓存的实现机制了。但是,强制缓存是基于时效性的,无论是人还是服务器,在大多数情况下,其实都没有什么把握去承诺某项资源多久不会发生变化。

所以,接下来我就要给你介绍另一种基于变化检测的缓存机制,也就是协商缓存。它在处理一致性问题上,比强制缓存会有更好的表现。不过它需要一次变化检测的交互开销,在性能上就会略差一些。

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

那么在开始了解协商缓存的实现机制之前你要先注意一个地方就是在HTTP中协商缓存与强制缓存并没有互斥性这两套机制是并行工作的。

比如说当强制缓存存在时客户端可以直接从强制缓存中返回资源无需进行变动检查而当强制缓存超过时效或者被禁止no-cache / must-revalidate协商缓存也仍然可以正常工作。

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

根据资源的修改时间进行检查

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

Last-Modified是服务器的响应Header用来告诉客户端这个资源的最后修改时间。

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

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

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

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

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是服务器的响应Header用于告诉客户端这个资源的唯一标识。HTTP服务器可以根据自己的意愿来选择如何生成这个标识比如Apache服务器的Etag值就默认是对文件的索引节点INode、大小和最后修改时间进行哈希计算后而得到的。

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

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

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

而如果此时服务端发现资源的唯一标识有变动也一样会返回200/OK的完整响应在消息体中包含最新的资源。

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

Content

另外我还想强调的是Etag是HTTP中一致性最强的缓存机制。

为什么会这么说呢?我直接给你举个例子。

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

但是Etag又是HTTP中性能最差的缓存机制。这个“最差”体现在每次请求时服务端都必须对资源进行哈希计算这比起简单获取一下修改时间开销要大了很多。

所以Etag和Last-Modified是允许一起使用的服务器会优先验证Etag在Etag一致的情况下再去对比Last-Modified这是为了防止有一些HTTP服务器没有把文件修改日期纳入哈希范围内。

HTTP的内容协商机制

那到这里为止HTTP的协商缓存机制就已经能很好地处理通过URL获取单个资源的场景了。不过你可能要问了为什么要强调“单个资源”呢

我们知道在HTTP协议的设计中一个URL地址是有可能提供多份不同版本的资源的比如说一段文字的不同语言版本一个文件的不同编码格式版本一份数据的不同压缩方式版本等等。因此针对请求的缓存机制也必须能够提供对应的支持。

所以针对这种情况HTTP协议设计了以Accept*Accept、Accept-Language、Accept-Charset、Accept-Encoding开头的一套请求Header以及对应的以Content-*Content-Language、Content-Type、Content-Encoding开头的响应Header。这些Headers被称为HTTP的内容协商机制。

那么与之对应的对于一个URL能够获取多个资源的场景中缓存同样也需要有明确的标识来获知它要根据什么内容来对同一个URL返回给用户正确的资源。这个就是Vary Header的作用Vary后面应该跟随一组其他Header的名字比如说

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

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

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

小结

HTTP协议以“无状态”作为基本的交互原则那么由此而来的资源重复访问问题就需要通过网络链路中的缓存来解决了。现在你也已经知道客户端缓存具体包括了“状态缓存”、“强制缓存”和“协商缓存”三类。

这节课,我们详细分析了强制缓存和协商缓存的工作原理。利用好客户端的缓存,能够节省大量网络流量,这是为后端系统分流,以实现更高并发的第一步。

一课一思

除了在HTTP协议中规定的缓存机制你还使用过其他客户端的缓存方式吗比如说浏览器WebAPI中的Cache Storage和Application Cache或者你是否通过LocalStorage、SessionStorage、IndexedDB、WebSQL等等去缓存信息

欢迎在留言区分享你的做法和见解。如果你觉得有收获,也欢迎把今天的内容分享给更多的朋友。感谢你的阅读,我们下一讲再见。