Node.js中的cluster模块深入解读


Posted in Javascript onJune 11, 2018

预备知识

在如今机器的CPU都是多核的背景下,Node的单线程设计已经没法更充分的"压榨"机器性能了。所以从v0.8开始,Node新增了一个内置模块——“cluster”,故名思议,它可以通过一个父进程管理一坨子进程的方式来实现集群的功能。

学习cluster之前,需要了解process相关的知识,如果不了解的话建议先阅读process模块、child_process模块。

cluster借助child_process模块的fork()方法来创建子进程,通过fork方式创建的子进程与父进程之间建立了IPC通道,支持双向通信。

cluster模块最早出现在node.js v0.8版本中

为什么会存在cluster模块?

Node.js是单线程的,那么如果希望利用服务器的多核的资源的话,就应该多创建几个进程,由多个进程共同提供服务。如果直接采用下列方式启动多个服务的话,会提示端口占用。

const http = require('http');
http.createServer((req, res) => {
 res.writeHead(200);
 res.end('hello world\n');
}).listen(8000);

// 启动第一个服务 node index.js &
// 启动第二个服务 node index.js &

 throw er; // Unhandled 'error' event
 ^

Error: listen EADDRINUSE :::8000
 at Server.setupListenHandle [as _listen2] (net.js:1330:14)
 at listenInCluster (net.js:1378:12)
 at Server.listen (net.js:1465:7)
 at Object.<anonymous> (/Users/xiji/workspace/learn/node-basic/cluster/simple.js:5:4)
 at Module._compile (internal/modules/cjs/loader.js:702:30)
 at Object.Module._extensions..js (internal/modules/cjs/loader.js:713:10)
 at Module.load (internal/modules/cjs/loader.js:612:32)
 at tryModuleLoad (internal/modules/cjs/loader.js:551:12)
 at Function.Module._load (internal/modules/cjs/loader.js:543:3)
 at Function.Module.runMain (internal/modules/cjs/loader.js:744:10)

如果改用cluster的话就没有问题

const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;

if (cluster.isMaster) {
 console.log(`Master ${process.pid} is running`);

 // Fork workers.
 for (let i = 0; i < numCPUs; i++) {
 cluster.fork();
 }

 cluster.on('exit', (worker, code, signal) => {
 console.log(`worker ${worker.process.pid} died`);
 });
} else {
 // Workers can share any TCP connection
 // In this case it is an HTTP server
 http.createServer((req, res) => {
 res.writeHead(200);
 res.end('hello world\n');
 }).listen(8000);

 console.log(`Worker ${process.pid} started`);
}

// node index.js 执行完启动了一个主进程和8个子进程(子进程数与cpu核数相一致)
Master 11851 is running
Worker 11852 started
Worker 11854 started
Worker 11853 started
Worker 11855 started
Worker 11857 started
Worker 11858 started
Worker 11856 started
Worker 11859 started

cluster是如何实现多进程共享端口的?

cluster创建的进程分两种,父进程和子进程,父进程只有一个,子进程有多个(一般根据cpu核数创建)

  • 父进程负责监听端口接受请求,然后分发请求。
  • 子进程负责请求的处理。

有三个问题需要回答:

  • 子进程为何调用listen不会进行端口绑定
  • 父进程何时创建的TCP Server
  • 父进程是如何完成分发的

子进程为何调用listen不会绑定端口?

net.js源码中的listen方法通过listenInCluster方法来区分是父进程还是子进程,不同进程的差异在listenInCluster方法中体现

function listenInCluster(server, address, port, addressType, backlog, fd, excluseive) {
 
 if (cluster.isMaster || exclusive) {
 server._listen2(address, port, addressType, backlog, fd);
 return;
 }

 const serverQuery = { address: address ......};

 cluster._getServer(server, serverQuery, listenOnMasterHandle);

 function listenOnMasterHandle(err, handle) {
 server._handle = handle;
 server._listen2(address, port, addressType, backlog, fd);
 }
}

上面是精简过的代码,当子进程调用listen方法时,会先执行_getServer,然后通过callback的形式指定server._handle的值,之后再调用_listen2方法。

