Node.js在Windows上出现内存溢出问题

4

我的一个项目存在问题,旨在扫描一个或多个目录以搜索MP3文件,并将其元数据和路径存储到MongoDB中。 运行代码的主计算机是Windows 10 64位,具有8GB RAM,CPU为AMD Ryzen 3.5GHz(4个核心)。 Windows安装在SSD上,而音乐则位于1TB的HDD上。
该nodejs应用程序可以通过命令行或通过NPM手动启动,从这里开始。 我正在使用递归函数来扫描所有目录,大约有20,000个文件左右。
我已经通过graceful-fs解决了EMFILE: too many files open问题,但现在我遇到了新问题:JavaScript heap out of memory
下面是我收到的完整输出:

C:\Users\User\Documents\GitHub\mp3manager>npm run scan

> experiments@1.0.0 scan C:\Users\User\Documents\GitHub\mp3manager
> cross-env NODE_ENV=production NODE_OPTIONS='--max-old-space-size=4096' node scripts/cli/mm scan D:\Musica

Scanning 1 resources in production mode
Trying to connect to  mongodb://localhost:27017/music_manager
Connected to mongo...

<--- Last few GCs --->

[16744:0000024DD9FA9F40]   141399 ms: Mark-sweep 63.2 (70.7) -> 63.2 (71.2) MB, 47.8 / 0.1 ms  (average mu = 0.165, current mu = 0.225) low memory notification GC in old space requested
[16744:0000024DD9FA9F40]   141438 ms: Mark-sweep 63.2 (71.2) -> 63.2 (71.2) MB, 38.9 / 0.1 ms  (average mu = 0.100, current mu = 0.001) low memory notification GC in old space requested


<--- JS stacktrace --->

==== JS stack trace =========================================

Security context: 0x02aaa229e6e9 <JSObject>
    0: builtin exit frame: new ArrayBuffer(aka ArrayBuffer)(this=0x027bb3502801 <the_hole>,0x0202be202569 <Number 8.19095e+06>,0x027bb3502801 <the_hole>)

    1: ConstructFrame [pc: 000002AF8F50D385]
    2: createUnsafeArrayBuffer(aka createUnsafeArrayBuffer) [00000080419526C9] [buffer.js:~115] [pc=000002AF8F8440B1](this=0x027bb35026f1 <undefined>,size=0x0202be202569 <Number 8.19095e+06>)
    3:...

FATAL ERROR: Committing semi space failed. Allocation failed - JavaScript heap out of memory
 1: 00007FF6E36FF04A
 2: 00007FF6E36DA0C6
 3: 00007FF6E36DAA30
 4: 00007FF6E39620EE
 5: 00007FF6E396201F
 6: 00007FF6E3E82BC4
 7: 00007FF6E3E79C5C
 8: 00007FF6E3E7829C
 9: 00007FF6E3E77765
10: 00007FF6E3989A91
11: 00007FF6E35F0E52
12: 00007FF6E3C7500F
13: 00007FF6E3BE55B4
14: 00007FF6E3BE5A5B
15: 00007FF6E3BE587B
16: 000002AF8F55C721
npm ERR! code ELIFECYCLE
npm ERR! errno 134

我尝试使用 NODE_OPTIONS='--max-old-space-size=4096',但我甚至不确定Node在Windows上是否考虑了这个选项。 我尝试过p-limit来限制有效运行的promise数量,但是老实说,我现在已经没有新想法了,我开始考虑使用另一种语言来看看它是否能更好地处理这些问题。 任何建议都将不胜感激。 祝你拥有愉快的一天。
编辑: 我试图用@Terry发布的processDir函数替换该函数,但结果是相同的。
更新2019-08-19: 为了避免堆栈问题,我删除了递归,并使用队列添加目录:

const path = require('path');
const mm = require('music-metadata');
const _ = require('underscore');
const fs = require('graceful-fs');
const readline = require('readline');

const audioType = require('audio-type');
// const util = require('util');
const { promisify } = require('util');
const logger = require('../logger');
const { mp3hash } = require('../../../src/libs/utils');
const MusicFile = require('../../../src/models/db/mongo/music_files');

const getStats = promisify(fs.stat);
const readdir = promisify(fs.readdir);
const readFile = promisify(fs.readFile);
// https://github.com/winstonjs/winston#profiling

class MusicScanner {
    constructor(options) {
        const { paths, keepInMemory } = options;

        this.paths = paths;
        this.keepInMemory = keepInMemory === true;
        this.processResult = {
            totFiles: 0,
            totBytes: 0,
            dirQueue: [],
        };
    }

    async processFile(resource) {
        const buf = await readFile(resource);
        const fileRes = audioType(buf);          
        if (fileRes === 'mp3') {
            this.processResult.totFiles += 1;

            // process the metadata
            this.processResult.totBytes += fileSize;
        }
    }

