1. 概述

我们都知道,JavaScript是一门单线程语言,单个Node.js实例运行在单个线程中,为了充分利用多核系统,有时需要启用一组Node.js进程去处理负载任务。cluster模块可以创建共享服务器端口的子进程。

一般多线程工作存在一个主进程和多个子进程,主进程负责派生(创建)子进程,主进程越简单越好,防止崩溃。子进程负责处理逻辑,也就是干活。

多进程对服务器来说,安全,性能高(更充分利用CPU)

  1. 普通程序不能创建进程,只有系统进程才能创建进程

  2. 进程是分裂出来的,只有主进程可以分裂。

  3. 分裂出来的两个进程,执行的是同一套代码

  4. 父子进程之间可以共享句柄, 端口就是一种句柄

const cluster = require('cluster');
// 分叉,
if (cluster.isMaster) { // 如果是主进程就分裂
    cluster.fork();
};

有几个cpu就开几个进程,不是越多越好

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

if (cluster.isMaster) {
    for (const i = 0; i < os.cpus().length; i++) {
        cluster.fork();
    }
    console.log('我是主进程');
    cluster.on('exit', (worker, code, signal) => {
        console.log(`工作进程 ${worker.process.pid} 已退出`);
    });
} else {
    console.log('我是子进程')
}

主进程 = 守护进程

子进程 = 工作进程

一般子进程是用来干活的,主进程做管理。

const cluster = require('cluster');
const os = require('os');
const process = require('process');

if (cluster.isMaster) {
    for (const i = 0; i < os.cpus().length; i++) {
        cluster.fork();
    }
    console.log('我是主进程');
} else {
    console.log(process.pid);
    http.createServer((req, res) => {
        res.write('aaaa');
        res.end();
    }).listen(8080);
    console.log('端口号8080');
}
  1. 上面的程序并不会报错,因为子进程可以共享端口号。

  2. process.pid: 进程的pid,作为进程的唯一区分。

  3. 进程调度,多个进程,只有第一个进程工作被塞满了才会启用第二个进程,第二个也满了,再启用第三个,

进程的开销和调度非常的耗费性能,计算机的运算是很快的,当肉眼可见时就已经表示很慢了。

多进程不会造成死锁,死锁的意思是对文件的读写进行时,其它程序对文件的访问会限制。

工作进程由child_process.fork()方法创建,因此它们可以使用IPC和父进程通信,从而使各进程交替处理连接服务。

cluster模块支持两种分发连接的方法。

第一种方法(也是除Windows外所有平台的默认方法)是循环法,由主进程负责监听端口,接收新连接后再将连接循环分发给工作进程,在分发中使用了一些内置技巧防止工作进程任务过载。

第二种方法是,主进程创建监听socket后发送给感兴趣的工作进程,由工作进程负责直接接收连接。

理论上第二种方法应该是效率最佳的。 但在实际情况下,由于操作系统调度机制的难以捉摸,会使分发变得不稳定。 可能会出现八个进程中有两个分担了70%的负载。

因为server.listen()将大部分工作交给主进程完成,因此导致普通Node.js进程与cluster工作进程差异的情况有三种:

server.listen({fd: 7})因为消息会被传给主进程,所以父进程中的文件描述符7将会被监听并将句柄传给工作进程,而不是监听文件描述符 7 指向的工作进程。

server.listen(handle)显式地监听句柄,会导致工作进程直接使用该句柄,而不是和主进程通信。

server.listen(0)正常情况下,这种调用会导致server在随机端口上监听。

但在cluster模式中,所有工作进程每次调用listen(0)时会收到相同的“随机”端口。 实质上,这种端口只在第一次分配时随机,之后就变得可预料。 如果要使用独立端口的话,应该根据工作进程的ID来生成端口号。

Node.js不支持路由逻辑。 因此在设计应用时,不应该过分依赖内存数据对象,例如session和登陆等。

由于各工作进程是独立的进程,它们可以根据需要随时关闭或重新生成,而不影响其他进程的正常运行。 只要有存活的工作进程,服务器就可以继续处理连接。 如果没有存活的工作进程,现有连接会丢失,新的连接也会被拒绝。Node.js不会自动管理工作进程的数量,而应该由具体的应用根据实际需要来管理进程池。

