分析 Node.js 应用程序 | Better Stack 社区

想象一下,您的应用程序运行顺畅,但突然间,您注意到负载很高,CPU 使用率飙升至 95% 甚至 100%。这通常表示您的 Node.js 应用程序中存在 CPU 密集型任务。

CPU 密集型任务需要大量处理能力,并且无法轻易转移到其他资源(例如 I/O 操作)。这些任务包括密集计算、图像/视频处理、加密操作和机器学习推理。

要找出罪魁祸首代码并解决 CPU 使用率过高的问题,您需要分析您的应用程序。本指南将探讨一些用于分析 Node.js 应用程序的工具和技术。

让我们开始吧!

先决条件

在开始之前,请确保您已准备好以下事项:

  • Node.js 的最新版本 并且 npm 已安装在你的机器上。
  • 熟悉使用 Node.js 构建基本应用程序。

步骤 1 — 下载演示项目

为了演示如何分析应用程序,您将使用一个使用 Fastify 服务器的 Node.js 项目。此服务器包含一个端点,允许用户通过提交密码进行注册。服务器使用随机生成的盐对密码进行哈希处理,以便安全存储。哈希处理过程受 CPU 限制,通常会导致 CPU 使用率过高。

首先使用以下命令将以下存储库克隆到您的机器:

git clone 

接下来,导航到新创建的目录:

安装依赖项,包括 Fastify (一个 Web 框架)和 Autocannon (负载测试工具),通过运行以下命令:

安装依赖项后,启动开发服务器:

您应该看到以下输出:

打开另一个终端来测试服务器:

curl -X POST -H "Content-Type: application/json" -d '{"password":"userPassword123"}' 

运行该命令后,您将收到与此类似但具有不同值的响应:

{"salt":"c2f1e60a8d47afed730eb7f3d84b7e39","hashedPassword":"4f9f64c8c631418c46392faa46f4ea2f247e1216fbec302ca49af3add4d5e9428f7ae9e07c716038082e14aaf7d01eac62f0d2820eabf79e398426720c819e13"}

密码经过散列处理以便安全存储,而不是直接存储在数据库中;应用程序仅仅记录它。

散列过程可以在 index.js 文件,特别是在 /register 终点:

fastify.post("/register", (request, reply) => {
  const { password } = request.body;

  if (!password) {
    return reply.status(400).send({ error: "Password is required" });
  }

  const salt = crypto.randomBytes(16).toString("hex");
  const hashedPassword = crypto
    .pbkdf2Sync(password, salt, 100000, 64, "sha512")
    .toString("hex");

  reply.send({ salt, hashedPassword });
});

然而,同步哈希操作 crypto.pbkdf2Sync 阻塞事件循环直到哈希过程完成,这可能导致性能下降和可扩展性问题。因此,在等待同步操作完成时,传入请求可能会遇到延迟或超时。

因此,在等待同步散列操作完成时,传入的请求可能会遇到延迟或超时。

第 2 步 – 区分 CPU 密集型任务和 I/O 密集型任务

计算机程序通常将任务分为两大类:I/O 密集型任务和 CPU 密集型任务。

I/O 密集型任务涉及通常由操作系统管理的操作,例如文件 I/O、网络请求或数据库交互。在 Node.js 中,由于这些任务具有单线程特性,因此不会阻塞事件循环,因为它们是异步处理的。Node.js 利用事件驱动架构和非阻塞 I/O 操作,允许它在等待 I/O 任务完成时继续执行其他代码。这可以同时高效管理多个 I/O 操作。示例包括从 API 获取数据、从磁盘读取文件或数据库查询。

另一方面,CPU 密集型任务需要大量处理能力,并且需要直接使用 CPU。如果执行时间过长,这些任务可能会导致事件循环阻塞,从而导致应用程序无响应。在 Node.js 中,CPU 密集型任务带来了特殊挑战,因为单线程事件循环必须等待这些任务完成后才能继续执行其他操作。CPU 密集型任务的示例包括图像处理、视频编码和复杂计算。

在排除高 CPU 使用率故障并考虑分析 CPU 使用率时,这通常是由于 CPU 密集型任务造成的。

步骤 3 — 使用内置 Node.js 分析器进行分析

