Genluo / Precipitation-and-Summary

梳理体系化的知识点&沉淀日常开发和学习
MIT License
16 stars 1 forks source link

cluster #51

Open Genluo opened 5 years ago

Genluo commented 5 years ago

文档部分

1、集群cluster工作原理?

工作进程是由child_process.fork()方法创建,因此他们可以使用IPC和父进程通信,从而使各进程交替处理连接服务,也就是常见的主从模型

Node中处理多进程有这两种方案:

普通Node进程和cluster作业进程差异的情况有三种:

  1. server.listen({fd: 7})由于文件描述符“7”是传递给父进程的,这个文件被监听后,将文件句柄(handle)传递给工作进程,而不是文件描述符“7”本身。
  2. server.listen(handle) 明确监听句柄,会导致工作进程直接使用该句柄,而不是和父进程通信。
  3. server.listen(0) 正常情况下,这种调用会导致server在随机端口上监听。但在cluster模式中,所有工作进程每次调用listen(0)时会收到相同的“随机”端口。实质上,这种端口只在第一次分配时随机,之后就变得可预料。如果要使用独立端口的话,应该根据工作进程的ID来生成端口号。

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

2、什么是"惊群现象"?

最初的Nodejs多进程模型是这样实现的:master进程创建socket,绑定到某个地址以及端口后,自身不调用listen来监听连接以及accept连接,而是将该socket的fd传递到fork出来的worker进程,worker接收到fd后在调用listen,accept新的连接,但是一个新的连接到来最终只能被一个worker进程accept再做处理,至于是哪个worker能够accept到,开发者完全无法预知以及干预,这势必造成一个新连接到来时,多个worker进程产生竞争,最终由胜出的worker获取连接

例如下面这种情况的产生:

const net = require('net');
const fork = require('child_process').fork;

// 内部方法,不建议调用
var handle = net._createServerHandle('0.0.0.0', 3000);

for (var i = 0; i < 4; i++) {
  fork('./worker').send({}, handle);
}
const net = require('net');
process.on('message', function (m, handle) {
  start(handle);
});

var buf = 'hello nodejs';
var res = ['HTTP/1.1 200 OK', 'content-length:' + buf.length].join('\r\n') + '\r\n\r\n' + buf;

function start(server) {
  server.listen();
  server.onconnection = function (err, handle) {
    console.log('got a connection on worker, pid = %d', process.pid);
    var socket = new net.Socket({
      handle: handle
    });
    socket.readable = socket.writable = true;
    socket.end(res);
  }
}

2、为什么使用cluster可以使得多进程监听同一个端口,这是如何实现多进程共享端口的?

net.js源码中的listen方法通过listenInCluster方法来区分是父进程还是子进程,子进程的server拿到的是围绕的TCPWrapper,当调用listen方法时并不会执行任何操作,所以在子进程中调用listen方法并不会绑定端口,因而也并不会报错。也就是说在worker进程中调用listen不会执行任何操作,也就是cluster是个集大成者,整个模块的改动为了适应多进程的cluster的处理。

3、 cluster的负载均衡策略

在v6.0版本之前,cluster的调用策略采用的是cluster.SCHED_NONE(依赖于操作系统),SCHED_NODE理论上来说性能最好(Ferando Micalli写过一篇Node.js 6.0版本的cluster和iptables以及nginx性能对比的文章,点此访问)但是从实际角度发现,在请求调度方面会出现不太均匀的情况(可能出现8个子进程中的其中2到3个处理了70%的连接请求)。因此在6.0版本中Node.js增加了cluster.SCHED_RR(round-robin:时间片轮转法),目前已成为默认的调度策略(除了windows环境),可以通过设置cluster的schedulingPolicy属性来更改使用的负载均衡策略

cluter.schedulingPolicy = cluster.SCHED_NONE;
cluster.schedulingPolicy = cluster.SCHED_RR;

和具体的操作系统有关

node内部维护两个队列,一个free队列记录当前可用的worker,另一个handles队列记录需要处理的TCP请求,当新的请求到达的时候,父进程将请求暂存handles队列,从free队列中出队一个worker,进入worker处理(handoff)阶段

worker处理阶段首先从handles队列出队一个请求,然后通过进程通信的方式通知自worker进行请求处理,当worker接收到通信消息后发送ack信息,继续响应handles队列中请求任务,当worker无法接受请求时,父进程负责重新地调度worker进行处理

通过给各个服务器分配一定的权重,每次选出权重最大的,给其权重减 1,直到权重全部为 0 后,按照此时生成的序列轮询,参考资料,

4、进程通信

matert通过fork创建子进程,他们之间通过IPC内部进程通信通道实现通信,操作系统的进程间通信方式主要有这几种:

具体可以通过看上一篇博文来详细学习进程间通信

5、node中的如何建立IPC通道

// server.ts

import net, { Socket } from 'net';
import os from 'os';
import path from 'path';

