Node进程

82 阅读3分钟

正常情况下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);
    });
});