资讯专栏INFORMATION COLUMN

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

leeon / 1446人阅读

摘要:通常的解决方案,便是使用中自带的模块,以模式启动多个应用实例。最后中的模块除了上述提到的功能外,其实还提供了非常丰富的供和进程之前通信,对于不同的操作系统平台,也提供了不同的默认行为。如果大家有闲,非常推荐完整领略一下模块的代码实现。

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

为什么我的应用代码中明明有app.listen(port);,但cluter模块在多次fork这份代码时,却没有报端口已被占用?

Master是如何将接收的请求传递至worker中进行处理然后响应的?

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

问题一

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

</>复制代码

  1. // lib/cluster.js
  2. // ...
  3. function createWorkerProcess(id, env) {
  4. // ...
  5. workerEnv.NODE_UNIQUE_ID = "" + id;
  6. // ...
  7. return fork(cluster.settings.exec, cluster.settings.args, {
  8. env: workerEnv,
  9. silent: cluster.settings.silent,
  10. execArgv: execArgv,
  11. gid: cluster.settings.gid,
  12. uid: cluster.settings.uid
  13. });
  14. }

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

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

</>复制代码

  1. // lib/net.js
  2. // ...
  3. function listen(self, address, port, addressType, backlog, fd, exclusive) {
  4. exclusive = !!exclusive;
  5. if (!cluster) cluster = require("cluster");
  6. if (cluster.isMaster || exclusive) {
  7. self._listen2(address, port, addressType, backlog, fd);
  8. return;
  9. }
  10. cluster._getServer(self, {
  11. address: address,
  12. port: port,
  13. addressType: addressType,
  14. fd: fd,
  15. flags: 0
  16. }, cb);
  17. function cb(err, handle) {
  18. // ...
  19. self._handle = handle;
  20. self._listen2(address, port, addressType, backlog, fd);
  21. }
  22. }

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

向master进程注册该worker,若master进程是第一次接收到监听此端口/描述符下的worker,则起一个内部TCP服务器,来承担监听该端口/描述符的职责,随后在master中记录下该worker。

Hack掉worker进程中的net.Server实例的listen方法里监听端口/描述符的部分,使其不再承担该职责。

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

</>复制代码

  1. // lib/cluster.js
  2. // ...
  3. function RoundRobinHandle(key, address, port, addressType, backlog, fd) {
  4. // ...
  5. this.handles = [];
  6. this.handle = null;
  7. this.server = net.createServer(assert.fail);
  8. if (fd >= 0)
  9. this.server.listen({ fd: fd });
  10. else if (port >= 0)
  11. this.server.listen(port, address);
  12. else
  13. this.server.listen(address); // UNIX socket path.
  14. /// ...
  15. }

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

</>复制代码

  1. // lib/cluster.js
  2. // ...
  3. function rr(message, cb) {
  4. // ...
  5. // 此处的listen函数不再做任何监听动作
  6. function listen(backlog) {
  7. return 0;
  8. }
  9. function close() {
  10. // ...
  11. }
  12. function ref() {}
  13. function unref() {}
  14. var handle = {
  15. close: close,
  16. listen: listen,
  17. ref: ref,
  18. unref: unref,
  19. };
  20. // ...
  21. handles[key] = handle;
  22. cb(0, handle); // 传入这个cb中的handle将会被赋值给net.Server实例中的_handle属性
  23. }
  24. // lib/net.js
  25. // ...
  26. function listen(self, address, port, addressType, backlog, fd, exclusive) {
  27. // ...
  28. if (cluster.isMaster || exclusive) {
  29. self._listen2(address, port, addressType, backlog, fd);
  30. return; // 仅在worker环境下改变
  31. }
  32. cluster._getServer(self, {
  33. address: address,
  34. port: port,
  35. addressType: addressType,
  36. fd: fd,
  37. flags: 0
  38. }, cb);
  39. function cb(err, handle) {
  40. // ...
  41. self._handle = handle;
  42. // ...
  43. }
  44. }

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

端口仅由master进程中的内部TCP服务器监听了一次。

不会出现端口被重复监听报错,是由于,worker进程中,最后执行监听端口操作的方法,已被cluster模块主动hack。

问题二

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

