nodejs中的子进程,深入解析child_process模块和cluster模块
Node.js的进程管理
node遵循的是单线程单进程的模式,node的单线程是指js的引擎只有一个实例,且在nodejs的主线程中执行,同时node以事件驱动的方式处理IO等异步操作。node的单线程模式,只维持一个主线程,大大减少了线程间切换的开销。
但是node的单线程使得在主线程不能进行CPU密集型操作,否则会阻塞主线程。对于CPU密集型操作,在node中通过child_process可以创建独立的子进程,父子进程通过IPC通信,子进程可以是外部应用也可以是node子程序,子进程执行后可以将结果返回给父进程。
此外,node的单线程,以单一进程运行,因此无法利用多核CPU以及其他资源,为了调度多核CPU等资源,node还提供了cluster模块,利用多核CPU的资源,使得可以通过一串node子进程去处理负载任务,同时保证一定的负载均衡型。本文从node的单线程单进程的理解触发,介绍了child_process模块和cluster模块,本文的结构安排如下:
- node中的单线程和单进程
- node中的child_process模块实现多进程
- node中的cluster模块
- 总结
原文的地址,在我的博客中:https://github.com/forthealll...
如有帮助,您的star是对我最好的鼓励~
一、node中的单线程和单进程
首先要理解的概念是,node的单线程和单进程的模式。node的单线程于其他语言的多线程模式相比,减小了线程间切换的开销,以及在写node代码的时候不用考虑锁以及线程池的问题。node宣称的单线程模式,比其他语言更加适合IO密集型操作。那么一个经典的问题是:
node是真的单线程的吗?
提到node,我们就可以立刻想到单线程、异步IO、事件驱动等字眼。首先要明确的是node真的是单线程的吗,如果是单线程的,那么异步IO,以及定时事件(setTimeout、setInterval等)又是在哪里被执行的。
严格来说,node并不是单线程的。node中存在着多种线程,包括:
- js引擎执行的线程
- 定时器线程(setTimeout, setInterval)
- 异步http线程(ajax)
....
我们平时所说的单线程是指node中只有一个js引擎在主线程上运行。其他异步IO和事件驱动相关的线程通过libuv来实现内部的线程池和线程调度。libv中存在了一个Event Loop,通过Event Loop来切换实现类似于多线程的效果。简单的来讲Event Loop就是维持一个执行栈和一个事件队列,当前执行栈中的如果发现异步IO以及定时器等函数,就会把这些异步回调函数放入到事件队列中。当前执行栈执行完成后,从事件队列中,按照一定的顺序执行事件队列中的异步回调函数。
上图中从执行栈,到事件队列,最后事件队列中按照一定的顺序执行回调函数,整个过程就是一个简化版的Event Loop。此外回调函数执行时,同样会生成一个执行栈,在回调函数里面还有可能嵌套异步的函数,也就是说执行栈存在着嵌套。
也就是说node中的单线程是指js引擎只在唯一的主线程上运行,其他的异步操作,也是有独立的线程去执行,通过libv的Event Loop实现了类似于多线程的上下文切换以及线程池调度。线程是最小的进程,因此node也是单进程的。这样就解释了为什么node是单线程和单进程的。
二、node中的child_process模块实现多进程
node是单进程的,必然存在一个问题,就是无法充分利用cpu等资源。node提供了child_process模块来实现子进程,从而实现一个广义上的多进程的模式。通过child_process模块,可以实现1个主进程,多个子进程的模式,主进程称为master进程,子进程又称工作进程。在子进程中不仅可以调用其他node程序,也可以执行非node程序以及shell命令等等,执行完子进程后,以流或者回调的形式返回。
1、child_process模块提供的API
child_process提供了4个方法,用于新建子进程,这4个方法分别为spawn、execFile、exec和fork。所有的方法都是异步的,可以用一张图来描述这4个方法的区别。
上图可以展示出这4个方法的区别,我们也可以简要介绍这4中方法的不同。
- spawn : 子进程中执行的是非node程序,提供一组参数后,执行的结果以流的形式返回。
- execFile:子进程中执行的是非node程序,提供一组参数后,执行的结果以回调的形式返回。
-
exec:子进程执行的是非node程序,传入一串shell命令,执行后结果以回调的形式返回,与execFile
不同的是exec可以直接执行一串shell命令。 - fork:子进程执行的是node程序,提供一组参数后,执行的结果以流的形式返回,与spawn不同,fork生成的子进程只能执行node应用。接下来的小节将具体的介绍这一些方法。
2、execFile和exec
我们首先比较execFile和exec的区别,这两个方法的相同点:
执行的是非node应用,且执行后的结果以回调函数的形式返回。
不同点是:
exec是直接执行的一段shell命令,而execFile是执行的一个应用
举例来说,echo是UNIX系统的一个自带命令,我们直接可以在命令行执行:
echo hello world
结果,在命令行中会打印出hello world.
(1) 通过exec来实现
新建一个main.js文件中,如果要使用exec方法,那么则在该文件中写入:
let cp=require('child_process');
cp.exec('echo hello world',function(err,stdout){
console.log(stdout);
});
执行这个main.js,结果会输出hello world。我们发现exec的第一个参数,跟shell命令完全相似。
(2)通过execFile来实现
let cp=require('child_process');
cp.execFile('echo',['hello','world'],function(err,stdout){
console.log(stdout);
});
execFile类似于执行了名为echo的应用,然后传入参数。execFlie会在process.env.PATH的路径中依次寻找是否有名为'echo'的应用,找到后就会执行。默认的process.env.PATH路径中包含了'usr/local/bin',而这个'usr/local/bin'目录中就存在了这个名为'echo'的程序,传入hello和world两个参数,执行后返回。
(3)安全性分析
像exec那样,可以直接执行一段shell是极为不安全的,比如有这么一段shell:
echo hello world;rm -rf
通过exec是可以直接执行的,rm -rf会删除当前目录下的文件。exec正如命令行一样,执行的等级很高,执行后会出现安全性的问题,而execFile不同:
execFile('echo',['hello','world',';rm -rf'])
在传入参数的同时,会检测传入实参执行的安全性,如果存在安全性问题,会抛出异常。除了execFile外,spawn和fork也都不能直接执行shell,因此安全性较高。
3、spawn
spawn同样是用于执行非node应用,且不能直接执行shell,与execFile相比,spawn执行应用后的结果并不是执行完成后,一次性的输出的,而是以流的形式输出。对于大批量的数据输出,通过流的形式可以介绍内存的使用。
我们用一个文件的排序和去重来举例:
上述图片示意图中,首先读取的input.txt文件中有acba未经排序的文字,通过sort程序后可以实现排序功能,输出为aabc,最后通过uniq程序可以去重,得到abc。我们可以用spawn流形式的输入输出来实现上述功能:
let cp=require('child_process');
let cat=cp.spawn('cat',['input.txt']);
let sort=cp.spawn('sort');
let uniq=cp.spawn('uniq');
cat.stdout.pipe(sort.stdin);
sort.stdout.pipe(uniq.stdin);
uniq.stdout.pipe(process.stdout);
console.log(process.stdout);
执行后,最后的结果将输入到process.stdout中。如果input.txt这个文件较大,那么以流的形式输入输出可以明显减小内存的占用,通过设置缓冲区的形式,减小内存占用的同时也可以提高输入输出的效率。
4、fork
在javascript中,在处理大量计算的任务方面,HTML里面通过web work来实现,使得任务脱离了主线程。在node中使用了一种内置于父进程和子进程之间的通信来处理该问题,降低了大数据运行的压力。node中提供了fork方法,通过fork方法在单独的进程中执行node程序,并且通过父子间的通信,子进程接受父进程的信息,并将执行后的结果返回给父进程。
使用fork方法,可以在父进程和子进程之间开放一个IPC通道,使得不同的node进程间可以进行消息通信。
在子进程中:
通过process.on('message')和process.send()的机制来接收和发送消息。
在父进程中:
通过child.on('message')和process.send()的机制来接收和发送消息。
具体例子,在child.js中:
process.on('message',function(msg){
process.send(msg)
})
在parent.js中:
let cp=require('child_process');
let child=cp.fork('./child');
child.on('message',function(msg){
console.log('got a message is',msg);
});
child.send('hello world');
执行parent.js会在命令行输出:got a message is hello world
中断父子间通信的方式,可以通过在父进程中调用:
child.disconnect()
来实现断开父子间IPC通信。
5、同步执行的子进程
exec、execFile、spawn和fork执行的子进程都是默认异步的,子进程的运行不会阻塞主进程。除此之外,child_process模块同样也提供了execFileSync、spawnSync和execSync来实现同步的方式执行子进程。
三、node中的cluster模块
cluster意为集成,集成了两个方面,第一个方面就是集成了child_process.fork方法创建node子进程的方式,第二个方面就是集成了根据多核CPU创建子进程后,自动控制负载均衡的方式。
我们从官网的例子来看:
const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;
if (cluster.isMaster) {
console.log(`主进程 ${process.pid} 正在运行`);
// 衍生工作进程。
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
cluster.on('exit', (worker, code, signal) => {
console.log(`工作进程 ${worker.process.pid} 已退出`);
});
} else {
// 工作进程可以共享任何 TCP 连接。
// 在本例子中,共享的是一个 HTTP 服务器。
http.createServer((req, res) => {
res.writeHead(200);
res.end('你好世界\n');
}).listen(8000);
console.log(`工作进程 ${process.pid} 已启动`);
}
最后输出的结果为:
$ node server.js
主进程 3596 正在运行
工作进程 4324 已启动
工作进程 4520 已启动
工作进程 6056 已启动
工作进程 5644 已启动
我们将master称为主进程,而worker进程称为工作进程,利用cluster模块,使用node封装好的API、IPC通道和调度机可以非常简单的创建包括一个master进程下HTTP代理服务器 + 多个worker进程多个HTTP应用服务器的架构。
总结
本文首先介绍了node的单线程和单进程模式,接着从单线程的缺陷触发,介绍了node中如何实现子进程的方法,对比了child_process模块中几种不同的子进程生成方案,最后简单介绍了内置的可以实现子进程以及CPU进程负载均衡的内置集成模块cluster。