虽然cluster模块主要用于网络相关的情况,但同样可以用于其他需要工作进程的情况。

2. Worker

Worker对象包含了关于工作进程的所有的公共的信息和方法。 在主进程中,可以使用cluster.workers来获取它。 在工作进程中,可以使用cluster.worker来获取它。

1. disconnect 事件

类似于cluster.on('disconnect')事件,但特定于此工作进程。

cluster.fork().on('disconnect', () => {
  // 工作进程已断开连接。
});

2. error 事件

此事件和child_process.fork()提供的事件相同。

在一个工作进程中,也可以使用process.on('error')

3. exit 事件

类似于cluster.on('exit')事件,但特定于此工作进程。

const worker = cluster.fork();
worker.on('exit', (code, signal) => {
  if (signal) {
    console.log(`工作进程已被信号 ${signal} 杀死`);
  } else if (code !== 0) {
    console.log(`工作进程退出,退出码: ${code}`);
  } else {
    console.log('工作进程成功退出');
  }
});

4. listening 事件

类似于cluster.on('listening')事件,但特定于此工作进程, 此事件不会在工作进程中触发。

cluster.fork().on('listening', (address) => {
  // 工作进程正在监听。
});

5. message 事件

类似于cluster.on('message')事件,但特定于此工作进程。

在工作进程内,也可以使用process.on('message')

以下是一个使用消息系统的示例。 它在主进程中对工作进程接收的HTTP请求数量保持计数:

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

if (cluster.isMaster) {

  // 跟踪 http 请求。
  let numReqs = 0;
  setInterval(() => {
    console.log(`请求的数量 = ${numReqs}`);
  }, 1000);

  // 对请求计数。
  function messageHandler(msg) {
    if (msg.cmd && msg.cmd === 'notifyRequest') {
      numReqs += 1;
    }
  }

  // 启动 worker 并监听包含 notifyRequest 的消息。
  const numCPUs = require('os').cpus().length;
  for (let i = 0; i < numCPUs; i++) {
    cluster.fork();
  }

  for (const id in cluster.workers) {
    cluster.workers[id].on('message', messageHandler);
  }

} else {

  // 工作进程有一个 http 服务器。
  http.Server((req, res) => {
    res.writeHead(200);
    res.end('你好世界\n');

    // 通知主进程接收到了请求。
    process.send({ cmd: 'notifyRequest' });
  }).listen(8000);
}

5. online 事件

类似于cluster.on('online')事件,但特定于此工作进程, 此事件不会在工作进程中触发。

cluster.fork().on('online', () => {
  // 工作进程已上线。
});

6. worker.disconnect()

在一个工作进程内,调用此方法会关闭所有的server,并等待这些serverclose事件执行,然后关闭IPC管道。

在主进程内,会给工作进程发送一个内部消息,导致工作进程自身调用.disconnect()

会设置.exitedAfterDisconnect

当一个server关闭后,它将不再接收新的连接,但新连接会被其他正在监听的工作进程接收。 已建立的连接可以正常关闭。 当所有连接都关闭后,通往该工作进程的IPC管道将会关闭,允许工作进程优雅地死掉,详见server.close()

以上情况只针对服务端连接,工作进程不会自动关闭客户端连接,disconnect方法在退出前并不会等待客户端连接关闭。

在工作进程中,也存在process.disconnect,但它不是这个函数,它是disconnect()

因为长时间运行的服务端连接可能阻止工作进程断开连接,可以采用发送消息的方法,让应用采取相应的动作来关闭连接。 也可以通过设置超时,当disconnect事件在某段时间后仍没有触发时关闭工作进程。

if (cluster.isMaster) {
  const worker = cluster.fork();
  let timeout;

  worker.on('listening', (address) => {
    worker.send('shutdown');
    worker.disconnect();
    timeout = setTimeout(() => {
      worker.kill();
    }, 2000);
  });

  worker.on('disconnect', () => {
    clearTimeout(timeout);
  });

} else if (cluster.isWorker) {
  const net = require('net');
  const server = net.createServer((socket) => {
    // 连接永远不会结束。
  });

  server.listen(8000);

  process.on('message', (msg) => {
    if (msg === 'shutdown') {
      // 将所有与服务器的连接优雅地关闭。
    }
  });
}

