详解通过源码解析Node.js中cluster模块的主要功能实现


Posted in Javascript onMay 16, 2018

众所周知,Node.js中的JavaScript代码执行在单线程中,非常脆弱,一旦出现了未捕获的异常,那么整个应用就会崩溃。这在许多场景下,尤其是web应用中,是无法忍受的。通常的解决方案,便是使用Node.js中自带的cluster模块,以master-worker模式启动多个应用实例。然而大家在享受cluster模块带来的福祉的同时,不少人也开始好奇:

  1. 为什么我的应用代码中明明有app.listen(port);,但cluter模块在多次fork这份代码时,却没有报端口已被占用?
  2. Master是如何将接收的请求传递至worker中进行处理然后响应的?

让我们从Node.js项目的lib/cluster.js中的代码里,来一勘究竟。

问题一

为了得到这个问题的解答,我们先从worker进程的初始化看起,master进程在fork工作进程时,会为其附上环境变量NODE_UNIQUE_ID,是一个从零开始的递增数:

// lib/cluster.js
// ...

function createWorkerProcess(id, env) {
 // ...
 workerEnv.NODE_UNIQUE_ID = '' + id;

 // ...
 return fork(cluster.settings.exec, cluster.settings.args, {
  env: workerEnv,
  silent: cluster.settings.silent,
  execArgv: execArgv,
  gid: cluster.settings.gid,
  uid: cluster.settings.uid
 });
}

随后Node.js在初始化时,会根据该环境变量,来判断该进程是否为cluster模块fork出的工作进程,若是,则执行workerInit()函数来初始化环境,否则执行masterInit()函数。

在workerInit()函数中,定义了cluster._getServer方法,这个方法在任何net.Server实例的listen方法中,会被调用:

// lib/net.js
// ...

function listen(self, address, port, addressType, backlog, fd, exclusive) {
 exclusive = !!exclusive;

 if (!cluster) cluster = require('cluster');

 if (cluster.isMaster || exclusive) {
  self._listen2(address, port, addressType, backlog, fd);
  return;
 }

 cluster._getServer(self, {
  address: address,
  port: port,
  addressType: addressType,
  fd: fd,
  flags: 0
 }, cb);

 function cb(err, handle) {
  // ...

  self._handle = handle;
  self._listen2(address, port, addressType, backlog, fd);
 }
}

你可能已经猜到,问题一的答案,就在这个cluster._getServer函数的代码中。它主要干了两件事:

  1. 向master进程注册该worker,若master进程是第一次接收到监听此端口/描述符下的worker,则起一个内部TCP服务器,来承担监听该端口/描述符的职责,随后在master中记录下该worker。
  2. Hack掉worker进程中的net.Server实例的listen方法里监听端口/描述符的部分,使其不再承担该职责。

对于第一件事,由于master在接收,传递请求给worker时,会符合一定的负载均衡规则(在非Windows平台下默认为轮询),这些逻辑被封装在RoundRobinHandle类中。故,初始化内部TCP服务器等操作也在此处:

// lib/cluster.js
// ...

function RoundRobinHandle(key, address, port, addressType, backlog, fd) {
 // ...
 this.handles = [];
 this.handle = null;
 this.server = net.createServer(assert.fail);

 if (fd >= 0)
  this.server.listen({ fd: fd });
 else if (port >= 0)
  this.server.listen(port, address);
 else
  this.server.listen(address); // UNIX socket path.

 /// ...
}

对于第二件事,由于net.Server实例的listen方法,最终会调用自身_handle属性下listen方法来完成监听动作,故在代码中修改之:

// lib/cluster.js
// ...

function rr(message, cb) {
 // ...
 // 此处的listen函数不再做任何监听动作
 function listen(backlog) {
  return 0;
 }

 function close() {
  // ...
 }
 function ref() {}
 function unref() {}

 var handle = {
  close: close,
  listen: listen,
  ref: ref,
  unref: unref,
 };
 // ...
 handles[key] = handle;
 cb(0, handle); // 传入这个cb中的handle将会被赋值给net.Server实例中的_handle属性
}

// lib/net.js
// ...
function listen(self, address, port, addressType, backlog, fd, exclusive) {
 // ...

 if (cluster.isMaster || exclusive) {
  self._listen2(address, port, addressType, backlog, fd);
  return; // 仅在worker环境下改变
 }

 cluster._getServer(self, {
  address: address,
  port: port,
  addressType: addressType,
  fd: fd,
  flags: 0
 }, cb);

 function cb(err, handle) {
  // ...
  self._handle = handle;
  // ...
 }
}

至此,第一个问题便已豁然开朗了,总结下:

  1. 端口仅由master进程中的内部TCP服务器监听了一次。
  2. 不会出现端口被重复监听报错,是由于,worker进程中,最后执行监听端口操作的方法,已被cluster模块主动hack。

问题二

解决了问题一,问题二的解决就明朗轻松许多了。通过问题一我们已得知,监听端口的是master进程中创建的内部TCP服务器,所以第二个问题的解决,着手点就是该内部TCP服务器接手连接时,执行的操作。Cluster模块的做法是,监听该内部TCP服务器的connection事件,在监听器函数里,有负载均衡地挑选出一个worker,向其发送newconn内部消息(消息体对象中包含cmd: 'NODE_CLUSTER'属性)以及一个客户端句柄(即connection事件处理函数的第二个参数),相关代码如下:

// lib/cluster.js
// ...

function RoundRobinHandle(key, address, port, addressType, backlog, fd) {
 // ...
 this.server = net.createServer(assert.fail);
 // ...

 var self = this;
 this.server.once('listening', function() {
  // ...
  self.handle.onconnection = self.distribute.bind(self);
 });
}

RoundRobinHandle.prototype.distribute = function(err, handle) {
 this.handles.push(handle);
 var worker = this.free.shift();
 if (worker) this.handoff(worker);
};

RoundRobinHandle.prototype.handoff = function(worker) {
 // ...
 var message = { act: 'newconn', key: this.key };
 var self = this;
 sendHelper(worker.process, message, handle, function(reply) {
  // ...
 });
};

Worker进程在接收到了newconn内部消息后,根据传递过来的句柄,调用实际的业务逻辑处理并返回:

// lib/cluster.js
// ...

// 该方法会在Node.js初始化时由 src/node.js 调用
cluster._setupWorker = function() {
 // ...
 process.on('internalMessage', internal(worker, onmessage));

 // ...
 function onmessage(message, handle) {
  if (message.act === 'newconn')
   onconnection(message, handle);
  // ...
 }
};

function onconnection(message, handle) {
 // ...
 var accepted = server !== undefined;
 // ...
 if (accepted) server.onconnection(0, handle);
}

至此,问题二也得到了解决,也总结一下:

  1. 所有请求先同一经过内部TCP服务器。
  2. 在内部TCP服务器的请求处理逻辑中,有负载均衡地挑选出一个worker进程,将其发送一个newconn内部消息,随消息发送客户端句柄。
  3. Worker进程接收到此内部消息,根据客户端句柄创建net.Socket实例,执行具体业务逻辑,返回。

最后

Node.js中的cluster模块除了上述提到的功能外,其实还提供了非常丰富的API供master和worker进程之前通信,对于不同的操作系统平台,也提供了不同的默认行为。本文仅挑选了一条功能线进行了分析阐述。如果大家有闲,非常推荐完整领略一下cluster模块的代码实现。

参考:

https://github.com/nodejs/node/blob/master/lib/cluster.js
https://github.com/nodejs/node/blob/master/lib/net.js

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
JavaScript 参考教程
Dec 29 Javascript
一个加载js文件的小脚本
Jun 28 Javascript
js获取url参数值的两种方式
Sep 10 Javascript
js数字转换为float,取N位小数
Feb 08 Javascript
基于jquery实现select选择框内容左右移动添加删除代码分享
Aug 25 Javascript
详解jquery easyui之datagrid使用参考
Dec 05 Javascript
js的三种继承方式详解
Jan 21 Javascript
Vue.js学习记录之在元素与template中使用v-if指令实例
Jun 27 Javascript
利用node.js爬取指定排名网站的JS引用库详解
Jul 25 Javascript
js异步上传多张图片插件的使用方法
Oct 22 Javascript
微信小程序中使用echarts的实现方法
Apr 24 Javascript
微信小程序导入Vant报错VM292:1 thirdScriptError的解决方法
Aug 01 Javascript
浅谈如何通过node.js对数据进行MD5加密
May 16 #Javascript
如何用input标签和jquery实现多图片的上传和回显功能
May 16 #jQuery
vue keep-alive请求数据的方法示例
May 16 #Javascript
解决vue项目中type=”file“ change事件只执行一次的问题
May 16 #Javascript
vue-cli与webpack处理静态资源的方法及webpack打包的坑
May 15 #Javascript
JavaScript常用截取字符串的三种方式用法区别实例解析
May 15 #Javascript
vue中keep-alive的用法及问题描述
May 15 #Javascript
You might like
PHP中读取照片exif信息的方法
2014/08/20 PHP
锋利的jQuery 第三章章节总结的例子
2010/03/23 Javascript
Javascript实现真实字符串剩余字数提示的实例代码
2013/10/22 Javascript
js文件Cookie存取值示例代码
2014/02/20 Javascript
JS中使用Array函数shift和pop创建可忽略参数的例子
2014/05/28 Javascript
JavaScript静态类型检查工具FLOW简介
2015/01/06 Javascript
Angularjs中UI Router的使用方法
2016/05/14 Javascript
EasyUI布局 高度自适应
2016/06/04 Javascript
Bootstrap布局之栅格系统详解
2016/06/13 Javascript
详解Python中logging日志模块在多进程环境下的使用
2016/12/26 Javascript
js验证手机号、密码、短信验证码代码工具类
2020/06/24 Javascript
jQuery使用JSONP实现跨域获取数据的三种方法详解
2017/05/04 jQuery
Gulp实现静态网页模块化的方法详解
2018/01/09 Javascript
mpvue中配置vuex并持久化到本地Storage图文教程解析
2018/03/15 Javascript
解决Echarts 显示隐藏后宽度高度变小的问题
2020/07/19 Javascript
Vue-cli assets SubDirectory及PublicPath区别详解
2020/08/18 Javascript
vue使用video插件vue-video-player详解
2020/10/23 Javascript
vue穿梭框实现上下移动
2021/01/29 Vue.js
详解Python操作RabbitMQ服务器消息队列的远程结果返回
2016/06/30 Python
Python2.7读取PDF文件的方法示例
2017/07/13 Python
Python 实现网页自动截图的示例讲解
2018/05/17 Python
pandas Dataframe行列读取的实例
2018/06/08 Python
Django migrations 默认目录修改的方法教程
2018/09/28 Python
python  创建一个保留重复值的列表的补码
2018/10/15 Python
详解python列表(list)的使用技巧及高级操作
2019/08/15 Python
如何在 Django 模板中输出 "{{"
2020/01/24 Python
pytorch实现保证每次运行使用的随机数都相同
2020/02/20 Python
python中pickle模块浅析
2020/12/29 Python
CSS3实现多重边框的方法总结
2016/05/31 HTML / CSS
美国家居用品和厨具购物网站:DealsDot
2019/10/07 全球购物
希腊品牌鞋类销售网站:epapoutsia.gr
2020/03/18 全球购物
荷兰天然和有机产品网上商城:BigGreenSmile.nl
2020/07/26 全球购物
捷克建筑材料网上商店:DEK.cz
2021/03/06 全球购物
人力管理专业毕业生求职信
2014/02/27 职场文书
事业单位工作人员2015年度思想工作总结
2015/10/15 职场文书
CSS+HTML 实现顶部导航栏功能
2021/08/30 HTML / CSS