findxc / blog

89 stars 5 forks source link

HTTP 缓存 & 用 NGINX 来验证缓存效果 #46

Open findxc opened 3 years ago

findxc commented 3 years ago

代码仓库见 GitHub - findxc/http-cache-example: use nginx to learn how http cache works

参考资料

什么是 HTTP 缓存

客户端请求资源(比如图片、代码文件等)时,将资源缓存在客户端或者客户端到服务端的中间节点的一种技术。

中间节点是指比如 CDN 节点,比如代理服务器等。

如果资源设置为允许缓存在客户端,则当客户端请求过一次资源后,资源会被缓存在客户端,下一次请求该资源时,如果缓存未过期,则可以直接使用缓存。

如果资源设置为允许缓存在中间节点,则当某个用户请求过该资源后,资源会被缓存在中间节点,当其他用户请求该资源时,速度会更快。

HTTP 缓存的好处

从客户端的角度,当用户已经访问过一次网站后,下一次访问时由于 HTTP 缓存减少了资源的下载,可以提高页面的加载速度。如果一些资源是部署在 CDN 上并且缓存设置为公开,那对于第一次访问网站也会有提速效果。

从服务端的角度,HTTP 缓存减轻了带宽压力和服务器请求数量,降低了运维成本。

HTTP 缓存怎么配置

HTTP 缓存相关的配置都是放在 header 中的,当客户端请求资源时,服务端通过 response header 来告诉客户端/中间节点该资源是否允许缓存、缓存有效期等信息。

Expires

Expires - HTTP | MDN

比如 Expires: Wed, 21 Oct 2015 07:28:00 GMT

资源的到期时间。如果客户端时间小于该值,则可以直接使用缓存,否则去服务端请求,如果资源未变更,服务端会返回 304 表示可以继续使用客户端缓存,否则返回 200 以及新的资源。

由于客户端时间的不可控性,一般会更倾向于使用 Cache-Control 来设置。

Cache-Control

Cache-Control - HTTP | MDN

关于缓存策略的设置:

关于缓存地点的设置:

比如 Cache-Control: public, max-age=604800 表示允许客户端和中间节点缓存,有效期是 7 天。

需要注意的是,如果同时设置了 ExpiresCache-Control ,并且 Cache-Control 中包含 no-cache 或者 max-age=xxx 这种和缓存时间有关的,那么 Expires 会被忽略。如果说只是设了 public 那不影响 Expires 生效。

协商缓存

我们一般说的强制缓存和协商缓存,其实就是指在需要某个资源时,是否还需要向服务端验证一下缓存是否过期。

比如 max-age=604800 就是强制缓存,因为在有效期内可以直接使用缓存,而 no-cache 就是协商缓存,每次都需要向服务器请求,如果返回 304 才可直接使用缓存。

协商缓存适用的场景是,当你请求同一个地址,对应的资源可能变化时。

比如当浏览器访问 https://reactjs.org 时,会去请求一个 HTML 文件,由于不知道这个 HTML 文件什么时候会变化,但是用户始终只会去访问这个地址,为了保证用户访问到的是最新的资源,就只有在请求 HTML 时去问一下服务端该资源是否有更新。

在控制台我们可以观察到响应的 header 中有 cache-control: public, max-age=0, must-revalidate

max-age=0 的效果和 no-cache 的效果一样。这里的 must-revalidate 其实不写效果也是一样的。怎么说呢,由于 no-cache 的字面意思和它实际意思的差异,有些人会更喜欢用 max-age=0 ,而这里补充写上 must-revalidate 也是希望指令足够清晰,我的理解哈。就像写 React 的 jsx 时,一个布尔值的属性,你会 <Tip visible /> 还是 <Tip visible={true} /> ,其实效果都是一样的,只是后者的写法更显式一点。

image

强制缓存

如果一个 URL 对应的资源不会变更,那就用强制缓存,并且把过期时间设特别长。

比如下面这个 JS 文件,由于 URL 中已经包含了版本号,这个文件理论上来说不会再变化了,所以是设的 cache-control: public, max-age=31536000 ,有效期 365 天。

image

我们在打包前端代码时,一般会在文件名中加入 hash ,当文件内容变化后,打包出来的文件名也会变化,不会存在同一个请求地址对应的文件变化的场景,所以对于有 hash 的文件直接使用强制缓存即可。

一个图来总结

图片来自 Prevent unnecessary network requests with the HTTP Cache

image

对于我们前端打包来说,为了尽可能利用缓存,可以首先把代码按路由切分,这样当只有 a 页面代码变更时,其它页面的文件可以命中缓存。再就是把一些不常变化的依赖(比如 Antd )打包成单独的文件,这样这部分可以走缓存。

ETag / If-None-Match 和 Last-Modified / If-Modified-Since

上面我们说到,对于强制缓存,缓存过期后,需要去重新请求服务端,如果是协商缓存,也需要每次去请求服务端看缓存是否过期。那服务端是如何得知客户端当前缓存的是啥呢?

