Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

node基础篇之集群Cluster #38

Open
kekobin opened this issue Sep 20, 2019 · 0 comments
Open

node基础篇之集群Cluster #38

kekobin opened this issue Sep 20, 2019 · 0 comments

Comments

@kekobin
Copy link
Owner

kekobin commented Sep 20, 2019

简介

集群(cluster)模块可以被用来在多核 CPU 环境负载均衡。基于子进程的 fork 方法并且主要允许我们根据 CPU 核数衍生很多次主应用进程。然后主进程将接管并且通过主进程与所有的子进程的交流实现负载均衡。

cluster的工作原理很简单,即主进程fork出N个(N小于cpu核数)worker进程,然后管理它们。每一个工作进程代理表了一个我们想要扩展的应用的一个实例。所有到来的请求都被主进程所处理,它决定着哪一个工作进程应该处理一个到来的请求,即主进程的工作只是分配请求到子进程。

image

主进程的工作很简单,因为它实际上只使用了一个调度轮询算法去选择一个工作进程。默认在除了 windows 的所有平台是可用的。

调度轮询算法在循环的基础上通过所有可用的进程,将负载均匀地分布。第一个请求转发给第一个工作进程,第二个被转发到列表中的下一个工作进程,以此类推。当到达列表的最后,算法将会再一次从列表的头部开始。

这是一个最简单和最广泛使用的负载均衡算法。但是并不是唯一的一个。更多有特色的算法允许赋权和选择最小负载量的或者最快响应的那一个服务器。

ab是Apache HTTP server benchmarking tool的缩写,可以用以测试HTTP请求的服务器性能,也是业界比较流行和简单易用的一种压力测试工具包。下载地址
node应用中,可以使用apache 基准测试工具来测试并发请求,如 ab -c200 -t10 http://localhost:8080/ 会每 10 秒钟发送 200 个并发请求来测试负载服务器。(可查看实际每秒发起的请求数越多,代表并发能力越强)

对比非cluster模式和cluster集群模式下的并发

非cluster模式下

// server.js
const http = require('http');
const pid = process.pid;
http.createServer((req, res) => {
    for(let i=0;i<1e7;i++) {} // simulate CPU work
    res.end(`handled by process.${pid}`);
}).listen(8080, () => {
    console.log(`started process`, pid);
});

使用ab测试:

ab -c200 -t10 http://localhost:8080/

结果如下:
image

也即平均每秒 182次请求。

使用cluster模式

// cluter.js
const cluster = require('cluster');
const os = require('os');
if (cluster.isMaster) {
    const cpus = os.cpus().length;
    console.log('forking for ', cpus, ' CPUS');
    for(let i = 0;i<cpus;i++) {
        cluster.fork();
    }
} else {
    requre('./server.js');
}

cluster提供了isMaster和isWorker属性,用于判断当前进程是cluster还是worker。
首次执行cluster.js时,是cluster进程,所以isMaster为true,进入根据机器cpu内核数fork worker进程的逻辑。当在主进程里执行 cluster.fork 之后,当前的文件 cluster.js 将会被再次执行,不过这次是在 worker 模式执行的 isMaster 被设置为了 false,此时每个worker分别执行 server.js 里的逻辑。

使用ab再次测试,得到如下结果:
image

即平均每秒930次并发请求,性能比非cluster的搞了好几倍。

向所有的工作进程广播信息

由于在 cluster 模块底层使用的是 child_process.fork API,所以根据这个API的特性,我们可以使用IPC在cluster和worker之间进行同行。
如:

// cluster.js
Object.values(cluster.workers).forEach(worker => {
    worker.send('hello son');
});

// server.js   
process.on('message', msg => {
    console.log(`Message from master:  ${msg}`);
});

增加服务器的可用性

后台的程序往往要保证可靠和稳定性,即即使在崩溃或者重启时,也尽量保证整体的稳定。通过cluster,这点在node中很容易实现:

// in server.js 模拟某个worker崩溃
setTimeout(() => {
    process.exit(1); // death by random timeout
}, Math.random() * 10000);    

