使用 Codex 开发头像上传、Excel 导入、视频处理或附件系统时,文件上传看起来只是一个很普通的功能。

但项目真正上线以后,经常会遇到:

  • 上传 500MB 文件后服务内存突然暴涨;

  • 两三个用户同时上传大文件,Node.js 进程直接 OOM;

  • 本地测试正常,生产环境却频繁重启;

  • 文件已经上传一半,网络断开后只能重新开始;

  • 临时文件越来越多,磁盘空间不断减少;

  • Codex 为了方便处理,直接把整个文件读进 Buffer;

  • 上传接口响应很慢,还拖慢了其他普通请求。

这类问题的核心通常不是“上传接口不能用”,而是文件处理方式没有考虑数据规模


一、最常见的问题:一次性读取整个文件

例如:

const buffer = await file.arrayBuffer();

const data = Buffer.from(buffer);

await processFile(data);

如果用户上传的是 10MB 文件,通常问题不明显。

但如果是:

100MB
500MB
1GB

程序就需要一次性把整个文件放进内存。

如果同时有 10 个用户上传 500MB:

500MB × 10

理论内存占用就可能达到数GB。

而且实际处理中还可能产生:

  • 原始 Buffer;

  • 解压后的数据;

  • JSON 对象;

  • Excel 行数据;

  • 图片解码数据;

  • 临时副本。

因此,文件大小不能只看磁盘占用,还要看处理过程中会产生多少份内存副本。


二、什么时候应该使用Stream?

如果文件可以边读取边处理,就应该优先考虑流式处理。

例如 Node.js:

import fs from "node:fs";

const stream = fs.createReadStream(filePath);

stream.on("data", chunk => {
  processChunk(chunk);
});

stream.on("end", () => {
  console.log("done");
});

流式处理的核心是:

读取一小块
→ 处理
→ 释放
→ 再读取下一块

而不是:

全部读取
→ 全部处理

它特别适合:

  • CSV 导入;

  • 大日志文件;

  • 视频转存;

  • 对象存储上传;

  • 文件复制;

  • 压缩与解压;

  • 大型文本处理。


三、不要默认把上传文件放进内存

一些上传中间件支持两种模式:

Memory Storage
Disk Storage

Memory Storage 会把文件保存为 Buffer。

小头像通常没问题,但如果接口允许:

ZIP
视频
Excel
PDF

就要非常谨慎。

更稳妥的方式通常是:

客户端上传
↓
写入临时文件 / 对象存储
↓
后台任务读取
↓
流式处理
↓
完成后删除临时文件

这样 Web 请求本身不需要长期占用大量内存。


四、必须设置文件大小上限

不要只依赖前端:

<input type="file">

或者前端代码:

if (file.size > MAX_SIZE) {
  alert("文件太大");
}

用户完全可以绕过前端直接请求接口。

服务端必须再次限制。

例如:

头像:5MB
普通附件:20MB
Excel导入:50MB
视频:500MB

不同业务应该使用不同上限。

不要写一个:

MAX_FILE_SIZE = 2GB

然后所有接口共用。


五、文件类型不能只看扩展名

错误做法:

if (file.name.endsWith(".jpg")) {
  allow();
}

攻击者可以把任意文件改名为:

test.jpg

更合理的方式是结合:

  • MIME Type;

  • 文件头;

  • Magic Number;

  • 实际解析结果。

例如图片上传后,可以尝试真正解析图片。

如果解析失败,就不能只因为扩展名是 .jpg 而继续处理。


六、临时文件一定要有清理机制

很多上传系统会经历:

上传
→ 写临时目录
→ 处理
→ 保存正式文件

问题是:

如果处理失败,临时文件还在。

如果服务突然重启,临时文件也可能留下。

时间一长:

/tmp/upload-a
/tmp/upload-b
/tmp/upload-c
...

最终磁盘空间耗尽。

因此要建立:

处理成功
→ 删除临时文件

处理失败
→ 删除临时文件

进程异常
→ 定时清理过期临时文件

可以增加一个定时任务:

删除超过24小时的临时上传文件

但需要确认目录中没有仍在处理的任务。


七、大文件处理建议改成异步任务

例如用户上传一个 300MB Excel。

不要让 HTTP 请求一直等待:

上传
→ 解析30万行
→ 校验
→ 写数据库
→ 生成报告
→ 返回结果

如果处理需要几十秒甚至几分钟,容易出现:

  • 网关超时;

  • 浏览器断开;

  • 请求资源长期占用;

  • 用户重复提交。

更合理的流程是:

上传文件
↓
返回 taskId
↓
后台任务处理
↓
前端查询任务状态
↓
完成后获取结果

接口可以先返回:

{
  "taskId": "import_10086",
  "status": "pending"
}

然后后台慢慢处理。


八、批量导入不要一次把所有行读进数组

例如:

const rows = await parseExcel(file);

await db.insertMany(rows);

如果 Excel 有:

500000行

内存中可能出现一个巨大数组。

更适合:

读取1000行
→ 校验
→ 写入
→ 清理
→ 再读取下一批

也就是分批处理。

例如:

const batchSize = 1000;

for await (const batch of readRows(file, batchSize)) {
  await saveBatch(batch);
}

这样可以控制内存峰值。


九、要处理Backpressure

流式处理并不代表永远不会内存暴涨。

如果:

读取速度
>
写入速度

数据仍然可能在内存里堆积。

例如:

磁盘读取:200MB/s
数据库写入:20MB/s

如果不停读取,就会产生大量等待数据。

这就是 Backpressure,也就是背压。

理想流程应该是:

下游处理不过来
↓
上游暂停读取
↓
下游恢复
↓
继续读取

Node.js 的 Stream 和 pipeline() 可以帮助处理这种情况。


