浅谈 HTTP 缓存与 CDN 缓存的那点事

HTTP 缓存与 CDN 缓存一直是提升 web 性能的两大利器,合理的缓存配置可以降低带宽成本、减轻服务器压力、提升用户的体验。而不合理的缓存配置会导致资源界面无法及时更新,从而引发一系列的衍生问题。本文将分别将从 HTTP 缓存与 cdn 缓存的规则、流程、配置入手,能让大家了解基础概念的同时,可对自己的项目配置定制化的缓存调优方案,以及在遇到缓存问题时如何快速定位解决。
首先,让我们来了解一下缓存在实际场景中的应用
用户第一次访问网站时,浏览器会从服务器获取所有的资源。在传输过程中,浏览器会通过一些约定好的响应头,从而确定是否需要将这个资源保存一份到本地作为缓存,当用户第二次访问该网站的时候,浏览器就会从缓存中加载资源,不用向服务器请求资源,从而提高了网站的访问速度,而若使用了 CDN,当浏览器本地缓存的资源过期之后,浏览器不是直接向源站点请求资源,而是向 CDN 边缘节点请求资源,CDN 边缘节点中也存在缓存,若 CDN 中的缓存也过期,那就由 CDN 边缘节点向源站点发出回源请求来获取最新资源。
HTTP 缓存
简介
http 缓存是一种客户端缓存,当 Web 浏览器向服务器发起资源请求时,服务器可以在响应报文头中包含缓存相关的信息。这些 HTTP Header 会告诉浏览器是否以及如何缓存资源,再次请求时如果命中缓存将直接读取本地缓存不再发出请求。
缓存规则
http 缓存规则由响应头中 Expires,Cache-Control ,Last-Modified ,Etag 这四个关键字段控制。其中 Expires 和 Cache-Control 为强缓存用来确定确定缓存的存储时间,Last-Modified 和 Etag 为协商缓存则用来确定缓存是否要被更新,接下来我们简单来看一下区别。
强缓存
•expires: HTTP1.0 中用来控制缓存时间的参数,header 里包含日期/时间,用 GMT 格式的字符串表示, 即在此时间之后,响应过期。
•cache-control: HTTP1.1 中用来控制缓存时间的参数
◦public: 表明响应可以被任何对象(包括:发送请求的客户端,代理服务器,等等)缓存。
◦private: 表明响应只能被单个用户缓存,不能作为共享缓存(即代理服务器例如 CDN 不能缓存它)。
◦max-age=<seconds>: 设置缓存存储的最大周期,相对于请求的时间缓存 seconds 秒,在此时间内,访问资源直接读取本地缓存,不向服务器发出请求。(与 expires 同时出现时,max-age 优先级更高)
◦s-maxage=<seconds>: 规则等同 max-age,覆盖 max-age 或者 Expires 头,但是仅适用于共享缓存(比如各个代理),并且私有缓存中它被忽略。(与 expires 或 max-age 同时出现时,s-maxage 优先级更高)
◦no-store: 不缓存服务器响应的任何内容,每次访问资源都需要服务器完整响应
◦no-cache: 缓存资源,但立即过期,每次请求都需要跟服务器对比验证资源是否被修改。(等同于 max-age=0)
协商缓存
•Last-modified: 源头服务器认定的资源做出修改的日期及时间。精确度比 Etag 低。包含有 If-Modified-Since ( 资源修改的时间 )或 If-Unmodified-Since 首部的条件请求会使用这个字段,Last-Modified 优先级低于 Etag。
•Etag: HTTP 响应头是资源的特定版本的标识符,如果客户端想再次请求相同的 URL,将会发送一个包含已保存的 ETag 和“If-None-Match”(标识符字符串)字段的请求。客户端请求之后,服务器可能会比较客户端的 ETag 和当前版本资源的 ETag(只要文件内容改动,ETag 就会重新计算)。如果 ETag 值匹配,这就意味着资源没有改变,服务器便会发送回一个极短的响应,包含 HTTP “304 未修改”的状态。304 状态告诉客户端,它的缓存版本是最新的,并应该使用它。
我们通过 chrome 控制台可以很轻松的找到一个案例:

图中配置
1.Cache-control: max-age=3600 代表相对于请求时间,缓存 3600 秒,即 1 小时,在此时间内,再次访问资源直接读取本地缓存,不向服务器发送请求.
2.Last-modified: Mon...上次修改时间,如果缓存时间过期,该字段将用于与请求中的 If-Modified-Since 字段进行对比,一致则继续使用之前缓存,不一致则认定缓存失效
3.Expires: 在 http1.0 版本下被 cache-control 覆盖,此处意为缓存至 Mon, 07 Nov ...
4.Etag: Web 服务器会返回资源和其相应的 ETag 值,该字段将用于与当前客户端版本资源的 ETag 进行对比,一致则继续使用之前缓存,不一致则认定缓存失效
缓存流程

