You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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无法回收已处理完的文件内容占用的内存,最终内存持续上涨触发溢出。
排查解决步骤
  1. 先定位内存泄漏点
    • 启动脚本时添加--expose-gc参数,在每个文件处理完成、createOne执行结束后手动调用global.gc()触发垃圾回收,如果此时仍然出现内存溢出,说明createOne内部存在持有大对象引用未释放的问题,逐行检查createOne逻辑,排查是否存在全局变量缓存、未清空的数组/对象缓存、数据库驱动未释放的查询缓存问题。
    • 提前统计所有CSV文件的单文件体积、总体积,如果单文件体积超过500M、总体积超过1G,全量读取文件到内存的实现逻辑本身就存在内存风险,必须调整读取模式。
  2. 替换低内存效率的实现逻辑
    放弃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回收内存。
  3. 临时兜底方案(仅应急使用,不推荐长期依赖)
    如果暂时无法调整为流式处理逻辑,可以临时调高Node.js的堆内存上限启动脚本,示例命令将堆内存上限设为4G:
    node --max-old-space-size=4096 your_entry_script.js
    
    该方案仅能缓解内存上限问题,无法解决全量读文件带来的内存效率低下问题,大文件场景下仍然有溢出风险。

内容的提问来源于stack exchange,提问作者Willian Mustafa

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.01 02:36:23