详解如何利用Nodejs构建多进程应用
作者:SaraiNoQ
前言
JavaScript 主线程运行在单个进程的单个线程上。这样做的好处是:
- 程序状态是单一的,在没有多线程的情况下没有锁、线程同步问题,
- 操作系统在调度时因为较少上下文的切换,可以很好地提高CPU的使用率。
但是单进程单线程并非完美的结构,一旦线程中某段代码发生异常阻塞,会阻塞代码执行而浪费资源。同时,如今CPU基本均是多核的,服务器往往还有多个CPU。
因此 Nodejs 不可避免的面临两个问题:如何充分利用多核CPU
****和 ****如何保证进程的健壮性和稳定性
。 另外,由于 Nodejs 中所有处理都在单线程上进行,影响事件驱动服务模型性能的点在于CPU的计算能力,而不受多进程或多线程模式中资源上限的影响,可伸缩性远比前两者高。如果解决掉多核CPU的利用问题,带来的性能上提升是可观的。
进程的创建和使用
多核利用率
面对单进程单线程对多核使用不足的问题,最简单的方法是启动多进程即可。理想状态下每个进程各自利用一个CPU,以此实现多核CPU的利用。Nodejs 提供了 child_process模块
,并且也提供了child_process.fork()
方法帮助我们实现进程的复制。
你可以通过这个方法在本地启动多个 HTTP 服务,首先编写一段创建 http服务端
的代码:
const http = require("http"); const server = http.createServer((req, res) => { res.writeHead(200, { "Content-Type": "text/plain" }); res.end("hello,world!"); }); // 随机监听1000-1999的任意一个端口 server.listen(Math.floor((1 + Math.random()) * 1000));
然后创建一个主进程 master.js
来启动和管理他们:
const fork = require("child_process").fork; const cpus = require("os").cpus(); console.log(cpus.length); for (let i = 0; i < cpus.length; i++) { fork('./server.js'); }
💡 在 Linux 系统中,你可以通过 ps aux | grep worker.js
来直接查看,在 Windows 中,你能通过 netstat -a
然后查看 1000-1999 端口的进程。
这样的模式称为 Master-Worker模式
,又称 主从模式
。如图,进程分为两种:主进程和工作进程。这是典型的分布式架构中用于并行处理业务的模式,具备较好的可伸缩性和稳定性:
- 主进程不负责具体的业务处理,而是负责调度或管理工作进程,它是趋向于稳定的。
- 工作进程负责具体的业务处理,趋于不稳定。
💡 fork() 能让我们复制进程使每个CPU内核都使用上,但是依然要切记 fork() 进程是昂贵的。因为Node通过事件驱动和异步IO的方式很大的缓解了并发问题,所以这里启动多个进程只是为了充分将CPU资源利用起来,而不是为了解决并发问题。
创建子进程
child_process模块
给予 Node 可以随意创建子进程(child_process)的能力。它提供了4个方法用于创建子进程:
spawn()
:启动一个子进程来执行命令。exec()
:启动一个子进程来执行命令。与spawn()不同的是其接口不同,它有一个回调函数获知子进程的状况。execFile()
:启动一个子进程来执行可执行文件。fork()
:与 spawn() 类似,不同点在于它创建 Node 的子进程只需指定要执行的 JavaScript 文件模块即可。
const cp = require("child_process"); cp.spawn("node", ["./server.js"]); cp.exec("node server.js", (err, stdout, stderr) => { // TODO }); cp.execFile("server.js", (err, stdout, stderr) => { // TODO }); cp.fork('./server.js');
💡
execFile()
只能用于可以直接执行的文件。在 Linux 中,你可以在文件开头加入#! /usr/bin/env node
进程间通信 IPC
在 Master-Worker模式
中,要实现主进程管理和调度工作进程的功能,需要主进程和工作进程之间的通信。子进程对象则由 send()
方法实现主进程向子进程发送数据,message事件
实现收听子进程发来的数据:
// parent.js const cp = require("child_process"); const n = cp.fork("./child.js"); n.on('message', (msg) => { console.log('parent got msg: ', msg); }); n.send({ s: 'hello, world', }); // child.js process.on('message', (msg) => { console.log('child got msg', msg); }); process.send({ b: 'bar', });
💡 HTML5提出了 WebWorker API 来创建工作线程,而主进程和工作进程之间通过 onmessage()
和 postMessage()
进行通信。具体参考MDN文档。
通过 fork()
或者其他API,创建子进程之后,为了实现父子进程之间的通信,父进程与子进程之间将会创建IPC通道(Inter-Process Communication,即进程间通信) 。通过IPC通道,父子进程之间才能通过message和send()传递消息。
父进程在实际创建子进程之前,会创建IPC通道并监听它,然后才真正创建出子进程,并通过环境变量(NODE_CHANNEL_FD)告诉子进程这个IPC通道的文件描述符。子进程在启动的过程中,根据文件描述符去连接这个已存在的IPC通道,从而完成父子进程之间的连接。
💡 只有启动的子进程是Node进程时,子进程才会根据环境变量去连接IPC通道。对于其他类型的子进程则无法实现进程间通信,除非其他进程也按约定去连接这个已经创建好的IPC通道
总结
今天我们讨论了 Nodejs 在多核利用方法的问题,然后介绍了创建子进程、进程间通信与IPC通道的内容。通过这些基础技术,用 child_process模块
在单机上搭建 Nodejs集群
是件相对容易的事情,让 Nodejs 能够在多核CPU的环境下充分利用计算资源。
以上就是详解如何利用Nodejs构建多进程应用的详细内容,更多关于Nodejs构建多进程应用的资料请关注脚本之家其它相关文章!