Node.js 包含一个内置分析工具,可在 Node.js 应用运行时对其进行分析。该工具使用 V8 分析器,它会定期对应用程序的调用堆栈进行采样。每个样本都会记录采样时堆栈顶部的函数。通过分析这些样本,您可以确定 CPU 使用率峰值发生的位置。

要分析您的应用程序,请传递 --prof 标记到 node 命令如下:

接下来,您应该让应用程序承受高负载,以便进行更有意义的 CPU 分析。您可以使用 Autocannon 等负载测试工具来实现这一点。

打开第二个终端并使用以下命令对应用程序进行 11 秒的负载测试:

npx autocannon --renderStatusCodes  -d 11 -m POST -H "Content-Type: application/json" -b '{"password":"userPassword123"}' 

当负载测试完成时,它将显示如下输出:

Running 11s test @ 
10 connections


┌─────────┬────────┬────────┬─────────┬─────────┬────────────┬───────────┬─────────┐
│ Stat    │ 2.5%   │ 50%    │ 97.5%   │ 99%     │ Avg        │ Stdev     │ Max     │
├─────────┼────────┼────────┼─────────┼─────────┼────────────┼───────────┼─────────┤
│ Latency │ 217 ms │ 991 ms │ 3683 ms │ 5830 ms │ 1068.69 ms │ 767.62 ms │ 5830 ms │
└─────────┴────────┴────────┴─────────┴─────────┴────────────┴───────────┴─────────┘
┌───────────┬────────┬────────┬─────────┬─────────┬─────────┬───────┬────────┐
│ Stat      │ 1%     │ 2.5%   │ 50%     │ 97.5%   │ Avg     │ Stdev │ Min    │
├───────────┼────────┼────────┼─────────┼─────────┼─────────┼───────┼────────┤
│ Req/Sec   │ 8      │ 8      │ 9       │ 10      │ 8.91    │ 0.67  │ 8      │
├───────────┼────────┼────────┼─────────┼─────────┼─────────┼───────┼────────┤
│ Bytes/Sec │ 2.9 kB │ 2.9 kB │ 3.27 kB │ 3.63 kB │ 3.23 kB │ 242 B │ 2.9 kB │
└───────────┴────────┴────────┴─────────┴─────────┴─────────┴───────┴────────┘
┌──────┬───────┐
│ Code │ Count │
├──────┼───────┤
│ 200  │ 98    │
└──────┴───────┘

Req/Bytes counts sampled once per second.
# of samples: 11

108 requests in 11.07s, 35.6 kB read

完成负载测试后,在第一个终端中使用以下命令停止服务器 CTRL + C。分析器输出将写入当前目录中的文件中。文件的名称将是 isolate-0x-v8.log, 在哪里 0x-v8 表示十六进制字符串。

使用以下命令列出目录内容:

您将在输出中看到该文件:

total 7264
-rw-rw-r--  1 stanley stanley     623 Jun  4 11:35 index.js
-rw-rw-r--  1 stanley stanley 7399577 Jun  4 11:39 isolate-0x650d000-56183-v8.log
...

isolate-0x-v8.log 文件包含不易读取的原始数据。要使其易于阅读,请使用以下命令运行 --prof-process 旗帜:

node --prof-process isolate-0x-v8.log > profile.txt

打开 profile.txt 使用您喜欢的文本编辑器:

导航至 [Summary] 文件内的部分:

 [Summary]:
   ticks  total  nonlib   name
     23    0.2%  100.0%  JavaScript
      0    0.0%    0.0%  C++
     50    0.4%  217.4%  GC
  11674   99.8%          Shared libraries

Node.js 执行您的 JavaScript 文件并运行一些 C++ 代码和其他库。摘要输出表明大多数 tick 发生在 JavaScript 中,这表明您应该将注意力集中在 JavaScript 代码上。