// 在 isMaster=true 块里面的 for 循环后面,这里的exitedAfterDisconnect指的是系统主动的断链
cluster.on('exit', (worker, code, signal) => {
    if(code !== 0 && !worker.exitedAfterDisconnect) {
        console.log(`工作进程 ${worker.id} 崩溃了,正在开始一个新的工作进程`);
        cluster.fork();
    }
})

不停机重启

在部署新代码时,最好的方式是一个进程一个进程的重启,而不是一下子整个重启。
linux可以通过监听SIGUSR2事件来做这个重启处理,SIGUSR2事件是在运行 kill -SIGUSR2 PID杀死进程时监听的事件。

整个流程如下:

// 主进程中cluster.js       
process.on('SIGUSR2', () => {
	const workers = Object.values(cluster.workers);
	const restartWorker = (workerIndex) => {
	    const worker = workers[workerIndex];
	    if(!worker) return;
	    worker.on('exit', () => {
	        if (!worker.exitedAfterDisconnect) return;
	        console.log('退出的进程', worker.process.pid);

	        cluster.fork().on('listening', () => {
	            restartWorker(workerIndex + 1);
	        });
	    });
	    worker.disconnect();
	};

	restartWorker(0);
});

master和worker如何实现端口共享

在集群模式下,node对外暴露的是同一个端口,那么worker是怎么共享这一端口的呢?
秘密在于,net模块中,对 listen() 方法进行了特殊处理。根据当前进程是master进程,还是worker进程:

  1. master进程:在该端口上正常监听请求。(没做特殊处理)
  2. worker进程:创建server实例。然后通过IPC通道,向master进程发送消息,让master进程也创建 server 实例,并在该端口上监听请求。当请求进来时,master进程将请求转发给worker进程的server实例。

image

封装库

一般我们不会自己手动去封装这些内容,而是使用已有的成熟工具:

多个进程为什么可以监听同一个端口?

Master 进程创建一个 Socket 并绑定监听到该目标端口,通过与子进程之间建立 IPC 通道之后,通过调用子进程的 send 方法,将 Socket(链接句柄)传递过去
下面展示一个使用 child_process.fork() 创建的子进程,进行 Socket 传递的示例:

// master.js
const fork = require('child_process').fork;
const cpus = require('os').cpus();
const server = require('net').createServer().listen(3000);

for (let i=0; i<cpus.length; i++) {
    const worker = fork('worker.js');
      // 将 Master 的 server 传递给子进程
    worker.send('server', server);
    console.log('worker process created, pid: %s ppid: %s', worker.pid, process.pid);
}

// worker.js 
const http = require('http');
const server = http.createServer((req, res) => {
    res.end('I am worker, pid: ' + process.pid + ', ppid: ' + process.ppid);
});

let worker;
// 第二个参数 sendHandle 就是句柄,可以是 TCP套接字、TCP服务器、UDP套接字等
process.on('message', function (message, sendHandle) {
    if (message === 'server') {
        worker = sendHandle;
        worker.on('connection', function(socket) {
            server.emit('connection', socket);
        });
    }
});

总结下: 端口只会被主进程绑定监听一次,但是主进程和子进程在建立 IPC 通信之后,发送 Socket 到子进程实现端口共享,在之后 Master 接收到新的客户端链接之后,通过负载均衡技术再转发到各 Worker 进程.

多个进程之间如何通信?

还是上面提到的,cluster.fork() 本质上还是使用的 child_process.fork() 这个方法来创建的子进程,进程间通信无非几种:pipe(管道)、消息队列、信号量、Domain Socket。
在 Nodejs 中是通过 pipe(管道)实现的,pipe 作用于之间有血缘关系的进程,通过 fork 传递,其本身也是一个进程,将一个进程的输出做为另外一个进程的输入,常见的 Linux 所提供的管道符 “|” 就是将两个命令隔开,管道符左边命令的输出就会作为管道符右边命令的输入。

进程间通信参考

参考

cluster模块

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant