生产事故排查不仅是硬核技术的体现,也是技术博客里最受欢迎的"流量密码"。深夜完成了一次常规的前端发布,本以为可以睡个好觉。没想到第二天开始,客服渠道陆续收到零星的用户报障:页面一直卡在 Loading 动画,怎么也打不开。
影响面不大,但伤害性极强。作为一个刚接手这个服务、对历史架构还在熟悉的开发来说,这无疑是一场心理与技术的双重考验。
🕵️♂️ 第一阶段:神秘的零星白屏与排查盲区
接到通知后,我第一时间尝试在本地和测试环境复现,结果非常完美------一切正常。接着尝试用无痕模式打开,或者让报错用户刷新一次,问题就离奇地消失了。
由于网络链路复杂,ALB(应用负载均衡)反馈报错用户的对应文件响应日志不全,无法通过日志追溯。无奈之下,只能远程拉上故障用户现场抓包。打开 F12 的那一刻,狐狸尾巴终于露出来了:页面请求的几个核心 JS 文件全部报 404。
作为一个资深前端,我一眼就看出了端倪:前端构建的静态资源都是带 [hash] 值的。页面在请求旧 hash 的 JS 文件,说明用户手里的入口 HTML 文件是过期的旧版本。
但难题才刚刚开始:
- 为什么以前的变更从来没出过这问题?
- 为什么这次变更我们压根没动网络链路层?
- 我第一时间询问了前任维护者,得到的反馈也是"以前从来没遇到过"。
这一切都显得极其诡异。
🧪 第二阶段:严谨的"盲注"与环境复刻
为了排除干扰,我决定在测试环境完全复刻生产环境的 ALB 配置。
这里有一个痛点:如果直接发布测试,只要用户一打开 F12 刷新,缓存可能就失效了,错失第一现场。于是我使出了一个破局的小技巧:
💡 破局小技巧:页面埋标记
- 先把旧版本软件包发布到测试环境,并在页面 DOM 里偷偷埋下一个微小的版本标记(例如
data-v="old")。- 本地浏览器打开该页面,让浏览器吃下旧版缓存。
- 接着在服务器上发布新版本软件包。
- 本地浏览器再次访问,直接观察标记是否改变,同时避免了频繁刷新导致的缓存失效。
然而,测试环境依然非常完美地加载了新页面,并没有复现生产的旧 HTML 缓存问题。
测试和生产一定有盲区差异!在天天被客服和业务催进度(开发懂的都懂,每天被问"定位出来没有"、"什么时候解决"的窒息感)的压力下,我开始肉眼一行行比对测试和生产环境的 Response Headers(响应头)。
终于,发现了一个致命的不同:生产环境的响应报文中,缺少了 Cache-Control 字段!
🤯 第三阶段:破案!浏览器扮演的"预言家"与 WCM 的背刺
为什么没有 Cache-Control 会导致入口 HTML 被顽固缓存?
通过翻阅 HTTP 规范并借助 AI 辅助确认,我锁定了浏览器一个极其隐蔽的底层机制------启发式缓存(Heuristic Caching)。
🧠 技术科普:什么是启发式缓存?
当服务器返回的响应报文中没有任何明确的缓存指令 (没有 Cache-Control,也没有 Expires)时,浏览器并不会任性地每次都去请求服务器。相反,它会扮演"预言家",根据 LM-Factor 算法 自己算出一个缓存时间:
缓存有效时间=(Date(当前收到响应的时间)−Last\-Modified(文件的最后修改时间))×10%
这与我们观察到的现象高度匹配!因为 HTML 文件可能很久没动过,通过这个公式算出来的缓存时间可能长达数天甚至一个月!这就解释了为什么用户会一直请求旧的 JS Hash,且很难复现(因为一旦由于某些原因触发了强刷,缓存就会被更新)。
那么,为什么生产环境会漏掉这个响应头?
我们顺藤摸瓜,把链路上的 ALB 团队和 WCM(Web内容管理)团队全拉到一个群里对齐。
- WCM 团队一开始坚称:"我们测试和生产默认都会加上缓存控制响应头。"
- 现场打脸:"来看生产的真实抓包,确实没有。"
WCM 团队回去排查源码,最终真相大白,令人啼笑皆非(WTF):WCM 系统在近期做了一次更新,它们只对默认的 index.html 自动加上缓存控制,而如果是其他名字的入口 HTML 文件,一律不加!
而我们出问题的那个项目,恰恰不是默认的 index.html。
🏗️ 第四阶段:架构复盘,屎山是如何形成的?
看到这里你可能会问:为什么你们前端项目的入口文件不是 index.html?为什么以前没踩坑,偏偏这次踩了?
这就不得不聊聊接手过来的这堆"精妙"的屎山架构了:
- Vue CLI 多页应用 (MPA) 架构 :前人使用了 MPA 架构,这导致构建出来的产物包含多个入口文件(如
subpage.html)。这就完美避开了 WCM 系统的index.html默认缓存策略。 - 多网站共享同一个部署单元(恐怖的路由指向) :本次变更其实根本没有改动这个出服务的网站代码。但是!因为在架构设计上,多个不同的网站在同一套代码里,共享同一个部署单元。这就意味着,多个网站的路由都指向这个地址。你只要更新了 A 网站,其余 3 个网站也会被迫一起重新打包发布。
- WCM 变更的滞后性 :兄弟团队的这个 WCM 变动其实已经上线好几个月了,但因为过去这几个月里,我们这个 MPA 项目一直没有触发被迫发布,所以它的
Last-Modified时间很旧,启发式缓存时间极长。直到这次因为隔壁网站发布,连带导致这个项目重新生成了 HTML,它的Last-Modified变成了发布当晚,LM-Factor 算法计算出的缓存时间变短了,这才让"零星用户报错"的现象暴露出来。
🩺 止血与规避方案
这个问题的恶心之处在于,对于已经被启发式缓存坑了的用户,前端是没有任何办法通过代码去帮他刷新浏览器的(因为浏览器压根不向服务器发请求,直接读了本地强存)。
我们最终的对应方案是:
- 整改升级:明确责任,将问题升级给 WCM 团队进行紧急配置整改,让他们修复"非 index.html 后缀文件"的缓存头缺失问题。
- 长远架构解耦:后续必须将这个多页应用从共享部署单元中剥离出来,做到真正的按需发布,从根源上断绝这种"城门失火,殃及池鱼"的连带发布风险。
📝 总结
这次排查虽然被催得头大,但收获也是满满的。作为高级前端,不仅要对业务代码了如指掌,网络协议层(HTTP 缓存机制)、浏览器底层的边界行为(启发式缓存),以及链路层(ALB/WCM)的运作机制,都是我们手里的武器。
永远不要轻信"以前都没问题"这句话,在分布式和复杂的共享部署时代,往往一个隐藏的底层改动,就能引爆你刚接手的整座屎山。
欢迎在评论区聊聊,你们在生产环境里被哪些隐蔽的"底层缓存机制"背刺过?