详解通过源码解析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 相关文章推荐
6款新颖的jQuery和CSS3进度条插件推荐
Mar 05 Javascript
JavaScript实现防止网页被嵌入Frame框架的代码分享
Dec 29 Javascript
Js实现简单的小球运动特效
Feb 18 Javascript
JavaScript编写点击查看大图的页面半透明遮罩层效果实例
May 09 Javascript
JS针对浏览器窗口关闭事件的监听方法集锦
Jun 24 Javascript
form表单转Json提交的方法(推荐)
Sep 23 Javascript
JavaScript中const、var和let区别浅析
Oct 11 Javascript
利用vscode编写vue的简单配置详解
Jun 17 Javascript
微信小程序promsie.all和promise顺序执行
Oct 27 Javascript
Node.js中读取TXT文件内容fs.readFile()用法
Oct 10 Javascript
详解vue-element Tree树形控件填坑路
Mar 26 Javascript
Vue路由前后端设计总结
Aug 06 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
全国中波电台频率表
2020/03/11 无线电
PHP array 的加法操作代码
2010/07/24 PHP
IIS+fastcgi下PHP运行超时问题的解决办法详解
2013/06/20 PHP
php防止SQL注入详解及防范
2013/11/12 PHP
php获取文件夹路径内的图片以及分页显示示例
2014/03/11 PHP
javascript中使用replaceAll()函数实现字符替换的方法
2010/12/25 Javascript
javascript 按键事件(兼容各浏览器)
2013/12/20 Javascript
JavaScript获取网页中第一个链接ID的方法
2015/04/03 Javascript
js获取当前页的URL与window.location.href简单方法
2017/02/13 Javascript
JS实现问卷星自动填问卷脚本并在两秒自动提交功能
2020/06/17 Javascript
ionic3实战教程之随机布局瀑布流的实现方法
2017/12/28 Javascript
jQuery中库的引用方法
2018/01/06 jQuery
jQuery实现table表格checkbox全选的方法分析
2018/07/04 jQuery
JS中数据结构之栈
2019/01/01 Javascript
详解js创建对象的几种方法及继承
2019/04/12 Javascript
Vue使用zTree插件封装树组件操作示例
2019/04/25 Javascript
layui自定义ajax左侧三级菜单
2019/07/26 Javascript
JavaScript之scrollTop、scrollHeight、offsetTop、offsetHeight等属性学习笔记
2020/07/15 Javascript
vue使用axios实现excel文件下载的功能
2020/07/16 Javascript
在Python中使用NLTK库实现对词干的提取的教程
2015/04/08 Python
解决Python字典写入文件出行首行有空格的问题
2017/09/27 Python
Python matplotlib画图实例之绘制拥有彩条的图表
2017/12/28 Python
python解决字符串倒序输出的问题
2018/06/25 Python
创建Shapefile文件并写入数据的例子
2019/11/26 Python
python爬虫爬取某网站视频的示例代码
2021/02/20 Python
Crucial英睿达法国官网:内存条及SSD固态硬盘升级
2018/07/13 全球购物
美国值得信赖的婚恋交友网站:eHarmony
2018/10/04 全球购物
简历的自我评价
2014/02/03 职场文书
银行优秀员工事迹
2014/02/06 职场文书
标准化管理实施方案
2014/02/25 职场文书
2014五一国际劳动节活动总结范文
2014/04/14 职场文书
教师四风自我剖析材料
2014/09/30 职场文书
师德师风整改措施
2014/10/24 职场文书
个人专业技术总结
2015/03/05 职场文书
先进个人自荐书
2015/03/06 职场文书
Spring中的@Transactional的工作原理
2022/06/05 Java/Android