Nodejs构建Cluster集群多线程Workerthreads
前言
前两天我们介绍了使用 Nodejs 中的 child_process
模块创建多个子进程,同时利用进程间通信的API构建了一个集群式的Web服务器。实际上,你可以通过 cluster
模块更方便的完成这一操作。
但是,cluster
创建的进程之间无法共享内存,通信必须使用 JSON 格式,有一定的局限性和性能问题。如果你不想要进程隔离,可以使用 worker_thread
模块,它允许在一个 Node.js 实例中运行多个应用程序线程。相比创建多个进程更轻量,并且可以共享内存。
进程间通过传输 ArrayBuffer 实例或共享 SharedArrayBuffer 实例来做到这一点,对数据格式没有太多要求。但是要注意,数据中不能包含函数。
Cluster 多进程
我们可以使用 cluster
模块提供的API重构昨天的案例:
// master.js
const cl = require("cluster");
const cpus = require("os").cpus().length;
// 修改默认的 fork() 方法配置
cl.setupPrimary({
exec: 'worker.js'
});
for(let i = 0; i < cpus; i++) {
cl.fork();
};
cl.on('listening', (data) => {
console.log(`listenning on: ${data.id}--${data.process.pid}`);
});
cl.on('exit', (data, code, signal) => {
console.log(`exited: ${data.id}--${data.process.pid}, kill code: $[code], signal: ${signal}`);
cl.fork();
});
子进程依旧使用昨天的代码:
const http = require("http");
const server = http.createServer((req, res) => {
res.writeHead(200, {
"Content-Type": "text/plain"
});
res.end("Hello,World!" + process.pid);
// 抛出异常,捕获后终止进程
throw new Error('throw exception');
}).listen(1337);
// 捕获异常后终止进程
process.on('uncaughtException', (err) => {
// 停止接收新的连接
server.close((data) => {
console.log(`worker: ${process.pid} is stopping!`);
process.exit(1);
})
// 避免长连接请求长时间无法终止,5s后自动终止
setTimeout(() => {
process.exit(1);
}, 5000)
});
执行 node master.js
,会得到与昨天利用 child_process
模块创建子进程集群相同的效果。
同样,你可以使用官方推荐的写法,利用 cluster.isPrimary 和 cluster.isWorker 来判断当前进程是否为主进程:
const cluster = require('node:cluster');
const http = require('node:http');
const numCPUs = require('node:os').cpus().length;
const process = require('node:process');
if (cluster.isPrimary) {
console.log(`Primary ${process.pid} is running`);
// Fork workers.
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
cluster.on('exit', (worker, code, signal) => {
console.log(`worker ${worker.process.pid} died`);
});
} else {
// Workers can share any TCP connection
// In this case it is an HTTP server
http.createServer((req, res) => {
res.writeHead(200);
res.end('hello world\n');
}).listen(1337);
console.log(`Worker ${process.pid} started`);
};
实现原理
事实上,cluster
模块就是将 child_process
和 net
模块的API组合起来实现的。cluster启动时,进程会在内部启动TCP服务器。而在调用 cluster.fork()
复制子进程时,会将这个TCP服务器端 Socket 的句柄发送给工作进程。如果进程是通过 cluster.fork()
复制出来的,那么它的环境变量里就存在 NODE_UNIQUE_ID
。如果工作进程中存在 listen()
侦听网络端口的调用,它将拿到该句柄,再通过 SO_REUSEADDR
端口重用,从而实现多个子进程共享端口。对于正常方式启动的进程,则不存在句柄共享和传递等过程。
在 cluster
内部隐式创建TCP服务器的方式对使用者是透明的,你不需要自己手动去实现句柄的传递,但也正是因此,它无法像使用 child_process
那样灵活。在 child_process
中你可以自行控制句柄的传送,因此可以灵活地控制工作进程,甚至控制多组工作进程。
cluster事件
- Event:
disconnect
主进程和工作进程之间IPC通道断开后会触发该事件。 - Event:
exit
有工作进程退出时触发该事件。 - Event:
fork
复制一个工作进程后触发该事件。 - Event:
listening
工作进程中调用listen()
后,发送该消息给主进程,主进程收到后,触发该事件。 - Event:
message
- Event:
online
fork好一个工作进程后,工作进程主动发送该消息给主进程,主进程收到消息后,触发该事件。 - Event:
setup
.setupPrimary()
方法执行后触发
? 这些事件大多跟 child_process
模块的事件相关,在进程间消息传递的基础上完成的封装。
使用 Node 构建集群能够充分利用多核CPU的计算性能,而 child_process
模块的进程间通信和多种事件能够极大提升Node的稳定性。但进程间无法共享资源,进程间通信有局限性和性能问题。此时就需要引入更轻量级的线程了。
Worker threads多线程
V8 多线程模型
众所周知,JavaScript 在运行时是单线程的。但 JavaScript 的 Runtime V8 引擎却不是单线程的。大致包括以下几个线程:
- JavaScript 主线程:编译、执行代码。
- 编译线程:当主线程在执行时,编译线程可以优化代码。
- Profiler 线程:记录方法耗时的线程。
- 其它线程:比如支持并行 GC 的多线程。
- libuv线程池,默认四个线程,全局共享,可以将异步操作和计算密集任务交给它执行。
对于 Node 来说,crypto
这种 CPU 密集 和 fs
这种 I/O 密集的任务是在 libuv线程池
中进行的。其执行模型是单独创建一个进程,在这个进程中同步执行任务,然后将结果返回到 Event Loop 中,Event Loop 可以通过回调函数获取并使用结果。
const fs = require("fs");
fs.writeFile('./target.txt', 'hello Node.js', (err) => {
if (err) throw err;
console.log('文件已被保存');
});
使用非阻塞方法,长耗时的方法不会阻塞主进程之后的代码,只需告诉 Worker Pool
去执行该命令,并将结果返回给预先设置好的回调函数,在计算完成时触发即可。
? 由于 Worker Pool 运行在 libuv线程池
中,主线程的 Event Loop 不会被阻塞。能够充分利用 CPU 资源。
多线程支持
Node v10.5.0 提供了 Worker threads
模块,开始支持多线程编程。在创建出的每个工作线程中,都会包含 V8 和 libuv,即都包含Event Loop:
你可以通过下面这段简单的代码来体验一下:
// main.js
const { Worker, isMainThread } = require('worker_threads');
if (isMainThread) {
console.log("I'm main thread: ", isMainThread);
// create subThread
new Worker(__filename);
}
else {
console.log("I'm not main thread: ", isMainThread);
// subThread destroy
}
我们在主线程中调用new方法创建了一个子线程,子线程执行完自动销毁。最后执行结果如下:
? 合理使用子线程,你能充分调用和分配资源。对于有计算密集型需求的应用,这是一个重要的优化手段。另外,由于频繁地创建、销毁一个线程的开销很大,你可以创建线程池来解决这个问题。
总结
通过构建集群,你能够充分调用CPU资源,赋予Node更强劲的性能。而利用多线程模型,将长耗时的任务交由子线程来处理,你能合理分配程序运行资源。
目前为止,我们介绍完了 Node 的网络、IO、进程模块,还剩下异步编程和Event Loop
两个重点。另外,今天在看 Node 文档时发现 Node v19 刚刚发布了,v18 即将成为稳定版
以上就是Nodejs 构建Cluster集群多线程Worker threads的详细内容,更多关于Nodejs 构建Cluster多线程的资料请关注编程网其它相关文章!
免责声明:
① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。
② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341