7. worker.id

每一个新衍生的工作进程都会被赋予自己独一无二的编号,这个编号就是储存在id里面。

当工作进程还存活时,这个编号可以作为在cluster.workers中的索引。

3. disconnect 事件

在工作进程的IPC管道被断开后触发。 可能导致事件触发的原因包括:工作进程优雅地退出、被杀死、或手动断开连接(如调用worker.disconnect())。

disconnectexit事件之间可能存在延迟。 这些事件可以用来检测进程是否在清理过程中被卡住,或是否存在长时间运行的连接。

cluster.on('disconnect', (worker) => {
  console.log(`工作进程 #${worker.id} 已断开连接`);
});

4. exit 事件

当任何一个工作进程关闭的时候,cluster模块都将会触发exit事件。

这可以用于重启工作进程(通过再次调用.fork())。

cluster.on('exit', (worker, code, signal) => {
  console.log('工作进程 %d 关闭 (%s). 重启中...',
              worker.process.pid, signal || code);
  cluster.fork();
});

5. fork 事件

当新的工作进程被衍生时,cluster模块将会触发fork事件。 可以被用来记录工作进程活动,并产生一个自定义的超时。

const timeouts = [];
function errorMsg() {
  console.error('连接出错');
}

cluster.on('fork', (worker) => {
  timeouts[worker.id] = setTimeout(errorMsg, 2000);
});
cluster.on('listening', (worker, address) => {
  clearTimeout(timeouts[worker.id]);
});
cluster.on('exit', (worker, code, signal) => {
  clearTimeout(timeouts[worker.id]);
  errorMsg();
});

6. listening 事件

当一个工作进程调用listen()后,工作进程上的 server 会触发listening事件,同时主进程上的cluster也会触发listening事件。

事件句柄使用两个参数来执行,其中worker包含了工作进程对象,address包含了以下的连接属性:addressportaddressType。 当工作进程同时监听多个地址时,这些参数非常有用。

cluster.on('listening', (worker, address) => {
  console.log(
    `工作进程已连接到 ${address.address}:${address.port}`);
});

addressType可选值包括:

4 (TCPv4)

6 (TCPv6)

-1 (Unixsocket)

udp4orudp6(UDP v4v6)

7. message 事件

当集群主进程从任何工作进程接收到消息时触发。

8. online 事件

当衍生一个新的工作进程后,工作进程应当响应一个上线消息。 当主进程收到上线消息后将会触发此事件。fork事件和online事件的区别在于,当主进程衍生工作进程时触发fork,当工作进程运行时触发online

cluster.on('online', (worker) => {
  console.log('工作进程被衍生后响应');
});

9. setup 事件

每当.setupMaster()被调用时触发。

settings 对象是.setupMaster()被调用时的cluster.settings对象,并且只能查询,因为在一个时间点内.setupMaster()可以被调用多次。

如果精确度十分重要,则使用cluster.settings

10. cluster.isMaster

如果该进程是主进程,则为true。这是由process.env.NODE_UNIQUE_ID决定的。 如果process.env.NODE_UNIQUE_ID未定义,则isMastertrue

11. cluster.isWorker

如果该进程不是主进程,则为true(与cluster.isMaster相反)

12. cluster.worker

当前工作进程对象的引用。 对于主进程则无效。

13. cluster.workers

这是一个哈希表,储存了活跃的工作进程对象,使用id作为键名。 这使得可以方便地遍历所有工作进程。 只能在主进程中调用。

工作进程断开连接以及退出后,将会从cluster.workers里面移除。 这两个事件的先后顺序并不能预先确定。 但可以保证的是,cluster.workers的移除工作在disconnectexit两个事件中的最后一个触发之前完成。

// 遍历所有工作进程。
function eachWorker(callback) {
  for (const id in cluster.workers) {
    callback(cluster.workers[id]);
  }
}
eachWorker((worker) => {
  worker.send('通知所有工作进程');
});

使用工作进程的唯一id是定位工作进程最简单的方式。

socket.on('data', (id) => {
  const worker = cluster.workers[id];
});

转载须知

如转载必须标明文章出处文章名称文章作者,格式如下:

转自:【致前端 - zhiqianduan.com】 cluster子进程  "隐冬"
请输入评论...