</>复制代码

  1. // lib/cluster.js
  2. // ...
  3. function RoundRobinHandle(key, address, port, addressType, backlog, fd) {
  4. // ...
  5. this.server = net.createServer(assert.fail);
  6. // ...
  7. var self = this;
  8. this.server.once("listening", function() {
  9. // ...
  10. self.handle.onconnection = self.distribute.bind(self);
  11. });
  12. }
  13. RoundRobinHandle.prototype.distribute = function(err, handle) {
  14. this.handles.push(handle);
  15. var worker = this.free.shift();
  16. if (worker) this.handoff(worker);
  17. };
  18. RoundRobinHandle.prototype.handoff = function(worker) {
  19. // ...
  20. var message = { act: "newconn", key: this.key };
  21. var self = this;
  22. sendHelper(worker.process, message, handle, function(reply) {
  23. // ...
  24. });
  25. };

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

</>复制代码

  1. // lib/cluster.js
  2. // ...
  3. // 该方法会在Node.js初始化时由 src/node.js 调用
  4. cluster._setupWorker = function() {
  5. // ...
  6. process.on("internalMessage", internal(worker, onmessage));
  7. // ...
  8. function onmessage(message, handle) {
  9. if (message.act === "newconn")
  10. onconnection(message, handle);
  11. // ...
  12. }
  13. };
  14. function onconnection(message, handle) {
  15. // ...
  16. var accepted = server !== undefined;
  17. // ...
  18. if (accepted) server.onconnection(0, handle);
  19. }

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

所有请求先同一经过内部TCP服务器。

在内部TCP服务器的请求处理逻辑中,有负载均衡地挑选出一个worker进程,将其发送一个newconn内部消息,随消息发送客户端句柄。

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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/78907.html

相关文章

  • 初识Node.js

    摘要:一旦替换已经完成,该模块将被完全弃用。用作错误处理事件文件,由在标准功能上的简单包装器提供所有模块都提供这些对象。 Node.js简介 Node 定义 Node.js是一个建立在Chrome v8 引擎上的javascript运行时环境 Node 特点 异步事件驱动 showImg(https://segmentfault.com/img/bVMLD1?w=600&h=237); no...

    lk20150415 评论0 收藏0
  • 深入理解Node.js 进程与线程(8000长文彻底搞懂)

    摘要:在单核系统之上我们采用单进程单线程的模式来开发。由进程来管理所有的子进程,主进程不负责具体的任务处理,主要工作是负责调度和管理。模块与模块总结无论是模块还是模块,为了解决实例单线程运行,无法利用多核的问题而出现的。 前言 进程与线程是一个程序员的必知概念,面试经常被问及,但是一些文章内容只是讲讲理论知识,可能一些小伙伴并没有真的理解,在实际开发中应用也比较少。本篇文章除了介绍概念,通过...

    Harpsichord1207 评论0 收藏0
  • dubbo源码解析(一)Hello,Dubbo

    摘要:英文全名为,也叫远程过程调用,其实就是一个计算机通信协议,它是一种通过网络从远程计算机程序上请求服务而不需要了解底层网络技术的协议。 Hello,Dubbo 你好,dubbo,初次见面,我想和你交个朋友。 Dubbo你到底是什么? 先给出一套官方的说法:Apache Dubbo是一款高性能、轻量级基于Java的RPC开源框架。 那么什么是RPC? 文档地址:http://dubbo.a...

    evin2016 评论0 收藏0
  • 系列3|走进Node.js之多进程模型

    摘要:例如,在方法中,如果需要主从进程之间建立管道,则通过环境变量来告知从进程应该绑定的相关的文件描述符,这个特殊的环境变量后面会被再次涉及到。 文:正龙(沪江网校Web前端工程师)本文原创,转载请注明作者及出处 之前的文章走进Node.js之HTTP实现分析中,大家已经了解 Node.js 是如何处理 HTTP 请求的,在整个处理过程,它仅仅用到单进程模型。那么如何让 Web 应用扩展到...

    snowell 评论0 收藏0
  • 前端20个灵魂拷问 彻底搞明白你就是级前端工程师 【下篇】

    摘要:安装后已经完成了安装,并且等待其他的线程被关闭。激活后在这个状态会处理事件回调提供了更新缓存策略的机会。并可以处理功能性的事件请求后台同步推送。废弃状态这个状态表示一个的生命周期结束。 showImg(https://segmentfault.com/img/bVbwWJu?w=2056&h=1536); 不知不觉,已经来到了最后的下篇 其实我写的东西你如果认真去看,跟着去写,应该能有...

    fireflow 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<