Open findxc opened 3 years ago
你好,请教一个问题。NGINX 默认的 ETag 只是简单的根据文件最后修改时间和文件长度来生成的。如何保证同一个代码版本在访问不同节点Etag保持一致(负载均衡时)
@AKclown 先考虑一下,能不能除了 index.html 以外其它文件都在打包时生成 hash 呢?这样有 hash 的文件都可以走强制缓存,就不用考虑 ETag 了。
然后 index.html 通常是走协商缓存所以确实会遇到你说的负载均衡时 ETag 一致性问题,这个我也不太清楚,可以网上查查?可以在每次部署后用脚本把所有负载均衡机器上的文件最后修改时间改为同一个值来解决吗?
好的,感谢回复。 我问了 GPT了。 给出了答案,例如通过nfs来挂载文件系统,是的 每个代理最终访问的文件路径都是同一个文件地址等
代码仓库见 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
关于缓存策略的设置:
no-store
:不允许缓存max-age=xxx
:允许缓存,这是设置多少秒后缓存过期,在有效期内可以直接使用缓存,过期后则向服务端请求,如果返回 304 则表示资源未变更可以继续使用缓存,否则返回 200 和最新的资源,然后客户端会缓存这个最新的资源s-maxage=xxx
:对于中间节点的缓存的有效期no-cache
:允许缓存,但是需要使用该资源时要先向服务器请求一下来判断缓存是否过期。max-gae=0
的效果和no-cache
是一样的。stale-while-revalidate=xxx
:单位为秒,需要和max-age
配合使用,在时间大于max-age
小于max-age
加stale-while-revalidate
时,会仍然使用缓存,但是同时会去请求服务器,目的是先快速展示内容,而并不是特别关心是否是最新的关于缓存地点的设置:
public
:中间节点和客户端均可缓存private
:只有客户端可以缓存,中间节点不能缓存比如
Cache-Control: public, max-age=604800
表示允许客户端和中间节点缓存,有效期是 7 天。需要注意的是,如果同时设置了
Expires
和Cache-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} />
,其实效果都是一样的,只是后者的写法更显式一点。强制缓存
如果一个 URL 对应的资源不会变更,那就用强制缓存,并且把过期时间设特别长。
比如下面这个 JS 文件,由于 URL 中已经包含了版本号,这个文件理论上来说不会再变化了,所以是设的
cache-control: public, max-age=31536000
,有效期 365 天。我们在打包前端代码时,一般会在文件名中加入 hash ,当文件内容变化后,打包出来的文件名也会变化,不会存在同一个请求地址对应的文件变化的场景,所以对于有 hash 的文件直接使用强制缓存即可。
一个图来总结
图片来自 Prevent unnecessary network requests with the HTTP Cache 。
对于我们前端打包来说,为了尽可能利用缓存,可以首先把代码按路由切分,这样当只有 a 页面代码变更时,其它页面的文件可以命中缓存。再就是把一些不常变化的依赖(比如 Antd )打包成单独的文件,这样这部分可以走缓存。
ETag / If-None-Match 和 Last-Modified / If-Modified-Since
上面我们说到,对于强制缓存,缓存过期后,需要去重新请求服务端,如果是协商缓存,也需要每次去请求服务端看缓存是否过期。那服务端是如何得知客户端当前缓存的是啥呢?
首先我们在第一次请求服务端时,可以设置 response header 返回
Last-Modified
或者ETag
。Last-Modified
表示文件在服务端的最后修改时间,ETag
是服务端返回的该文件的唯一标识,具体生成方式由服务端决定,当文件发生变化时,ETag
会变化。然后在我们下一次请求服务端时,浏览器会在 request header 对应自动带上
If-Modified-Since
和If-None-Match
,服务端根据这个值去和服务端的文件进行比对来判断缓存是否过期。至于具体用哪个,
Last-Modified
存在一个问题就是时间只能精确到秒,如果你的文件在一秒内发生了变更,那用户获取的还是旧的文件,但是考虑到这种场景其实很少,然后ETag
会相对来说会更消耗服务器性能,所以我是觉得Last-Modified
就够用了。如果你去观察别的网站,有两者都用的,也有只用其中一种的。
如果文件内容是一样的,但是最后修改时间变了,ETag 会变吗
这个是和服务端怎么去生成
ETag
有关。如果说你服务端是根据文件内容的 hash 来作为ETag
,那只要内容没变,ETag
就不会变,但是这种计算方式会更消耗服务器性能,也会影响接口响应速度。NGINX 默认的
ETag
只是简单的根据文件最后修改时间和文件长度来生成的,如下所示:ETag
中的60803386
对应的是Last-Modified
,44
对应的是Content-Length
,如下图所示:详见 【Q111】http 响应头中的 ETag 值是如何生成的 · Issue #112 · shfshanyue/Daily-Question · GitHub 。
如果以单页面应用来举例,除了 HTML ,其它文件都会带 hash ,所以其它文件直接强制缓存一年就行了,而对于 HTML ,每次发版后内容不变的可能性也特别小,所以
ETag
不根据内容 hash 也是 ok 的,因为很少遇到内容一样的场景。如果 response header 中不设置 Expires 和 Cache-Control
在 HTTP 缓存 - HTTP | MDN 有说到:
也就是尽管你不设缓存,浏览器还是会根据文件的最后修改时间来决定缓存有效时间。
用 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 。
设置 Cache-Control: max-age=60
在 A 处是已经有缓存了,所以是 200 并且是直接读取的缓存,然后我修改了 index.js ,然后等过期后再刷新页面,index.css 是 304 ,然后 index.js 变为 200 ,然后如果在下一个 60s 内刷新,还是会直接读缓存,超过 60s 后才会再去请求服务器看缓存是否失效。也就是每一份缓存的有效期都是 60s 。
设置 Cache-Control: no-cache
每次都会去请求服务器,如果缓存没过期就是返回 304 ,否则返回 200 和新的资源。
如果你对其它命令感兴趣可以拉一下代码自己本地慢慢测。
也可以去研究一下别人的网站上缓存都咋做的,比如 React – A JavaScript library for building user interfaces ,不得不说 React 官网真的很快(你会发现它还做了很多预加载)。