gongxiangp2/docs/生产图片视频内存事故与处理记录-2026-07-31.md
chenpei 7326e273e1 fix: harden media upload and release safety
Protect upload cleanup and atomic completion paths, add durable media worker leases and brand asset validation, and introduce verified migration/bootstrap/deployment gates.
2026-08-12 18:04:08 +08:00

7.7 KiB
Raw Permalink Blame History

生产图片/视频处理内存事故与处理记录

日期:2026-07-31

结论

这次“处理图片后整个服务崩掉”不是单一 Node.js 请求超出 6GB 容器限制,而是生产宿主机发生了全局内存耗尽(global_oom)。

真正的内存来源分两层:

  1. 宿主机的 TrueNAS middlewared 长时间占用异常高的内存,历史峰值约 9.54GiB,并设置了 oom_score_adj=-1000,内核不会优先杀它。
  2. 应用容器内的 libvips(Sharp 的底层图片库)和 ffmpeg 在处理大图/视频时产生原生内存尖峰。它们是把整机推过 OOM 阈值的触发器,内核随后杀掉了 next-server 或 MinIO。

因此,单纯把 Node.js 堆内存或应用容器上限调大不能解决问题,反而可能更快吃光宿主机余量。

已核实的生产时间线

时间(北京时间) 生产证据 业务对应
7 月 30 日 15:18 libvips worker 触发 global_oom;内核杀掉 next-server,其匿名 RSS 约 1.405GiB 同时段任务详情完成接口进入 sharp.metadata()、sliceImage/compressImage,Nginx 随后出现 502
7 月 30 日 16:18 同一应用容器同时有两个 ffmpeg,RSS 约 642MiB 和 372–408MiB;之后 MinIO 也被杀 事故时旧实现未配置转码并发,默认并发为 2;后续已固定为同一容器 1 条任务、FFmpeg 1 线程(见下文)
7 月 30 日 17:39 libvips worker 再次触发 global_oom;同时还有一个约 325MiB 的 ffmpeg,最终 next-server 被杀 图片任务详情和抖音视频任务叠加,随后大量 502/504

三次事件都显示:宿主机没有 Swap,且是 global_oom,不是应用容器自己的 cgroup OOM。

本次代码处理

  • Sharp 默认并发降为 1,限制 libvips 缓存和输入像素,并增加进程级图片处理门闩。
  • 白底图内部和外部接口统一增加请求大小、单图大小、图片数量、格式、像素和动图门禁;服务层再次校验,防止新调用方绕过路由。
  • 主上传的多张预览图改为串行处理,不再通过 Promise.all 同时解码多张图片。
  • 任务详情预览在第一次 sharp.metadata() 前增加像素上限;长图限制最大尺寸和切片数。
  • 视频转码队列固定为同一容器最多 1 个任务,FFmpeg 默认 1 线程,并限制进度日志缓存。
  • 同批次关闭上传路径越界、任意文件删除、跨用户白底图替换等高危入口,并移除头像菜单中的网络诊断/缓存刷新入口。

仍需运维处理的风险

代码修复不能替代宿主机治理。生产运维需要单独完成:

  • 查明 middlewared 从正常内存增长到数 GiB 的原因,并设置可用内存告警。
  • 评估在维护窗口增加 Swap 或等效内存缓冲;这不是永久修复,只是降低瞬时 OOM 概率。
  • 评估把 FFmpeg/Sharp 拆到独立 worker,避免媒体任务与 Next.js、MinIO 共用进程/容器。
  • 持续采集容器 RSS、cgroup memory.events、宿主机可用内存和 OOM 日志。

本次排查未重启、杀进程、清理生产数据或修改生产部署脚本。

2026-08-01 第二阶段处理记录

