NodeJS 集群模块: 为App创建集群实例
@TOC
推荐超级课程:
本文介绍了在 Node.js 中进行集群处理以及如何使您的应用程序受益于扩展。您将使用集群模块 以创建集群实例。您将学到:
- 为什么需要集群作为扩展 Node.js 应用程序的策略。
- 如何使用原生和 PM2 模块使用 express 创建和扩展 Node.js 集群。
- 集群和非集群 Node.js 应用程序之间的扩展比较。
- 对 Node.js 集群进行负载测试作为性能监控方法。
- 如果 Node.js 集群不符合您的应用设计,需要了解的事项。
Node.js 集群介绍:终极扩展策略
Node.js 默认情况下是单线程的运行时。这意味着运行中的 Node.js 仅利用计算机的一个核心(CPU 单元),即使任何一台计算机都有多个处理器。例如,如果您在一台有 4 个处理器的计算机上运行 Node.js 应用程序,则只使用其中一个。然而,Node.js 允许您使用集群,充分利用多核处理器,并将您的 Node.js 应用程序扩展到新的水平。
使用集群模块开始扩展 Node.js
以拥有四个处理器的计算机为例。Node.js 只会利用其处理能力的 25%。Node.js 运行一个 worker Node 来执行所有你的 Node.js 函数。这种策略在您的应用程序中运行了密集任务时会受到影响。worker Node 将在重任务进行中被阻塞。
Node.js 集群的概念是创建多个您的 Node.js 应用程序实例(worker)。这将为同一应用程序创建一个跨可用 CPU 核心分布负载的集群。在这种情况下,当您的重任务正在处理时,您的集群模块将将任何连续的任务生成到剩余的处理器中。简而言之,集群将增强应用程序的性能和可扩展性。
让我们深入学习如何添加 Node.js worker 集群并利用计算机的全部处理能力。
扩展 Node.js 的两种集群策略
如果您希望在 Node.js 中使用集群,有两种方式可以实现这一点:
- 使用原生 Cluster 模块。
- 使用进程管理器,例如PM2 。
- 原生 Cluster
Node.js 自带一个内置的集群模块。它允许您手动将 worker 分叉到可用处理器上。这样,Node.js 将跨多个子进程分配传入请求以增强可伸缩性。
- 使用 PM2 进行集群处理
PM2 是一个生产级流程管理器 。像原生集群模块一样,PM2 具有内置的集群支持 。但是,它固有地还伴随其他与生产相关的元素,例如:
- 零停机部署。
- 应用程序监控。
- 日志和指标管理。
- 自动应用程序重启。
使用集群受益的 Node.js 应用程序示例
现在您了解了 Node.js 集群以及您可以使用的方法,让我们演示如何集群对于可伸缩性是如何有利的。您将创建两个简单示例,一个不使用集群,另一个使用集群,并使用想象的用户来模拟和扩展测试以比较性能。
没有集群的 Node.js:不可伸缩的原则
由于 Node.js 是单线程的,它必须在处理应用程序内的其他任务之前完成一个任务的执行。这个概念关乎 Node.js 单线程方面,它只使用一台计算机处理器。
要演示这个工作原理,您可以使用Express 创建一个简单的 Node.js 应用程序,如下所示:
const express = require("express");
const PORT = 3000;
const app = express();
// 模拟一个耗时任务
app.get("/compute", (req, res) => {
const startTime = Date.now();
// 模拟一个 10 秒的计算
const duration = 10000;
while (Date.now() - startTime < duration) {}
res.send("计算完成!");
});
// 提供一个简单的 HTTP 请求
app.get("/hello", (req, res) => {
res.send("来自 worker 进程的问候!");
});
app.listen(PORT, () => {
console.log(`应用程序正在端口 ${PORT} 上监听`);
});
上面的示例创建了两个 GET 请求:
- 一个简单的 GET 请求,发送一个基本请求
- 一个模拟的 GET 请求,执行 10 秒来返回用户请求
假设的模拟 10 秒任务现在代表任何您可以执行的耗时任务。根据 Node.js 的工作方式,如果发送一个请求来执行 localhost:3000/compute
,Node.js 将锁定到单个处理器上,并且无法执行其他任务。
这个任务需要 10 秒,并锁定您的计算资源,直到其计算完成:
这意味着如果您在 localhost:3000/compute
正在运行时执行 localhost:3000/hello
,localhost:3000/hello
将无法返回其响应,即使它只是一个简单的 GET 请求:
考虑到这种简单的方法,一个生产环境的 Node.js 应用程序一定会有许多耗时的函数。您不希望这样的场景锁定其他任务的执行。为了解决这个问题,Node.js 可以创建相同应用程序的分支,将它们复制到每一个可用的处理器,并创建一个并发执行的集群。
集群操作:一个扩展的 Node.js 应用程序
使用上面的非集群示例,这是如何将 Node.js 集群引入相同应用程序的方法:
const express = require("express");
const cluster = require("cluster");
const os = require('os');
const PORT = 3000;
- 检查当前进程是否运行为主进程。这样,Node.js 集群模块将能够获取可用 CPU 核心数量,并将其分叉为工作进程:
if (cluster.isMaster) {
// 获取可用 CPU 核心数量
const numCPUs = os.cpus().length;
// 为每个 CPU 核心分叉工作进程
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
// 监听工作进程退出并替换它们
cluster.on("exit", (worker, code, signal) => {
console.log(`工作进程 ${worker.process.pid} 已死亡。重新启动...`);
cluster.fork();
});
}
请注意,cluster
将分叉所有可用的处理器。如果某个处理器正在忙碌,Node.js 将分配任何可用核心来处理后续任务。如果先前的工作进程完成了其任务,那个工作进程将被终止,重启,并重新添加到集群中以便在有需要时分配其他任务。
- 一旦检查当前进程不是主进程,创建您的应用程序并在工作进程中运行它:
else {
const app = express();
// 当 worker 启动时记录日志
console.log(`工作进程 ${process.pid} 正在启动...`);
// 监听工作进程的 'exit' 事件
app.use((req, res, next) => {
res.on("finish", () => {
console.log(`工作进程 ${process.pid} 即将退出并重新启动...`);
});
next();
});
// 模拟一个耗时任务
app.get("/compute", (req, res) => {
console.log(`工作进程 ${process.pid} 正在处理 /compute 请求。`);
const startTime = Date.now();
// 模拟一个 10 秒的计算
const duration = 10000;
while (Date.now() - startTime < duration) {
}
res.send("计算完成!");
});
// 提供一个简单的 HTTP 请求
app.get("/hello", (req, res) => {
console.log(`工作进程 ${process.pid} 正在处理 /hello 请求。`);
res.send("来自 worker 进程的问候!");
});
app.listen(PORT, () => {
console.log(`工作进程 ${process.pid} 正在端口 ${PORT} 上监听`);
});
}
就这么简单。您的应用程序将在可用的工作进程之间分配工作负载。下面是这个应用程序在一台 4 核计算机上运行的示例。
现在,如果您在 localhost:3000/compute
正在运行时执行 localhost:3000/hello
,您应该可以正常发送您的请求,而不会被锁定在资源之外:
每个 worker 将分配一个唯一的 ID,这样 Node.js 就可以知道哪些 worker 需要被终止、重新启动和添加。
对两个示例进行负载测试:哪个扩展效果更好?
让我们现在深入研究并对有集群和没有集群的 Node.js 服务器进行负载测试。这将帮助您评估两者之间的最佳扩展方法。正如您可能已经了解的,集群利用多个 CPU 核心,使这个选项性能良好。我们将定义使用并发用户和请求速率的测试场景。这样可以模拟流量并监控性能参数,如响应时间和错误率。
此测试将使用一个从 0 到 10000 的简单素数计算器。创建一个名为 isPrime.js
的文件如下所示:
const isPrime = (num) => {
if (num <= 1) return false;
if (num <= 3) return true;
if (num % 2 === 0 || num % 3 === 0) return false;
for (let i = 5; i * i <= num; i += 6) {
if (num % i === 0 || num % (i + 2) === 0) return false;
}
return true;
};
module.exports = isPrime;
- 没有集群的 Node.js 服务器:
const express = require("express");
const port = 3001;
const process = require("process");
const isPrime = require("./isPrime");
const app = express();
console.log(`Worker ${process.pid} started`);
app.get("/", (req, res) => {
console.time("findPrimes");
const maxNumber = 10000;
const primes = [];
for (let num = 2; num <= maxNumber; num++) {
if (isPrime(num)) {
primes.push(num);
}
}
console.timeEnd("findPrimes");
console.log(`Found ${primes.length} prime numbers on process ${process.pid}`);
res.json({ primes });
});
app.listen(port, () => {
console.log(`App listening on port ${port}`);
});
- 具有集群的 Node.js 服务器:
const express = require("express");
const port = 3000;
const cluster = require("cluster");
const totalCPUs = require("os").cpus().length;
const process = require("process");
const isPrime = require("./isPrime");
if (cluster.isMaster) {
for (let i = 0; i < totalCPUs; i++) {
cluster.fork();
}
cluster.on("exit", (worker, code, signal) => {
cluster.fork();
});
} else {
const app = express();
app.get("/", (req, res) => {
console.time("findPrimes");
const maxNumber = 10000;
const primes = [];
for (let num = 2; num <= maxNumber; num++) {
if (isPrime(num)) {
primes.push(num);
}
}
console.log(
`Found ${primes.length} prime numbers on process ${process.pid}`,
);
res.json({ primes });
});
app.listen(port, () => {
console.log(`App listening on port ${port}`);
});
}
运行您的两个服务器。集群应用程序将暴露在端口 3000
上,另一个示例将在 3001
上运行。
要对它们进行测试,您可以使用几种基准测试工具,如Vegeta 、Apache JMeter 、load test 或autocannon 。本指南将使用 autocannon。运行以下命令进行安装:
npm i autocannon -g
一旦两个服务器都在运行,请使用以下命令使用 autocannon:
# 没有集群
npx autocannon -c 100 -a 10000 http://localhost:3001/
# 具有集群
npx autocannon -c 100 -a 10000 http://localhost:3000/
根据这些测试,每个服务器收到了来自 100 个用户的 10,000 个请求。具有集群的应用程序花费了 26 秒,而普通 Node.js 服务器花费了 56 秒。
它们都有相同的工作负载。然而,处理给定请求的延迟和时间也不同。这展示了集群如何帮助您的 Node.js 服务器利用资源并改善可扩展性。
集群化限制:何时不应使用集群扩展Node.js
现在,如果您收到的调用量超过了可用的CPU呢?这意味着如果您的应用程序收到的请求超过了工作进程的数量,可能会出现阻塞。每个工作进程一次只会处理一个请求。如果您有超出核心的耗时请求,那么会增加性能瓶颈。
如果您的应用程序必须应对这一挑战,以下是您需要做的事情:
- 在生产环境中运行应用程序时,请使用负载均衡器。负载均衡器可以将传入请求均匀分配和扩展到可用的工作进程/实例。这样,您的应用程序可以同时处理更多请求。
- 使用代理添加队列并对传入请求进行排队。工作进程随后将从队列中提取任务,并在任务可用时避免超负荷。
- Node.js具有可同时执行CPU绑定任务的工作线程,而不会阻塞主事件循环。线程将从主线程卸载CPU密集型任务,并保持主线程开放以处理传入请求。
- 利用Docker和Kubernetes等容器技术。它们允许您扩展实例以满足需求。有趣的是,您可以指导它们根据应用程序在任何给定时间处理的工作负载来自动扩展。
结论
通过集群化Node.js服务器可以将工作负载分布到多个子进程以扩展您的应用程序。这充分利用了您服务器的硬件资源。然而,集群并非总是必要的。简单的应用程序可能不会看到集群扩展的显著好处。然而,当处理多个请求时,一个需要更多资源的应用程序将充分利用集群的全部优势。您学到了以下内容:
- 为何需要集群化作为扩展Node.js应用程序的策略。
- 如何使用express创建和扩展Node.js Cluster,并使用原生和PM2模块。
- 集群化和非集群化Node.js应用程序之间的扩展比较。
- 使用负载测试来监视Node.js集群的性能。
- 如果Node.js集群不适合您的应用程序设计,您需要了解什么。