cluster._getServer = function(obj, options, cb) {
 ...
 const message = util._extend({
 act: 'queryServer',
 index: indexes[indexesKey],
 data: null
 }, options);

 message.address = address;

 send(message, (reply, handle) => {
 if (handle)
 shared(reply, handle, indexesKey, cb); // Shared listen socket.
 else
 rr(reply, indexesKey, cb); // Round-robin.
 });
 ...
};

_getServer方法会向主进程发送queryServer的message,父进程执行完会调用回调函数,根据是否返回handle来区分是调用shared方法还是rr方法,这里其实是会调用rr方法。而rr方法的主要作用就是伪造了TCPWrapper来调用net的listenOnMasterHandle回调函数

function rr(message, indexesKey, cb) {

 var key = message.key;

 function listen(backlog) {
 return 0;
 }

 function close() {
 if (key === undefined)
 return;

 send({ act: 'close', key });
 delete handles[key];
 delete indexes[indexesKey];
 key = undefined;
 }

 function getsockname(out) {
 if (key)
 util._extend(out, message.sockname);

 return 0;
 }

 const handle = { close, listen, ref: noop, unref: noop };
 handles[key] = handle;
 cb(0, handle);
}

由于子进程的server拿到的是围绕的TCPWrapper,当调用listen方法时并不会执行任何操作,所以在子进程中调用listen方法并不会绑定端口,因而也并不会报错。

父进程何时创建的TCP Server

在子进程发送给父进程的queryServer message时,父进程会检测是否创建了TCP Server,如果没有的话就会创建TCP Server并绑定端口,然后再把子进程记录下来,方便后续的用户请求worker分发。

父进程是如何完成分发的

父进程由于绑定了端口号,所以可以捕获连接请求,父进程的onconnection方法会被触发,onconnection方法触发时会传递TCP对象参数,由于之前父进程记录了所有的worker,所以父进程可以选择要处理请求的worker,然后通过向worker发送act为newconn的消息,并传递TCP对象,子进程监听到消息后,对传递过来的TCP对象进行封装,封装成socket,然后触发connection事件。这样就实现了子进程虽然不监听端口,但是依然可以处理用户请求的目的。

cluster如何实现负载均衡

负载均衡直接依赖cluster的请求调度策略,在v6.0版本之前,cluster的调用策略采用的是cluster.SCHED_NONE(依赖于操作系统),SCHED_NODE理论上来说性能最好(Ferando Micalli写过一篇Node.js 6.0版本的cluster和iptables以及nginx性能对比的文章)但是从实际角度发现,在请求调度方面会出现不太均匀的情况(可能出现8个子进程中的其中2到3个处理了70%的连接请求)。因此在6.0版本中Node.js增加了cluster.SCHED_RR(round-robin),目前已成为默认的调度策略(除了windows环境)

可以通过设置NODE_CLUSTER_SCHED_POLICY环境变量来修改调度策略

NODE_CLUSTER_SCHED_POLICY='rr'
NODE_CLUSTER_SCHED_POLICY='none'

或者设置cluster的schedulingPolicy属性

cluster.schedulingPolicy = cluster.SCHED_NONE;
cluster.schedulingPolicy = cluster.SCHED_RR;

Node.js实现round-robin

Node.js内部维护了两个队列:

  • free队列记录当前可用的worker
  • handles队列记录需要处理的TCP请求

当新请求到达的时候父进程将请求暂存handles队列,从free队列中出队一个worker,进入worker处理(handoff)阶段,关键逻辑实现如下:

RoundRobinHandle.prototype.distribute = function(err, handle) {
 this.handles.push(handle);
 const worker = this.free.shift();

 if (worker) {
 this.handoff(worker);
 }
};

worker处理阶段首先从handles队列出队一个请求,然后通过进程通信的方式通知子worker进行请求处理,当worker接收到通信消息后发送ack信息,继续响应handles队列中的请求任务,当worker无法接受请求时,父进程负责重新调度worker进行处理。关键逻辑如下:

RoundRobinHandle.prototype.handoff = function(worker) {
 const handle = this.handles.shift();
 if (handle === undefined) {
 this.free.push(worker); // Add to ready queue again.
 return;
 }

 const message = { act: 'newconn', key: this.key };
 sendHelper(worker.process, message, handle, (reply) => {
 if (reply.accepted)
 handle.close();
 else
 this.distribute(0, handle); // Worker is shutting down. Send to another.
 this.handoff(worker);
 });
};

