【问题标题】:Node.js memory leak when reading and writing large files读写大文件时Node.js内存泄漏
【发布时间】:2020-03-05 22:31:00
【问题描述】:

我目前正在尝试在Node中实现SPIMI索引构造方法,但遇到了问题。

代码如下:

let fs = require("fs");
let path = require("path");

module.exports = {

    fileStream: function (dirPath, fileStream) {
        return buildFileStream(dirPath, fileStream);
    },

    buildSpimi: function (fileStream, outDir) {
        let invIndex = {};
        let sortedInvIndex = {};
        let fileNameCount = 1;
        let outputTXT = "";
        let entryCounter = 0;
        let resString = "";
        fileStream.forEach((filePath, fileIndex) => {
            let data = fs.readFileSync(filePath).toString('utf-8');
            data = data.toUpperCase().split(/[^a-zA-Z]/).filter(function (ch) { return ch.length != 0; });
            data.forEach(token => {
                //CHANGE THE SIZE IF NECESSARY (4e+?)
                if (entryCounter > 100000) {
                    Object.keys(invIndex).sort().forEach((key) => {
                        sortedInvIndex[key] = invIndex[key];
                    });
                    outputTXT = outDir + "block" + fileNameCount;
                    for (let SItoken in sortedInvIndex) {
                        resString += SItoken + "," + sortedInvIndex[SItoken].toString();
                    };
                    fs.writeFile(outputTXT, resString, (err) => { if (err) console.log(error); });
                    resString = "";
                    entryCounter = 0;
                    sortedInvIndex = {};
                    invIndex = {};
                    console.log(outputTXT + " - written;");
                    fileNameCount++;
                };
                if (invIndex[token] == undefined) {
                    invIndex[token] = [];
                    entryCounter++;
                };
                if (!invIndex[token].includes(fileIndex)) {
                    invIndex[token].push(fileIndex);
                    entryCounter++;
                };
            });
        });
        Object.keys(invIndex).sort().forEach((key) => {
            sortedInvIndex[key] = invIndex[key];
        });
        outputTXT = outDir + "block" + fileNameCount;
        for (let SItoken in sortedInvIndex) {
            resString += SItoken + "," + sortedInvIndex[SItoken].toString();
        };
        fs.writeFile(outputTXT, resString, (err) => { if (err) console.log(error); });
        console.log(outputTXT + " - written;");
    }

}

function buildFileStream(dirPath, fileStream) {
    fileStream = fileStream || 0;
    fs.readdirSync(dirPath).forEach(function (file) {
        let filepath = path.join(dirPath, file);
        let stat = fs.statSync(filepath);
        if (stat.isDirectory()) {
            fileStream = buildFileStream(filepath, fileStream);
        } else {
            fileStream.push(filepath);
        }
    });
    return fileStream;
}

我在单独的文件中使用导出的函数:

let spimi = require("./spimi");
let outputDir = "/Users/me/Desktop/SPIMI_OUT/"
let inputDir = "/Users/me/Desktop/gutenberg/2/2";

fileStream = [];
let result = spimi.fileStream(inputDir, fileStream);
console.table(result)
console.log("Finished building the filestream");

let t0 = new Date();
spimi.buildSpimi(result, outputDir);
let t1 = new Date();

console.log(t1 - t0);

虽然这种代码在尝试处理相对较小的数据量时有效(我测试了高达 1.5 GB),但显然某处存在内存泄漏,因为在监控 RAM 使用情况时,我可以看到它上升到4-5 GB)。

我花了很多时间试图找出可能的原因,但我仍然找不到问题所在。

我将不胜感激任何提示! 谢谢!

【问题讨论】:

  • 你试过writeFileSync吗?

标签: node.js memory-leaks


【解决方案1】:

关于语言和垃圾收集的一般理解是:

data = data.toUpperCase().split(/[^a-zA-Z]/).filter(...)

创建三个额外的数据副本。首先,大写副本。然后,拆分数组副本。然后,拆分数组的过滤副本。

因此,此时,您在内存中拥有 四个 数据副本。当 GC 有机会运行时,除了过滤后的数组之外的所有内容现在都可以进行垃圾收集,但是如果此数据最初很大,那么您将使用至少 3x-4x 的文件大小的内存(取决于在您的.filter() 操作中删除了多少数组项)。

这都不是泄漏,但它是一个非常大的峰值内存使用,这可能是一个问题。

处理大文件的一种更节省内存的方法是将它们作为流处理(而不是一次将它们全部读入内存)。你读取一个小块(比如 1024 字节),处理它,读取一个块,处理它,同时注意块边界。如果您的文件自然有行边界,那么已经有预先构建的解决方案用于逐行处理。如果没有,您可以创建自己的块处理机制。我们必须查看您的数据样本才能提出更具体的块处理建议。


另外一点,如果你最终在invIndex 中有很多键,那么这行代码开始变得低效,你在循环中这样做:

Object.keys(invIndex).sort()

这将获取您的对象并获取临时数组中的所有键,您仅将其用于更新sortedInvIndex,这是您的数据的另一个副本。因此,仅就这一点而言,这组代码会生成所有键的三个副本和所有值的两个副本。而且,它每次通过你的循环都会这样做。同样,在您的函数完成之前,GC 通常不会清理大量 peak 内存使用量。


重新设计处理这些数据的方式可能会将峰值内存使用量降低 100 倍。为了提高内存效率,您只需要初始数据、最终数据表示形式,然后再多一点用于临时转换以同时过度使用。您不希望多次处理所有数据,因为每次执行此操作时,都会创建所有数据的另一个完整副本,这会导致内存使用量达到峰值。


如果你展示了数据输入的样子以及你试图最终得到的数据结构,我可能会尝试一个更有效的实现。

【讨论】:

    【解决方案2】:

    Mykhailo,加上 jfriend 所说的,这实际上不是内存泄漏。它按预期工作。

    需要考虑的是 readFile 缓冲整个文件!这将导致巨大的内存膨胀。更好的选择是实现fs.createReadStream(),它只会缓冲您当前正在阅读的文件部分。不幸的是,实现该解决方案可能需要完全重写您的代码,因为它返回 fs.ReadStream,这与您当前处理文件的方式不同 Checkout this link and read the bottom of the section to see what I'm referencing

    【讨论】:

      猜你喜欢
      • 2019-02-06
      • 2012-11-19
      • 2012-03-11
      • 2016-01-01
      • 2014-01-20
      • 1970-01-01
      相关资源
      最近更新 更多