有趣的BUG——Last-Modified命中强缓存

2,333 阅读6分钟

有趣的BUG——Last-Modified命中强缓存

1、背景

团队前段时间上线了一个qiankun子应用,本周有内部同事反馈页面报错,监控平台也出现相关的js错误和静态资源加载错误,导致本周的js错误率上升较大,错误类型如下:

  1. 子应用启动失败;

    • application 'micro-app-xxxxxx' died in status LOADING_SOURCE_CODE: Failed to fetch;
  2. 子应用静态资源拉取失败;

    • 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)
  3. blah, blah, blah...;

经过排查,发现调用fetch API请求html的时候,http的Last-Modified字段命中了强缓存,html的状态码是“Status Code: 200 OK (from disk cache)”。特意记录一下相关知识点和修复方法。

2、HTTP缓存拾遗

2.1、强制缓存

对于强制缓存来说,响应header中会有两个字段来标明失效规则( ExpiresCache-Control

  1. Expires的值为服务端返回的到期时间,即下一次请求时,请求时间小于服务端返回的到期时间,直接使用缓存数据; Expires 是HTTP 1.0的东西,现在浏览器均默认使用HTTP 1.1,所以它的作用基本忽略。此外到期时间是由服务端生成的,但是客户端时间可能跟服务端时间有误差,这就会导致缓存命中的误差。所以HTTP 1.1 的版本,使用Cache-Control替代。

  2. Cache-Control常见的取值有privatepublicno-cachemax-ageno-store,默认为private

    • private: 客户端可以缓存,防止信息泄漏;
    • public: 客户端和代理服务器都可缓存;
    • max-age=xxx: 缓存的内容将在 xxx 秒后失效;
    • no-cache: 需要使用对比缓存来验证缓存数据;
    • no-store: 所有内容都不会缓存,强制缓存,对比缓存都不会触发;
  3. Pragma,HTTP/1.0 中规定的通用首部; 用来向后兼容只支持 HTTP/1.0 协议的缓存服务器,那时候 HTTP/1.1 协议中的 Cache-Control 还没有出来。 只有一个值“no-cache”,与 Cache-Control: no-cache 效果一致。

2.2、协商缓存

浏览器第一次请求数据时,服务器会将缓存标识与数据一起返回给客户端。再次请求数据时,客户端将备份的缓存标识发送给服务器,服务器根据缓存标识进行判断,判断成功后,返回304状态码,通知客户端可以使用缓存数据。缓存标识在请求header和响应header间进行传递,一共分为两种。

  1. Last-Modified(response header) / If-Modified-Since(request header);
  2. 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 命中强缓存

如果服务器总是提供强缓存所需字段( ExpiresCache-Control ),浏览器可以通过判断是否使用本地缓存文件,来实现更好的加载性能。但由于服务器不是总返回强缓存的response字段,此时浏览器会根据其他的response header 字段来计算 Cache-Controlmax-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。

6、参考