NodeJS Cluster模块源码学习

一段常见的示例代码

const cluster = require('cluster');
const http = require('http');

if (cluster.isMaster) {
  // 根据cpu核心数出fork相同数量的子进程
} else {
  // 用http模块创建server监听某一个端口
}
复制代码

引出如下问题:

  1. cluster模块如何区分子进程和主进程?

  2. 代码中没有在主进程中创建服务器,那么如何主进程如何承担代理服务器的职责?

  3. 多个子进程共同侦听同一个端口为什么不会造成端口reuse error

1. cluster模块如何区分主进程/子进程

cluster.js - 源码

const childOrMaster = 'NODE_UNIQUE_ID' in process.env ? 'child' : 'master';
module.exports = require(`internal/cluster/${childOrMaster}`);
复制代码

结论: 判断环境变量中是否含有NODE_UNIQUE_ID, 有则为子进程,没有则为主进程

1.1 isMaster & isWorker

这样的话, 在对应的文件中isMasterisWorker的值就明确啦

// child.js
module.exports = cluster;

cluster.isWorker = true;
cluster.isMaster = false;

// master.js
module.exports = cluster;

cluster.isWorker = false;
cluster.isMaster = true;
复制代码

那么接下来的问题是: NODE_UNIQUE_ID从哪里来?

1.2 NODE_UNIQUE_ID从哪里来的?

internal/cluster/master.js文件中搜索NODE_UNIQUE_ID ----> 上层为createWorkerProcess函数 ----> 上层为cluster.fork函数

master.js源码中相关部分

const { fork } = require('child_process');

cluster.workers = {}

var ids = 0;

cluster.fork = function(env) {
  const id = ++ ids;
  const workerProcess = createWorkerProcess(id, env);
  const worker = new Worker({
    id: id,
    process: workerProcess
  });
  cluster.workers[worker.id] = worker;
  return worker
}

function createWorkerProcess(id, env) {
  const workerEnv = { ...process.env, ...env, NODE_UNIQUE_ID: `${id}` };
  
  return fork(args, {
    env: workerEnv
  })
}
复制代码

结论: 变量NODE_UNIQUE_ID是在主进程fork子进程时传递进去的参数,因此采用cluster.fork创建的子进程是一定包含NODE_UNIQUE_ID的,而直接使用child_process.fork的子进程是没有NODE_UNIQUE_ID

并且, NODE_UNIQUE_ID将作为主进程中存储活跃的工作进程对象的键值

2. 主进程中是否存在TCP服务器, 如果有, 什么时候创建的?

继续描述一下这个问题的由来:

const cluster = require('cluster');
const http = require('http');

if (cluster.isMaster) {
  // 根据cpu核心数出fork相同数量的子进程
} else {
  // 用http模块创建server监听某一个端口
}
复制代码

并没有在cluster.isMaster条件语句中创建服务器, 也没有提供服务器相关的路径,接口。而主进程又需要承担代理服务器的 职责,那么主进程中是否存在TCP服务器?

我们来猜猜看可能的步骤

  • 子进程会执行http.createServer

  • http模块会调用net模块, 因为http.Server继承net.Server

  • 同时侦听端口, 创建net.Server实例, 创建的实例调用listen(port), 等待链接

这时如果主进程要创建服务器就需要把创建服务器相关信息给主进程, 继续猜测

  • 假设主进程已经拿到了服务器相关的信息, 主进程自己来创建

  • 后面的fork子进程就不用自己创建了,而是从主进程中get到相关数据

既然要在主进程需要得到完整的创建服务器相关信息, 那么很可能在net模块listen相关方法中进行处理

2.1 在源码中找答案

github net 模块源码

Server.prototype.listen找找看,什么时候把服务器相关信息传递给主进程了?

Server.prototype.listen = function(...args) {
  // 无视其他的判断逻辑, 直达它的内心!
  if (成功) {
    listenInCluster()
    return this
  } else {
    // 无视
  }
}
复制代码

