通过集群优化 Node.js 性能

2025-05-25

通过集群优化 Node.js 性能

作者:Geshan Manandhar ✏️

Node.js 在过去几年中获得了巨大的成功。LinkedIn、eBay 和 Netflix 等知名公司都在使用它,这证明了它久经考验。在本教程中,我们将学习如何在 Node.js 中使用集群,充分利用所有可用的 CPU 来获得卓越的性能提升。让我们开始吧。

Node.js 中的集群需求

Node.js 的一个实例在单线程上运行(您可以点击此处了解更多关于 Node.js 线程的信息)。Node.js 官方的“关于”页面指出:“Node.js 设计时没有线程,但这并不意味着您无法在环境中充分利用多核。” 这正是集群模块的由来。

集群模块文档补充道:“为了利用多核系统,用户有时会想要启动一个 Node.js 进程集群来处理负载。”因此,为了充分利用运行 Node.js 的系统上的多个处理器,我们应该使用集群模块。

利用可用的核心来分配负载,可以提升 Node.js 应用的性能。由于大多数现代系统都拥有多核,我们应该使用 Node.js 中的集群模块,以最大限度地发挥这些新机器的性能。

Node.js 集群模块如何工作?

简而言之,Node.js 集群模块充当负载均衡器,将负载分配给在共享端口上同时运行的子进程。Node.js 不擅长处理阻塞代码,这意味着如果只有一个处理器,并且它被一个繁重且占用大量 CPU 的操作阻塞,其他请求就只能在队列中等待该操作完成。

在多进程模式下,如果一个进程正忙于处理相对 CPU 密集型的操作,其他进程可以处理其他传入的请求,从而利用其他可用的 CPU/核心。这就是集群模块的强大之处,多个工作进程可以分担负载,确保应用程序不会因高负载而停止运行。

主进程可以通过两种方式将负载分配给子进程。第一种(也是默认方式)是循环调度。第二种方式是主进程监听套接字并将工作发送给感兴趣的工作进程。然后,工作进程处理传入的请求。

然而,第二种方法并不像基本的循环方法那样非常清晰且易于理解。

理论已经讲得足够多了,在深入研究代码之前,让我们先看看一些先决条件。

先决条件

要遵循有关 Node.js 中的集群的本指南,您应该具备以下内容:

  • 您的机器上运行的 Node.js
  • 具备 Node.js 和 Express 的工作知识
  • 有关进程和线程如何工作的基本知识
  • 具备 Git 和 GitHub 的工作知识

现在让我们进入本教程的代码。

构建一个简单的 Express 服务器(无需集群)

我们将从创建一个简单的 Express 服务器开始。该服务器将执行一个相对较重的计算任务,并故意阻塞事件循环。我们的第一个示例将不使用任何集群。

要在新项目中设置 Express,我们可以在 CLI 上运行以下命令:

mkdir nodejs-cluster
cd nodejs-cluster
npm init -y
npm install --save express
Enter fullscreen mode Exit fullscreen mode

no-cluster.js然后,我们将在项目根目录下创建一个名为的文件,如下所示:

node.js 文件系统的屏幕截图

该文件的内容no-cluster.js如下:

const express = require('express');
const port = 3001;

const app = express();
console.log(`Worker ${process.pid} started`);

app.get('/', (req, res) => {
  res.send('Hello World!');
})

app.get('/api/slow', function (req, res) {
  console.time('slowApi');
  const baseNumber = 7;
  let result = 0;   
  for (let i = Math.pow(baseNumber, 7); i >= 0; i--) {      
    result += Math.atan(i) * Math.tan(i);
  };
  console.timeEnd('slowApi');

  console.log(`Result number is ${result} - on process ${process.pid}`);
  res.send(`Result number is ${result}`);
});

app.listen(port, () => {
  console.log(`App listening on port ${port}`);
});
Enter fullscreen mode Exit fullscreen mode

让我们看看代码的作用。我们先从一个简单的 Express 服务器开始,它将在端口 上运行3001。它有两个 URI(/)显示Hello World!,以及另一个路径/api/slow

