众所周知,Node 是单线程应用,所以无需考虑死锁等问题。然而,在方便我们书写代码的同时,也暴露出 Node 一个巨大的缺陷,即无法充分利用计算机资源。如果你机器牛逼,有几百个核,对不起,使用 Node 你最多也只能用到机器百分之一不到的性能。

使用 Node 的 clusterchild_process 模块,可以使得我们通过启动多个 Node 实例来达到充分利用 CPU 的目的。

先说下 child_process 模块,它允许我们通过子进程的方式调用系统命令。最常用的就是 execspawn 方法了。

exec 用起来比较简单,exec('ls -al', function (err, stdout, stderr) {}) 即可。而 spawn 则用起来会稍显复杂:

1
2
3
const cmd = spawn('ls', ['-al']);
cmd.on('stdout', function () {})
cmd.on('stderr', function () {})

当然,复杂也不一定是坏事,比如,exec 对于大量的输出就不行了,而 spawn 则可以像流一样慢慢处理。事实上,网上有一堆关于 exec 踩坑的记录。

再看 cluster 模块,它可以看作是对 child_process 的一个封装,为发挥多核的优势而专门提供的系统模块,无需开发者过多的处理负载等,即可实现多进程部署。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const cluster = require('cluster');
const http = require('http');
if (cluster.isMaster) {
for (var i = 0; i < 4; i++) {
cluster.fork();
}
cluster.on('exit', (worker, code, signal) => {
console.log(`worker ${worker.process.pid} died`);
});
} else {
http.createServer((req, res) => {
res.writeHead(200);
res.end('hello world\n');
}).listen(8000);
}

具体的原理可以参考 https://nodejs.org/api/cluster.html

对于旧版 Node,当一个请求过来时,master 进程会在网络层收到请求,并将该请求上下文原封不动的转发给其子进程,但具体转发给哪个,则是由系统分配。这种方式会有一个缺点,即所谓的「惊群效应」。何谓惊群效应呢?因为 cluster 使用系统的负责均衡模块进行分配,当一个请求过来的时候,所有的子进程都会被唤醒,并进行处理的抢夺。从这样来看,其实所谓的 master 也只是个傀儡。不过新版的 Node 已经提供了基于 Round Robin 算法实现其自主控制负载的方法了。

不过,一个更合理的方式是在上层使用 Nginx 进行负载分配,这样对子应用来说,完全无需关注其是否是多进程状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
http {
upstream cluster {
server 127.0.0.1:3000;
server 127.0.0.1:3001;
server 127.0.0.1:3002;
server 127.0.0.1:3003;
}
server {
listen 80;
server_name www.domain.com;
location / {
proxy_pass http://cluster;
}
}
}

此外,我们关心的热重启等,也是可以通过这些来做到。

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// worker.js
const http = require('http');
const pid = process.pid;
http.createServer(function(req, res) {
const tic = Date.now();
console.log('handling request from ' + pid);
setTimeout(function () {
res.end('Hello from ' + pid + '\n');
}, 10);
}).listen(8080, function() {
console.log('Started ' + pid);
});
// master.js
const cluster = require('cluster');
const os = require('os');
if (cluster.isMaster) {
const cpus = os.cpus().length;
for (var i = 0; i < cpus; i++) {
cluster.fork();
}
cluster.on('exit', (worker, code, signal) => {
// TODO: make sure about each code and signal meaning
console.log('worker %d exit with code %d, \tsignal: %s', worker.process.pid, code, signal);
if (!worker.exitedAfterDisconnect) {
console.log('worker %d died (%s). restarting...', worker.process.pid, signal || code);
cluster.fork();
}
});
process.on('SIGUSR2', function () {
console.log('upgrading workers');
const workers = Object.keys(cluster.workers);
function restartWorker(i) {
if (i >= workers.length) return;
const worker = cluster.workers[workers[i]];
console.log('Stopping worker: ' + worker.process.pid);
worker.disconnect();
worker.on('exit', function () {
if (!worker.exitedAfterDisconnect) return;
var newWorker = cluster.fork();
newWorker.on('listening', function() {
restartWorker(i + 1);
})
});
}
restartWorker(0);
});
} else {
require('./worker');
}