某天,团队的开发环境出现了一个诡异的问题:
/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 实际上有两层存储:
缓存查找流程
当请求到达 Nginx 时:
"幽灵缓存"的形成
在我们的场景中:
- 旧版本部署时:Nginx 将
/login、/admin的 HTML 缓存到了 keys_zone 和磁盘目录 - 新版本部署后:磁盘缓存文件可能因为某些原因被清理(如
inactive=1d过期、或手动清理),导致proxy_cache_dir为空 - 但 keys_zone 中的缓存键没有被清除!
这就形成了一个诡异的状态:
- 内存中的缓存元数据仍然存在
- 磁盘上的实际缓存文件已消失
- Nginx 根据内存中的元数据,构造并返回了缓存的响应
为什么 nginx -s reload 无效?
很多运维同学的直觉是:nginx -s reload 会刷新所有状态。
但实际上:
reload 只是重新加载配置,不会重新初始化共享内存中的 keys_zone。内存中的缓存键在 reload 后仍然存活。
为什么 curl 正常、浏览器异常?
这是整个排查过程中最迷惑人的一点。
Nginx 的 proxy_cache_key 默认是:
nginx
proxy_cache_key $scheme$proxy_host$request_uri;
理论上,curl 和浏览器访问同一个 URL,cache key 应该相同。
但实际中,缓存命中可能还受到以下因素影响:
- TLS 会话复用:浏览器可能复用了之前建立的 TLS 会话,该会话关联了特定的连接状态
- HTTP/2 连接复用:HTTP/2 的多路复用特性,使得同一连接上的请求可能共享某些缓存上下文
- 内部缓存分片: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、缓存穿透、前端部署