缓存规则在其中是如何起作用的呢,我们来看几个重点关注部分
重点关注 1: 缓存是否过期
基于该资源上次响应缓存规则同时满足下列条件则视为缓存未过期,不发请求直接从本地缓存读取该文件。需要注意的是,判断缓存是否过期只跟客户端有关系,与服务端无关。1&2&3 同时满足即认为缓存未过期,相反则是已过期
1.cache-control 值为 max-age
2.max-age > 0
3.当前 date < 上次请求时的 date + max-age
注:如果 HTTP 为 1.0 时,则用 expires 判断是否过期,如果 HTTP 为 1.1 及其以上时,则查看 cache-control。
重点关注 2: 询问服务器资源是否修改
判断资源是否修改,需要客户端与服务器共同协作,客户端在首次拿到资源缓存后会存储 Etag(若有)和 Last-Modified(若有),在下次缓存过期时会将 Etag 写在请求头部中的 If-None-Match 中,将 Last-Modified 值写在请求头部中的 If-Modified-Since 中,服务端优先对 Etag 进行对比,然后再对比 Last-Modified,一致即视为缓存没有修改,命中协商缓存,返回 304,不一致则返回新文件并带上新的 Etag 或 Last-Modified 值。
重点关注 3: 缓存规则
参考上文缓存规则,不在赘述。
小结
对于 http 缓存的配置,我们只有在了解 http 缓存的原理、规则、流程后,才能根据不同的情况定制不同的规则,真正的发挥 http 缓存在实际业务中的价值。
CDN 缓存
cdn 缓存是一种服务端缓存,cdn 服务商可以将源站上的资源缓到其各地的边缘服务器节点上。当用户访问该资源时,cdn 再通过负载均衡将用户的请求调度到最近的缓存节点上,有效减少了链路回源,提高了资源访问效率及可用性,降低带宽消耗。
缓存规则
与 http 缓存规则不同的是,这个规则并不是规范性的,而是由 cdn 服务商来制定,我们以 JD 内部 CDN 举例,打开 cdn 接入界面,面板如下。

可以看到,提供给我们的配置项只有文件类型(或文件目录)和 Http2,在 cdn 节点上缓存默认遵循源站设置缓存时长。
运作流程

由图我们可以看出 CDN 的主要处理逻辑集中在缓存处理阶段,除了关注 CDN 缓存的文件类型及时间外,我们还需要引入一个概念——回源,客户端请求访问资源时,如果 CDN 节点上未缓存该资源,或者部署预热任务给 CDN 节点时,CDN 节点会回源站获取资源。如图中所示,接入 cdn 后,我们提供服务的服务器就是源站,源站一般情况下只会在 cdn 节点没有资源或 cdn 资源失效时接收到 cdn 节点的资源请求,其他时间,源站并不会接收请求。简单的概括就是,没有资源就去源站读取,有资源就直接发送给用户。值得注意的是 cdn 中有 s-maxage=0、max-age=0、no-cache、no-store、private 中的任一种时候,该类型文件就被认定为不缓存文件,就是所有请求直接转发源站,只有当缓存时间大于 0 且缓存过期的时候,才会与源站对比缓存是否被修改。
缓存配置
与在 Web 浏览器中的缓存规则类似,可通过发送缓存指令标头来控制缓存在 CDN 中的执行方式。尽管大部分标头最初都旨在解决客户端浏览器中的缓存问题,但现在所有中间缓存(如 CDN)也会使用这些标头,可使用两个标头来定义缓存刷新:Cache-Control 和 Expires。 如果两者都存在,则 Cache-Control 为最新且优先于 Expires。 还有两种用于验证的标头类型(称为验证程序):ETag 和 Last-Modified。 如果两者均已定义,则 ETag 为最新且优先于 Last-Modified。以 OSS 对象存储为例,在缓存配置的文档中特别有以下说明。
缓存继承
当用户请求您某一业务资源时,源站对应的 Response HTTP Header 中存在 Cache-Control 字段,此时默认策略如下:
•Cache-Control 字段为 max-age,对该资源的缓存时间以配置的缓存时间为主,对于小于 1 小时的缓存时长,不继承 max-age 指定时间。
•Cache-Control 字段为 s-maxage=0、max-age=0、no-cache、no-store、private、nil 或无 Cache-Control 字段时,对象存储会源节点会为 CDN 默认添加:Cache-Control: max-age=3600 头部字段,已确保提高缓存的命中率,同时应对高并发回源流量带来的风险与成本的增加。
缓存影响
1.如果 http 缓存设置 cache-control: max-age=600,即缓存 10 分钟,但对象存储 cdn 缓存配置中设置文件缓存时间默认为 1 小时,那么就会出现如下情况,文件被访问后第 20 分钟修改并上传到服务器,用户重新访问资源,响应码会是 304,对比缓存未修改,资源依然是旧的,一个小时后再次访问才能更新为最新资源
2.如果不设置 cache-control 呢,在 http 缓存中我们说过,如果不设置 cache-control,那么会有默认的缓存时间,但在这里,对象存储 cdn 服务商明确会在没有 cache-control 字段时主动帮我们添加 cache-control: max-age=3600。
注:针对问题 1,也并非没有办法,当我们必须要在缓存期内修改文件,并且不向想影响用户体验,那么我们可以使用 cdn 服务商提供的强制更新缓存功能,主要注意的是,这里的强制更新是更新服务端缓存,http 缓存依然按照 http 头部规则进行自己的缓存处理,并不会受到影响。
小结
cdn 缓存的配置并不复杂, 复杂的情况在于 cdn 缓存配置会受到 http 缓存配置的影响,并且不同的 cdn 运营商有各自的运营规则计费标准,结合来看才能让 cdn 缓存在业务中发挥最大的效能。
HTTP 缓存与 CDN 缓存的结合
在我们分别了解 http 缓存配置和 cdn 缓存配置后,让我们再结合引言看一次二者结合的请求过程

