Node.js 是一种服务器端 JavaScript 运行时,使用事件驱动、无阻塞输入输出(I/O)模型。它在构建快速、可扩展的网络应用程序方面广受认可。它还拥有一个庞大的社区和丰富的模块库,可简化各种任务和流程。
集群可使 Node.js 应用程序在多个进程上运行,从而提高其性能。这种技术可让应用程序充分发挥多核系统的潜力。
本文将全面介绍 Node.js 中的集群及其对应用程序性能的影响。
什么是集群?
默认情况下,Node.js 应用程序在单线程上运行。这种单线程特性意味着 Node.js 无法使用多核系统中的所有内核,而目前大多数系统都是如此。
通过利用非阻塞 I/O 操作和异步编程技术,Node.js 仍然可以同时处理多个请求。
但是,繁重的计算任务会阻塞事件循环,导致应用程序无响应。因此,Node.js 提供了一个本地集群模块(无论其单线程性质如何),以充分利用多核系统的总处理能力。
运行多个进程可充分利用多个中央处理器(CPU)内核的处理能力,从而实现并行处理、缩短响应时间并提高吞吐量。这反过来又提高了 Node.js 应用程序的性能和可扩展性。
集群是如何工作的?
Node.js 群集模块允许 Node.js 应用程序创建一个由并发运行的子进程组成的群集,每个子进程处理应用程序的部分工作负载。
在初始化集群模块时,应用程序会创建主进程,然后将子进程分叉为工作进程。主进程充当负载平衡器,将工作负载分配给工作进程,同时每个工作进程监听传入的请求。
Node.js 群集模块有两种分配传入连接的方法。
- 轮循方法 – 主进程在端口上监听,接受新连接并平均分配工作量,以确保没有进程超载。这是 Windows 以外所有操作系统的默认方法。
- 第二种方法 – 主进程创建监听套接字,并将其发送给 “感兴趣” 的工作进程,后者直接接受传入的连接。
从理论上讲,第二种方法更为复杂,应该能提供更好的性能。但实际上,连接的分布非常不平衡。Node.js 文档中提到,所有连接的 70% 都在八个进程中的两个进程中结束。
如何对 Node.js 应用程序进行集群
现在,让我们来看看集群在 Node.js 应用程序中的效果。本教程使用一个 Express 应用程序,该程序有意运行一项繁重的计算任务,以阻塞事件循环。
首先,在不使用集群的情况下运行此应用程序。然后,使用基准测试工具记录性能。然后,在应用程序中实施聚类,并重复进行基准测试。最后,比较结果,看看聚类如何提高应用程序的性能。
开始
要理解本教程,您必须熟悉 Node.js 和 Express。设置您的 Express 服务器:
- 首先创建项目。
mkdir cluster-tutorial
- 导航至应用程序目录,运行以下命令创建两个文件:no-cluster.js 和 cluster.js。
cd cluster-tutorial && touch no-cluster.js && touch cluster.js
- 在项目中初始化 NPM:
npm init -y
- 最后,运行下面的命令安装 Express:
npm install express
创建非集群应用程序
在 no-cluster.js 文件中,添加以下代码块:
const express = require("express"); const PORT = 3000; const app = express(); app.get("/", (req, res) => { res.send("Response from server"); }); app.get("/slow", (req, res) => { //Start timer console.time("slow"); // Generate a large array of random numbers let arr = []; for (let i = 0; i < 100000; i++) { arr.push(Math.random()); } // Perform a heavy computation on the array let sum = 0; for (let i = 0; i { console.log(`Server listening on port ${PORT}`); });
上面的代码块创建了一个在 3000
端口运行的 express 服务器。服务器有两个路由,一个是根( /
)路由,另一个是 /slow
路由。根路由会向客户端发送一条响应信息: “来自服务器的响应”。
不过, /slow
路由会故意进行一些繁重的计算,以阻断事件循环。该路由启动一个计时器,然后使用 for
循环将 100,000 个随机数填入一个数组。
然后,使用另一个 for
循环,对生成数组中的每个数字进行平移并相加。完成后定时器结束,服务器响应结果。
运行下面的命令启动服务器:
node no-cluster.js
然后向 localhost:3000/slow
发起 GET 请求。
在此期间,如果您尝试向服务器发出任何其他请求(例如向根路由 ( /
) 发出请求),响应速度都会很慢,因为 /slow
路由阻塞了事件循环。
创建集群应用程序
使用集群模块生成子进程,以确保您的应用程序在执行繁重的计算任务时不会出现无响应和阻塞后续请求的情况。
每个子进程运行其事件循环,并与父进程共享服务器端口,从而更好地利用可用资源。
首先,在 cluster.js 文件中导入 Node.js 集群和 os
模块。集群模块允许创建子进程,以便在多个 CPU 内核之间分配工作量。
os
模块提供有关计算机操作系统的信息。您需要使用该模块来获取系统上可用的内核数量,确保创建的子进程数量不会超过系统内核数量。
注:创建的子进程数量超过系统内核数量会导致系统在进程间切换花费更多时间。这会增加开销并降低性能。
添加下面的代码块,导入这些模块并获取系统内核的数量:
const cluster = require("node:cluster"); const numCores = require("node:os").cpus().length;
接下来,将下面的代码块添加到 cluster.js 文件中:
if (cluster.isMaster) { console.log(`Master ${process.pid} is running`); console.log(`This machine has ${numCores} cores`); // Fork workers. for (let i = 0; i { console.log(`worker ${worker.process.pid} died`); // Replace the dead worker console.log("Starting a new worker"); cluster.fork(); }); }
上面的代码块会检查当前进程是主进程还是工作进程。如果为真,代码块将根据系统内核数生成子进程。接下来,它会监听进程的退出事件,并通过生成新进程来替换它们。
最后,用 else 代码块封装所有相关的表达逻辑。完成后的 cluster.js 文件应与下面的代码块相似。
//cluster.js const express = require("express"); const PORT = 3000; const cluster = require("node:cluster"); const numCores = require("node:os").cpus().length; if (cluster.isMaster) { console.log(`Master ${process.pid} is running`); console.log(`This machine has ${numCores} cores`); // Fork workers. for (let i = 0; i { console.log(`worker ${worker.process.pid} died`); // Replace the dead worker console.log("Starting a new worker"); cluster.fork(); }); } else { const app = express(); app.get("/", (req, res) => { res.send("Response from server"); }); app.get("/slow", (req, res) => { console.time("slow"); // Generate a large array of random numbers let arr = []; for (let i = 0; i < 100000; i++) { arr.push(Math.random()); } // Perform a heavy computation on the array let sum = 0; for (let i = 0; i { console.log(`Server listening on port ${PORT}`); }); }
实施集群后,多个进程将处理请求。这意味着,即使在计算任务繁重的情况下,您的应用程序也能保持响应速度。
如何使用 loadtest 对性能进行基准测试
要在 Node.js 应用程序中准确演示和显示集群的效果,请使用 npm 软件包 loadtest 比较集群前后应用程序的性能。
运行以下命令全局安装 loadtest
:
npm install -g loadtest
loadtest
软件包会在指定的 HTTP/WebSockets URL 上运行负载测试。
接下来,在终端实例中启动 no-cluster.js 文件。然后,打开另一个终端实例,运行下面的负载测试:
loadtest http://localhost:3000/slow -n 100 -c 10
上面的命令向未集群的应用程序发送了 100
个并发量为 10
的请求。运行该命令会产生以下结果:
非群集应用程序负载测试结果。
根据结果,在没有群集的情况下,完成所有请求大约需要 100 秒,最长的请求需要 12 秒才能完成。
测试结果因系统而异。
接下来,停止运行 no-cluster.js 文件,在终端实例上启动 cluster.js 文件。然后,打开另一个终端实例并运行此负载测试:
loadtest http://localhost:3000/slow -n 100 -c 10
上述命令将向集群应用程序发送 100
个并发量为 10
的请求。
运行该命令会产生以下结果:
集群应用程序负载测试结果。
使用集群后,请求只需 0.13 秒(136 毫秒)即可完成,比未集群的应用程序需要 100 秒的时间大大缩短。此外,集群应用程序上最长的请求完成时间为 41 毫秒。
这些结果表明,实施集群可以显著提高应用程序的性能。请注意,您应该使用 PM2 等流程管理软件来管理生产环境中的集群。
小结
Node.js 中的集群可以创建多个工作进程来分配工作负载,从而提高 Node.js 应用程序的性能和可扩展性。正确实施集群对于充分发挥这项技术的潜力至关重要。
在 Node.js 中实施集群时,设计架构、管理资源分配和尽量减少网络延迟都是至关重要的因素。这种实现方式的重要性和复杂性正是 PM2 等进程管理器应在生产环境中使用的原因。
您如何看待 Node.js 集群?您以前使用过吗?请在评论区分享!
评论留言