const tmpDir = os.homedir();
const sockPath = path.join(tmpDir, 'miday.sock');

const deal = (socket: Socket) => {
  // 当发起连接时候,传递socket进行处理
  socket.on('data', (buf) => {
    console.log(buf);
  })
  socket.write('dingidng');
}

const server = net.createServer(deal);
server.listen(sockPath);
// client通信
import net, { Socket } from 'net';
import os from 'os';
import path from 'path';

const tmpDir = os.homedir();
const sockPath = path.join(tmpDir, 'miday.sock');

const socket = net.connect(sockPath);
socket.on('data', () => {
  console.log('client收到请求');
});
socket.write('client发送消息');
const WriteWrap = process.binding('stream_wrap').WriteWrap;
var cp = require('child_process');

var worker = cp.fork(__dirname + '/worker.js');
var channel = worker._channel;

channel.onread = function (len, buf, handle) {
    if (buf) {
        console.log(buf.toString())
        channel.close()
    } else {
        channel.close()
        console.log('channel closed');
    }
}

var message = { hello: 'worker',  pid: process.pid };
var req = new WriteWrap();
var string = JSON.stringify(message) + '\n';
channel.writeUtf8String(req, string, null);
}
const WriteWrap = process.binding('stream_wrap').WriteWrap;
const channel = process._channel;

channel.ref();
channel.onread = function (len, buf, handle) {
    if (buf) {
        console.log(buf.toString())
    }else{
        process._channel.close()
        console.log('channel closed');
    }
}

var message = { hello: 'master',  pid: process.pid };
var req = new WriteWrap();
var string = JSON.stringify(message) + '\n';
channel.writeUtf8String(req, string, null);

6、如果进程之间没有父子关系,如何实现任意进程之间的通信嘞?

参考文档

7、master、worker内部通信细节

开发过程中我们通常使用process.on("message", fn)来实现进程间通信,那么我们提到master进程和worker进程在server实例创建过程中,也是通过IPC通道进行通信的。那么为什么不会对我么的开发造成影响:

当发送的消息包含cmd字段的,并且该字段以NODE_作为前缀,那这个消息会被视为内部保留消息,不会通过message事件抛出,但可以通过监听internalMessage进行捕获

8、代码片段

(1) 超时直接kill子进程

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

8、本地进程通信方式

参考

Genluo commented 5 years ago

我们可以这样理解指针和句柄关系:

指针:永远是指向一个地方,但是指针和指向的实例并没有什么联系,如果指针指向的实例如果搬走了,那么指针将指向一个空的地址,甚至是一个非法的地址 句柄:有一个传送门(垃圾收集器)记录这个对象的地址,如果有一天传送中心让这个对象换到其他的地方,并更新了所有指向这个对象的传送门,那么这样你通过句柄再访问这个对象的时候,那么还是这个对象

当一个对象没有句柄的引用时,那么这个对象将会被垃圾收集器回收

Handle 概念

在 V8 中,内存分配都是在 V8 的 Heap 中进行分配的,JavaScript 的值和对象也都存放在 V8 的 Heap 中。这个 Heap 由 V8 独立的去维护,失去引 用的对象将会被 V8 的 GC 掉并可以重新分配给其他对象。而 Handle 即是对 Heap 中对象的引用。V8 为了对内存分配进行管理,GC 需要对 V8 中的 所有对象进行跟踪,而对象都是用 Handle 方式引用的,所以 GC 需要对 Handle 进行管理,这样 GC 就能知道 Heap 中一个对象的引用情况,当一个对象的 Handle 引用发生改变的时候,GC 即可对该对象进行回收或者移动。因此,V8 编程中必须使用 Handle 去引用一个对象,而不是直接通过 C ++ 的方式去获取对象的引用,直接通过 C++ 的方式去引用一个对象,会使得该对象无法被 V8 管理。

Handle 分为 Local 和 Persistent 两种。 从字面上就能知道,Local 是局部的,它同时被 HandleScope 进行管理。 persistent,类似与全局的,不受 HandleScope 的管理,其作用域可以延伸到不同的函数,而 Local 是局部的,作用域比较小。 Persistent Handle 对象需要 Persistent::New, Persistent::Dispose 配对使用,类似于 C++ 中 new 和 delete。 Persistent::MakeWeak 可以用来弱化一个 Persistent Handle,如果一个对象的唯一引用 Handle 是一个 Persistent,则可以使用 MakeWeak 方法来弱化该引用,该方法可以触发 GC 对被引用对象的回收。

Genluo commented 5 years ago

简单来讲:开启多进程时候端口疑问讲解:如果多个Node进程监听同一个端口时会出现 Error:listen EADDRIUNS的错误,而cluster模块为什么可以让多个子进程监听同一个端口呢?原因是master进程内部启动了一个TCP服务器,而真正监听端口的只有这个服务器,当来自前端的请求触发服务器的connection事件后,master会将对应的socket具柄发送给子进程。