当用户访问我们的业务服务器时,首先进行的就是 http 缓存处理,如果 http 缓存通过校验,则直接响应给用户,如果未通过校验,则继续进行 cdn 缓存的处理,cdn 缓存处理完成后返回给客户端,由客户端进行 http 缓存规则存储并响应给用户。再回到开篇缓存在实际场景中的应用,当我们分析缓存问题时,一定要将两个流程独立开来分析判断,是由于 http 缓存配置的不合理,还是 cdn 缓存未及时更新引起的问题。
实战场景推荐(懒人版)
不同访问场景下的缓存规则选择
1.不更新文件内容
优先使用 http 的本地缓存,配置 cache-control: max-age=seconds //seconds > 0(且设置为较大值 31536000,即 1 年):强缓存,缓存当前资源,在配置时期内,再次请求资源直接读取本地缓存。
使用 cdn 缓存,当本地缓存无法使用时,配置较大的 cache-control:同样可以让业务直接访问 cdn 资源,且配置时间内不会再发生回源请求。
2.很少更新文件内容
对于 img,css,js,fonts 等非 html 资源,我们可以直接考虑配置 cache-control: max-age=seconds //seconds > 0,并且 max-age 配置的时间可以相对久一些,类似于缓存规则案例中,cache-control: max-age=36000 配置 10 小时的缓存,需要注意的是,这样配置并不代表这些资源就一定十小时不变,其根本原因在于目前前端构建工具在静态资源中都会加入戳的概念(例如,webpack 中的[hash],gulp 中的 gulp-rev),每次修改均会改变文件名或增加 query 参数,本质上改变了请求的地址,也就不存在缓存更新的问题。
3.频繁更新文件内容
对于 html 资源,作为前端资源的入口文件,一旦被强缓存,那么相关的 js,css,img 等均无法更新。对于高频维护的业务类项目,建议配置 cache-control: no-cache 或 cache-control: max-age=0:采用协商缓存,缓存当前资源,但每次访问都需要跟服务器对比,检查资源是否被修改。但是基于流量和成本的考虑更推荐于 max-age 设置一个较小值,例如 3600,一小时过期。对于一些活动项目,上线后不会进行较大改动,建议业务配置一个较小的 max-age 值,否则一旦出现 bug 或是未知问题,用户无法及时更新。
除了以上考虑,有时候其他因素也会影响缓存的配置,例如春晚红包除夕活动,高并发大流量很容易给服务器带来极大挑战,这时我们作为前端开发,就可以采用静态页面提前加载兜底来避免用户多次进入带来的流量压力。
如何减少缓存规则带来的访问影响
1.通过清理缓存控制
我们可以使用 cdn 服务商提供的强制更新缓存功能,主要注意的是,这里的强制更新是更新服务端缓存,http 缓存依然按照 http 头部规则进行自己的缓存处理,并不会受到影响。
2.通过 url 带版本号或者版本发布时间
我们在使用对象存储发布资源时,可根据版本号或发布时间定义 url,以作为区分。
例:https://storage.jd.com/xxx/xx?verson=1.1.1
例:https://storage.jd.com/xxx/xx?verson=20221111
线上禁止的访问策略
对于访问同一资源,url 带时间戳、uuid 等具有唯一性参数会直接回源至后端服务,带来极高的带宽成本及击穿底层服务的风险,业务在使用 CDN 域名访问时,请务必谨慎使用带有时间戳、唯一性参数的 url。
总结
技术行业发展到今天,海量的流量已然成为常态,而 http 缓存和 cdn 缓存分别作为客户端缓存和服务端缓存基石更是值得我们去深入学习、思考。
作者:管宸昊
版权声明: 本文为 InfoQ 作者【京东科技开发者】的原创文章。
原文链接:【http://xie.infoq.cn/article/a3e8007adc7248e5356d359e6】。文章转载请联系作者。
评论