继续 [JavaScript] 文件的部分来确定哪些函数消耗了最多的 CPU 时间:

 [JavaScript]:
   ticks  total  nonlib   name
      2    0.0%    8.7%  JS: ^processTimers node:internal/timers:499:25
      2    0.0%    8.7%  JS: +wrappedFn node:internal/errors:535:21
      2    0.0%    8.7%  JS: *normalizeString node:path:66:25
      1    0.0%    4.3%  RegExp: ^((?:@[^/\%]+/)?[^./\%][^/\%]*)(/.*)?$
      1    0.0%    4.3%  JS: ^toRealPath node:internal/modules/helpers:49:20
      1    0.0%    4.3%  JS: ^sendTrailer /home/stanley/nodejs-profiling-demo/node_modules/fastify/lib/reply.js:765:22
      1    0.0%    4.3%  JS: ^pbkdf2Sync node:internal/crypto/pbkdf2:61:20
    ...
      1    0.0%    4.3%  JS: + node:internal/validators:458:42

此部分显示哪些函数占用了最多的 CPU 时间,使您可以针对特定区域进行优化。值得注意的是, crypto 模块表明加密操作可能是 CPU 使用率的重要因素。此见解可以指导您优化代码的特定部分。

确认高 CPU 使用率的更可靠方法是检查调用堆栈。导航到分析输出的“自下而上”部分:

Bottom up (heavy) profile]:
  Note: percentage shows a share of a particular caller in the total
  amount of its parent calls.
  Callers occupying less than 1.0% are not shown.

   ticks parent  name
  10566   90.3%  /home/stanley/.nvm/versions/node/v22.2.0/bin/node
   9272   87.8%    JS: ^pbkdf2Sync node:internal/crypto/pbkdf2:61:20
   9272  100.0%      JS: ^ file:///home/stanley/nodejs-profiling-demo/index.js:6:27
   9272  100.0%        JS: ^preHandlerCallback /home/stanley/nodejs-profiling-demo/node_modules/fastify/lib/handleRequest.js:126:29
   9272  100.0%          JS: ^validationCompleted /home/stanley/nodejs-profiling-demo/node_modules/fastify/lib/handleRequest.js:103:30
   9272  100.0%            JS: ^preValidationCallback /home/stanley/nodejs-profiling-demo/node_modules/fastify/lib/handleRequest.js:83:32
    829    7.8%    JS: ~pbkdf2Sync node:internal/crypto/pbkdf2:61:20
    829  100.0%      JS: ~ file:///home/stanley/nodejs-profiling-demo/index.js:6:27
    829  100.0%        JS: ~preHandlerCallback /home/stanley/nodejs-profiling-demo/node_modules/fastify/lib/handleRequest.js:126:29
    829  100.0%          JS: ~validationCompleted /home/stanley/nodejs-profiling-demo/node_modules/fastify/lib/handleRequest.js:103:30
    829  100.0%            JS: ~preValidationCallback /home/stanley/nodejs-profiling-demo/node_modules/fastify/lib/handleRequest.js:83:32

本节进一步证实 pbkdf2Sync 函数消耗了大部分 CPU 时间。

另一种方法是使用 Chrome DevTools,它可以收集性能数据并生成有关哪些函数占用最多 CPU 时间的报告。这可让您轻松识别性能瓶颈。

要启动此过程,请使用 --inspect 旗帜:

服务器启动并运行后,启动 Chrome 或任何基于 Chromium 的浏览器并输入 chrome://inspect 在地址栏中。找到 检查 与您的 Node.js 脚本对应的链接并单击它:

Chrome 检查页面截图
DevTools 中的“开始录制”按钮
npx autocannon --renderStatusCodes -d 11 -m POST -H "Content-Type: application/json" -b '{"password":"userPassword123"}' 

完成负载测试后,返回 DevTools 窗口并单击 停止 完成分析过程如下:

停止分析屏幕截图
框架图截图。它有一个横轴表示时间,还有一个纵轴表示调用堆栈。

您可以放大横轴来仔细检查调用堆栈。最简单的方法是单击横轴上的任意一点,然后按住鼠标拖动以选择一小部分:

调用堆栈放大的屏幕截图processTicksAndRejections, endReadableNTemit)并通过自定义解析进行(defaultJsonParseronEnd)包括各种请求处理步骤 preValidationCallbackvalidationCompletedpreHandlerCallbackhandler. 该序列以匿名函数和关键 pbkdf2Sync 称呼。

要从下往上查看回调,请切换到“从下往上”选项卡以查看操作所花费的总时间:

自下而上的调用堆栈屏幕截图run. 当你扩展 run,你会看到 pbkdf2Sync 函数就在那里,这很大程度上暗示了它消耗了大部分的 CPU 时间。

这样,您现在就可以清除内容:

清除按钮的屏幕截图第 5 步 – 使用节点检查器进行分析

另一种方法是使用 Node Inspector API,它允许您以编程方式与 V8 检查器交互。您可以按照以下步骤使用 Inspector API 来分析您的应用程序。

首先,创建一个 inspector.js 根目录中的文件内容如下:

import * as inspector from "node:inspector/promises";
import fs from "node:fs/promises"; // Use promises for cleaner async/await usage

const session = new inspector.Session();

async function enableProfiling() {
  try {
    await session.connect();
    await session.post("Profiler.enable");
  } catch (error) {
    console.error("Error enabling profiling:", error);
  }
}

async function startCpuProfiling() {
  try {
    await enableProfiling();
    await session.post("Profiler.start");
  } catch (error) {
    console.error("Error starting CPU profiling:", error);
  }
}

async function stopCpuProfiling() {
  try {
    const { profile } = await session.post("Profiler.stop");
    await fs.writeFile("./profile.cpuprofile", JSON.stringify(profile));
  } catch (error) {
    console.error("Error stopping CPU profiling:", error);
  } finally {
    await session.disconnect();
  }
}

process.on("SIGUSR1", startCpuProfiling);
process.on("SIGUSR2", stopCpuProfiling);

在此代码中,您导入必要的模块并创建一个 inspector.Session 实例。 enableProfiling 函数连接到会话并启用 Profiler。

startCpuProfiling 函数调用 enableProfiling 并开始 CPU 分析。相比之下, stopCpuProfiling 函数停止分析,将分析数据保存到文件,断开会话,处理发生的任何错误。

该脚本监听 SIGUSR1SIGUSR2 信号。接收 SIGUSR1 开始分析,并且 SIGUSR2 停止分析并保存结果,允许通过 Unix 信号控制分析。

为了确保此代码在启动开发服务器时运行,请导入模块 index.js

import Fastify from "fastify";
import crypto from "node:crypto";

import "./inspector.js";

这样,就可以在没有任何标志的情况下运行开发服务器,并使用 &

它将显示进程 ID;在我的系统上,它是 15565

现在通过发送触发 CPU 分析 SIGUSR1 信号:

再次启动负载测试:

npx autocannon --renderStatusCodes  -d 11 -m POST -H "Content-Type: application/json" -b '{"password":"userPassword123"}' 

当负载测试完成后,发送 SIGUSR2 停止分析的信号:

停止分析会创建一个 profile.cpuprofile 根目录中有一个文件,其中包含已收集的分析数据。幸运的是,Chrome 可以读取此文件。

要在 DevTools 中快速分析它,请返回“性能”选项卡,然后点击 负载曲线 按钮:

Chrome DevTools 中的“加载配置文件”按钮的屏幕截图perf,一款功能强大的 Linux 工具,可以分析 Node.js 应用程序和其他语言编写的应用程序。它具有许多功能,包括记录 CPU 样本、上下文切换和详细的内核信息。

首先,检查 perf 安装:

现在,使用 --perf-basic-prof 旗帜:

node --perf-basic-prof index.js &

这告诉编译器在将代码转换为机器码时包含文件名。如果没有这个, perf 在分析期间将仅显示内存地址而不是函数名称。

接下来,记下进程 ID 并运行 perf 命令:

sudo perf record -F 99 -p  -g

您可以在另一个终端继续进行负载测试:

npx autocannon --renderStatusCodes  -d 11 -m POST -H "Content-Type: application/json" -b '{"password":"userPassword123"}' 

负载测试完成后,发送 SIGINT (Ctrl-C) 来停止 perf 过程。输出将如下所示:

...
[ perf record: Woken up 1 times to write data ]
[ perf record: Captured and wrote 0.240 MB perf.data (1156 samples) ]

perf 将在 /tmp 文件夹,通常命名为 /tmp/perf-.map,包含所调用函数的踪迹。要汇总结果,请运行:

sudo perf script > perfs.out

