有趣的BUG——Last-Modified命中强缓存
1、背景
团队前段时间上线了一个qiankun子应用,本周有内部同事反馈页面报错,监控平台也出现相关的js错误和静态资源加载错误,导致本周的js错误率上升较大,错误类型如下:
-
子应用启动失败;
application 'micro-app-xxxxxx' died in status LOADING_SOURCE_CODE: Failed to fetch;
-
子应用静态资源拉取失败;
Failed to fetch,https://xxx.cdnxxxxx.com/static/fe-xxxxx-xxxxx/assets/vendor.xxxxxxxx.js;Loading chunk chunk-xxxxxxxx failed. (timeout: https://xxxxxxxx.xxxxxx.xxx/static/xxxxx/js/chunk-xxxxxxxxxx.js);
-
blah, blah, blah...;
经过排查,发现调用fetch API请求html的时候,http的Last-Modified字段命中了强缓存,html的状态码是“Status Code: 200 OK (from disk cache)”。特意记录一下相关知识点和修复方法。
2、HTTP缓存拾遗
2.1、强制缓存
对于强制缓存来说,响应header中会有两个字段来标明失效规则( Expires 、 Cache-Control )
-
Expires的值为服务端返回的到期时间,即下一次请求时,请求时间小于服务端返回的到期时间,直接使用缓存数据;Expires是HTTP 1.0的东西,现在浏览器均默认使用HTTP 1.1,所以它的作用基本忽略。此外到期时间是由服务端生成的,但是客户端时间可能跟服务端时间有误差,这就会导致缓存命中的误差。所以HTTP 1.1 的版本,使用Cache-Control替代。 -
Cache-Control常见的取值有private、public、no-cache、max-age,no-store,默认为private;private: 客户端可以缓存,防止信息泄漏;public: 客户端和代理服务器都可缓存;max-age=xxx: 缓存的内容将在 xxx 秒后失效;no-cache: 需要使用对比缓存来验证缓存数据;no-store: 所有内容都不会缓存,强制缓存,对比缓存都不会触发;
-
Pragma,HTTP/1.0 中规定的通用首部; 用来向后兼容只支持 HTTP/1.0 协议的缓存服务器,那时候 HTTP/1.1 协议中的Cache-Control还没有出来。 只有一个值“no-cache”,与Cache-Control: no-cache效果一致。
2.2、协商缓存
浏览器第一次请求数据时,服务器会将缓存标识与数据一起返回给客户端。再次请求数据时,客户端将备份的缓存标识发送给服务器,服务器根据缓存标识进行判断,判断成功后,返回304状态码,通知客户端可以使用缓存数据。缓存标识在请求header和响应header间进行传递,一共分为两种。
Last-Modified(response header) /If-Modified-Since(request header);ETag(response header) /If-None-Match(request header);
ETag在服务器响应请求时,告诉浏览器当前资源在服务器的唯一标识,生成规则由服务器决定。其优先级高于Last-Modified,这里优先级指服务端优先级,客户端两者并存的情况下,都会在request header中带上,但是nginx会优先匹配Etag。
3、排查、修复
3.1、为什么会强缓存
监控平台问题暴露出来后,第一时间对比了同类型的请求,发现使用fetch请求此子应用的html时,相较于其他的子应用html的response header 多了Last-Modified 字段。在我们的认知里,通常Last-Modified是和协商缓存相关,但一些情况下也会触发启发式(heuristic)缓存。
首次请求时,响应头返回Last-Modified。
再次请求,默认走强缓存。
3.2、Last-Modified字段从哪里来
服务端返回的静态资源默认都会带有 Last-Modified 字段,前端的html文件默认会去掉该字段,相关的nginx配置(部分)如下:
# 前端项目中配置
include /etc/nginx/location.d/*.conf;
location ~* \.(html)$ {
add_header Last-Modified "";
}
默认html文件会命中“ \.(html)$ ”,去除html的 Last-Modified 字段。
FE可以通过在项目中添加 " /location.d/*.conf "文件,来自定义的nginx规则,该子应用自定义nginx规则如下:
location ^~ /static/pathxxxxx/ {
alias /home/homework/www/static/webappnamexxxxxxxx/;
}
通过 /static/pathxxxxx/ 来命中html文件,导致默认的 add_header Last-Modified ""; 配置 失效,fetch 请求html文件的response header 中带有 Last-Modified 字段,导致再次请求的时候命中强缓存。
3.3、禁用强缓存
因为该子应用的html文件默认通过 /static/pathxxxxx/ 来匹配,所以只要当我们匹配到 /static/pathxxxxx/ 的时后添加 Cache-Control 字段即可,如下:
location ~ .*\.(css|js|html|swf|php|htm|html|....)$ {
break;
}
location ~ /static/pathxxxxx/ {
add_header Cache-Control no-cache;
alias /home/homework/www/static/webappnamexxxxxxxx/;
break;
}
在请求html的时候 response header 返回 Cache-Control: no-cache 。
再次请求时强缓存失效,走协商缓存。
3.3 为什么静态资源报错
通常静态资源统一走cdn,静态资源不太会出现not found的情况。由于该子应用webpack运行时的 publicPath 配置有问题,动态加载的资源默认从废弃的html所在的pod 中拉取。但每次前端的docker镜像部署的时候,会重新创建新的pod,在新的pod中启动容器,并且销毁老的pod。以上原因导致旧的html文件静态资源not found。
4、扩展
4.1、Last-Modified 命中强缓存
如果服务器总是提供强缓存所需字段( Expires 、 Cache-Control ),浏览器可以通过判断是否使用本地缓存文件,来实现更好的加载性能。但由于服务器不是总返回强缓存的response字段,此时浏览器会根据其他的response header 字段来计算 Cache-Control 的max-age 值(通常是Last-Modified字段)。HTTP/1.1 规范没有给出特定的实现算法,使得不同浏览器内核的浏览器对此表现不尽相同。具体查看 [6、参考](# 6、参考)。
通常推荐的计算方法是 过期时间 < 时间间隔 * 系数。时间间隔指的是response的返回时间与最后更新时间的间隔,而这个系数的典型值是 10%,计算公式为:
max-age = ( date - last-modified ) * 0.1
绝大多数的客户端,包括浏览器和各类app都是采用的这一推荐算法。
用在本次例子中, max-age= (Date.now() - new Date("Tuu, 20 Jan 2022 09:58:55 GMT").getTime() ) / (1000 * 60 * 60 * 24) ,大约4天多时间。
5、总结
该子系统接入qiankun主应用已经有一段时间,这个问题也是本周才暴露出来,未暴露出来原因也很多。最主要的原因是该子应用有一段时间没更新,导致缓存时间足够长,再者由于是新业务用的人少,样本较少;而且PC端缓存问题往往不容易被发现,很多前端老师习惯开启 disable cache。一般而言pc端浏览器很多操作都会导致浏览器略过强制缓存阶段,比如在地址栏按回车,点击刷新按钮,或者command + R等。在接入qiankun主应用后,子应用html入口都是通过fetch的方式引入进来的,即使手动刷新也不会绕过强制缓存阶段,还是走的disk cache。