十、优先使用pipeline而不是手写事件链

例如:

import { pipeline } from "node:stream/promises";

await pipeline(
  sourceStream,
  transformStream,
  destinationStream
);

相比手动写:

stream.on("data")
stream.on("error")
stream.on("end")

pipeline() 更容易统一处理:

  • 错误传播;

  • 流关闭;

  • Backpressure;

  • 资源清理。

对于大文件转存、压缩、加密等任务尤其适合。


十一、对象存储可以避免应用服务器中转

如果使用 OSS、S3 或其他对象存储,可以考虑:

浏览器
→ 直接上传对象存储

而不是:

浏览器
→ 应用服务器
→ 对象存储

后者意味着所有文件流量都经过应用服务器。

如果大量用户上传视频,应用服务器会承担:

  • 带宽;

  • 内存;

  • CPU;

  • 连接数。

直传模式可以让应用服务器只负责:

生成上传凭证
记录文件信息
验证上传结果

而不承担完整文件传输。


十二、大文件建议支持分片上传

对于数百MB甚至数GB文件,一次上传风险很高。

网络中途断开后:

99%完成
→ 网络断开
→ 从0重新上传

用户体验非常差。

可以使用分片:

文件
↓
chunk 1
chunk 2
chunk 3
...
chunk N

每个分片单独上传。

最后服务端或对象存储执行:

合并分片

这样失败时只需要重新上传缺失部分。


十三、分片上传必须避免重复合并

客户端可能重复提交:

chunk 5
chunk 5

或者重复发送“完成上传”请求。

因此需要:

uploadId
chunkNumber
checksum

例如:

{
  "uploadId": "upload_1001",
  "chunk": 5,
  "checksum": "abc123"
}

服务端可以判断:

这个分片是否已经存在?
内容是否一致?

最终合并也必须具备幂等性。


十四、上传完成后建议做Checksum验证

大文件上传完成后,可以计算:

MD5
SHA-256

或者其他校验值。

客户端:

文件Hash = ABC

服务端:

合并后Hash = ABC

一致才认为上传完整。

否则可能出现:

  • 分片缺失;

  • 数据损坏;

  • 顺序错误;

  • 网络传输异常。


十五、不要在上传接口里做所有业务逻辑

一个错误设计是:

上传
→ 病毒扫描
→ OCR
→ 图片压缩
→ AI识别
→ 入库
→ 通知
→ 返回

这会让一个简单上传接口承担过多职责。

更合理:

上传完成
↓
生成文件记录
↓
进入任务队列
↓
扫描
↓
解析
↓
业务处理

用户可以看到:

上传中
处理中
已完成
失败

而不是一直卡在一个 HTTP 请求里。


十六、让Codex先分析文件生命周期

遇到上传内存问题时,可以先这样问:

请先不要修改代码。

分析当前文件生命周期:

1. 文件进入服务器后保存在哪里;
2. 是否一次性读入Buffer;
3. 最大允许文件多大;
4. 同时上传多个文件时内存峰值是多少;
5. 临时文件在哪里删除;
6. 是否支持流式处理;
7. 是否存在后台任务;
8. 是否可以改成对象存储直传。

先把数据路径画清楚,再决定重构方式。


十七、测试不能只上传一个1MB文件

上传测试至少应该覆盖:

0字节文件
正常小文件
接近上限文件
超过上限文件
错误类型文件
网络中断
重复分片
缺失分片
合并失败
处理任务失败

还可以进行并发测试:

10个用户同时上传
20个用户同时上传

观察:

  • 内存;

  • CPU;

  • 磁盘;

  • 连接数;

  • 处理耗时。

只有这样才能判断上传接口是否真的稳定。


十八、把上传规则写进AGENTS.md

可以加入:

# 文件上传规则

- 大文件禁止一次性完整读入内存
- 服务端必须限制文件大小
- 文件类型不能只检查扩展名
- 临时文件必须有失败清理机制
- 长时间处理必须进入后台任务
- 批量数据必须分批处理
- 流式处理必须考虑Backpressure
- 大文件优先评估分片上传
- 分片必须包含uploadId和checksum
- 对象存储场景优先评估客户端直传
- 修改上传逻辑后必须执行大文件和并发测试

这样 Codex 后续生成文件处理代码时,就不会只以“能上传成功”为完成标准。


十九、Plus还是Pro?

如果主要使用 Codex 处理:

  • 头像上传;

  • 普通附件;

  • 单个文件接口;

  • 小规模文件处理;

Plus 通常已经能够覆盖多数需求。

如果项目包含:

  • 大文件上传;

  • 视频和批量Excel;

  • 对象存储;

  • 分片上传;

  • 后台任务;

  • 多模块持续调试;

可以根据实际开发强度评估 Pro。

不过更大的使用空间不能代替正确的文件架构。

上传稳定性最终取决于文件从进入系统到最终存储的整个生命周期设计。

总结

Codex 写文件上传以后服务器内存暴涨,很多时候不是 Node.js 或框架本身有问题,而是代码把本应该流式处理的数据一次性放进了内存。

通过 Stream、Backpressure、分批处理、临时文件清理、对象存储直传和分片上传,可以显著降低大文件对服务器的压力。

真正可靠的上传系统,不只是能够把文件传上去,还应该能够回答:

这个文件现在在哪里?占用了多少资源?上传失败后怎么恢复?处理失败后谁负责清理?

这几个问题越清楚,文件上传系统就越稳定。

CSDN文章描述

本文介绍 Codex 编写文件上传功能时常见的内存暴涨问题,并通过 Stream、Backpressure、分片上传、对象存储直传和后台任务,解决大文件上传导致的 OOM 与资源占用问题。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