Lee's Blog
  • 首页
  • 博客
  • 作品
  • 成长
  • 学习
  • 打榜
  • 练习
  • 关于
登录 / 注册
✦

保持联系

关注我的最新动态

GitHubbilibili

© 2026 Lee's Blog

首页/博客/EZTor/又偷摸修BUG
又偷摸修BUG
EZTor2026年5月25日8 分钟阅读

又偷摸修BUG

后端部署
X微博

EZTor 线上故障排查实录

一个 Next.js 16 全栈项目在部署运维中踩过的坑和修复记录

背景

EZTor 是一个基于 Next.js 16 + PostgreSQL + Prisma 的英语词汇学习平台。最近在对线上版本进行升级部署后,陆续暴露出一系列问题。以下是完整的排查与修复过程。


问题一:Prisma 引擎版本不匹配导致 $queryRaw 全部崩溃

症状

打开"当然"(单词卡复习)功能时,始终显示**"公共词库中暂时没有单词"**,但公共词库管理页面显示数据库中有 17,000+ 条词汇记录。

Health check API 返回 {"status":"error"}。

排查

查看 PM2 日志发现:

TypeError: e.map is not a function
  at xl (library.js:122:8241)
  step: "flashcard_public"
  msg: "Query $queryRaw flashcard_public failed"

这个错误同时出现在所有使用 $queryRaw 的地方:flashcard_public、topWordsRaw、dauRaw、dailyTrendRaw。而普通 Prisma 方法(findMany、count、findUnique)全部正常工作。

进一步检查发现,服务器的 OpenSSL 版本是 3.0.2,但 PM2 进程环境变量中强行指定了使用 openssl-1.1.x 的 Prisma 查询引擎:

PRISMA_QUERY_ENGINE_LIBRARY=.../libquery_engine-debian-openssl-1.1.x.so.node

虽然这个 .so 文件通过兼容库成功加载,但在处理 $queryRaw 的结果时,老引擎产生的结果格式与 Prisma 5.14.0 运行时预期不符,导致 e.map is not a function 的内部崩溃。safeQueryRaw 工具函数捕获了这个错误并静默返回空数组,前端于是渲染出"公共词库中暂时没有单词"。

修复

将 ecosystem.config.js 中的 PRISMA_QUERY_ENGINE_LIBRARY 指向 openssl-3.0.x 引擎:

- libquery_engine-debian-openssl-1.1.x.so.node
+ libquery_engine-debian-openssl-3.0.x.so.node

重启 PM2 后,Health check 恢复为 {"status":"ok"},$queryRaw 全部正常工作。

启示

  • PRISMA_QUERY_ENGINE_LIBRARY 这个环境变量非常危险——它会覆盖 Prisma 的自动引擎检测
  • 在不同构建环境(macOS)和目标环境(Linux)之间部署时,引擎文件必须匹配目标系统的 OpenSSL 版本
  • safeQueryRaw 这种静默吃错误的封装模式让问题变得极难排查——错误被吞掉,用户只看到一个空洞的 UI 提示

问题二:PostgreSQL 大小写敏感的列名引用

症状

日志中看到另一个 $queryRaw 错误:

column rgw.wordid does not exist (Code: 42703)

排查

问题是原始 SQL 查询中的列名引用问题:

JOIN "ReviewGroupWord" rgw ON w.id = rgw.wordId

PostgreSQL 对不带双引号的标识符会自动转为小写。rgw.wordId 实际执行时会变成 rgw.wordid,所以报了"列不存在"。

而同一查询中的其他列名是正确的:

WHERE rgw."reviewGroupId" = ${groupId}  -- ✅ 带引号
LEFT JOIN "PublicWord" pw ON pw.id = w."publicWordId"  -- ✅ 带引号

修复

- JOIN "ReviewGroupWord" rgw ON w.id = rgw.wordId
+ JOIN "ReviewGroupWord" rgw ON w.id = rgw."wordId"

启示

Prisma 生成的数据库表名和列名默认是 camelCase(如 wordId、reviewGroupId)。在写原生 SQL 时,所有 camelCase 列名都必须加双引号,否则 PostgreSQL 会全部转为小写。


问题三:前端对 API 错误处理不当

症状

当用户未登录时打开"当然"功能,显示的是"公共词库中暂时没有单词"而不是"请先登录"。

排查

Middleware 对未认证用户返回 401:

{"success":false,"error":"未登录"}

但前端 fetchWords 函数只检查了成功路径:

if (data.success && data.data) {
  setWords(data.data)
}

失败时 words 保持初始值 [],触发空状态渲染。

修复

增加 fetchError 状态变量,在 API 返回非成功响应时设置错误信息:

if (data.success && data.data) {
  setWords(data.data)
} else if (!data.success) {
  setFetchError(data.error || '获取单词失败')
}

UI 也相应区分显示:

{fetchError || '公共词库中暂时没有单词'}

问题四:Next.js standalone 构建静态资源不完整

症状

部署后浏览器报大量 chunk 加载错误:

GET /_next/static/chunks/0_mnfdu5xtgcn.js 404 (Not Found)
ChunkLoadError: Failed to load chunk /_next/static/chunks/0_mnfdu5xtgcn.js
Refused to execute script because its MIME type ('text/html') is not executable

排查

0_mnfdu5xtgcn.js 在本地 .next/static/chunks/ 中存在,但在 .next/standalone/.next/static/chunks/ 中缺失。Next.js 的 standalone 构建没有完整复制所有静态资源到最终的输出目录。

