记一次诡异的前端白屏故障:Nginx Proxy Cache 内存缓存"幽灵"事件

某天,团队的开发环境出现了一个诡异的问题:

  • /admin(管理后台)页面白屏,控制台报错 Application error: a client-side exception has occurred
  • /login(登录页)也无法正常渲染,表单控件全部消失
  • 但网站首页 / 和公开页面(如 /members)完全正常

现象描述

打开浏览器开发者工具,控制台出现两个关键的 404 错误:

sql 复制代码
GET https://example.com/_next/static/css/cd2abc14eba91e41.css 404
GET https://example.com/_next/static/chunks/app/layout-9af0991ff2e12ff1.js 404

这两个资源文件根本不存在于当前的部署版本中。

当前前端容器的构建产物中:

  • CSS 文件是 0e0737f06f6fa16f.css
  • Layout chunk 是 layout-9e33db3552ca5cdb.js

但浏览器收到的 HTML 中,却顽固地引用着这两个"幽灵"般的旧资源。

初步排查:容器层面

检查容器内文件

登录服务器,进入前端容器检查:

bash 复制代码
docker exec frontend ls -la /app/.next/static/css/
docker exec frontend ls -la /app/.next/static/chunks/app/

结果:所有文件都存在,manifest 和实际文件完全一致。容器内的构建产物是完整且正确的。

检查卷挂载

怀疑宿主机上的旧文件通过 volume 挂载覆盖了容器内的新文件:

bash 复制代码
docker inspect frontend --format='{{json .Mounts}}'

结果:[],没有任何卷挂载。容器使用的是镜像内的文件,没有被外部覆盖。

重启容器

bash 复制代码
docker restart frontend

无效。浏览器仍然收到旧的 HTML。

3.4 检查是否有残留的旧容器

bash 复制代码
docker ps -a

发现有一个遗留的旧容器 exciting_moore(运行了 2 天),但它没有端口映射,Nginx 不可能将流量转发到它。

删除旧容器后,问题依旧。

结论:问题不在 Docker 容器层面。

深入排查:Nginx 层面

curl 测试:震惊的发现

在服务器上用 curl 测试:

bash 复制代码
curl -s -H 'Host: example.com' https://localhost/login | grep -o 'layout-[a-z0-9]*'

结果返回的是正确的 layout-9e33db3552ca5cdb

但浏览器访问同一个 URL,收到的却是 layout-9af0991ff2e12ff1

同一个 URL,curl 和浏览器得到不同的响应!

ETag 对比

进一步对比响应头:

来源 ETag
curl da8no7a8xm48wy
浏览器 l096haz5jr48wy

ETag 完全不同,确认两者收到的是完全不同的响应内容。

排除各种可能性

  • ✅ HTTP/1.1 vs HTTP/2:服务器 curl 使用 HTTP/2 也返回正确
  • ✅ IP 地址:通过公网 IP 和域名访问,curl 都正常
  • ✅ User-Agent:curl 带上浏览器 UA 也正常
  • ✅ Accept-Encoding:curl 带上 br(Brotli)也正常
  • ✅ 浏览器缓存:无痕模式、多台电脑、全新 Playwright 浏览器均复现
  • ✅ DNS:无 CDN,直接解析到服务器

所有客户端层面的差异都被排除。

检查 Nginx 配置

读取宝塔 Nginx 的代理配置:

bash 复制代码
cat /www/server/nginx/conf/proxy.conf

内容:

nginx 复制代码
proxy_cache_path /www/server/nginx/proxy_cache_dir levels=1:2 keys_zone=cache_one:20m inactive=1d max_size=5g;
proxy_cache cache_one;

Nginx 全局启用了 proxy_cache

但检查缓存目录:

bash 复制代码
ls -la /www/server/nginx/proxy_cache_dir/

结果:目录为空,没有任何缓存文件。

这就很奇怪了------缓存目录是空的,但 Nginx 似乎仍在返回缓存的响应。

