优化 Node.js 性能:检测内存泄漏和高 CPU 使用率

优化 Node.js 性能:检测内存泄漏和高 CPU 使用率

Node.js 是一种流行的 JavaScript 运行时,以其速度、性能和可扩展性而闻名。然而,即使是优化和编写得非常好的 Node.js 应用程序也可能会遇到性能问题,例如内存泄漏和 CPU 使用率过高。分析是帮助识别和解决这些性能瓶颈的关键过程。在本教程中,我们将介绍用于分析 Node.js 应用程序的最佳实践和工具,包括如何检测内存泄漏和高 CPU 使用率。

分析的重要性

分析 Node.js 很重要,它可以帮助我们了解应用程序如何使用 CPU、内存和 I/O 等资源并识别性能瓶颈。分析可以提供有价值的信息,可用于优化应用程序、减少完成任务所需的时间、减少内存使用并提高整体响应能力。

  • 优化性能 - 分析有助于识别应用程序中的性能瓶颈,例如响应时间慢和 CPU 使用率高。通过解决这些瓶颈,我们可以提高应用程序的整体性能。
  • 提高稳定性 - 分析可帮助我们识别应用程序中的稳定性问题,例如内存泄漏。通过解决这些问题,我们可以提高应用程序的整体稳定性。
  • 节省资源 - 分析可帮助我们识别应用程序中的资源使用情况,例如内存和 CPU 使用情况。通过减少资源使用,我们可以节省成本并提高应用程序的可扩展性。

可使用的工具

Chrome 开发工具

Chrome DevTools 是一款功能强大的 Node.js 应用程序调试和分析工具。它们提供有关应用程序的大量信息,包括内存使用情况、CPU 使用情况、请求计数、响应时间等。要访问 Chrome DevTools,可以通过在终端中键入node --inspect命令来使用命令行界面。

Node Inspector

Node InspectorNode.js 应用程序的强大调试和分析工具。它提供了一个图形用户界面,可以轻松监控我们的应用程序,包括内存使用情况、CPU 使用情况、请求计数、响应时间等。要使用 Node Inspector,可以通过在终端中键入npm install -g node-inspector命令安装。

node-memwatch

node-memwatch是一个 Node.js 模块,可帮助跟踪 Node.js 应用程序中的内存使用情况并检测内存泄漏。它提供了一个API来监视堆大小、堆使用情况和堆中对象的数量,以及跟踪GC(垃圾收集)统计信息。它还提供了用于检测内存泄漏和生成堆快照的工具,可用于识别内存问题的原因。

memwatch.on('leak', function(info) { ... });

v8-profiler

v8-profiler是一个用于分析 Node.js 应用程序的 npm 库。它提供了一个 API,用于使用 V8 JavaScript 引擎的内置分析器来分析 Node.js 运行时中运行的 JavaScript 代码。探查器收集有关 JavaScript 代码执行的信息,例如函数调用计数、函数计时和内存使用情况,可以分析这些信息以识别应用程序中的性能瓶颈和内存泄漏。

该库允许我们启动和停止分析会话、拍摄堆和分析数据的快照,并以人类可读的格式生成分析数据的报告。配置文件数据还可以保存到文件中并加载到可视化工具中以进行进一步分析。

const express = require('express');
const v8Profiler = require('v8-profiler');
const path = require('path');
const fs = require('fs');

const app = express();
const snapshots = new Map();

// 用于获取堆快照
app.get('/api/heap-snapshot', (req, res) => {
  const snapshot = v8Profiler.takeSnapshot();
  const name = snapshot.getHeader().title;
  snapshots.set(name, snapshot);

  res.send({ name });
});

// 用于按名称下载堆快照
app.get('/api/heap-snapshot/:name', (req, res) => {
  const name = req.params.name;
  const snapshot = snapshots.get(name);
  if (!snapshot) {
    return res.status(404).send({ error: 'Snapshot not found' });
  }

  const fileName = `${name}.heapsnapshot`;
  const filePath = path.resolve(__dirname, 'snapshots', fileName);
  snapshot.serialize(
    { write: chunk => fs.appendFileSync(filePath, chunk) },
    () => {
      res.download(filePath, fileName, error => {
        if (error) {
          console.error(error);
        }
        snapshots.delete(name);
      });
    }
  );
});

