深入node源码系列之一: cluster深入原理分析

1,414 阅读17分钟

一、使用cluster示例

在node中,启动一个server非常简单,简单的几行代码就能启动一个服务器。而且node实例也是单线程的,并使用事件循环(event loop)来做到高并发。当我们为了让我们服务器的内核数都利用起来的时候(提升qps吞吐),那么其中就有一个cluster共我们使用,使用方法也很简单:

const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;
// 当fork出子进程后,子进程也会重新执行这个文件
console.log('Begin to run js file. pid = ' + process.pid);

// 默认程序第一次加载的是master逻辑,所以isMaster一定为true(后面会分析node源码)
if (cluster.isMaster) {
  console.log(`主进程 ${process.pid} 正在运行`);

  // 衍生工作进程。
  for (let i = 0; i < numCPUs; i++) {
    cluster.fork();
  }
  cluster.on('exit', (worker, code, signal) => {
    console.log(`工作进程 ${worker.process.pid} 已退出`);
  });
} else {
  // 工作进程可以共享任何 TCP 连接。
  // 在本例子中,共享的是 HTTP 服务器。
  http.createServer((req, res) => {
    res.writeHead(200);
    res.end(`response from worker ${process.pid}`);
  }).listen(9000);

  console.log(`工作进程 ${process.pid} 已启动`);
}

按照上面的做法,一个多进程的node服务就搭建起来了。

(1)提出几个问题

但是,喜欢源码之美的我和你,一定会疑惑这怎么就把几个进程给启动了。 可能会列下面的问题:

  1. 如果进了if,那这是怎么进的else,并且新建了好几个子进程中的server的呢?
  2. 主进程和子进程各自的作用是什么?他们是如何协调工作的?
  3. 为什么子进程中创建server的时候,监听了同一个端口,但没有报端口占用的错误呢?

随着分析的深入,会有更多的疑问出现在面前,不着急,先把上面的框架问题解决。

先把结论抛出来。

(2)从架构层面做简单解答

问题一答案:

在cluster内部,在执行我们业务代码中的fork方法时,会把当前这个js文件传入,当做fork的第一个参数(具体可以看下child_process.fork api文档),所以,子进程在创建时,会同样执行这个js文件。

问题二答案:

这就涉及到主进程和子进程之间的关系了。先上图

image.png

主进程负责请求的收集和分发,也就是说: 一个请求来了后,会首先打到master,然后master运用某种方式转发到具体工作进程去处理。(这里的Master角色是不是跟nginx的反向代理很像?)。

在这时,一个细节问题就出来了,我们并没有在代码中创建master啊! 那我们可以大胆猜测,是cluster帮我们创建了。后面会详细的通过源码来讨论。

问题三答案:

cluster内部的处理方法是:当创建第一个工作进程的时候,如果发现主进程没有启动server,那就创建一个server,并且把那个端口进行监听。然后用方法不让子进程监听具体端口。这样,现象就是: 主进程启动了一个server,并且启动了端口监听;子进程启动一个server,但是没有去监听具体的端口。 然后,通过一些方法,把主进程和子进程进行连接通信,等有请求过来的时候,主进程握有子进程的句柄,就自然能够调用子进程进行处理,并在处理完成后把结果返回给主进程。

其实,通过上面的问答,对整个cluster就有了一个整体的框架观了。接下来,我将通过一个一个的例子来剖析作者为什么要这么做,这样,对cluster的理解就会更深一层。

现在如果要自己也实现一个多进程的的架构,要咱们处理呢?

二、 我能想到的实现多进程的方式

下面开始吧!

要自己实现,那么需要知道是需要通过child_process.fork()来创建子进程。cluster也是底层调用的这个方法,前面也说过。

2.1 直接创建子进程server

例1:

// master.js

 const {fork} = require('child_process');
 const os = require('os');
// 比如是4核cpu
for(let i=0;i< os.cpus().length; i++) {
     fork('./worker.js');
 }
 
 
 // woker.js
 
 const http = require('http');

http.createServer((req, res) => {
    res.send('创建子进程');
}).listen(3000);

通过执行这个例子,可以发现报端口已被占用的错误了。 原因是通常来说Tcp链接中同一台机器的同一个端口只可以被一个进程使用。所以这种方式创建多个子进程是行不通的。

2.2 通过监听多个端口创建server

例2:

监听同一个端口号不行,那么我们就监听多个端口