这也创造了 perf.data 包含二进制数据的文件。您可以打开 perf.out 在文本编辑器中打开文件来定位调用堆栈

node   56676 444594.095023:   10101010 task-clock:ppp:
        7db1716add39 cfree+0x19 (/usr/lib/x86_64-linux-gnu/libc.so.6)
             2189d76 evp_md_ctx_clear_digest+0x36 (/home/stanley/.nvm/versions/node/v22.2.0/bin/node)
             218a4e3 EVP_MD_CTX_copy_ex+0x73 (/home/stanley/.nvm/versions/node/v22.2.0/bin/node)
             21c7d30 HMAC_CTX_copy+0x90 (/home/stanley/.nvm/versions/node/v22.2.0/bin/node)
             22af0df kdf_pbkdf2_derive+0x3ef (/home/stanley/.nvm/versions/node/v22.2.0/bin/node)
             21b2274 PKCS5_PBKDF2_HMAC+0x234 (/home/stanley/.nvm/versions/node/v22.2.0/bin/node)
          ....
             374723 v8::internal::(anonymous namespace)::Invoke(v8::internal::Isolate*, v8::internal

来自的调用堆栈信息 perfs.out 显示 Node.js 进程的执行顺序,从内存管理开始(cfreelibc)并通过几个加密函数(evp_md_ctx_clear_digestEVP_MD_CTX_copy_exHMAC_CTX_copykdf_pbkdf2_derivePKCS5_PBKDF2_HMAC)。堆栈继续进行内部Node.js和V8引擎操作,表明在Node.js运行环境中大量使用CPU资源进行加密处理和函数调用。

由于数据量巨大,阅读此文件可能会让人不知所措,而且信息量不大。理解这一点的更好方法是将数据可视化。

要可视化这些数据,您可以使用 FlameGraph 工具。首先,返​​回主目录:

然后,克隆 FlameGraph 存储库:

git clone 

进入目录:

接下来,复制 perf.data 文件放入当前目录:

cp ../nodejs-profiling-demo/perf.data .

现在,使用以下命令创建火焰图:

sudo perf script | ./stackcollapse-perf.pl |./flamegraph.pl > perf-flamegraph.svg

该命令创建一个 perf-flamegraph.svg 文件,它是 SVG 格式的火焰图。

接下来,打开 perf-flamegraph.svg 在您选择的浏览器中打开文件来查看火焰图:

火焰图的屏幕截图pbkdf2Sync 占用大量 CPU 时间。这从代表 pbkdf2Sync 和相关的加密函数,表明 CPU 使用率很高。

另一个令人兴奋的功能是您可以单击框来查看更多详细信息,甚至执行搜索。

放大框的屏幕截图perf 命令并使用 FlameGraph 可视化数据可以让您有效地分析应用程序中的 CPU 使用率。

第 7 步 – 了解持续分析

到目前为止,您已手动分析了应用程序以识别性能问题。然而,这可能很费力,尤其是对于微服务架构而言,因为其中多个服务在不同机器上运行。

要持续了解 CPU 使用率,您可以使用持续分析。这是一种动态方法,可以持续分析应用程序,从而更轻松地监控 CPU 使用率并找出高内存或 CPU 消耗的罪魁祸首。

要实现持续分析,您可以使用以下工具:

  • 火光仪:一个持续分析平台,可帮助实时监控和分析 CPU 利用率。
  • 公园:一个开源的持续分析项目,使用其自定义查询语言收集、存储并使分析结果可查询。
  • Google 云端剖析器:与 Google Cloud 集成的持续分析工具,通过收集 CPU 和内存配置文件来帮助可视化和优化性能。
  • 康普罗夫:Prometheus 生态系统的持续分析工具,可轻松与现有的 Prometheus 设置集成。

使用这些工具,您可以有效地监控和优化应用程序中的 CPU 利用率。

最后的想法

在本文中,您探索了用于分析 Node.js 应用程序以识别高 CPU 使用率来源的各种技术和工具。分析对于性能优化至关重要,但进一步增强应用程序需要了解内存泄漏检测和预防,您可以从本指南中学习这些内容。

Leave a Reply

Your email address will not be published. Required fields are marked *

近期新闻​

编辑精选​