const port = process.env.PORT || 3000;
app.listen(port, () => {
  console.log(`Server listening on port ${port}`);
});

示例

内存泄漏

以下代码创建一个简单的 HTTP 服务器。

const express = require("express");
const app = express();
let data = [];

app.get("/leak", (req, res) => {
  setInterval(() => {
    data.push(new Array(1000000).join("a"));
  }, 1000);
  res.send("Memory leak started!");
});

app.listen(3000, () => {
  console.log("Server started on port 3000");
});

setInterval函数创建一个包含大量数据的新数组,并每秒将其推送到data数组。问题在于数据永远不会从内存中释放,导致内存使用量持续增加,称为内存泄漏。随着时间的推移,应用程序的内存使用量将会增加,直到最终由于内存不足而崩溃。要修复此内存泄漏,我们需要从内存中删除未使用的数据,例如,通过设置data = []来处理不需要的数据。

要检测内存泄漏,我们可以通过在终端中键入node --inspect命令来使用 Chrome DevToolsChrome DevTools 打开后,我们可以导航到“memory”选项卡并选择Take Heap Snapshot。这将为我们提供内存使用情况的快照。然后,我们可以将此快照与以后的快照进行比较,看看内存使用量是否随着时间的推移而增加。

目的是比较在不同时间点拍摄的两个堆快照,以查看哪些对象在内存中累积。这使我们可以查看哪些对象没有被垃圾收集,并确定是否发生内存泄漏。

在这里插入图片描述

重要的是比较不同时间点拍摄的堆快照以确定是否发生内存泄漏。比较堆快照时,我们除了查找快照之间大小或数量增加的对象外,还应该查看每个对象的参考图,以确定它没有被垃圾收集的原因。在许多情况下,对象之间的循环引用可能会导致内存泄漏,因为对象无法被垃圾收集,因为它们仍然被其他对象引用。

  • Shallow size:这是内存中对象的大小,包括其属性占用的内存以及存储在这些属性中的任何值的大小。
  • Retained size:这是如果对象被垃圾回收将释放的总内存,不仅包括对象本身的浅层大小,还包括该对象引用的所有对象的浅层大小,即使它们不是直接引用的代码的其他部分。

换句话说,对象的保留大小是其浅层大小与所有因该对象引用而无法被垃圾收集的对象的浅层大小之和。此测量可以帮助我们识别占用大量内存的对象,即使它们本身没有很大的浅层大小。

CPU 使用率高

const express = require("express");
const app = express();
function highCPUFunction() {
    let sum = 0;
    for (let i = 0; i < 10000000; i++) {
        sum += Math.pow(Math.sin(i), i);
    }
    return sum;
}
app.get("/high-cpu", (req, res) => {
    const start = Date.now();
    // 执行计算密集型操作
    const result = highCPUFunction();
    const end = Date.now();
    console.log(`Computation took ${end - start}ms`);
    res.send(result);
});
app.listen(3000, () => {
    console.log("Server started on port 3000");
});

上面的代码创建了一个highCPUFunction函数,该函数执行 10M 次迭代的循环。计算完成循环所需的时间并将其记录到控制台。

这种计算密集型操作可能会导致 CPU 使用率较高,因为 CPU 将努力执行循环中的计算。CPU 使用量取决于 CPU 的处理能力和可用系统资源等因素。

导航到Performance选项卡并选择Start profiling,一旦开始分析,我们就可以访问http://localhost:3000/high-cpu,这将开始对应用程序进行分析,并为我们提供 CPU 使用情况的详细视图,包括哪些函数使用最多的 CPU 时间。
在这里插入图片描述

结论

分析是优化 Node.js 应用程序性能、稳定性和可扩展性的重要过程。通过使用 Node.js 内置进程管理器、Chrome DevToolsNode Inspector 等工具,我们可以轻松监控和解决内存泄漏、CPU 使用率过高等性能问题。