node-process
Node 进程初认识
我们都知道Node是基于V8的,而在V8中JavaScropt是单线程的。单线程带来的好处就是:程序是单一的,在没有多线程的情况下没有锁、线程同步问题,操作系统在调度时也因为较少上下文的切换,可以很好的提高CPU的使用率。
但是这种单进程单线程存在缺陷,就是我们无法利用服务器CPU多核的性能,一个Node进程只能利用一个CPU核。而且单线程模式下一旦代码抛出异常没有被捕获,将会引起整个进程的奔溃。
在Node v0.8版本之前,多进程架构必须通过的child_process来实现,要创建单机Node集群,由于有很多细节需要工程师自己处理。于是在v0.8时直接引入了cluster模块,用以解决多核CPU的利用率问题, 通过master-worker模式启用多个进程实例。下面我们了解下,Node如何使用多进程模型利用多核CPU,以及自带的cluster模块具体的工作原理。
如何创建子进程
Node提供了child_process模块,用来创建子进程。他提供了4个方法用于创建子进程。
spawn
1
2
3
4
5
6
7
8
9
10
11
12
13
14let cp = require('child_process');
let child = cp.spawn('ls', ['-lh']);
child.stderr.on('data', function (data) {
console.log('stderr', data);
});
child.stdout.on('data', function (data) {
console.log('stdout', data.toString());
});
// child.stdin.on('data', function (data) {
// console.log('stdin', data);
// });
child.on('close', function (code) {
console.log('child close', code);
});exec / execFile
exce最后调用的就是execFile,唯一的区别就是,exec通过normalizeExecArgs方法将option中的shell属性默认设置为true,源码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19function exec(command, options, callback) {
const opts = normalizeExecArgs(command, options, callback);
return module.exports.execFile(opts.file,
opts.options,
opts.callback);
}
function normalizeExecArgs(command, options, callback) {
if (typeof options === 'function') {
callback = options;
options = undefined;
}
// Make a shallow copy so we don't clobber the user's options object.
options = { ...options };
options.shell = typeof options.shell === 'string' ? options.shell : true;
return { file: command, options: options,callback: callback };
}在execFile中,最终调用的是spawn方法
1
2
3
4
5
6
7
8
9
10
11
12
13function execFile(file /* , args, options, callback */) {
// ...
const child = spawn(file, args, {
cwd: options.cwd,
env: options.env,
gid: options.gid,
uid: options.uid,
shell: options.shell,
windowsHide: !!options.windowsHide,
windowsVerbatimArguments: !!options.windowsVerbatimArguments
});
// ...
}exec会将spawn的输入输出流转换成String,默认使用UTF-8的编码,然后传递给回调函数
1
2
3
4
5let cp = require('child_process');
let child = cp.exec('ls -lh', function (error, stdout, stderr) {
console.log(`stdout: ${stdout}`);
stderr && console.log(`stderr: ${stderr}`);
});fork
fork也是通过spawn创建子进程的,是spawn的一个特例,专门用于衍生新的Node.js进程。1
2
3
4function fork(modulePath /* , args, options */) {
// ...
return spawn(options.execPath, args, options);
}通过fork创建子进程后,父子进程之间会建立一条IPC(Inter-Procoss Commounication,即进程间通信)通道,方便父子进程间通信。在JavaScript层,父子进程通过message和send传递消息,而在Node底层具体细节实现是由libuv提供,不同操作系统实现不同。在Windows下由命名管道(name pipe)实现,在*nix系统则采用Unix Domain Socket实现。
> 常见进程间通信方式:命名管道、匿名管道、socket、信号量、共享内存、消息队列、Domain Socket等下面通过一个实例来了解父子进程间是如何通信的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17// Parent.js
let cp = require('child_process');
let n = cp.fork(__dirname + '/Child.js');
n.on('message', function (msg) {
console.log('message from child', msg);
});
n.send({'Hello': 'World'});
// Child.js
process.on('message', function (msg) {
console.log('message from parent->', msg);
});
process.send({'Child': 'Test Send'});
// node Parent.js
// out->
// message from parent-> { Hello: 'World' }
// message from child { Child: 'Test Send' }父进程在创建子进程之前,会创建IPC通道并监听它,然后才真正创建出子进程,并通过环境变量(NODE_CHANNEL_ID)告诉子进程这个IPC通道的文件描述符。子进程在启动的过程中,根据文件描述符去连接这个已存在的IPC通道,从而完成父子进程之间的连接。
> **只有启动的子进程是Node进程时,子进程才会根据环境变量去连接IPC通道,对于其它类型的子进程则无法实现进程间通信, 除非其它进程也按约定去连接这个已经创建好的IPC通道**接下来,我们试通过fork实现Master-Worker模型:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// Master.js
let cp = require('child_process');
let os = require('os');
let cpus = os.cpus();
for (let i = 0; i < cpus.length; i++) {
cp.fork('./Worker.js');
}
// Child.js
let http = require('http');
http.createServer(function (req, res) {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('Hello World!/n');
}).listen(8000);上述代码,如果CPU多核的话。我们fork了多个子进程,这时只有一个工作进程监听到端口上,其余的进程在监听的过程中都抛出了异常(Error: listen EADDRINUSE),这是端口被占用的情况。要解决这个问题,通常的做法是让每个进程监听不同的端口,其中主进程监听主端口,主进程对外接收所有的网络请求,再将这些请求分别代理到不同的端口的进程上。
通过代理,可以避免端口不能重复监听的问题,甚至可以在代理过程上做适当的负载平衡。由于进程每接收到一个连接,将会用掉一个文件描述符。操作系统的文件描述符是有限的。为了解决这个问题,Node引入了进程间发送句柄的功能。
> 句柄是一种可以用来标记资源的引用,他的内部包含了指向对象的文件描述符。我们可以在Master中启动一个TCP,然后通过IPC服务将句柄发送给子进程,子进程再对服务的连接监听:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33// Master.js
let cp = require('child_process');
let os = require('os');
let cpus = os.cpus();
let net = require('net');
let server = net.createServer();
server.on('connection', function (socket) {
socket.end('handle by parent!\n');
});
server.listen(8000);
for (let i = 0; i < cpus.length; i++) {
let child = cp.fork('./Worker.js');
server.on('listening', function () {
child.send('server', server);
});
}
// Child.js
let http = require('http');
let server = http.createServer(function (req, res) {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('handled by child ,pid is ' + process.pid + '!\n');
});
process.on('message', function (m, tcp) {
console.log('msg from parent', m == 'server');
if ('server' == m)
tcp.on('connection', function (socket) {
server.emit('', socket);
});
});
Cluster 模块
Cluster是Node在后来引入用以解决多核CPU的利用率问题的模块,提供了较完善的API,用以处理进程的健壮性问题。有了Cluster模块,实现Node进程集群是很轻松的事情了,如下:
1 | let cluster = require('cluster'); |
cluster模块就是child_process和net模块的组合应用。cluster启动时,通过isMaster判断是否主进程,如果主进程就执行fork操作,子进程创建http服务。这里cluster.fork是执行的当前文件,最终调用的也是child_process.fork。因此它们可以使用IPC和父进程通信,从而使各进程交替处理连接服务。