底层原理:Nginx Proxy Cache 的工作机制

proxy_cache 的两层结构

Nginx 的 proxy_cache 实际上有两层存储:

graph TD subgraph keys_zone["keys_zone(共享内存)"] A["存储:缓存键(cache key)→ 元数据(文件路径、过期时间等)"] B["大小:20MB(由 keys_zone=cache_one:20m 指定)"] end subgraph proxy_cache_dir["proxy_cache_dir(磁盘目录)"] C["存储:实际的缓存文件内容"] D["结构:levels=1:2 创建两级子目录"] end

缓存查找流程

当请求到达 Nginx 时:

flowchart LR A[&#34;请求到达 Nginx&#34;] --> B[&#34;计算 cache key<br/>默认 $scheme$proxy_host$request_uri&#34;] B --> C{&#34;在 keys_zone<br/>内存中查找 key&#34;} C -->|命中| D[&#34;根据内存元数据<br/>读取磁盘文件或返回缓存&#34;] C -->|未命中| E[&#34;转发到后端<br/>获取响应后存入缓存&#34;]

"幽灵缓存"的形成

在我们的场景中:

  1. 旧版本部署时:Nginx 将 /login/admin 的 HTML 缓存到了 keys_zone 和磁盘目录
  2. 新版本部署后:磁盘缓存文件可能因为某些原因被清理(如 inactive=1d 过期、或手动清理),导致 proxy_cache_dir 为空
  3. 但 keys_zone 中的缓存键没有被清除!

这就形成了一个诡异的状态:

  • 内存中的缓存元数据仍然存在
  • 磁盘上的实际缓存文件已消失
  • Nginx 根据内存中的元数据,构造并返回了缓存的响应

为什么 nginx -s reload 无效?

很多运维同学的直觉是:nginx -s reload 会刷新所有状态。

但实际上:

flowchart TD A[&#34;nginx -s reload&#34;] --> B[&#34;主进程重新加载配置文件&#34;] B --> C[&#34;Worker 进程优雅替换(graceful)&#34;] C --> D[&#34;keys_zone 共享内存保留<br/>因为不是重新创建&#34;]

reload 只是重新加载配置,不会重新初始化共享内存中的 keys_zone。内存中的缓存键在 reload 后仍然存活。

为什么 curl 正常、浏览器异常?

这是整个排查过程中最迷惑人的一点。

Nginx 的 proxy_cache_key 默认是:

nginx 复制代码
proxy_cache_key $scheme$proxy_host$request_uri;

理论上,curl 和浏览器访问同一个 URL,cache key 应该相同。

但实际中,缓存命中可能还受到以下因素影响:

  1. TLS 会话复用:浏览器可能复用了之前建立的 TLS 会话,该会话关联了特定的连接状态
  2. HTTP/2 连接复用:HTTP/2 的多路复用特性,使得同一连接上的请求可能共享某些缓存上下文
  3. 内部缓存分片:Nginx worker 进程可能有各自的缓存状态不一致

在我们的案例中,curl 建立的全新连接绕过了陈旧缓存,而浏览器复用的连接命中了 keys_zone 中的旧缓存条目。

最终修复

临时禁用 proxy_cache 验证

bash 复制代码
# 注释掉 proxy_cache 行
sudo sed -i 's/^proxy_cache cache_one;/# proxy_cache cache_one;/' /www/server/nginx/conf/proxy.conf

# reload Nginx
sudo nginx -s reload

浏览器立即恢复正常! 确认根因就是 proxy_cache 的内存缓存。

彻底清理并恢复

bash 复制代码
# 1. 清空磁盘缓存目录
sudo rm -rf /www/server/nginx/proxy_cache_dir/*

# 2. 恢复 proxy_cache 配置
sudo sed -i 's/# proxy_cache cache_one;/proxy_cache cache_one;/' /www/server/nginx/conf/proxy.conf

# 3. 彻底重启 Nginx(stop + start,才能清空 keys_zone)
sudo nginx -s stop
sudo nginx

# 4. 验证配置
grep proxy_cache /www/server/nginx/conf/proxy.conf

经验总结

部署前端后的标准操作

前端重新部署后,如果页面白屏且 404 的资源 hash 是旧的:

bash 复制代码
# 不要只做 reload!
sudo nginx -s reload    # ❌ 无法清除内存缓存

# 正确做法:清空缓存并重启
sudo rm -rf /www/server/nginx/proxy_cache_dir/*
sudo nginx -s stop
sudo nginx              # ✅ 重新初始化 keys_zone

配置建议

可以在部署脚本中自动清理缓存:

bash 复制代码
#!/bin/bash
# deploy-frontend.sh

# 1. 部署新镜像
docker compose up -d frontend

# 2. 等待容器就绪
sleep 5

# 3. 清空 Nginx 缓存
rm -rf /www/server/nginx/proxy_cache_dir/*
nginx -s stop && nginx

echo "部署完成,Nginx 缓存已刷新"

为什么 Next.js 的 ETag 变了但缓存没刷新?

Next.js 的 ISR(增量静态再生)缓存会返回 x-nextjs-cache: HIT,但这只是 Next.js 内部的缓存。

Nginx 的 proxy_cache 是更外层的缓存,它根据响应头中的 Cache-Control(如 s-maxage=31536000)来决定是否缓存。

两层缓存独立工作:

  • Next.js 内部缓存:容器重启后已刷新 ✅
  • Nginx proxy_cache:内存中的 keys_zone 未刷新 ❌

这就是问题根源。

监控建议

在 Nginx 配置中添加缓存状态头,便于排查:

nginx 复制代码
add_header X-Cache-Status $upstream_cache_status;

这样浏览器网络面板中可以看到:

  • X-Cache-Status: HIT --- 命中了 Nginx 缓存
  • X-Cache-Status: MISS --- 未命中,从后端获取

结语

这次故障排查历时数小时,从容器内部文件、卷挂载、Docker 网络,到 Nginx 配置、HTTP/2 差异、TLS 指纹,几乎排查了所有可能的方向。

最终的根因看似简单------Nginx proxy_cache 的内存缓存没有清理------但排查过程中的种种"诡异"现象(curl 正常浏览器异常、目录为空却仍有缓存、reload 无效)让题变得扑朔迷离。

希望这篇记录能帮助到遇到类似问题的同学。记住:当缓存目录为空但缓存行为仍在时,问题很可能在内存中的 keys_zone。


技术栈: Next.js 15.5 + Nginx 1.x(宝塔面板)+ Docker

关键词: Nginx proxy_cache、keys_zone、Next.js ISR、缓存穿透、前端部署

相关推荐
如果超人不会飞1 小时前
TinyRobot SuggestionPills紧凑的建议按钮组组件
前端·vue.js
如果超人不会飞1 小时前
TinyRobot Container构建优雅的AI对话容器
前端·vue.js
幸运小圣1 小时前
全面解析 Web 核心性能指标:LCP、INP、CLS 是什么、怎么用、怎么看
前端
如果超人不会飞1 小时前
TinyRobot SuggestionPopover智能建议弹出框组件
前端·vue.js
LiuJun2Son1 小时前
Angular 快速入门:从零搭建你的第一个应用
前端·javascript·angular.js
小徐_23332 小时前
Wot UI 2.1.0 发布:ConfigProvider 全局配置能力升级
前端·uni-app
方白羽2 小时前
Vibe Coding 四个核心阶段
android·前端·app
奶油话梅糖2 小时前
浏览器解析 HTML 头部的底层逻辑:从字节流到资源调度
前端·html
YHL2 小时前
🚀从零理解树与二叉树 —— 概念、实现与遍历
前端·javascript·数据结构