详解通过源码解析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 进阶篇2 CSS XML学习
Mar 14 Javascript
jQuery阻止事件冒泡具体实现
Oct 11 Javascript
原生Javascript封装的一个AJAX函数分享
Oct 11 Javascript
利用HTML5的画布Canvas实现刮刮卡效果
Sep 06 Javascript
javascript实现很浪漫的气泡冒出特效
Sep 05 Javascript
简单的JS控制button颜色随点击更改的实现方法
Apr 17 Javascript
Angular实现类似博客评论的递归显示及获取回复评论的数据
Nov 06 Javascript
mongoose设置unique不生效问题的解决及如何移除unique的限制
Nov 07 Javascript
Angular4 ElementRef的应用
Feb 26 Javascript
手把手教你写一个微信小程序(推荐)
Oct 17 Javascript
JS匿名函数内部this指向问题详析
May 10 Javascript
JavaScript实现刮刮乐效果
Nov 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实现MD5加密16位(不要默认的32位)
2013/08/12 PHP
php字符串操作针对负值的判断分析
2016/07/28 PHP
PHP入门教程之自定义函数用法详解(创建,调用,变量,参数,返回值等)
2016/09/11 PHP
mysql查找删除重复数据并只保留一条实例详解
2016/09/24 PHP
ThinkPHP删除栏目(实现批量删除栏目)
2017/06/21 PHP
PHP一个简单的无需刷新爬虫
2019/01/05 PHP
PHP parse_ini_file函数的应用与扩展操作示例
2019/01/07 PHP
laravel 自定义常量的两种方案
2019/10/14 PHP
jQuery使用一个按钮控制图片的伸缩实现思路
2013/04/19 Javascript
JS性能优化笔记搜索整理
2013/08/21 Javascript
js中点击空白区域时文本框与隐藏层的显示与影藏问题
2013/08/26 Javascript
angularjs实现与服务器交互分享
2014/06/24 Javascript
javascript实现分栏显示小技巧附图
2014/10/13 Javascript
Jquery左右滑动插件之实现超级炫酷动画效果附源码下载
2015/12/02 Javascript
Angularjs实现多个页面共享数据的方式
2016/03/29 Javascript
详解如何将 Vue-cli 改造成支持多页面的 history 模式
2017/11/20 Javascript
基于three.js编写的一个项目类示例代码
2018/01/05 Javascript
使用Angular自定义字段校验指令的方法示例
2019/02/01 Javascript
vue3修改link标签默认icon无效问题详解
2019/10/09 Javascript
vue之封装多个组件调用同一接口的案例
2020/08/11 Javascript
Javascript新手入门之字符串拼接与变量的应用
2020/12/03 Javascript
[02:35]DOTA2英雄基础教程 末日使者
2013/12/04 DOTA
[06:07]刀塔密之二:攻之吾命受之吾幸
2014/07/03 DOTA
Python使用Redis实现作业调度系统(超简单)
2016/03/22 Python
Python 安装setuptools和pip工具操作方法(必看)
2017/05/22 Python
一篇文章读懂Python赋值与拷贝
2018/04/19 Python
Python实现购物评论文本情感分析操作【基于中文文本挖掘库snownlp】
2018/08/07 Python
Python列表list常用内建函数实例小结
2019/10/22 Python
吸烟检讨书2000字
2014/02/13 职场文书
人事专员岗位说明书
2014/07/29 职场文书
客房领班岗位职责
2015/02/11 职场文书
员工辞退通知书
2015/04/17 职场文书
迎国庆主题班会
2015/08/17 职场文书
什么是执行力?9个故事告诉您:成功绝非偶然!
2019/07/05 职场文书
python opencv通过按键采集图片源码
2021/05/20 Python
恶魔之树最顶端的三颗果实 震震果实上榜,第一可以制造岩浆
2022/03/18 日漫