通过集群优化 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
no-cluster.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}`);
});
让我们看看代码的作用。我们先从一个简单的 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 如下面的屏幕截图所示:
如上所示,根据我们添加console.time
和console.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}`);
});
}
让我们看看这段代码做了什么。我们首先 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
我们会看到以下输出:
我们可以看到,所有 8 个 CPU 都有 8 个相关的工作进程正在运行,随时准备处理任何传入的请求。如果命中,http://localhost:3000/api/slow
我们将看到以下输出,与之前非集群服务器的输出相同:
带有集群模块的服务器代码在此拉取请求中。接下来,我们将对启用和不启用集群的 Express 服务器进行负载测试,以评估其响应时间和每秒可处理的请求数 (RPS) 的差异。
具有和不具有集群的负载测试服务器
为了对 Node.js 服务器进行集群和非集群负载测试,我们将使用Vegeta 负载测试工具。其他选项包括loadtest npm 包或Apache 基准测试工具。我发现 Vegeta 更易于安装和使用,因为它是一个 Go 二进制文件,并且预编译的可执行文件可以无缝安装和启动。
在我们的机器上运行 Vegeta 之后,我们可以运行以下命令来启动 Node.js 服务器,而无需启用任何集群:
node no-cluster.js
在另一个 CLI 选项卡中,我们可以运行以下命令,使用 Vegeta 发送 50 RPS 持续 30 秒:
echo "GET http://localhost:3001/api/slow" | vegeta attack -duration=30s -rate=50 | vegeta report --type=text
大约 30 秒后,它将产生如下所示的输出。如果您检查正在运行 Node.js 的另一个选项卡,您将看到大量日志流淌:
从上述负载测试中我们可以快速了解到一些信息。测试共发送了 1,500(50*30)个请求,服务器的最大良好响应速度为 27.04 RPS。最快响应时间为 96.998 微秒,最慢响应时间为 21.745 秒。同样,只有 1,104 个请求返回了200
响应代码,这意味着在没有集群模块的情况下,成功率为 73.60%。
让我们停止该服务器并使用集群模块运行另一台服务器:
node index.js
如果我们以 50 RPS 的速度运行相同的测试 30 秒,在第二台服务器上我们可以看到差异。我们可以通过运行以下命令来运行负载测试:
echo "GET http://localhost:3000/api/slow" | vegeta attack -duration=30s -rate=50 | vegeta report --type=text
30 秒后,输出将如下所示:
我们可以清楚地看到这里的巨大差异,因为服务器可以利用所有可用的 CPU,而不仅仅是一个。所有 1,500 个请求均成功,并返回了200
响应代码。最快的响应时间为 31.608 毫秒,最慢的响应时间为 42.883 毫秒,而没有集群模块时则为 21.745 秒。
吞吐量同样为 50,因此这次服务器在 30 秒内处理 50 RPS 毫无问题。由于所有八个核心都可用,它可以轻松处理比之前的 27 RPS 更高的负载。
如果您查看带有集群的 Node.js 服务器的 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就像 Web 应用的 DVR,可以记录您网站上发生的所有事件。您无需猜测问题发生的原因,而是可以汇总并报告有问题的网络请求,从而快速了解根本原因。
LogRocket 会为您的应用提供工具,记录基准性能时间,例如页面加载时间、首字节时间、慢速网络请求,以及 Redux、NgRx 和 Vuex 的操作/状态。立即免费开始监控。
文章来源:https://dev.to/logrocket/optimize-node-js-performance-with-clustering-7ki