新确认的根因

  • 用户在 Edge 中看到“浏览器预览处理失败”并不等于 Edge 不受支持。原链路把“浏览器能力不足、图片损坏、图片超限”等不同原因混成了同一个提示,导致用户更换浏览器后仍然失败。
  • 任务详情上传在请求内直接完成图片预览、切图、压缩和打包,浏览器断开、进程重启或多用户同时上传时,容易出现长时间等待、任务丢失和服务器内存尖峰。
  • 生产日志中的大量 P2024 主要来自素材筛选与部门权限查询放大,并不是本轮图片处理再次触发 OOM。素材筛选一次请求曾重复建立多套查询上下文,部门权限也存在逐部门查询。
  • 同一任务号并发完成时缺少数据库级互斥,存在两个素材同时占用同一任务号的竞争窗口。
  • Edge 服务器兜底链路曾存在客户端与服务器限制不一致:超宽长图、动画图、预览总量和源文件总量可能在文件全部上传后才失败。
  • 同一批同名 PSD 的永久文件名只依赖毫秒时间,极端情况下可能覆盖;PSD 上传会话也可能在完成或失败后残留。

已完成的代码处理

  • 浏览器一次只处理一张图片;网络层允许 2 个文件并行、每个文件最多 2 个分片请求,在上传速度和内存峰值之间取平衡。
  • 浏览器能力不足时上传原图,由服务器受限兜底;损坏图片、格式不支持、宽度/像素/切片数量超限会在客户端直接拦截,不再伪装成浏览器兼容问题。
  • 上传完成改为持久化后台任务:接口快速返回任务编号,页面轮询状态;进程重启后可恢复,卡死任务有心跳与超时恢复。
  • 全站服务器图片 worker 同时只处理 1 个任务;单用户最多排队 3 个、全站最多排队 20 个,避免多容器并发重新制造内存尖峰。
  • 任务详情服务器兜底统一为:源图宽度不超过 12,000px、高度不超过 90,000px、总像素不超过 6,000 万;输出宽度不超过 1,600px、单片高度不超过 1,500px、单图最多 60 片。
  • 任务详情动画 GIF/WebP 在服务器兜底时只处理首帧;其它图片处理链路仍默认拒绝动画,避免扩大影响范围。
  • 服务端重新校验真实文件格式、尺寸、动画帧、文件数量和大小;源文件总量 1GB 与客户端预览总量 512MB 分开计算。
  • 上传取消和失败会等待并发请求全部收口后执行一次完整回滚;大量会话清理采用最多 12 路受控并发。
  • 同名 PSD 使用带序号和随机值的唯一永久文件名;PSD 上传会话纳入成功与失败清理。
  • 素材筛选复用同一个查询上下文,并把数据库查询数从 13 次降到 7 次;部门权限改为一次批量加载并在内存中解析,减少数据库连接占用。
  • 任务号完成写入增加 PostgreSQL 事务级互斥;直接主管匹配同时配置工号和钉钉 ID 时改为两者都必须匹配。

本轮验证证据

  • 全量 Jest:135 个测试套件、873 项测试全部通过。
  • TypeScript 类型检查通过。
  • Prisma 数据模型校验通过。
  • 全项目 ESLint:0 个错误;剩余提示均为项目原有提示,本轮修改未新增阻断项。
  • $impeccable 前端反模式扫描:0 项命中。
  • Next.js 正式生产构建编译成功并生成 BUILD_ID 与 standalone 产物。本地隔离工作树使用外部 node_modules 联接,因此日志中出现一次 traced files 联接权限提示;生产 Docker 构建不使用该本地联接。

尚未关闭但不阻断本轮发布的事项

  • 极端上限任务可能包含 1,140 个上传会话,后台 worker 当前逐个核验 Redis/元数据。它不占用 PostgreSQL 连接,也不阻塞 HTTP 返回,但极端任务的“校验中”阶段会较长;后续应压测后再决定使用 8~12 路有限并发或 Redis 批量读取。
  • 素材月份筛选仍会读取符合条件的 createdAt 后在应用内汇总。当前已经显著减少查询次数,但数据量继续增长后应改为数据库按月聚合并增加短时缓存。
  • 上线后必须观察 P2024 数量、数据库连接数、素材筛选耗时、后台任务队列长度、容器 RSS 和宿主机可用内存;若 P2024 仍持续出现,需要继续定位剩余长事务或连接池等待。
  • 宿主机 middlewared 异常内存、无 Swap 等运维风险仍未由代码解决,继续按本文“仍需运维处理的风险”执行。

截至本记录更新时,本轮代码已完成本地验证,尚未合并到 main,也尚未部署生产。