    async processDirectory() {
        while(this.processResult.dirQueue.length > 0) {
            const dir = this.processResult.dirQueue.shift();
            const dirents = await readdir(dir, { withFileTypes: true });
            const filesPromises = [];

            for (const dirent of dirents) {
                const resource = path.resolve(dir, dirent.name);
                if (dirent.isDirectory()) {
                    this.processResult.dirQueue.push(resource);
                } else if (dirent.isFile()) {
                    filesPromises.push(this.processFile(resource));
                }
            }

            await Promise.all(filesPromises);
        }
    }


    async scan() {
        const promises = [];

        const start = Date.now();

        for (const thePath of this.paths) {
            this.processResult.dirQueue.push(thePath);
            promises.push(this.processDirectory());
        }

        const paths = await Promise.all(promises);
        this.processResult.paths = paths;
        return this.processResult;
    }
}

module.exports = MusicScanner;

这里的问题是该过程需要54分钟来读取21K个文件,我不确定在这种情况下如何加速该过程。有什么提示吗?

你的目录中有一些大文件吗?你的代码将每种类型的文件读入堆中,而视频很容易就会占用几个G。在切换到Terry提供的scanDir后,堆栈应该显示分配的缓冲区大小,这可能会有所不同。使用原始版本可能会因为目录总内容而崩溃,但使用他的遍历方式应该可以幸存,除非单个文件足够大。 - lossleader
2个回答

2
我不确定这个翻译对你有多大的帮助,但是我创建了一个测试脚本来查看是否得到了与你相同的结果,我也在运行Windows 10。
你可以运行这个脚本,看看是否会出现任何问题。我能够列出/program files/(约91k个文件)甚至/windows(约265k个文件)中的所有文件,而不会导致崩溃。也许是其他操作而不仅仅是简单地列出文件导致了问题。
脚本将返回路径中的所有文件列表,所以这基本上就是你需要的内容。一旦你拥有了这个列表,它可以被线性迭代,然后你可以将详细信息添加到你的Mongo DB实例中。原始答案翻译成"最初的回答"。
const fs = require('fs');
const path = require('path');
const { promisify } = require('util');
const getStats = promisify(fs.stat);
const readdir = promisify(fs.readdir);

async function scanDir(dir, fileList) {

    let files = await readdir(dir);
    for(let file of files) {
        let filePath = path.join(dir, file);
        fileList.push(filePath);
        try {
            let stats = await getStats(filePath);
            if (stats.isDirectory()) {
                await scanDir(filePath, fileList);
            }
        } catch (err) {
            // Drop on the floor.. 
        }
    }

    return fileList;   
}

function logStats(fileList) {
    console.log("Scanned file count: ", fileList.length);
    console.log(`Heap total: ${parseInt(process.memoryUsage().heapTotal/1024)} KB, used: ${parseInt(process.memoryUsage().heapUsed/1024)} KB`);
}

async function testScan() {
    let fileList = [];
    let handle = setInterval(logStats, 5000, fileList);
    let startTime = new Date().getTime();
    await scanDir('/program files/', fileList);
    clearInterval(handle);
    console.log(`File count: ${fileList.length}, elapsed: ${(new Date().getTime() - startTime)/1000} seconds`);
}

testScan();

即使我尝试你的代码,结果也是一样的。奇怪的是,即使我通过命令行使用CLI应用程序启动应用程序,Visual Code也会崩溃... - Chris
VS Code有时会崩溃,可能是内存问题的副作用。 - Chris
好的,很棒。感谢您尝试这段代码。我想知道是什么差异导致了问题。我正在使用Node.js v10.15.1。我还更新了我的答案以记录内存统计信息,这可能会有用。我注意到随着扫描的文件越来越多,内存使用量也在增加。 - Terry Lennox
另一个问题是,你是否在这里遇到了无限递归的情况?测试目录结构中是否存在导致进程继续运行而不停止的内容?20000个文件似乎是一个非常小的数字,不足以导致内存错误。我看到扫描整个c驱动器时可能会使用257 MB的内存。远高于最优值,但并不会导致内存错误。 - Terry Lennox
扫描我的驱动器(如上所述)用于大约170万个文件时使用了约257 MB的内存(作为比较的参考)。 - Terry Lennox

0

我已经考虑解决了这个问题(至少在Linux上,我还需要在Windows上尝试),按照以下步骤进行(在这里使用Intel(R) Core(TM) i5-7200U CPU @ 2.50GHz和8GB RAM):

  • 删除递归函数,采用队列策略:我将目录路径存储在数组中,并将处理文件的承诺存储在临时数组中,在超过100个维度长度时执行它们;
  • 使用mediainfoeyeD3代替music-metadata:尽管music-metadata是一个很棒的模块,但我注意到它消耗了我的CPU的140%和RAM的30%。结合使用mediainfoeyeD3(仅提取图像)大大提高了性能。没有更多的堆问题,堆总计

现在将20329个文件存储到Mongo中只需要不到4分钟,而如果我存储封面图像,则需要大约16分钟(由于额外的文件读取和eyeD3执行)。

此处完整源代码


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接