Node.js v14.17.0下使用与不使用Stream API的内存消耗测试结果一致问题排查
问题分析与优化建议
首先,你的测试结果之所以几乎无差异,大概率是因为测试时机、S3 SDK的使用方式,以及内存测量的方法这几个关键点没踩对。咱们一步步拆解:
1. 内存测量时机错误
你是在上传完成后才读取内存使用情况,但这里有个核心问题:
- 对于
readFileSync,文件内容已经全部加载到内存,即使上传完成,这些数据可能还没被GC回收; - 对于Stream,上传完成后,流式读取的缓冲区数据已经被处理完毕,Node.js的GC很可能已经把这部分内存回收了。
这就导致你最终测得的内存状态其实是两者都完成任务后的“收尾状态”,自然差异不大。正确的做法是在上传过程中持续采样内存使用,比如用定时器每隔几百毫秒打印一次内存数据,观察峰值差异。
2. 你用错了S3上传的API!
PutObjectCommand本身是为小文件设计的——哪怕你传入的是Stream,AWS SDK v3的PutObject也会把整个Stream读取到内存后再一次性发送到S3,根本没用到流式上传的优势!
想要真正实现流式分段上传,你需要使用@aws-sdk/lib-storage包中的Upload类,它会自动把大文件分成多个Part(默认5MB),边读边传,不会把整个文件加载到内存里。
改造后的上传代码示例:
const { Upload } = require("@aws-sdk/lib-storage"); const { S3Client } = require("@aws-sdk/client-s3"); const s3Client = new S3Client({ /* 你的配置 */ }); const _uploadFile = async (fileName, bucket, streamOrContent) => { const uploadParams = { Bucket: bucket, Key: fileName, Body: streamOrContent }; // 使用Upload类实现分段流式上传 const upload = new Upload({ client: s3Client, params: uploadParams }); // 可选:监听上传进度 upload.on("httpUploadProgress", (progress) => { console.log(`上传进度: ${progress.loaded}/${progress.total}`); }); return await upload.done(); };
3. GC干扰了测试结果
Node.js的垃圾回收是自动触发的,如果你没有控制GC的时机,可能会导致内存数据不准。建议测试前强制触发GC,并在测试过程中禁用自动GC(或者至少在关键阶段禁用):
- 启动Node时加上
--expose-gc参数,允许手动调用global.gc(); - 测试开始前先调用
global.gc()清理内存; - 在上传过程中可以用
--no-gc参数启动Node,避免GC干扰测试(测试完记得恢复)。
4. 测试文件的大小可能还不够“极端”
504MB的文件在现代机器的内存里完全能装下,哪怕用同步读取,内存压力也不会特别大。建议换成更大的文件(比如2GB以上),这样Stream的内存优势会更明显——同步读取会直接把整个2GB文件加载到内存,而流式上传只会占用几MB的缓冲区内存。
优化后的测试流程建议
- 安装依赖:
npm install @aws-sdk/lib-storage - 用
Upload类替换PutObjectCommand实现流式上传 - 调整内存测量逻辑,在上传过程中定时采样:
test('create read stream and upload to s3', async () => { // 测试前清理内存 global.gc(); const readStream = fs.createReadStream(path.join(__dirname, './benda.js')); // 上传过程中定时打印内存 const interval = setInterval(() => { const used = process.memoryUsage(); console.log(`Stream上传中: heapUsed ${Math.round(used.heapUsed / 1024 / 1024 * 100) / 100} MB`); }, 500); await _uploadFile(`${FILE_NAME}`, bucket, readStream); clearInterval(interval); // 最后再测一次收尾状态 const used = process.memoryUsage(); for (let key in used) { console.log(`${key} ${Math.round(used[key] / 1024 / 1024 * 100) / 100} MB`); } });
额外注意点
- 第一个测试中你用了
utf8编码读取文件,把Buffer转换成了字符串,字符串在V8内存中的占用会比原始Buffer大(因为每个字符占2-4字节),但你的测试结果中heapUsed差异不大,可能是因为S3 SDK在上传时又把字符串转成了Buffer,抹平了差异。如果要更精准对比,第一个测试应该直接读取Buffer(去掉'utf8'参数)。
内容的提问来源于stack exchange,提问作者Oron Bendavid
相关产品推荐
相关产品推荐