总的来说就是: Server.prototype.listen函数中,在成功进入条件语句后所有的情况都执行了listenInCluster函数后返回

接下来看listenInCluster函数

function listenInCluster(server, 创建服务器需要的数据) {
  if (cluster === undefined) cluster = require('cluster')
  // 判断是否是主进程
  if (cluster.isMaster) {
    server._listen2(创建服务器需要的数据)
    return
  }
  // 创建服务器需要的数据
  const serverQuery = {
      address: address,
      port: port,
      addressType: addressType,
      fd: fd,
      flags,
    };
  // 只剩下子进程
  cluster._getServer(server, 创建服务器需要的数据, listenOnMasterHandle);
 
  function listenOnMasterHandle(err, handle) {
    server._handle = handle
    server._listen2(创建服务器需要的数据)
  }
}
复制代码

按照前面的推断: 子进程会给主进程发送创建server需要的数据, 主进程去创建

所以接下来去看cluster模块的child._getServer函数

cluster._getServer = function(obj, options, cb) {
  // 组装发送的数据
  const message = {
    act: 'queryServer',
    ...options,
  }
  // 发送数据
  send(message, (reply, handle) => {
  
  })
}
复制代码

那么接下来主进程就应该对queryServer作出想要的处理

具体可以看cluster/master.js

const RoundRobinHandle = require('internal/cluster/round_robin_handle');

const handles = new Map()

function onmessage(message, handle) {
  if (message.act === 'queryServer') {
    queryServer(worker, message)
  }
}
queryServer(worker, message) {
  const key = `${message.address}:${message.port}:${message.addressType}:` +
              `${message.fd}:${message.index}`;
  const constructor = RoundRobinHandle
  
  let handle = new constructor(创建服务器相关信息)
  
  handles.set(key, handle);
}
复制代码

终于要到终点了:

internal/cluster/round_robin_handle.js

function RoundRobinHandle(创建服务器相关信息) {
  this.server = net.createServer()
  this.server.listen(.....)
}
复制代码

2.2 主进程在cluster模式下如何创建服务器的结论

主进程fork子进程, 子进程中有显式创建服务器的操作,但实际上在cluster模式下, 子进程是把创建服务器所需要的数据发送给主进程, 主进程来隐式创建TCP服务器

流程图

3. 为什么多个子进程可以监听同一个端口?

这个问题可以转换为: 子进程中有没有也创建一个服务器,同时侦听某个端口呢?

其实,上面的源码分析中可以得出结论:子进程中确实创建了net.Server对象,可是它没有像主进程那样在libuv层构建socket句柄,子进程的net.Server对象使用的是一个假句柄来'欺骗'使用者端口已侦听

3.1 首先要明确默认的调度策略: round-robin

这部分可以参考文章Node.js V0.12 新特性之 Cluster 轮转法负载均衡

主要就是说:Node.js v0.12引入了round-robin方式, 用轮转法来分配请求, 每个子进程的获取的时间的机会都是均等的(windows除外)

源码在internal/cluster/master.js

var schedulingPolicy = {
  'none': SCHED_NONE,
  'rr': SCHED_RR
}[process.env.NODE_CLUSTER_SCHED_POLICY];

if (schedulingPolicy === undefined) {
  // FIXME Round-robin doesn't perform well on Windows right now due to the
  // way IOCP is wired up.
  schedulingPolicy = (process.platform === 'win32') ? SCHED_NONE : SCHED_RR;
}

cluster.schedulingPolicy = schedulingPolicy;
复制代码

3.2 证明子进程拿到的是假句柄

上面说明了:默认的调度策略是round-robin, 那么子进程将创建服务器的数据发送给主进程, 当主进程发送创建服务器成功的消息后,子进程会执行回调函数

源码在internal/cluster/child.js _getServer中

