MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

JavaScript分析Node进程资源占用情况

2021-07-043.7k 阅读

一、Node 进程资源占用概述

在深入探讨如何分析 Node 进程资源占用情况之前,我们先来理解一下 Node 进程在运行过程中所涉及的各类资源。Node.js 作为一个基于 Chrome V8 引擎的 JavaScript 运行时,其进程会占用系统的多种资源,主要包括内存、CPU 以及文件描述符等。

(一)内存资源

  1. 堆内存与栈内存 Node 进程中的内存主要分为堆内存和栈内存。栈内存主要用于存储局部变量和函数调用栈,其大小相对固定且较小。而堆内存则用于存储对象、数组等动态分配的数据,是我们重点关注的部分。在 Node 应用中,对象的创建、函数的调用以及数据的存储都会在堆内存中进行操作。例如:
let obj = { name: 'John', age: 30 };
// 这里创建的obj对象就存储在堆内存中
  1. 内存泄漏 内存泄漏是 Node 应用中常见的内存问题。当对象不再被应用程序使用,但由于某些原因,其内存空间未能被释放时,就会发生内存泄漏。随着时间的推移,这会导致 Node 进程占用的内存不断增加,最终可能导致系统内存耗尽,应用程序崩溃。例如:
function memoryLeak() {
    let leaks = [];
    while (true) {
        leaks.push(new Array(1000000).join('x'));
    }
}
memoryLeak();

在上述代码中,leaks 数组不断地添加新的大数组,但这些数组永远不会被释放,从而导致内存泄漏。

(二)CPU 资源

  1. 单线程与事件循环 Node.js 是单线程运行的,这意味着它在同一时间只能执行一个任务。然而,它通过事件循环机制来处理大量的并发请求。事件循环会不断地检查事件队列,当有事件到达时,将对应的回调函数放入执行栈中执行。例如,处理 HTTP 请求时,Node 会将 I/O 操作(如读取文件、网络请求等)交给操作系统异步处理,当操作完成后,通过事件循环将回调函数放入执行栈执行。
const http = require('http');
const server = http.createServer((req, res) => {
    // 处理请求的逻辑
    res.end('Hello World');
});
server.listen(3000, () => {
    console.log('Server running on port 3000');
});

在这个简单的 HTTP 服务器示例中,事件循环会持续监听端口 3000 的请求,并处理每个请求的回调函数。 2. CPU 密集型任务 当 Node 进程执行 CPU 密集型任务时,会导致 CPU 使用率升高。例如,进行大量的数学计算或复杂的字符串处理等操作。如下代码:

function cpuIntensiveTask() {
    let sum = 0;
    for (let i = 0; i < 1000000000; i++) {
        sum += i;
    }
    return sum;
}
cpuIntensiveTask();

这段代码进行了一个庞大的循环计算,会使 CPU 在一段时间内保持高负荷运行。

(三)文件描述符

  1. 文件操作与描述符 在 Node 中,进行文件操作(如读取、写入文件)时,会使用到文件描述符。文件描述符是一个非负整数,它指向内核为每个进程维护的打开文件记录表中的一项。每个打开的文件都有一个对应的文件描述符。例如:
const fs = require('fs');
const fd = fs.openSync('test.txt', 'r');
// 这里通过openSync打开文件,返回文件描述符fd
  1. 描述符泄漏 如果在 Node 应用中没有正确关闭文件描述符,就会发生文件描述符泄漏。随着时间的推移,可用的文件描述符数量会逐渐减少,最终导致应用程序无法再打开新的文件或进行其他需要文件描述符的操作。例如:
function fileDescriptorLeak() {
    for (let i = 0; i < 10000; i++) {
        const fd = fs.openSync('test.txt', 'r');
        // 这里没有关闭文件描述符,会导致泄漏
    }
}
fileDescriptorLeak();

二、分析内存资源占用情况

(一)使用 Node.js 内置工具

  1. process.memoryUsage() Node.js 提供了 process.memoryUsage() 方法,它可以返回一个对象,包含当前进程的内存使用信息。该对象包含 rss(resident set size,进程占用的物理内存大小,单位为字节)、heapTotal(堆内存的总大小)、heapUsed(堆内存中已使用的大小)等属性。示例如下:
const memoryUsage = process.memoryUsage();
console.log(`RSS: ${memoryUsage.rss} bytes`);
console.log(`Heap Total: ${memoryUsage.heapTotal} bytes`);
console.log(`Heap Used: ${memoryUsage.heapUsed} bytes`);

通过定期调用 process.memoryUsage(),可以观察到内存使用情况的变化,从而判断是否存在内存增长过快或异常的情况。 2. V8 堆快照 V8 引擎提供了生成堆快照的功能,这对于分析内存泄漏非常有帮助。我们可以使用 node --inspect 启动 Node 应用,然后通过 Chrome DevTools 来生成堆快照。 首先,使用 node --inspect 启动应用:

node --inspect app.js

然后,在 Chrome 浏览器中访问 chrome://inspect,找到对应的 Node 进程并打开 DevTools。在 DevTools 的 Memory 面板中,可以点击 Take a snapshot 生成堆快照。生成的堆快照会显示当前堆内存中的所有对象,我们可以通过筛选、搜索等功能来查找可能导致内存泄漏的对象。例如,如果发现某个对象的实例数量不断增加且不应该被如此频繁使用,就有可能是内存泄漏的源头。

(二)第三方工具

  1. Node.js 堆分析器(heapdump heapdump 是一个用于生成 Node.js 堆快照的第三方模块。通过安装 heapdump 模块(npm install heapdump),我们可以在代码中手动生成堆快照。示例如下:
const heapdump = require('heapdump');
// 在某个需要的地方生成堆快照
setTimeout(() => {
    heapdump.writeSnapshot('snapshot.heapsnapshot', (err, filename) => {
        if (err) {
            console.error('Error writing heap snapshot:', err);
        } else {
            console.log('Heap snapshot written to:', filename);
        }
    });
}, 5000);

生成的堆快照文件(.heapsnapshot)可以使用 Chrome DevTools 或其他堆分析工具进行分析。通过这种方式,我们可以在应用运行过程中的特定时间点生成堆快照,以便更精确地分析内存变化情况。 2. Node.js 性能分析工具(node -prof node -prof 是 Node.js 自带的性能分析工具,它可以生成性能分析报告,其中包含内存使用情况的详细信息。首先,使用 node -prof 启动应用:

node -prof app.js

应用运行结束后,会生成一个 v8-prof-<timestamp>.log 文件。然后,可以使用 node --prof-process 工具来处理这个日志文件,生成更易读的报告:

node --prof-process v8-prof-<timestamp>.log > processed.txt

在生成的 processed.txt 文件中,可以查看函数的调用次数、执行时间以及内存使用情况等信息,有助于找出内存占用较大的函数和模块。

三、分析 CPU 资源占用情况

(一)使用 Node.js 内置工具

  1. process.cpuUsage() Node.js 的 process.cpuUsage() 方法可以返回一个对象,包含当前进程的 CPU 使用时间。该对象有 user(用户态 CPU 使用时间,单位为微秒)和 system(内核态 CPU 使用时间,单位为微秒)两个属性。通过比较不同时间点调用 process.cpuUsage() 的返回值,可以计算出进程在一段时间内的 CPU 使用情况。示例如下:
const startUsage = process.cpuUsage();
// 执行一些 CPU 密集型任务
function cpuIntensiveTask() {
    let sum = 0;
    for (let i = 0; i < 1000000000; i++) {
        sum += i;
    }
    return sum;
}
cpuIntensiveTask();
const endUsage = process.cpuUsage(startUsage);
console.log(`User CPU time: ${endUsage.user} microseconds`);
console.log(`System CPU time: ${endUsage.system} microseconds`);
  1. 事件循环性能监控 Node.js 提供了 --trace-event-categories--trace-event-file 命令行选项来监控事件循环的性能。通过启用这些选项,可以生成一个事件跟踪文件,使用 Chrome 的 chrome://tracing 工具进行分析。例如,使用以下命令启动应用:
node --trace-event-categories=v8,node,uv --trace-event-file=trace.json app.js

在 Chrome 中打开 chrome://tracing,加载生成的 trace.json 文件,就可以看到事件循环的详细信息,包括任务的执行时间、等待时间等。这有助于发现事件循环中的性能瓶颈,例如某个回调函数执行时间过长导致事件循环阻塞。

(二)第三方工具

  1. Node.js 性能分析工具(node -prof 除了分析内存使用情况,node -prof 工具也可以用于分析 CPU 使用情况。在使用 node -prof 启动应用并生成日志文件后,通过 node --prof-process 处理日志文件,生成的报告中会包含函数的 CPU 使用时间等信息。例如,在报告中可以看到哪些函数执行时间较长,从而确定 CPU 密集型任务所在的位置。
  2. node-cpu-profiler node-cpu-profiler 是一个专门用于分析 Node.js 进程 CPU 使用情况的第三方模块。通过安装该模块(npm install node-cpu-profiler),可以在代码中启动和停止 CPU 分析,并生成分析报告。示例如下:
const cpuProfiler = require('node-cpu-profiler');
cpuProfiler.startProfiling();
// 执行一些操作
function cpuIntensiveTask() {
    let sum = 0;
    for (let i = 0; i < 1000000000; i++) {
        sum += i;
    }
    return sum;
}
cpuIntensiveTask();
cpuProfiler.stopProfiling((profile) => {
    // 生成分析报告
    const report = cpuProfiler.getReport(profile);
    console.log(report);
});

生成的报告中会详细列出每个函数的 CPU 使用时间、调用次数等信息,方便我们找出 CPU 占用过高的原因。

四、分析文件描述符资源占用情况

(一)使用 Node.js 内置工具

  1. process.getrlimit() 和 process.setrlimit() Node.js 提供了 process.getrlimit()process.setrlimit() 方法来获取和设置资源限制,包括文件描述符的限制。通过 process.getrlimit('nofile') 可以获取当前进程的文件描述符限制,返回一个对象,包含 soft(软限制)和 hard(硬限制)两个属性。例如:
const rlimit = process.getrlimit('nofile');
console.log(`Soft limit: ${rlimit.soft}`);
console.log(`Hard limit: ${rlimit.hard}`);

如果发现文件描述符接近软限制,可以考虑通过 process.setrlimit('nofile', { soft: newSoftLimit, hard: newHardLimit }) 来调整限制。但需要注意,硬限制通常需要管理员权限才能提高。 2. fs.close() 和文件流事件 在进行文件操作时,正确关闭文件描述符是避免文件描述符泄漏的关键。对于通过 fs.openSync() 打开的文件描述符,需要使用 fs.close() 进行关闭。例如:

const fs = require('fs');
const fd = fs.openSync('test.txt', 'r');
// 进行文件操作
fs.closeSync(fd);

对于使用文件流(如 fs.createReadStream()fs.createWriteStream())的情况,可以监听 endclose 事件来确保文件流关闭时文件描述符被正确释放。例如:

const fs = require('fs');
const readStream = fs.createReadStream('test.txt');
readStream.on('end', () => {
    readStream.close();
});

(二)第三方工具

  1. lsof 工具(适用于 Unix - like 系统) 在 Unix - like 系统中,可以使用 lsof(list open files)工具来查看当前系统中打开的文件和对应的进程。通过过滤出与 Node 进程相关的文件描述符信息,可以了解 Node 进程打开了哪些文件以及文件描述符的使用情况。例如,使用以下命令查看 Node 进程(假设进程 ID 为 12345)打开的文件:
lsof -p 12345

该命令会列出该 Node 进程打开的所有文件,包括文件描述符编号、文件类型、访问模式等信息。通过分析这些信息,可以找出可能存在文件描述符泄漏的地方,例如某个文件被频繁打开但未关闭。 2. node -fd node -fd 是一个用于监控 Node.js 进程文件描述符使用情况的工具。通过安装该工具(npm install -g node -fd),可以在命令行中实时监控 Node 进程的文件描述符数量变化。例如,使用以下命令启动监控:

node -fd app.js

该工具会在控制台实时显示文件描述符的数量变化,方便我们在应用运行过程中观察文件描述符的使用情况,及时发现异常增长。

五、综合优化 Node 进程资源占用

(一)内存优化

  1. 优化对象创建与销毁 在 Node 应用中,尽量减少不必要的对象创建。例如,避免在循环中频繁创建新对象,可以复用已有的对象。对于不再使用的对象,确保其引用被清除,以便垃圾回收机制能够及时回收内存。例如:
// 避免在循环中频繁创建新对象
let arr = [];
for (let i = 0; i < 1000; i++) {
    let obj = { value: i };
    arr.push(obj);
}
// 复用对象
let objTemplate = { value: null };
let arr2 = [];
for (let i = 0; i < 1000; i++) {
    objTemplate.value = i;
    arr2.push({...objTemplate });
}
  1. 合理设置内存参数 在启动 Node 应用时,可以通过 --max-old-space-size--max-semi-space-size 等参数来设置 V8 堆内存的大小。根据应用的实际需求,合理调整这些参数可以提高内存使用效率。例如,如果应用处理大量数据,可以适当增加 --max-old-space-size 的值。启动命令如下:
node --max-old-space-size=4096 app.js

(二)CPU 优化

  1. 避免 CPU 密集型任务阻塞事件循环 对于 CPU 密集型任务,可以考虑将其分解为多个小任务,通过 setImmediate()process.nextTick() 等方法将任务放入事件循环队列,避免长时间阻塞事件循环。例如:
function cpuIntensiveTask() {
    let sum = 0;
    for (let i = 0; i < 1000000000; i++) {
        sum += i;
    }
    return sum;
}
function splitTask() {
    let total = 1000000000;
    let step = 1000000;
    let sum = 0;
    function doStep() {
        for (let i = 0; i < step; i++) {
            sum += total - step + i;
        }
        total -= step;
        if (total > 0) {
            setImmediate(doStep);
        } else {
            console.log('Final sum:', sum);
        }
    }
    setImmediate(doStep);
}
splitTask();
  1. 使用多进程或线程(适用于多核 CPU) 对于一些无法避免的 CPU 密集型任务,可以利用 Node.js 的多进程(child_process 模块)或线程(worker_threads 模块)来充分利用多核 CPU 的优势。例如,使用 child_process 模块创建子进程来处理 CPU 密集型任务:
const { fork } = require('child_process');
const child = fork('cpu - intensive - worker.js');
child.on('message', (result) => {
    console.log('Result from child:', result);
});
child.send('start');

cpu - intensive - worker.js 文件中:

process.on('message', (msg) => {
    if (msg ==='start') {
        let sum = 0;
        for (let i = 0; i < 1000000000; i++) {
            sum += i;
        }
        process.send(sum);
    }
});

(三)文件描述符优化

  1. 及时关闭文件描述符 在文件操作完成后,确保及时关闭文件描述符。无论是使用同步还是异步的文件操作方法,都要遵循正确的关闭流程。对于文件流,要监听 endclose 事件来关闭文件流。例如:
const fs = require('fs');
const readStream = fs.createReadStream('test.txt');
readStream.on('end', () => {
    readStream.close();
});
  1. 优化文件操作频率 尽量减少不必要的文件打开和关闭操作。如果需要频繁读取或写入文件,可以考虑使用缓存机制,将文件内容读取到内存中进行操作,然后批量写入文件。例如:
const fs = require('fs');
const path = require('path');
const cache = [];
function readFileCache(filePath) {
    return new Promise((resolve, reject) => {
        if (cache.length > 0) {
            resolve(cache);
        } else {
            fs.readFile(path.join(__dirname, filePath), 'utf8', (err, data) => {
                if (err) {
                    reject(err);
                } else {
                    cache.push(data);
                    resolve(cache);
                }
            });
        }
    });
}

通过对 Node 进程内存、CPU 和文件描述符等资源占用情况的分析,并采取相应的优化措施,可以提高 Node 应用的性能和稳定性,使其能够更好地应对各种业务场景。在实际开发中,需要根据应用的具体特点和需求,灵活运用这些分析方法和优化技巧。