正常情况下Node都是运行在单线程里的,通过异步IO和事件通知机制,可以处理密集型的IO,可是计算量大的任务还是会阻塞程序,所以Node也提供了进程相关的方法,让我们可以使用多进程。
比如我们有一个计算量大的任务,我们可以再启动一个进程,把这个计算量大的任务丢给新启动的进程去执行,执行完再通过进程间的消息传递,把结果告诉主进程就好了。
我们可以通过child_process
这个模块来实现多进程。
多进程架构
简单的例子
我们实现一个简单的例子:
我们在主进程里通过require('os').cpus()拿到我们的cpu信息,如果是多核cpu那我们就fork多个进程。
// master.js
const fork = require('child_process').fork;
const os = require('os');
const cpus = os.cpus();
for (let i = 0; i < cpus.length; i++) {
fork('./worker.js')
}
在工作进程里我们随机生成一个1000 ~ 2000的端口,创建http服务。
// worker.js
const http = require('http');
const port = parseInt((1 + Math.random()) * 1000);
http.createServer((req, res) => {
res.writeHead(200);
res.end('Hello');
}).listen(port, () => {
console.log('Listen at 127.0.0.1:' + port);
});
比如我的电脑是十核的,那么控制台会打印出如下信息:
Listen at 127.0.0.1:1174
Listen at 127.0.0.1:1345
Listen at 127.0.0.1:1332
Listen at 127.0.0.1:1199
Listen at 127.0.0.1:1103
Listen at 127.0.0.1:1642
Listen at 127.0.0.1:1839
Listen at 127.0.0.1:1571
Listen at 127.0.0.1:1111
Listen at 127.0.0.1:1742
每个端口号后满都有一个对应的进程在服务。
进程间通信
进程间需要进行通信来传递数据,可以通过send和message方法来进行数据的传送和接收,相关代码如下:
// master.js
const fork = require('child_process').fork;
const os = require('os');
const cpus = os.cpus();
const cp = fork('./worker.js');
cp.on('message', data => {
console.log('主进程接收来自子进程的数据:', data);
});
cp.send('这是来自主进程的数据');
//worker.js
process.on('message', data => {
console.log('子进程接收来自主进程的数据:', data);
});
process.send('这是来自子进程的数据');
执行node master.js
控制台会输出:
子进程接收来自主进程的数据: 这是来自主进程的数据
主进程接收来自子进程的数据: 这是来自子进程的数据
集群服务
如何让多个进程监听一个端口,实现集群服务:
// master.js
const net = require('net');
const fork = require('child_process').fork;
const server = net.createServer();
const child1 = fork('./worker.js');
const child2 = fork('./worker.js');
const child3 = fork('./worker.js');
server.listen(8111, () => {
child1.send('server', server);
child2.send('server', server);
child3.send('server', server);
console.log('Listen at 8111');
server.close();
});
主进程通过监听8111端口,然后将tcp socket的句柄通过send发送给子进程。
// worker.js
process.on('message', (m, server) => {
if (m === 'server') {
server.on('connection', socket => {
socket.end('connection in child, pid: ' + process.pid);
})
}
});
子进程通过message可以拿到句柄,然后通过句柄还原得到tcp socket套接字。子进程对服务是抢占式的,每个子进程都有机会处理我们发起的服务。
这时候我们发起访问会返回
curl "http://127.0.0.1:8111/"
connection in child, pid: 17585
curl "http://127.0.0.1:8111/"
connection in child, pid: 17584
curl "http://127.0.0.1:8111/"
connection in child, pid: 17583
curl "http://127.0.0.1:8111/"
connection in child, pid: 17585
curl "http://127.0.0.1:8111/"
connection in child, pid: 17583
这样就实现了一个单机集群。
上面实现的是tcp服务,如果是http服务,那么如何处理呢,只需要对子进程的代码做一点改造:
// worker.js
const http = require('http');
const server = http.createServer((req, res) => {
res.writeHead(200, 'okk');
res.end('handle by child, pid: ' + process.pid);
});
process.on('message', (m, tcp) => {
if (m === 'server') {
tcp.on('connection', socket => {
server.emit('connection', socket);
})
}
});
我们在子进程内部创建一个http服务,然后监听到tcp服务的connection事件的时候,触发http服务的connection事件,并且把对应的套接字传过去。这时候当我们访问http://127.0.0.1:8111/
就可以随机得到不同进程处理的结果。
集群稳定之路
进程可能会因为各种各样的原因退出,我们需要在进程挂掉的时候重启:
// master.js
const net = require('net');
const fork = require('child_process').fork;
const os = require('os');
const server = net.createServer();
const workers = {};
const createWorker = () => {
const worker = fork('./worker.js');
workers[worker.pid] = worker;
worker.send('server', server);
console.log('Create worker: ', worker.pid);
worker.on('exit', () => {
console.log('Worker exit: ', worker.pid);
delete workers[worker.pid];
createWorker();
});
};
server.listen(8111, () => {
for (let i = 0; i < os.cpus().length; i++) {
createWorker();
}
console.log('Listen at 8111');
});
// worker.js
const http = require('http');
const server = http.createServer((req, res) => {
res.writeHead(200, 'okk');
res.end('handle by child, pid: ' + process.pid);
});
let worker;
process.on('message', (m, tcp) => {
if (m === 'server') {
worker = tcp;
tcp.on('connection', socket => {
server.emit('connection', socket);
})
}
});
// 一些未捕捉到的错误
process.on('uncaughtException', () => {
// 停止接收新的连接
worker.close(() => {
// 连接都关闭后退出进程
process.exit(1);
});
});