我们知道nodejs中实现了cluster模块,实现了服务器的多进程架构下,多个进程可以共同处理请求的能力。本文介绍如何实现一个cluster模块。
下面我们来看一下实现。
parent.js
const childProcess = require('child_process');
const net = require('net');
const workers = [];
const workerNum = 10;
let index = 0;
// 创建多个worker进程
for (let i = 0; i < workerNum; i++) {
workers.push(childProcess.fork('child.js', {env: {index: i}}));
}
// 主进程监听请求,轮流分发
const server = net.createServer((client) => {
workers[index].send(null, client);
console.log('dispatch to', index);
index = (index + 1) % workerNum;
});
server.listen(11111);
child.js
const handle = require('../handle');
process.on('message', (message, client) => {
console.log('receive connection from master', client);
});
主进程负责监听请求,主进程收到请求后,按照一定的算法把请求通过文件描述符的方式传给worker进程,worker进程就可以处理连接了。在分发算法这里,我们可以根据自己的需求进行自定义,比如根据当前进程的负载,正在处理的连接数。
parent.js
const childProcess = require('child_process');
const net = require('net');
const workers = [];
const workerNum = 10 ;
// 绑定端口
const handle = net._createServerHandle('127.0.0.1', 11111, 4);
// 把handle传给worker进程
for (let i = 0; i < workerNum; i++) {
const worker = childProcess.fork('child.js', {env: {index: i}});
workers.push(worker);
worker.send(null ,handle);
}
// 防止文件描述符泄漏
handle.close();
child.js
const net = require('net');
process.on('message', (message, handle) => {
net.createServer(() => {
console.log(process.env.index, 'receive connection');
}).listen({handle});
});
我们看到主进程负责绑定端口,然后把handle传给worker进程,worker进程各自执行listen监听socket。当有连接到来的时候,操作系统会选择某一个worker进程处理该连接。我们看一下共享模式下操作系统中的架构。
实现共享模式的重点在于理解EADDRINUSE错误是怎么来的。当主进程执行bind的时候。有以下结构。
如果其他进程也执行bind并且ip和端口也一样,则操作系统会告诉我们端口已经被监听了(EADDRINUSE)。但是如果我们在子进程里不执行bind的话,就可以绕过这个限制。那么重点在于,如何在子进程中不执行bind,但是又可以绑定到同样的端口呢?有两种方式。
1 fork
我们知道fork的时候,子进程会继承主进程的文件描述符。
这时候,主进程可以执行bind和listen,然后fork子进程,最后close掉自己的fd,让所有的连接都由子进程处理就行。但是在nodejs中,我们拿不到这个fd,所以这种方式不能满足需求。
2 文件描述符传递。
nodejs的子进程是通过fork+exec模式创建的,并且nodejs文件描述符设置了close_on_exec标记,这就意味着,在nodejs中,创建子进程后,文件描述符的结构体如下(有标准输入、标准输出、标准错误三个fd)。
这时候我们可以通过文件描述符传递的方式。把方式1中拿不到的fd传给子进程。因为在nodejs中,虽然我们拿不到fd,但是我们可以拿得到fd对应的handle,我们通过ipc传输handle的时候,nodejs会为我们处理fd的问题。最后通过操作系统对传递文件描述符的处理。结构如下。
通过这种方式,我们就绕过了bind同一个端口的问题。通过以上的例子,我们知道绕过bind的问题重点在于让主进程和子进程共享socket而不是单独执行bind。实现共享的方式有两种,第一是fork,第二是文件描述符传递。对于传递文件描述符,nodejs中支持很多种方式。上面的方式是子进程各自执行listen。还有另一种模式如下
parent.js
const childProcess = require('child_process');
const net = require('net');
const workers = [];
const workerNum = 10;
const server = net.createServer(() => {
console.log('master receive connection');
})
server.listen(11111);
for (let i = 0; i < workerNum; i++) {
const worker = childProcess.fork('child.js', {env: {index: i}});
workers.push(worker);
worker.send(null, server);
}
// 防止文件描述符泄漏
server.close()
child.js
const net = require('net');
process.on('message', (message, server) => {
server.on('connection', () => {
console.log(process.env.index, 'receive connection');
})
});
上面的方式中,主进程完成了bind和listen。然后把server实例传给子进程,子进程就可以监听连接的到来了。但是不管哪种模式,有一个问题需要处理的是需要关闭主进程的文件描述符,否则会造成文件描述符泄漏。
github仓库:https://github.com/theanarkh/node-cluster