Node.js循环中调用readFile读取文件出现内存溢出如何解决
问题背景
需求为读取指定文件夹下98个.csv文件,将所有文件内容存储到Postgres数据库中,初始实现代码如下:
// Files in folder const filesInFolder = fs.readdirSync('../pages-csv'); for (let i = 0; i < filesInFolder.length; i++) { console.log(`Saving file ${filesInFolder[i]}`) await fs.promises.readFile(path.join(__dirname, `../pages-csv/${filesInFolder[i]}`), 'utf8').then(async (data) => { await createOne(data) // database function }) }
运行上述代码时触发报错:
FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory
初始判断问题为文件内容插入数据库后未关闭文件、对应内存未释放导致,需要对应的排查解决方法。
根因说明
初始判断的“文件未关闭”不是核心原因:fs.promises.readFile 读取完成后会自动关闭文件句柄,不会长期占用文件相关资源。内存溢出的核心原因有两点:
readFile会将目标文件的完整内容一次性加载到Node.js堆内存中,如果单个CSV文件体积较大,叠加98个文件处理过程中数据库操作环节的内存占用,很容易触及Node.js默认的堆内存上限(32位系统约0.7G,64位系统约1.4G)- 如果
createOne方法存在大字符串/数据集引用未释放的问题,比如将读取到的CSV内容挂在全局变量/闭包缓存中、拼接超大体量SQL文本、使用ORM逐行插入时缓存全量待插入数据,会导致GC无法回收已处理完的文件内容占用的内存,最终内存持续上涨触发溢出。
排查解决步骤
- 先定位内存泄漏点
- 启动脚本时添加
--expose-gc参数,在每个文件处理完成、createOne执行结束后手动调用global.gc()触发垃圾回收,如果此时仍然出现内存溢出,说明createOne内部存在持有大对象引用未释放的问题,逐行检查createOne逻辑,排查是否存在全局变量缓存、未清空的数组/对象缓存、数据库驱动未释放的查询缓存问题。 - 提前统计所有CSV文件的单文件体积、总体积,如果单文件体积超过500M、总体积超过1G,全量读取文件到内存的实现逻辑本身就存在内存风险,必须调整读取模式。
- 启动脚本时添加
- 替换低内存效率的实现逻辑
放弃readFile全量读文件的模式,改用文件流+CSV流式解析的方案,逐行读取CSV内容、分批插入数据库,保证内存中同一时间只驻留少量待处理数据,参考实现:const fs = require('fs'); const path = require('path'); const { parse } = require('csv-parse'); const { Pool } = require('pg'); // 填入自身数据库连接配置 const pool = new Pool(); const filesInFolder = fs.readdirSync('../pages-csv').filter(file => file.endsWith('.csv')); async function processSingleFile(filePath) { const BATCH_INSERT_SIZE = 1000; let pendingRows = []; // 创建读流,不一次性加载全量文件 const csvStream = fs.createReadStream(filePath, 'utf8') .pipe(parse({ columns: true, // 若CSV无表头则设为false,根据实际情况调整 skip_empty_lines: true })); for await (const row of csvStream) { pendingRows.push(row); // 攒够一批就插入,避免内存堆积 if (pendingRows.length >= BATCH_INSERT_SIZE) { await batchInsertToDb(pendingRows); // 清空批次引用,提示GC回收 pendingRows = []; } } // 处理最后不足一批的剩余数据 if (pendingRows.length > 0) { await batchInsertToDb(pendingRows); pendingRows = null; } csvStream.destroy(); } async function batchInsertToDb(rows) { // 参数化批量插入,避免拼接超长SQL占用额外内存 const tableColumns = Object.keys(rows[0]); const valuePlaceholders = rows.map((_, rowIdx) => { const posOffset = rowIdx * tableColumns.length + 1; return `(${tableColumns.map((_, colIdx) => `$${posOffset + colIdx}`).join(',')})`; }).join(','); const flatValues = rows.flatMap(row => tableColumns.map(col => row[col])); const insertSql = `INSERT INTO your_target_table (${tableColumns.join(',')}) VALUES ${valuePlaceholders}`; await pool.query(insertSql, flatValues); } (async () => { // 顺序处理文件,不要并发开启多个读流占用过多内存 for (const fileName of filesInFolder) { console.log(`Start processing: ${fileName}`); const fullFilePath = path.join(__dirname, '../pages-csv', fileName); await processSingleFile(fullFilePath); console.log(`Finish processing: ${fileName}`); } await pool.end(); })();- 注意如果
createOne的逻辑是将CSV完整文本作为单个字段存入数据库,插入完成后需要手动将存储文件内容的变量置为null,切断引用方便GC回收内存。
- 注意如果
- 临时兜底方案(仅应急使用,不推荐长期依赖)
如果暂时无法调整为流式处理逻辑,可以临时调高Node.js的堆内存上限启动脚本,示例命令将堆内存上限设为4G:
该方案仅能缓解内存上限问题,无法解决全量读文件带来的内存效率低下问题,大文件场景下仍然有溢出风险。node --max-old-space-size=4096 your_entry_script.js
内容的提问来源于stack exchange,提问作者Willian Mustafa
相关产品推荐
相关产品推荐

