如何通过集群扩大 Node.js 应用程序
- 原文链接
- 演示代码
介绍
当你在一个多核 CPU 的零碎上运行一个 node 程序,默认状况下会以单核的模式去创立一个过程。因为 Node.js 是以单线程的形式执行 javascript 代码,所以利用的所有申请都必须由单核上的线程去解决。如果应用程序有 CPU 密集型工作,操作系统必须安顿它们共享单个 CPU,直到实现。如果单个过程收到太多申请,可能会导致该过程不堪重负,从而导致性能降落。如果过程解体了,用户也不能持续拜访你的利用了。
Node.js 引入了 cluster 模块去解决这个问题,它会在同一台计算机上创立同一个应用程序的多个正本并让它们同时运行。同时它也应用了 round-robin 算法去实现负载平衡。如果一个实例解体了,剩下运行中的实例仍然能够为用户提供服务。得益于负载平衡,利用的性能也会显著进步。
在本教程汇总,你将会在一台领有四个或更多个 CPU 的机器上,应用 Node.js 的集群(cluster)模块来扩大一个应用程序。您将创立一个不应用集群的应用程序,而后将该利用改良到应用集群模块。你还将应用 pm2 模块来将应用程序扩大到多个 CPU。你将应用负载测试工具来比拟应用集群和不应用集群的应用程序的性能,以及评估 pm2 模块的体现。
筹备
要追随学习本教程,你须要:
-
大于或等于 4 核零碎
a. 如果您应用的是 Ubuntu 22.04 近程服务器,您能够依照咱们的初始服务器设置来设置您的零碎.
- 在您的开发环境中设置 Node.js(最好大于 16)。
- 对 express 的根本理解
步骤一:创立目录
在这一步中,你将创立我的项目的目录并下载应用程序所需的依赖。在第二步中,你将应用 Express 构建应用程序。而后在第三步中,你将应用内置的 node-cluster 模块将其扩大到多个 CPU,在第四步中会应用 loadtest 软件包进行压力测试。接着你将应用 pm2 软件包扩大以后利用,并在第五步中再次进行压力测试。
首先,创立一个目录。您能够将其命名为 cluster_demo 或您喜爱的任何目录名称,而后进入目录,接着对其初始化
# 创立目录
mkdir cluster_demo
# 进入目录
cd cluster_demo
# 初始化
npm init -y
-y 选项通知 NPM 承受所有默认选项。
{
"name": "cluster_demo",
"version": "1.0.0",
"description": "","main":"index.js","scripts": {"test":"echo \"Error: no test specified\" && exit 1"},"keywords": [],"author":"",
"license": "ISC"
}
这些属性须要与我的项目保持一致:
- name:npm 包的名称。
- version:你的包的版本号。
- main:我的项目的入口点。
在 package.json 文件中,增加启用对 ES 模块的反对:
{
...
"author": "","license":"ISC","type":"module"
}
接着,下载一些依赖包
- express:一个用于在 Node.js 中构建 Web 应用程序的框架。
- loadtest:一种负载测试工具,可用于生成应用程序的流量以测量其性能。
- pm2:一种主动将应用程序扩大到多个 CPU 的工具。
npm install express
npm install -g loadtest pm2
步骤二:创立不应用集群的利用
在这一步中,您将创立一个蕴含单个路由的示例程序,该路由将在每个用户拜访时启动 CPU 密集型工作。该程序不会应用集群模块,因而您能够拜访在一个 CPU 上运行应用程序的单个实例的性能影响。在本教程的前面,您将比拟这种办法与集群模块的性能。
首先,创立 index.js
文件
import express from "express";
const port = 3000;
const app = express();
console.log(`worker pid=${process.pid}`);
在第一行,导入 express 包。在第二行中,将端口变量设置为端口 3000,应用程序的服务器将侦听该端口。接下来,将 app 变量设置为 Express 的实例。之后,您能够应用内置的过程模块在控制台中记录应用程序过程的过程 ID。
而后,新增一个路由,它是一个 CPU 密集的循环操作
...
app.get("/heavy", (req, res) => {
let total = 0;
for (let i = 0; i < 5_000_000; i++) {total++;}
res.send(`The result of the CPU intensive task is ${total}\n`);
});
在 / heavy 门路中,你定义了一个循环,将总变量减少了 500 万次。而后应用 res. send ()办法发送蕴含总变量中的值的响应。尽管 CPU 受限工作的示例是任意的,但它演示了 CPU 受限工作,而没有减少复杂性。你也能够为路由应用其余名称,但本教程应用 / heavy 示意沉重的性能工作。
接下来,调用 Express 模块的 Listen() 办法,让服务器监听存储在 port 变量中的端口 3000:
...
app.listen(port, () => {console.log(`App listening on port ${port}`);
});
index.js
残缺代码如下:
import express from "express";
const port = 3000;
const app = express();
console.log(`worker pid=${process.pid}`);
app.get("/heavy", (req, res) => {
let total = 0;
for (let i = 0; i < 5_000_000; i++) {total++;}
res.send(`The result of the CPU intensive task is ${total}\n`);
});
app.listen(port, () => {console.log(`App listening on port ${port}`);
});
启动服务
node index.js
输入显示正在运行的过程的过程 ID 以及确认服务器正在侦听端口 3000 的音讯。要测试应用程序是否失常工作,请关上另一个终端并运行以下命令:
worker pid=11023
App listening on port 3000
能够在浏览器中间接关上http://localhost:3000/heavy
也能够在终端中执行 curlcurl http://localhost:3000/heavy
当你应用 node 命令运行 index. js 文件时,操作系统(OS)会创立一个过程。过程是操作系统为运行程序所做的形象。操作系统为程序分配内存,并在过程列表中创立一个对应运行程序的过程 ID。
程序二进制文件会被定位并加载到为过程调配的内存中。从那里开始执行。运行时,它对系统中的其余过程没有任何感知,并且在该过程中产生的任何事件都不会影响其余过程。
因为您在具备多个 CPU 的服务器上运行 Node.js 应用程序只有一个过程,因而它将接管并解决所有传入申请。在此图中,所有传入申请都被定向到在单个 CPU 上运行的过程,而其余 CPU 放弃闲暇:
步骤三:创立集群利用
在这一步中,你将增加集群(cluster)模块,以创立同一程序的多个实例,以解决更多的负载并进步性能。当你应用集群模块运行过程时,你能够在你的机器上的每个 CPU 上运行多个过程:
在这个图示中,申请通过主过程中的负载均衡器,而后应用循环轮询算法将申请散发到各个过程之间。
当初来创立 primary.js
文件
import cluster from "cluster";
import os from "os";
import {dirname} from "path";
import {fileURLToPath} from "url";
const __dirname = dirname(fileURLToPath(import.meta.url));
在前两行中,导入了集群(cluster)和操作系统(os)模块。在接下来的两行中,导入了 dirname 和 fileURLToPath,它们用于将 __dirname 变量的值设置为执行 index.js 文件所在目录的绝对路径。因为当应用 ES 模块时,__dirname 并未定义,它默认只在 CommonJS 模块中定义。
接下来,增加以下代码来援用 index.js 文件:
const cpuCount = os.cpus().length;
console.log(`The total number of CPUs is ${cpuCount}`);
console.log(`Primary pid=${process.pid}`);
cluster.setupPrimary({exec: __dirname + "/index.js",});
首先,咱们将 cpuCount 变量设置为你的计算机上的 CPU 数量,应该是四个或更多。接下来,在控制台中打印了 CPU 数量和主过程的过程 ID,这个主过程将接管所有的申请,并应用负载均衡器将它们分发给工作过程。
随后,你应用集群(cluster)模块的 setupPrimary() 办法援用了 index.js 文件,以便在每个工作过程中执行。
而后,增加以下代码来创立过程:
...
for (let i = 0; i < cpuCount; i++) {cluster.fork();
}
cluster.on("exit", (worker, code, signal) => {console.log(`worker ${worker.process.pid} has been killed`);
console.log("Starting another worker");
cluster.fork();});
以上代码会循环迭代 cpu 的数量,并在每次迭代中调用集群(cluster)模块的 fork() 办法。同时应用集群模块的 on() 办法附加了 exit 事件,以便监听过程何时收回 exit 事件,通常是当过程终止时。当触发 exit 事件时,你会记录已终止的工作过程的过程 ID,而后调用 fork() 办法创立一个新的工作过程,以替换已终止的过程。
残缺代码如下:
import cluster from "cluster";
import os from "os";
import {dirname} from "path";
import {fileURLToPath} from "url";
const __dirname = dirname(fileURLToPath(import.meta.url));
const cpuCount = os.cpus().length;
console.log(`The total number of CPUs is ${cpuCount}`);
console.log(`Primary pid=${process.pid}`);
cluster.setupPrimary({exec: __dirname + "/index.js",});
for (let i = 0; i < cpuCount; i++) {cluster.fork();
}
cluster.on("exit", (worker, code, signal) => {console.log(`worker ${worker.process.pid} has been killed`);
console.log("Starting another worker");
cluster.fork();});
当初能够 node primary.js
启动服务,察看输入. 能够看到服务以集群的形式启动了。
步骤四:应用测试工具比照性能
在这一步中,你将应用 loadtest 软件包对你构建的两个程序生成流量。你将比拟应用集群(cluster)模块的 primary.js 程序与不应用集群的 index.js 程序的性能。你会留神到应用集群模块的程序在特定工夫内执行得更快,能够解决更多的申请,而不应用集群的程序则不如此。
对单核的测试
首先,先启动单核服务node index.js
接下来,在终端中运行测试命令(在步骤一中曾经下载了全局 loadtest)
loadtest -n 1200 -c 200 -k http://localhost:3000/heavy
# -n 1200:这个参数指定了要发送的申请数量,即压测将模仿发送 1200 个申请到指定的 URL。#-c 200:这个参数指定了并发连接数,即同时发送的申请数量。在这个命令中,将同时发送 200 个申请。#-k:这是一个选项,示意应用 HTTP Keep-Alive 连贯。Keep-Alive 容许单个连贯复用,而不用为每个申请建设新的连贯。
能够看到测试报告:
Max requests: 1200:设置的最大申请数。
Concurrency level: 200:并发连接数,即同时发送的申请数量。
Running on cores: 4:示意测试是在一个领有 4 个 CPU 外围的机器上运行的。
Agent: keepalive:测试时应用的代理(Agent),这里是 HTTP Keep-Alive 连贯。
Completed requests: 1200:实现的申请数量,与设置的最大申请数统一。
Total errors: 0:总共的谬误申请数量,这里是 0,示意没有谬误的申请。
Total time: 2.486 s:总共破费的工夫,单位是秒。
Mean latency: 1100.3 ms:均匀提早,即申请从发送到接管的均匀工夫,单位是毫秒。
Effective rps: 483:每秒的无效申请数,即胜利实现的申请数。
Percentage of the requests served within a certain time:显示在特定工夫内实现的申请的百分比和相应的工夫。
50% 1240 ms:50% 的申请在 1240 毫秒内实现。
90% 1596 ms:90% 的申请在 1596 毫秒内实现。
95% 1607 ms:95% 的申请在 1607 毫秒内实现。
99% 1680 ms:99% 的申请在 1680 毫秒内实现。
100% 1723 ms (longest request):所有申请中,最长的申请破费了 1723 毫秒。
对多核的测试
先启动单核服务node index.js
,同样执行
loadtest -n 1200 -c 200 -k http://localhost:3000/heavy
能够将测试报告的后果数据和单核模式下比照。
这个响应证实了扩大曾经失效,你的应用程序能够在短时间内解决更多的申请而无需提早。如果你将计算机降级为更多的 CPU,该应用程序将主动扩大到相应数量的 CPU,并进一步提高性能。
须要揭示的是,因为你的网络和处理器速度的不同,终端输入中的指标会有所不同。总工夫和均匀提早会显著降落,总工夫会迅速减少。
在下一步中,咱们将应用 pm2 代替集群模块。
步骤五:应用 pm2
应用 pm2 启动
到目前为止,你曾经应用集群(cluster)模块依据你计算机上的 CPU 数量创立了工作过程。你还增加了在工作过程终止时重新启动它的能力。在这一步中,你将设置一个代替计划,通过应用建设在集群模块之上的 pm2 过程管理器来主动扩大你的应用程序。这个过程管理器蕴含一个负载均衡器,并能够主动创立与你计算机上的 CPU 数量雷同的工作过程。它还容许你监控这些过程,并且如果有一个过程终止,它能够主动产生一个新的工作过程。
在终端中,应用以下命令启动 pm2 集群:
pm2 start index.js -i 0
-i 选项承受你想要 pm2 创立的工作过程数量。如果你传递参数 0,pm2 将会主动创立与你计算机上的 CPU 数量雷同的工作过程。
启动后能够看到相似表格
表格蕴含了每个工作过程的过程 ID、状态、CPU 利用率和内存耗费,你能够用它来理解过程的行为。
当应用 pm2 启动集群时,该软件包会在后盾运行,并且甚至在重新启动零碎后会主动重新启动。
如果你想要从工作过程中读取日志,你能够应用以下命令:
pm2 logs
如果你想要查看过程的状态,你能够应用以下命令:pm2 ls
应用 pm2 启动服务之后,你能够再尝试下运行测试命令,去查看服务的性能。
应用配置文件
为了改良你应用 pm2 的工作流程,你能够生成一个配置文件,以传递应用程序的配置设置。这种办法将容许你在启动或重新启动集群时无需传递选项。
为了应用配置文件,删除以后的集群:
pm2 delete index.js
接下来,生成配置文件:
pm2 ecosystem
能够看到 ecosystem.config.js
文件在当前目录被生成。
须要留神的是,须要批改这个文件的后缀名来启动对 ES 模块的反对。
// ecosystem.config.cjs
module.exports = {
apps : [{
script: 'index.js',
watch: '.',
name: "cluster_app",
instances: 0,
exec_mode: "cluster",
}],
deploy : {
production : {
user : 'SSH_USERNAME',
host : 'SSH_HOSTMACHINE',
ref : 'origin/master',
repo : 'GIT_REPOSITORY',
path : 'DESTINATION_PATH',
'pre-deploy-local': '','post-deploy':'npm install && pm2 reload ecosystem.config.cjs --env production','pre-setup':''
}
}
};
启动命令:pm2 start ecosystem.config.cjs
其余指令:
Command | Description |
---|---|
pm2 start app_name | 启动 |
pm2 restart app_name | 先删除再启动 |
pm2 reload app_name | 重启集群 |
pm2 stop app_name | 进行集群 |
pm2 delete app_name | 删除集群 |
ok, 您当初能够应用 pm2 模块和 cluster 模块扩大您的应用程序了。
总结
在本教程中,你应用了集群(cluster)模块来扩大你的应用程序。首先,你创立了一个不应用集群模块的程序。而后,你创立了一个应用集群模块的程序,将应用程序扩大到你机器上的多个 CPU。随后,你比照了应用集群模块和不应用集群模块的应用程序的性能。最初,你应用了 pm2 软件包作为集群模块的代替计划,将应用程序扩大到多个 CPU 上。
要进一步学习,你能够拜访集群模块的文档页面,理解更多对于该模块的信息。
Node.js 还附带了 worker_threads 模块,容许你将 CPU 密集型任务分配给工作线程,以便它们能够更快地实现。你能够尝试咱们对于如何在 Node.js 中应用多线程的教程。你还能够通过专门的 Web Workers 在前端优化 CPU 绑定的工作,你能够通过遵循如何应用 Web Workers 解决 CPU 绑定工作的教程来实现。如果你想学习如何防止 CPU 绑定工作影响应用程序的申请 / 响应循环,请查阅如何应用 Node.js 和 BullMQ 解决异步工作的教程。