cluster._getServer = function(obj, options, cb) {
  const indexesKey = [address,
                      options.port,
                      options.addressType,
                      options.fd ].join(':');
  send(message, (reply, handle) => {
    if (typeof obj._setServerData === 'function')
      obj._setServerData(reply.data);
      
    // 这里可以反推出主进程返回的handle为null
    if (handle)
      shared(reply, handle, indexesKey, cb);  // Shared listen socket.
    else
      rr(reply, indexesKey, cb);              // Round-robin.
  });
}
复制代码

rr函数, 注意这里的回调函数其实就是net模块中的listenOnMasterHandle方法

function rr(message, indexesKey, cb) {
  const key = message.key
  const handle = { close, listen, ref: noop, unref: noop };
  handles.set(key, handle)
  // 将假句柄传递给上层的net.Server
  cb(0, handle)
}
复制代码

所以结论是这样:子进程压根没有创建底层的服务端socket做侦听,所以在子进程创建的HTTP服务器侦听的端口根本不会出现端口复用的情况

3.3 子进程没有创建底层socket, 如何接收请求和发送响应呢?

显而易见:主进程的服务器中会创建RoundRobinHandle决定分发请求给哪一个子进程,筛选出子进程后发送newconn消息给对应的子进程

4. 请求分发策略 RoundRobin

源码见internal/cluster/round_robin_handle

module.exports = RoundRobinHandle

function RoundRobinHandle(创建服务器需要的参数) {
  // 存储空闲的子进程
  this.free = []
  // 存放待处理的用户请求
  this.handles = []
}
// 负责筛选出处理请求的子进程
RoundRobinHandle.prototype.distribute = function(err, handle) {
  this.handles.push(handle)
  const worker = this.free.shift()
  
  if (worker) {
    this.handoff(worker)
  }
}
// 获取请求,并通过IPC发送句柄handle和newconn消息,等待子进程返回
RoundRobinHandle.prototype.handoff = function(worker) {
  const handle = this.handles.shift()
  
  if (handle === undefined) {
    this.free.push(worker)
    return
  }
  
  const message = { act: 'newconn', key: this.key }
  
  sendHelper(worker.process, message, handle, reply => {
    if (reply.accepted)
      handle.close();
    // 某个子进程办事不力,给下一个子进程再试试  
    else
      this.distribute(0, handle)  
    this.handoff(worker)  
  })
}
复制代码

参考

Nodejs cluster模块深入探究

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
关于NodeJS点餐系统源码,需要先明确一些概念。NodeJS是运行在服务器端的JavaScript语言,它具有事件驱动、非阻塞I/O等特点,因此可以轻松地开发高性能的网络应用程序。 点餐系统,是指通过网络进行点餐的系统。这种系统需要与用户互动,提供菜单、下单、结算等功能,并与后台进行交互,包括更新菜单、订单管理、支付等。 在开发NodeJS点餐系统源码时,主要需要考虑以下几个方面: 1. 前端界面设计:包括菜单展示、购物车、订单确认、支付等界面设计,需要考虑用户体验,易用性和美观度。 2. 后台接口设计:需要定义前端和后台交互的接口,包括获取菜单、添加菜品、下单、结算等功能。这些接口需要满足RESTful风格,保证接口清晰明确,易于维护。 3. 数据库设计:点餐系统需要存储菜单、订单、用户信息等数据,需要设计合理的数据库结构,保证数据安全可靠。 4. 系统架构:NodeJS点餐系统需要考虑分布式架构,提高系统的可扩展性、高可用性等。 在实现上述方面时,我们可以采用一些现有的技术,比如Express框架、Sequelize ORM库、MySQL数据库等。这些技术都可以轻松地与NodeJS集成,提高开发效率和代码质量。 总之,NodeJS点餐系统源码的开发需要综合考虑前端界面、后台接口、数据库设计和系统架构等方面,并采用现有技术和工具提高开发效率和代码质量。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值