速度较慢的 API GET 方法有一个很长的循环,循环次数为 7 7,共计 823,543 次。每次循环中,它会对math.atan()一个数进行 a (即反正切,以弧度math.tan()为单位)和 a (即正切)。它会将这些数加到 result 变量中。之后,它会记录并返回这个数作为响应。

是的,我们故意把它设计得既耗时又占用大量处理器,以便稍后在集群中测试其效果。我们可以快速测试一下,node no-cluser.js并点击它http://localhost:3001/api/slow,它会给出以下输出:

函数输出的屏幕截图

Node.js 进程运行的 CLI 如下面的屏幕截图所示:

与之前编号相同的 CLI 的屏幕截图

如上所示,根据我们添加console.timeconsole.timeEnd调用的分析,API 花费 37.432 毫秒完成 823,543 个循环。

到目前为止的代码可以通过Pull Request获取,供您参考。接下来,我们将创建另一个看起来类似但包含集群模块的服务器。

将 Node.js 集群添加到 Express 服务器

我们将添加一个index.js与上述文件类似的文件no-cluster.js,但在本例中它将使用 cluster 模块。该index.js文件的代码如下所示:

const express = require('express');
const port = 3000;
const cluster = require('cluster');
const totalCPUs = require('os').cpus().length;

if (cluster.isMaster) {
  console.log(`Number of CPUs is ${totalCPUs}`);
  console.log(`Master ${process.pid} is running`);

  // Fork workers.
  for (let i = 0; i < totalCPUs; i++) {
    cluster.fork();
  }

  cluster.on('exit', (worker, code, signal) => {
    console.log(`worker ${worker.process.pid} died`);
    console.log("Let's fork another worker!");
    cluster.fork();
  });

} else {
  startExpress();
}

function startExpress() {
  const app = express();
  console.log(`Worker ${process.pid} started`);

  app.get('/', (req, res) => {
    res.send('Hello World!');
  });

  app.get('/api/slow', function (req, res) {
    console.time('slowApi');
    const baseNumber = 7;
    let result = 0; 
    for (let i = Math.pow(baseNumber, 7); i >= 0; i--) {        
      result += Math.atan(i) * Math.tan(i);
    };
    console.timeEnd('slowApi');

    console.log(`Result number is ${result} - on process ${process.pid}`);
    res.send(`Result number is ${result}`);
  });

  app.listen(port, () => {
    console.log(`App listening on port ${port}`);
  });
}
Enter fullscreen mode Exit fullscreen mode

让我们看看这段代码做了什么。我们首先 requireexpress模块,然后 requirecluster模块。之后,我们使用 获取可用的 CPU 数量require('os').cpus().length。在我的 Macbook Pro 上,运行 Node.js 14 时,CPU 数量是 8。

因此,我们会检查集群是否为主节点。几次之后,console.logs我们会 fork 工作进程,次数与可用 CPU 数量相同。我们只需捕获一个工作进程的退出,并记录日志,然后 fork 另一个工作进程即可。

如果不是主进程,则是子进程,然后我们调用该startExpress函数。该函数与上一个示例中没有集群的 Express 服务器相同。

当我们运行上述index.js文件时,node index.js我们会看到以下输出:

Nodejs集群模块截图

我们可以看到,所有 8 个 CPU 都有 8 个相关的工作进程正在运行,随时准备处理任何传入的请求。如果命中,http://localhost:3000/api/slow我们将看到以下输出,与之前非集群服务器的输出相同:

先前在端口 3000 上计算的数字的屏幕截图

带有集群模块的服务器代码在此拉取请求中。接下来,我们将对启用和不启用集群的 Express 服务器进行负载测试,以评估其响应时间和每秒可处理的请求数 (RPS) 的差异。

具有和不具有集群的负载测试服务器

为了对 Node.js 服务器进行集群和非集群负载测试,我们将使用Vegeta 负载测试工具。其他选项包括loadtest npm 包或Apache 基准测试工具。我发现 Vegeta 更易于安装和使用,因为它是一个 Go 二进制文件,并且预编译的可执行文件可以无缝安装和启动。

在我们的机器上运行 Vegeta 之后,我们可以运行以下命令来启动 Node.js 服务器,而无需启用任何集群:

node no-cluster.js
Enter fullscreen mode Exit fullscreen mode