修复

将本地完整的 .next/static/ 同步到服务器,同时同步到 .next/standalone/.next/static/,然后重启 PM2。

rsync -avz .next/static/ root@server:/path/to/.next/static/
rsync -avz .next/static/ /path/to/.next/standalone/.next/static/

另外,nginx 配置中 /_next/static/ 是从项目根目录 .next/static/ 直接提供文件的,不是从 standalone 目录提供,所以两个位置都需要保持同步。

启示

Next.js standalone 模式会生成一个独立的运行目录 .next/standalone/,但 nginx 等反向代理可能直接从项目根目录的 .next/ 提供静态资源。每次部署时两个位置都需要保持同步。


问题五:TTS 依赖包未打包到 standalone 输出

症状

/api/tts 返回 500 Internal Server Error,但本地开发环境正常。

日志:

Error: WebSocket error occurred.

排查

src/lib/tts.ts 依赖 @lobehub/tts 和 ws 两个包。它们在 package.json 中有声明,但 next.config.ts 的 serverExternalPackages 配置只列了 svg-captcha:

serverExternalPackages: ['svg-captcha'],

在 Next.js standalone 模式下,没有被列入 serverExternalPackages 的包如果无法被 Turbopack 内联打包,就不会被复制到 .next/standalone/node_modules/。而 @lobehub/tts 和 ws 正好是这种情况——编译后的 chunk 引用了它们,但运行时的 node_modules 里却没有。

修复

将它们加入 serverExternalPackages:

- serverExternalPackages: ['svg-captcha'],
+ serverExternalPackages: ['svg-captcha', '@lobehub/tts', 'ws'],

启示

serverExternalPackages 的作用是告诉 Next.js:"这个包不要尝试内联打包,在最终输出中保留为外部 Node.js 模块"。对于使用了原生模块(或 WebSocket 等特殊 API)的包,这是必不可少的配置。


问题六:ws 包入口文件缺失

症状

即使 ws 包已经存在于 .next/standalone/node_modules/ 中,require('ws') 仍然报错:

Error: Cannot find module 'ws/index.js'

排查

对比发现,standalone 目录中的 ws 包缺少了两个入口文件:

# 源 node_modules/ws/
index.js
browser.js
lib/
wrapper.mjs

# standalone node_modules/ws/(缺失)
index.js    ❌
browser.js  ❌
lib/        ✅
wrapper.mjs ✅

Next.js 的 standalone 复制逻辑似乎将 .js 文件从根目录过滤掉了,只保留了 lib/ 子目录和 .mjs 文件。

修复

手动复制缺失的文件:

cp node_modules/ws/index.js .next/standalone/node_modules/ws/index.js
cp node_modules/ws/browser.js .next/standalone/node_modules/ws/browser.js

启示

Next.js standalone 构建的 node_modules 复制并非完美——某些包的顶层 .js 文件可能被遗漏。部署前检查关键依赖的完整性是必要的步骤。


总结

这次排查修复涉及了从数据库驱动到前端 UI 的6 个不同层面的问题:

# 问题 层面 根因
1 Prisma $queryRaw 崩溃 数据库/部署 引擎与 OpenSSL 版本不匹配
2 PostgreSQL 列不存在 数据库/SQL camelCase 列名缺少双引号
3 UI 显示混淆的空状态 前端 未处理 API 错误响应
4 Chunk 404 构建/部署 standalone 静态资源不完整
5 TTS 500 构建/部署 依赖未配置 serverExternalPackages
6 require('ws') 失败 构建 standalone 遗漏入口文件

最值得反思的是问题一的 safeQueryRaw 模式——一个本意是"防止崩溃"的工具函数,却成了问题排查的最大障碍。静默吞错让数据库层面的崩溃表现为最友善、最误导的 UI 提示,把排查方向引向完全不相关的地方。"安全"不等于"静默",生产环境的错误降级策略需要可观测性兜底。

← 上一篇明天四级口语考试,我的压力就像下一篇 →我的软件工程经验集(草稿)

相关文章

  • 退出后重登卡在"登录中":一次靠 Network 面板定位的 NextAuth session 时序问题约 5 分钟
  • 用户说"第一天的闪卡更好玩"——一次把 bug 当需求改的复盘约 6 分钟
  • meteorological 变成了 neteorological?记一次长单词被"削头"的 UI 排查约 6 分钟

评论

需要登录账号才能发表评论。

  • 加载中…

本页内容

  • EZTor 线上故障排查实录
  • 背景
  • 问题一:Prisma 引擎版本不匹配导致 `$queryRaw` 全部崩溃
  • 症状
  • 排查
  • 修复
  • 启示
  • 问题二:PostgreSQL 大小写敏感的列名引用
  • 症状
  • 排查
  • 修复
  • 启示
  • 问题三:前端对 API 错误处理不当
  • 症状
  • 排查
  • 修复
  • 问题四:Next.js standalone 构建静态资源不完整
  • 症状
  • 排查
  • 修复
  • 启示
  • 问题五:TTS 依赖包未打包到 standalone 输出
  • 症状
  • 排查
  • 修复
  • 启示
  • 问题六:ws 包入口文件缺失
  • 症状
  • 排查
  • 源 node_modules/ws/
  • standalone node_modules/ws/(缺失)
  • 修复
  • 启示
  • 总结