首先我们在第一次请求服务端时,可以设置 response header 返回 Last-Modified 或者 ETagLast-Modified 表示文件在服务端的最后修改时间, ETag 是服务端返回的该文件的唯一标识,具体生成方式由服务端决定,当文件发生变化时,ETag 会变化。

然后在我们下一次请求服务端时,浏览器会在 request header 对应自动带上 If-Modified-SinceIf-None-Match ,服务端根据这个值去和服务端的文件进行比对来判断缓存是否过期。

至于具体用哪个,Last-Modified 存在一个问题就是时间只能精确到秒,如果你的文件在一秒内发生了变更,那用户获取的还是旧的文件,但是考虑到这种场景其实很少,然后 ETag 会相对来说会更消耗服务器性能,所以我是觉得 Last-Modified 就够用了。

如果你去观察别的网站,有两者都用的,也有只用其中一种的。

如果文件内容是一样的,但是最后修改时间变了,ETag 会变吗

这个是和服务端怎么去生成 ETag 有关。如果说你服务端是根据文件内容的 hash 来作为 ETag ,那只要内容没变, ETag 就不会变,但是这种计算方式会更消耗服务器性能,也会影响接口响应速度。

NGINX 默认的 ETag 只是简单的根据文件最后修改时间和文件长度来生成的,如下所示:

image

ETag 中的 60803386 对应的是 Last-Modified44 对应的是 Content-Length ,如下图所示:

image

详见 【Q111】http 响应头中的 ETag 值是如何生成的 · Issue #112 · shfshanyue/Daily-Question · GitHub

如果以单页面应用来举例,除了 HTML ,其它文件都会带 hash ,所以其它文件直接强制缓存一年就行了,而对于 HTML ,每次发版后内容不变的可能性也特别小,所以 ETag 不根据内容 hash 也是 ok 的,因为很少遇到内容一样的场景。

如果 response header 中不设置 Expires 和 Cache-Control

HTTP 缓存 - HTTP | MDN 有说到:

如果max-age和expires属性都没有,找找头里的 Last-Modified 信息。如果有,缓存的寿命就等于头里面Date的值减去Last-Modified的值除以10(注:根据rfc2626其实也就是乘以10%)。

也就是尽管你不设缓存,浏览器还是会根据文件的最后修改时间来决定缓存有效时间。

用 NGINX 来实际测试一下

这里要注意,不同浏览器的表现可能会稍有差异,比如 Chrome 在请求 HTML 文件时 request header 会始终带上 max-age=0 ,就算你 response header 设了 max-age=xxx ,在下一次请求 HTML 时还是会 request header 带上 max-age=0 ,这是 Chrome 浏览器自己做的,在 Firefox 中就不会这样。

所以测试时用 JS、CSS、图片等来测试会更符合预期。

设置 Expires

在 A 处是已经有缓存了,所以是 200 并且是直接读取的缓存,然后我修改了 index.js ,然后等过期后再刷新页面,index.css 是 304 ,然后 index.js 变为 200 ,然后再继续刷新,由于此时还是过期状态,所以还是会先去服务端请求,然后返回 304 。

image

设置 Cache-Control: max-age=60

在 A 处是已经有缓存了,所以是 200 并且是直接读取的缓存,然后我修改了 index.js ,然后等过期后再刷新页面,index.css 是 304 ,然后 index.js 变为 200 ,然后如果在下一个 60s 内刷新,还是会直接读缓存,超过 60s 后才会再去请求服务器看缓存是否失效。也就是每一份缓存的有效期都是 60s 。

image

设置 Cache-Control: no-cache

每次都会去请求服务器,如果缓存没过期就是返回 304 ,否则返回 200 和新的资源。

image

如果你对其它命令感兴趣可以拉一下代码自己本地慢慢测。

也可以去研究一下别人的网站上缓存都咋做的,比如 React – A JavaScript library for building user interfaces ,不得不说 React 官网真的很快(你会发现它还做了很多预加载)。

AKclown commented 1 year ago

你好,请教一个问题。NGINX 默认的 ETag 只是简单的根据文件最后修改时间和文件长度来生成的。如何保证同一个代码版本在访问不同节点Etag保持一致(负载均衡时)

findxc commented 1 year ago

@AKclown 先考虑一下,能不能除了 index.html 以外其它文件都在打包时生成 hash 呢?这样有 hash 的文件都可以走强制缓存,就不用考虑 ETag 了。

然后 index.html 通常是走协商缓存所以确实会遇到你说的负载均衡时 ETag 一致性问题,这个我也不太清楚,可以网上查查?可以在每次部署后用脚本把所有负载均衡机器上的文件最后修改时间改为同一个值来解决吗?

AKclown commented 1 year ago

好的,感谢回复。 我问了 GPT了。 给出了答案,例如通过nfs来挂载文件系统,是的 每个代理最终访问的文件路径都是同一个文件地址等