注意:主进程与子进程之间建立了IPC,因此主进程与子进程之间可以通信,但是各个子进程之间是相互独立的(无法通信)

参考资料

https://medium.com/@fermads/node-js-process-load-balancing-comparing-cluster-iptables-and-nginx-6746aaf38272

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对三水点靠木的支持。

Javascript 相关文章推荐
使用jquery实现select添加实现后台权限添加的效果
May 28 Javascript
jQuery+CSS 半开折叠效果原理及代码(自写)
Mar 04 Javascript
JQ获取动态加载的图片大小的正确方法分享
Nov 08 Javascript
JavaScript学习笔记之基础语法
Jan 22 Javascript
BootStrap制作导航条实例代码
May 06 Javascript
原生js实现轮播图
Feb 27 Javascript
Vue.js如何优雅的进行form validation
Apr 07 Javascript
ionic中的$ionicPlatform.ready事件中的通用设置
Jun 11 Javascript
vue自定义一个v-model的实现代码
Jun 21 Javascript
webpack4简单入门实例
Sep 06 Javascript
详解SPA中前端路由基本原理与实现方式
Sep 12 Javascript
微信小程序点击滚动到指定位置的实现
May 22 Javascript
详解vue-router 初始化时做了什么
Jun 11 #Javascript
node中间层实现文件上传功能
Jun 11 #Javascript
几个你不知道的技巧助你写出更优雅的vue.js代码
Jun 11 #Javascript
Vue.js 中取得后台原生HTML字符串 原样显示问题的解决方法
Jun 10 #Javascript
实例详解Node.js 函数
Jun 10 #Javascript
微信小程序实现倒计时调用相机自动拍照功能
Jun 10 #Javascript
深入浅析Vue中的Prop
Jun 10 #Javascript
You might like
PHP4(windows版本)中的COM函数
2006/10/09 PHP
PHP 循环列出目录内容的函数代码
2010/05/26 PHP
php的array数组和使用实例简明教程(容易理解)
2014/03/20 PHP
php文件读取方法实例分析
2015/06/20 PHP
PHP利用Socket获取网站的SSL证书与公钥
2017/06/18 PHP
一种JavaScript的设计模式
2006/11/22 Javascript
JS鼠标滑过图片时切换图片实现思路
2013/09/12 Javascript
javascript中数组中求最大值示例代码
2013/12/18 Javascript
微信分享的标题、缩略图、连接及描述设置方法
2014/10/14 Javascript
基于jquery实现简单的手风琴特效
2015/11/24 Javascript
利用C/C++编写node.js原生模块的方法教程
2017/07/07 Javascript
Vue项目添加动态浏览器头部title的方法
2018/07/11 Javascript
微信小程序实现带参数的分享功能(两种方法)
2019/05/17 Javascript
基于webpack4+vue-cli3项目实现换肤功能
2019/07/17 Javascript
python虚拟环境 virtualenv的简单使用
2020/01/21 Javascript
小程序实现简单语音聊天的示例代码
2020/07/24 Javascript
跟老齐学Python之类的细节
2014/10/13 Python
python制作websocket服务器实例分享
2016/11/20 Python
python模块之time模块(实例讲解)
2017/09/13 Python
python网络爬虫之如何伪装逃过反爬虫程序的方法
2017/11/23 Python
python列表生成式与列表生成器的使用
2018/02/23 Python
对python中的pop函数和append函数详解
2018/05/04 Python
获取django框架orm query执行的sql语句实现方法分析
2019/06/20 Python
VSCode中自动为Python文件添加头部注释
2019/11/14 Python
Python对称的二叉树多种思路实现方法
2020/02/28 Python
详解基于python的全局与局部序列比对的实现(DNA)
2020/10/07 Python
html5本地存储之localstorage 、本地数据库、sessionStorage简单使用示例
2014/05/08 HTML / CSS
详解HTML5 window.postMessage与跨域
2017/05/11 HTML / CSS
实习生的自我评价
2014/01/08 职场文书
小学见习报告
2014/10/31 职场文书
期末复习计划
2015/01/19 职场文书
酒会开场白大全
2015/06/01 职场文书
家庭教育教师培训学习体会
2016/01/14 职场文书
《清澈的湖水》教学反思
2016/02/17 职场文书
Python Parser的用法
2021/05/12 Python
OpenCV实现普通阈值
2021/11/17 Java/Android