// master.js

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

 for(let i=0;i< 1; i++) {
     let port = Math.ceil(Math.random() * 5000);
     fork('./worker.js', [port]);
 }
 
 
 // woker.js
 
const port = process.argv[2];
 const http = require('http');

 http.createServer((req, res) => {
     res.send('创建子进程');
 }).listen(port);

上面的这个方法,可以解决问题。但是nodejs大神们为什么不用呢?是因为端口本身就有限,且这不是一种优雅的方式,而且更重要的是现在无法做到请求的分发。 (其实,可以使用连接池来做,用连接池来调度子进程,这也是一种实现方式,但cluster并不是这么实现的

2.3 创建主进程server和子进程server处理

例3:

// master.js

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

const workers = [];
for (let i =0; i< os.cpus().length; i++) {
    const worker = fork('./worker.js');
    workers.push(worker);
}

// 创建一个tcp server,并且把tcp server传给工作进程
const server = net.createServer();
server.listen(3000, () => {
    console.log('向worker进程发送');
    workers.forEach(worker => {
        worker.send('SERVER', server);
    });
    // 阻止 server 接受新的连接并保持现有的连接
    server.close();
})


// worker.js

const http = require('http');

const httpServer = http.createServer((req, res) => {

    res.end(`Hello worker by ${process.pid}`);
});

process.on('message', (msg, tcpServer) => {
    console.log('工作进程监听message, pid:' + process.pid);
    if (msg === 'SERVER') {
        tcpServer.on('connection', socket => {
            // 把主进程server的socket传给子进程
            httpServer.emit('connection', socket);
        })
    }
})

例3是master把句柄(标识资源的引用,内部包含了指向对象的文件描述符)传给工作进程,这种方式相对来说最接近cluster的实现方式。但是,这种情况下,无法做到有效的负载均衡,4个进程去争抢过来的请求,结果是可能某个进程处理了97%以上的请求。

上面几种方式都有其缺陷。也相信大家通过上面的几个实现方式,对多进程能有所感触了。那么分析正式开始。

三、源码分析

这里想了很久怎么分享,因为自己理解的也不是那么那么的透彻,其中有些c++的底层逻辑我并说不太好,强说可能说错,但如果说的太粗,那么只知道骨架,没有血肉填充,总会有抓不到的感觉。所以,我还是在尽力讲明白总体思路的前提下,对js部分尽量讲的细致一些,c++的部分先说个大概,以后对c++这部分内部逻辑熟悉后再往上加。各位有补充的,那非常感谢。

3.1 解决客户端请求到来之前的多进程初始化问题

  1. 我们的cluster业务代码初始加载的情形

上来就这句判断,那么我们去node源码中看一看

if (cluster.isMaster) {

// cluster.js

'use strict';
// 在导入模块时,会首先判断NODE_UNIQUE_ID, 这个其实是子进程的id,后面会看到在子进程创建中,对其赋值
const childOrPrimary = 'NODE_UNIQUE_ID' in process.env ? 'child' : 'primary';
// 默认把primary(主进程)导出
module.exports = require(`internal/cluster/${childOrPrimary}`);

那么去primary.js中看一看

const handles = new SafeMap();
cluster.isWorker = false;
// 这里可以看到主动赋值为true,同时这个属性将会在未来版本去掉,用isPrimary代替
cluster.isMaster = true; // Deprecated alias. Must be same as isPrimary.
cluster.isPrimary = true;
cluster.Worker = Worker;
cluster.workers = {};
cluster.settings = {};
cluster.SCHED_NONE = SCHED_NONE;  // Leave it to the operating system.
cluster.SCHED_RR = SCHED_RR;

再沿着业务代码逻辑继续向下走, 进入了fork逻辑, 那么也就进入了primary.js中的fork方法

cluster.fork = function(env) {
  // 进行一些setting操作, 并且监听internalMessage事件
  cluster.setupPrimary();
  const id = ++ids;
  // 开始创建worker进程,使用child_process 的fork
  const workerProcess = createWorkerProcess(id, env);
  // 实例化Worker
  const worker = new Worker({
    id: id,
    process: workerProcess
  });

  worker.on('message', function(message, handle) {
    cluster.emit('message', this, message, handle);
  });
  // ......
  // 下面的逻辑后面再看

createWorkerProcess()方法主要是fork出子进程了。(画重点)


function createWorkerProcess(id, env) {
  // NODE_UNIQUE_ID 这个变量出现了!!也就是只有创建子进程时才会出现。
  const workerEnv = { ...process.env, ...env, NODE_UNIQUE_ID: `${id}` };
  /* node --harmony script.js --version
    process.execArgv 的结果:
      ['--harmony']

    process.argv 的结果:
      ['/usr/local/bin/node', 'script.js', '--version']
  */
  const execArgv = [...cluster.settings.execArgv];
  const debugArgRegex = /--inspect(?:-brk|-port)?|--debug-port/;
  const nodeOptions = process.env.NODE_OPTIONS || '';

  // 与inspect相关,这个细节可以略过
  // ......
  
  
  // 开始执行fork,注意fork里执行的还是那个我们写的业务代码,这就对应上了为什么会再执行我们业务代码中的else逻辑。
  // 所以,我们会发现开启子进程的业务代码文件会被多次执行,
  // 而每次执行,它的process.pid实际上是不一样的。

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

创建出子进程后,通过new Worker 创建了worker实例。

看到这,相信开头的第一个问题已经从源码角度解决了。再看问题2: 主进程和子进程是如何协调工作的?在文章开头的时候起了一个头,现在详细分析。开篇的时候留下的疑问是主进程的server是如何创建的?

关键的关键在于创建服务时候的listen方法

目前,我们已经知道已经创建出了主进程和几个子进程,而且从我们的业务代码中,知道创建子进程成功后,就需要创建http或者net服务了。

http.createServer((req, res) => {
    res.writeHead(200);
    res.end(`response from worker ${process.pid}`);
  }).listen(9000);

继续走到node的源码中去,其实http.createServer调用的也是net.createServer, 调用的也是net的listen方法。

Server.prototype.listen = function(...args) {
  const normalized = normalizeArgs(args);
  let options = normalized[0];
  const cb = normalized[1];
  
  // ......
  const backlogFromArgs =
    // (handle, backlog) or (path, backlog) or (port, backlog)
    toNumber(args.length > 1 && args[1]) ||
    toNumber(args.length > 2 && args[2]);  // (port, host, backlog)

  options = options._handle || options.handle || options;
  
  // ....
  
  // 在我们业务代码中监听的是port:9000,所以会进这段逻辑
  if (typeof options.port === 'number' || typeof options.port === 'string') {
    validatePort(options.port, 'options.port');
    backlog = options.backlog || backlogFromArgs;
    // start TCP server listening on host:port
    if (options.host) {
      lookupAndListen(this, options.port | 0, options.host, backlog,
                      options.exclusive, flags);
    } else { // Undefined host, listens on unspecified address
      // Default addressType 4 will be used to search for primary server
     // 并没有传host,所以会走到这里
      listenInCluster(this, null, options.port | 0, 4,
                      backlog, undefined, options.exclusive);
    }
    return this;
  }
  
 }
 
 // listenInCluster 逻辑
 
 function listenInCluster(server, address, port, addressType,
                         backlog, fd, exclusive, flags) {
      exclusive = !!exclusive;

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

      if (cluster.isPrimary || exclusive) {
        // Will create a new handle
        // _listen2 sets up the listened handle, it is still named like this
        // to avoid breaking code that wraps this method
        // 如果是主进程,就直接进行监听,设置新的句柄,目前不会走这里的逻辑
        server._listen2(address, port, addressType, backlog, fd, flags);
        return;
      }
      // 目前会走这里逻辑,因为我们还是在子进程逻辑中
      const serverQuery = {
        address: address,
        port: port,
        addressType: addressType,
        fd: fd,
        flags,
      };

      // Get the primary's server handle, and listen on it
      // 子进程 将Server实例直接传入函数,去执行./child.js中的_getServer,
      // 因为在子进程中,所以会走到node-master/lib/internal/cluster/child.js中

      // 并且,会继续执行 queryServer(),在primary.js中,开始创建RoundRobinHandle
      cluster._getServer(server, serverQuery, listenOnPrimaryHandle);

      function listenOnPrimaryHandle(err, handle) {
        err = checkBindError(err, port, handle);

        if (err) {
          const ex = exceptionWithHostPort(err, 'bind', address, port);
          return server.emit('error', ex);
        }

        // Reuse primary's server handle
        server._handle = handle;
        // _listen2 sets up the listened handle, it is still named like this
        // to avoid breaking code that wraps this method
        server._listen2(address, port, addressType, backlog, fd, flags);
      }
}
 

现在在子进程中创建server,这条逻辑不要乱 所以,就会进入child.js中去执行_getServer 方法,并且我们假设是在类unix系统中,不考虑win32

// obj 是server实例
cluster._getServer = function(obj, options, cb) {
  let address = options.address;

  // Resolve unix socket paths to absolute paths
 
  if (options.port < 0 && typeof address === 'string' &&
      process.platform !== 'win32')
    address = path.resolve(address);

  const indexesKey = ArrayPrototypeJoin(
    [
      address,
      options.port,
      options.addressType,
      options.fd,
    ], ':');

  let indexSet = indexes.get(indexesKey);

  if (indexSet === undefined) {
    indexSet = { nextIndex: 0, set: new SafeSet() };
    indexes.set(indexesKey, indexSet);
  }
  const index = indexSet.nextIndex++;
  indexSet.set.add(index);

  const message = {
    act: 'queryServer',
    index,
    data: null,
    ...options
  };

  message.address = address;

  // Set custom data on handle (i.e. tls tickets key)
  if (obj._getServerData)
    message.data = obj._getServerData();
  // 子进程发送message后,执行回调,
  // send函数中有这句: message = util._extend({ cmd: 'NODE_CLUSTER' }, message);
  // 这会触发node内部的internalMessage事件。这里需要对照codejs API提一下
  send(message, (reply, handle) => {
    if (typeof obj._setServerData === 'function')
      obj._setServerData(reply.data);

    if (handle) {
      // Shared listen socket
      shared(reply, { handle, indexesKey, index }, cb);
    } else {
      // Round-robin.
      rr(reply, { indexesKey, index }, cb);
    }
  });

  obj.once('listening', () => {
    cluster.worker.state = 'listening';
    const address = obj.address();
    message.act = 'listening';
    message.port = (address && address.port) || options.port;
    send(message);
  });
};

请细致的看上面方面中的send方法。 send方法很重要,他会把message包装一下,继而发给主进程,主进程如果没有创建server,那么就会进行创建,并且监听端口。

// child.js

function send(message, cb) {
  return sendHelper(process, message, null, cb);
}

// internal/cluster/utils.js
function sendHelper(proc, message, handle, cb) {
  if (!proc.connected)
    return false;

  // Mark message as internal. See INTERNAL_PREFIX in lib/child_process.js
  // 执行 internalMessage 事件
  message = { cmd: 'NODE_CLUSTER', ...message, seq };

  if (typeof cb === 'function')
    callbacks.set(seq, cb);

  seq += 1;
  return proc.send(message, handle);
}

{cmd: 'NODE_CLUSTER'} 是node内部的事件标识,具体解释在nodejs api中:

子 Node.js 进程有一个自己的 process.send() 方法,允许子进程发送消息回父进程。

当发送 {cmd: 'NODE_foo'} 消息时有一种特殊情况。 cmd 属性中包含 NODE_ 前缀的消息是预留给 Node.js 内核内部使用的,将不会触发子进程的 'message' 事件。 相反,这种消息可使用 'internalMessage' 事件触发,且会被 Node.js 内部消费。 应用程序应避免使用此类消息或监听 'internalMessage' 事件,因为它可能会被更改且不会通知。

传到主进程,主进程在fork函数中进行监听的,是的,就是前面分析的child_process中的fork,只是前面的时候没有把fork函数全部写出来。现在补充完整

cluster.fork = function(env) {
  // 进行一些setting操作, 并且监听internalMessage事件
  cluster.setupPrimary();
  const id = ++ids;
  // 开始创建worker进程,使用child_process 的fork
  const workerProcess = createWorkerProcess(id, env);
  // 实例化Worker
  const worker = new Worker({
    id: id,
    process: workerProcess
  });
  // 设置监听每个子进程的事件
  worker.on('message', function(message, handle) {
    cluster.emit('message', this, message, handle);
  });

  worker.process.once('exit', (exitCode, signalCode) => {
    /*
     * Remove the worker from the workers list only
     * if it has disconnected, otherwise we might
     * still want to access it.
     */
    if (!worker.isConnected()) {
      removeHandlesForWorker(worker);
      removeWorker(worker);
    }

    worker.exitedAfterDisconnect = !!worker.exitedAfterDisconnect;
    worker.state = 'dead';
    worker.emit('exit', exitCode, signalCode);
    cluster.emit('exit', worker, exitCode, signalCode);
  });

  worker.process.once('disconnect', () => {
    /*
     * Now is a good time to remove the handles
     * associated with this worker because it is
     * not connected to the primary anymore.
     */
    removeHandlesForWorker(worker);

    /*
     * Remove the worker from the workers list only
     * if its process has exited. Otherwise, we might
     * still want to access it.
     */
    if (worker.isDead())
      removeWorker(worker);

    worker.exitedAfterDisconnect = !!worker.exitedAfterDisconnect;
    worker.state = 'disconnected';
    worker.emit('disconnect');
    cluster.emit('disconnect', worker);
  });

  // 当消息到达后,消息对象要进行过滤,如果message.cmd的值如果以NODE为前缀,它将响应一个内部事件internalMessage;
  // 如果message.cmd值为NODEHANDLE,它将取出 message.type值和得到的文件描述符一起还原出一个对应的对象。
  // 子进程根据message.type创建对应的TCP服务器对象
  worker.process.on('internalMessage', internal(worker, onmessage));
  process.nextTick(emitForkNT, worker);
  // 把创建的worker进程存储到cluster.workers中,
  // 供以后Round-robin调用
  cluster.workers[worker.id] = worker;
  return worker;
};

// onmessage方法
function onmessage(message, handle) {
  // 主进程收到子进程在创建http/net服务器时,执行listen方法时的逻辑
  const worker = this;

  // 在创建时message.act会传入 'queryServer'
  const fn = methodMessageMapping[message.act];

  if (typeof fn === 'function')
     // 执行 queryServer方法
    fn(worker, message);
}



// queryServer 方法执行

function queryServer(worker, message) {
  // Stop processing if worker already disconnecting
  if (worker.exitedAfterDisconnect)
    return;

  const key = `${message.address}:${message.port}:${message.addressType}:` +
              `${message.fd}:${message.index}`;
  let handle = handles.get(key);
  // 第一次执行的时候,handle是undefined,所以会走下面的逻辑
  if (handle === undefined) {
    let address = message.address;

    // Find shortest path for unix sockets because of the ~100 byte limit
    if (message.port < 0 && typeof address === 'string' &&
        process.platform !== 'win32') {

      address = path.relative(process.cwd(), address);

      if (message.address.length < address.length)
        address = message.address;
    }

    // UDP is exempt from round-robin connection balancing for what should
    // be obvious reasons: it's connectionless. There is nothing to send to
    // the workers except raw datagrams and that's pointless.
    // 开始创建 RoundRobinHandle实例,会在父进程中生成实例。
    // 这里是非常重要的。也就找到了主进程的server是怎么创建的。
    if (schedulingPolicy !== SCHED_RR ||
        message.addressType === 'udp4' ||
        message.addressType === 'udp6') {
       // 如果创建UDP服务
      handle = new SharedHandle(key, address, message);
    } else {
    // 创建TCP服务,走Round-Robin算法,负责处理请求时,主进程server选取哪个子进程server进行处理。
      handle = new RoundRobinHandle(key, address, message);
    }

    handles.set(key, handle);
  }

  if (!handle.data)
    handle.data = message.data;

  // Set custom server data
  // 执行RoundRobinHandle实例的add方法
  handle.add(worker, (errno, reply, handle) => {
    const { data } = handles.get(key);

    if (errno)
      handles.delete(key);  // Gives other workers a chance to retry.

    // 又重新发送给子进程, 但hanle为null,也就是说并没有把主进程的句柄传给子进程。
    send(worker, {
      errno,
      key,
      ack: message.seq,
      data,
      ...reply
    }, handle);
  });
}

是很有必要看一下RoundRobinHandle做了什么的。

function RoundRobinHandle(key, address, { port, fd, flags }) {
  this.key = key;
  this.all = new SafeMap();
  this.free = new SafeMap();
  this.handles = [];
  this.handle = null;
  // 在这里创建了主进程中的server,并且监听在子进程中定义的端口
  this.server = net.createServer(assert.fail);
  // 判断文件描述符
  if (fd >= 0)
    this.server.listen({ fd });
  else if (port >= 0) {
    // 业务代码中传入了port
    this.server.listen({
      port,
      host: address,
      // Currently, net module only supports `ipv6Only` option in `flags`.
      ipv6Only: Boolean(flags & constants.UV_TCP_IPV6ONLY),
    });
  } else
    this.server.listen(address);  // UNIX socket path.

  this.server.once('listening', () => {
    this.handle = this.server._handle;
    // 当执行onconnection时,会执行this.distribute
    this.handle.onconnection = (err, handle) => this.distribute(err, handle);
    this.server._handle = null;
    this.server = null;
  });
}

因为执行this.server.listen(),所以又走到了我们开始时的Server.prototype.listen方法,同样也会进 listenInCluster方法,只是这次, 走的是主进程逻辑(完整代码在前面有,可以回过头看看):


if (cluster.isPrimary || exclusive) {
    // Will create a new handle
    // _listen2 sets up the listened handle, it is still named like this
    // to avoid breaking code that wraps this method
    // 如果是主进程,就直接进行监听,设置新的句柄,目前不会走这里的逻辑
    server._listen2(address, port, addressType, backlog, fd, flags);
    return;
  }

server._listen2 中会创建主进程的TCP连接句柄,这个句柄会在以后客户端请求的时候用到。具体会执行 setupListenHandle方法,这个后面会讲,这里知道主进程走这个函数的功能就好了。

小结: 到这一阶段,创建了子进程的server,并且也在主进程中创建了一个server,并且主进程也监听了我们定义的端口。

下一步需要解决端口问题: 现在主进程的server已经监听好我们预设的端口,但是并没有把主进程server句柄传给子进程。 那么子进程server需要做:

  • 子进程不要进行端口的监听。否则会出现例1中出现的端口被占用,或者例2中的开多个端口,这都与cluster的初衷不符。

当创建好主进程的server并且监听了端口后, 子进程还在等着回调函数的执行,当执行后面的

 handle.add(worker, (errno, reply, handle) => {
     // ....
 }

会去调用RoundRobinHandle中的add原型方法:

RoundRobinHandle.prototype.add = function(worker, send) {
  assert(this.all.has(worker.id) === false);
  this.all.set(worker.id, worker);

  const done = () => {
    if (this.handle.getsockname) {
      const out = {};
      this.handle.getsockname(out);
      // TODO(bnoordhuis) Check err.
      // send方法就是调用add方法的那个回调函数,进而向子进程发送一个事件。
      send(null, { sockname: out }, null);
    } else {
      console.log('【round_robin_handle.js】 send(null, null, null);');
      send(null, null, null);  // UNIX socket.
    }
    // 将当前的子进程推入this.free中(现在只是推入,等客户端请求时,会取出子进程使用)
    this.handoff(worker);  // In case there are connections pending.
  };

  if (this.server === null)
    return done();

  // Still busy binding.
  this.server.once('listening', done);
  this.server.once('error', (err) => {
    send(err.errno, null);
  });
};

发送操作中, child_process.js中的target.send会执行,进而最终让上面所说的回调函数触发:

// child.js

 send(message, (reply, handle) => {
    if (typeof obj._setServerData === 'function')
      obj._setServerData(reply.data);

    if (handle) {
      // Shared listen socket
      shared(reply, { handle, indexesKey, index }, cb);
    } else {
      // Round-robin.
      // 这时走了这里,进入rr的方法执行
      rr(reply, { indexesKey, index }, cb);
    }
  });
  
  // rr 方法
  
  function rr(message, { indexesKey, index }, cb) {
  if (message.errno)
    return cb(message.errno, null);

  let key = message.key;

  function listen(backlog) {
    // TODO(bnoordhuis) Send a message to the primary that tells it to
    // update the backlog size. The actual backlog should probably be
    // the largest requested size by any worker.
    return 0;
  }

  function close() {
    // lib/net.js treats server._handle.close() as effectively synchronous.
    // That means there is a time window between the call to close() and
    // the ack by the primary process in which we can still receive handles.
    // onconnection() below handles that by sending those handles back to
    // the primary.
    if (key === undefined)
      return;

    send({ act: 'close', key });
    handles.delete(key);
    removeIndexesKey(indexesKey, index);
    key = undefined;
  }

  function getsockname(out) {
    if (key)
      ObjectAssign(out, message.sockname);

    return 0;
  }

  // Faux handle. Mimics a TCPWrap with just enough fidelity to get away
  // with it. Fools net.Server into thinking that it's backed by a real
  // handle. Use a noop function for ref() and unref() because the control
  // channel is going to keep the worker alive anyway.
  // 这里做了hack操作, fake了一个假的handle对象,欺骗上层的调用者。当listen函数执行时,
  // 返回的永远是0
  const handle = { close, listen, ref: noop, unref: noop };

  if (message.sockname) {
    handle.getsockname = getsockname;  // TCP handles only.
  }

  assert(handles.has(key) === false);
  handles.set(key, handle);
  // 把一个假的handle传给了cb
  cb(0, handle);
}
  
 // 这里的cb是 net.js中的listenOnPrimaryHandle回调
 
 function listenOnPrimaryHandle(err, handle) {
    // 主进程创建完成后,把数据回传给子进程,然后子进程fack了一个假的handle,传给了这个回调函数
    err = checkBindError(err, port, handle);

    if (err) {
      const ex = exceptionWithHostPort(err, 'bind', address, port);
      return server.emit('error', ex);  
    }

    // Reuse primary's server handle
    server._handle = handle;
    // _listen2 sets up the listened handle, it is still named like this
    // to avoid breaking code that wraps this method
    // 子进程实际上不需要再设置listened handle, 但是为了代码命名完整性,也调用这个函数
    // 在这个函数中做区分
    server._listen2(address, port, addressType, backlog, fd, flags);
  }
  
  

这时,就会走上面说过的setupListenHandle方法,这是因为有这句:

Server.prototype._listen2 = setupListenHandle; 

setupListenHandle函数分为两部分逻辑,一部分是上面提到过的,创建TCP句柄,另一部分功能是fack一个handle,实际上子进程就不会再进行端口监听了。

function setupListenHandle(address, port, addressType, backlog, fd, flags) {
  debug('setupListenHandle', address, port, addressType, backlog, fd);

  // If there is not yet a handle, we need to create one and bind.
  // In the case of a server sent via IPC, we don't need to do this.
  // 当子进程在调用进这个方法的时候,已经有_handle了,所以不会再进行handle的创建
  if (this._handle) {
    debug('setupListenHandle: have a handle already');
  } else {
  // 主进程会调用
    debug('setupListenHandle: create a handle');

    let rval = null;

    // Try to bind to the unspecified IPv6 address, see if IPv6 is available
    if (!address && typeof fd !== 'number') {
      // 实际上是创建了主进程的handle句柄,创建好了tcp连接通路
      rval = createServerHandle(DEFAULT_IPV6_ADDR, port, 6, fd, flags);

      if (typeof rval === 'number') {
        rval = null;
        address = DEFAULT_IPV4_ADDR;
        addressType = 4;
      } else {
        address = DEFAULT_IPV6_ADDR;
        addressType = 6;
      }
    }

    if (rval === null)
      rval = createServerHandle(address, port, addressType, fd, flags);

    if (typeof rval === 'number') {
      const error = uvExceptionWithHostPort(rval, 'listen', address, port);
      process.nextTick(emitErrorNT, this, error);
      return;
    }
    this._handle = rval;
  }

  this[async_id_symbol] = getNewAsyncId(this._handle);
  // 子进程也会绑定onconnection事件
  this._handle.onconnection = onconnection;
  this._handle[owner_symbol] = this;

  // Use a backlog of 512 entries. We pass 511 to the listen() call because
  // the kernel does: backlogsize = roundup_pow_of_two(backlogsize + 1);
  // which will thus give us a backlog of 512 entries.
  // 这里,主进程的_handle是真实存在的,定义主线程最多可积压511个请求,跟redis也是相同的,实际上就是为了防止不一致可能出现的问题
  // 而子进程因为handle是fack的,所以不管传多少,都是返回0
  const err = this._handle.listen(backlog || 511);

  if (err) {
    const ex = uvExceptionWithHostPort(err, 'listen', address, port);
    this._handle.close();
    this._handle = null;
    defaultTriggerAsyncIdScope(this[async_id_symbol],
                               process.nextTick,
                               emitErrorNT,
                               this,
                               ex);
    return;
  }

  // Generate connection key, this should be unique to the connection
  // 主进程创建时返回 '6::::9000', 子进程创建时返回 '4:null:9000'
  this._connectionKey = addressType + ':' + address + ':' + port;

  // Unref the handle if the server was unref'ed prior to listening
  if (this._unref)
    this.unref();

  defaultTriggerAsyncIdScope(this[async_id_symbol],
                             process.nextTick,
                             emitListeningNT,
                             this);
}

当走到这里的时候,整个多进程服务基本搭建就完成了,当然,还没有讲客户端请求来了后,主进程和子进程的链路。

3.2 客户端请求主进程、子进程处理链路

3.1完成后,实际上还有一步,子进程向主进程emit ’listening‘, server的listen方法执行的时候会触发 listening事件。

this.server.once('listening', () => {
    this.handle = this.server._handle;
    // 客户端请求时 onconnection会执行
    this.handle.onconnection = (err, handle) => this.distribute(err, handle);
    this.server._handle = null;
    this.server = null;
});

now, 一个客户端请求打过来了----连接事件首先触发:

// round_robin_handle.js
RoundRobinHandle.prototype.distribute = function(err, handle) {
  // 先将客户端请求push进handles中
  ArrayPrototypePush(this.handles, handle);
  // eslint-disable-next-line node-core/no-array-destructuring
  // round-robin选择worker进程的逻辑:从this.free中获取一个空闲的进程
  const [ workerEntry ] = this.free; // this.free is a SafeMap

  if (ArrayIsArray(workerEntry)) {
    const { 0: workerId, 1: worker } = workerEntry;
    this.free.delete(workerId);
    this.handoff(worker);
  }
};

// handoff
RoundRobinHandle.prototype.handoff = function(worker) {
  if (!this.all.has(worker.id)) {
    return;  // Worker is closing (or has closed) the server.
  }
  // 从请求列表中(this.handles)获取一个
  const handle = ArrayPrototypeShift(this.handles);
  // 如果获取不到了,就说明主进程没有缓存的客户端请求了,
  // 那么将当前的子进程推入this.free中,等待下一轮客户端请求。
  if (handle === undefined) {
    this.free.set(worker.id, worker);  // Add to ready queue again.
    return;
  }
  // 选出要处理的子进程后, 向这个子进程发送处理消息。
  const message = { act: 'newconn', key: this.key };
  // handle -> 主进程的Tcp句柄  传给前面选定的worker进程
  // sendHelper 之前已经见过,包装成message = { cmd: 'NODE_CLUSTER', ...message, seq }; 发送给子进程。
  sendHelper(worker.process, message, handle, (reply) => {
    if (reply.accepted)
      handle.close();
    else
      this.distribute(0, handle);  // Worker is shutting down. Send to another. 也就是说如果当前的worker没有接受,则需要重新发送给另一个

    this.handoff(worker);
  });
};

这时,就会send到子进程,子进程开始处理了。


// child.js

function onmessage(message, handle) {
    // 如果message.act是newconn,说明是worker进程获取到的新请求,需要处理
    if (message.act === 'newconn')
      onconnection(message, handle);
    else if (message.act === 'disconnect')
      ReflectApply(_disconnect, worker, [true]);
}
  
function onconnection(message, handle) {
  // 'null:9000:4:undefined:0'
  const key = message.key;
  // 获取之前定义的fake handle
  const server = handles.get(key);
  // 表示可以接受本次请求
  const accepted = server !== undefined;
  // 这里主要是告诉主进程,我这个子进程可以接受此次请求并进行处理
  send({ ack: message.seq, accepted });

  if (accepted)
    // 这里面会创建一个new Socket实例, 走c++底层逻辑,至于底层逻辑交互暂不清楚
    server.onconnection(0, handle);
}

// net.js 

function onconnection(err, clientHandle) {
  debugger;
  const handle = this;
  const self = handle[owner_symbol];

  debug('onconnection');

  if (err) {
    self.emit('error', errnoException(err, 'accept'));
    return;
  }

  if (self.maxConnections && self._connections >= self.maxConnections) {
    clientHandle.close();
    return;
  }

  const socket = new Socket({
    handle: clientHandle,
    allowHalfOpen: self.allowHalfOpen,
    pauseOnCreate: self.pauseOnConnect,
    readable: true,
    writable: true
  });

  self._connections++;
  socket.server = self;
  socket._server = self;

  DTRACE_NET_SERVER_CONNECTION(socket);
  // 构建node层的socket对象,并触发connection事件,完成底层socket与node net模块的连接与请求打通
  self.emit('connection', socket);
}

具体的socket怎么通过c++进行处理的,因不熟悉c++, 无法详细的说。 但是,按照逻辑,一定是:

主进程server监听着端口,当客户端请求来的时候 ---> 从this.free中获取到空闲的子进程,并将TCP handle 发送到子进程 ----> 子进程判断是否接受请求处理----> 如果接受,则告知主进程,同时进行处理 ——---> 子进程net server 创建一个新socket实例,带着传进来的TCP handle , 跟底层c++ 进行交互 ---> 处理 ----> 处理结果返回主进程,主进程把数据返回给客户端。


通过上面两个大部分的分析,知道了总体的通路。我不知道的最后这部分c++底层逻辑,也欢迎大家告知~~

希望通过这1000多行的纯手敲分析,能给大家带来一点点启发,欢迎点赞/阅读/评论~~