在另一个 CLI 选项卡中,我们可以运行以下命令,使用 Vegeta 发送 50 RPS 持续 30 秒:

echo "GET http://localhost:3001/api/slow" | vegeta attack -duration=30s -rate=50 | vegeta report --type=text
Enter fullscreen mode Exit fullscreen mode

大约 30 秒后,它将产生如下所示的输出。如果您检查正在运行 Node.js 的另一个选项卡,您将看到大量日志流淌:

无聚类测试日志的屏幕截图

从上述负载测试中我们可以快速了解到一些信息。测试共发送了 1,500(50*30)个请求,服务器的最大良好响应速度为 27.04 RPS。最快响应时间为 96.998 微秒,最慢响应时间为 21.745 秒。同样,只有 1,104 个请求返回了200响应代码,这意味着在没有集群模块的情况下,成功率为 73.60%。

让我们停止该服务器并使用集群模块运行另一台服务器:

node index.js
Enter fullscreen mode Exit fullscreen mode

如果我们以 50 RPS 的速度运行相同的测试 30 秒,在第二台服务器上我们可以看到差异。我们可以通过运行以下命令来运行负载测试:

echo "GET http://localhost:3000/api/slow" | vegeta attack -duration=30s -rate=50 | vegeta report --type=text
Enter fullscreen mode Exit fullscreen mode

30 秒后,输出将如下所示:

性能更佳的Vegeta测试截图

我们可以清楚地看到这里的巨大差异,因为服务器可以利用所有可用的 CPU,而不仅仅是一个。所有 1,500 个请求均成功,并返回了200响应代码。最快的响应时间为 31.608 毫秒,最慢的响应时间为 42.883 毫秒,而没有集群模块时则为 21.745 秒。

吞吐量同样为 50,因此这次服务器在 30 秒内处理 50 RPS 毫无问题。由于所有八个核心都可用,它可以轻松处理比之前的 27 RPS 更高的负载。

如果您查看带有集群的 Node.js 服务器的 CLI 选项卡,它应该显示如下内容:

集群测试的 CLI 屏幕截图

这告诉我们至少有两个处理器用于处理请求。如果我们尝试使用 100 RPS,它会根据需要占用更多 CPU 和进程。您可以尝试以 100 RPS 运行 30 秒,看看效果如何。在我的机器上,它的最高速度大约是 102 RPS。

从无集群时的 27 RPS 到有集群时的 102 RPS,集群模块的响应成功率提高了近四倍。这就是使用集群模块可以利用所有可用 CPU 资源的优势。

后续步骤Next steps

如上所示,自行使用集群对性能有益。对于生产级系统,最好使用像PM2这样久经考验的软件。它内置了集群模式,并包含进程管理和日志等其他强大功能。

类似地,对于在 Kubernetes 上的容器中运行的生产级 Node.js 应用程序,资源管理部分可能更适合由 Kubernetes 来处理。

这些是您和您的软件工程团队需要做出的决策和权衡,以便在生产环境中运行更具可扩展性、性能更强、弹性更大的 Node.js 应用程序。

结论

在本文中,我们学习了如何利用 Node.js 集群模块来充分利用可用的 CPU 核心,从而提升 Node.js 应用程序的性能。除此之外,集群技术也是 Node.js 提升吞吐量的又一利器。


仅 200 个✔️ 监控生产环境中失败和缓慢的网络请求

部署基于 Node 的 Web 应用或网站很容易,但确保 Node 实例持续为应用提供资源才是关键。如果您需要确保对后端或第三方服务的请求成功,不妨尝试一下 LogRocket

LogRocket 网络请求监控

LogRocket就像 Web 应用的 DVR,可以记录您网站上发生的所有事件。您无需猜测问题发生的原因,而是可以汇总并报告有问题的网络请求,从而快速了解根本原因。

LogRocket 会为您的应用提供工具,记录基准性能时间,例如页面加载时间、首字节时间、慢速网络请求,以及 Redux、NgRx 和 Vuex 的操作/状态。立即免费开始监控

文章来源:https://dev.to/logrocket/optimize-node-js-performance-with-clustering-7ki
PREV
10+ 面向开发者的高级项目创意:挑战你的技能!12. 买一辆 Cyber​​truck
NEXT
同形异义词,进攻!