Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cluster #51

Open
Genluo opened this issue Sep 6, 2019 · 2 comments
Open

cluster #51

Genluo opened this issue Sep 6, 2019 · 2 comments
Labels
Node Node相关

Comments

@Genluo
Copy link
Owner

Genluo commented Sep 6, 2019

文档部分

  • cluster集群

    • Worker类: 通过cluster.fork方法返回一个Worker类实例,主进程中可以通过cluster.workers来获取worker类实例,工作进程中可以通过cluster.worker获取当前worker类实例

      • 事件

        • disconnect
        • error
        • exit
        • listening
        • message:针对特定的工作进程
        • online:针对特定的工作进程
      • 方法

        • disconnect():在一个工作进程中,调用此方法会关闭所有的server并等待这些的server的close事件执行,然后关闭IPC通道,在主进程中调用,会给工作进程发送一个内部消息,导致工作进程自身调用.disconnect(),处理网连接后关闭,会更新exitedAfterDisconnect和触发disconnect事件,只针对服务端连接,注意不要和process.disconnect搞混,

        • isConnected()

        • isDead():当工作进程被终止时(包括自动退出或者发送信号),这个方法返回true,否则返回false

        • kill([signal="SIGTERM"]):注意与process.kill进行区分

        • send():主进程调用这个方法会发送消息给具体的工作进程。还有一个等价的方法是ChildProcess.send()

          工作进程调用这个方法会发送消息给主进程。还有一个等价方法是process.send()

      • 属性

        • exitedAfterDisconnect:当调用kill或者disconnect方法时被设置,其余都是undefined
        • id
        • process:返回Worker对象
        • Worker: 当前工作进程的引用
    • 事件

      • disconnect
      • exit:任何一个工作进程关闭
      • fork:当新的工作进程被fork
      • listening
      • message
      • online:当新建一个工作进程后,工作进程应当响应一个online消息给主进程,当主进程收到online消息后粗发这个事件,fork和online事件的不同之处在于:前者是在主进程新建工作进程后触发,后者是在工作进程运行时触发
      • setup
    • 方法

      • disconnect():在每个工作进程中调用disconnect()方法
      • fork():只能通过主进程调用
      • setupMaster()
    • 属性

      • isWorker
      • isMaster
      • schedulingPolicy:调度策略
      • settiings
      • workers:一个hash表,存储活跃的工作进程对象

1、集群cluster工作原理?

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

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

  • 循环法:主进程负责监听端口,接收连接后将连接循环分发给 工作进程,在分发中使用了一些内置技巧防止工作进程任务过载
  • 主进程创建监听socket后发送给感兴趣的工作进程,由工作进程负责直接接收连接

普通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获取连接

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

  • master进程
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);
}
  • worker进程
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);
  }
}
  • 多个进程之间会竞争accept一个连接,产生惊群现象,效率比较低
  • 由于无法控制一个新的连接由那个进程来处理,必然导致各worker进程之间的负载非常不均衡

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;
  • SCHED_NONE

和具体的操作系统有关

  • SCHED_RR,时间片轮转地调度算法

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

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

  • Nginx上采用的加权轮转法

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

4、进程通信

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

  • 共享内存
  • 消息传递
  • 信号量
  • 管道

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

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

  • 通过文件socket进行通信
// 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发送消息');
  • master进程
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);
}
  • Worker 进程
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、如果进程之间没有父子关系,如何实现任意进程之间的通信嘞?

  • 手动建立IPC通道
  • 使用文件socket进行通信,信息共享

参考文档

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、本地进程通信方式

  • socket通信
    • 文件socket
    • tcp类型的socket

参考

  • 《Nodejs设计模式》
@Genluo Genluo added the Node Node相关 label Sep 6, 2019
@Genluo
Copy link
Owner Author

Genluo commented Sep 6, 2019

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

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

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

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
Copy link
Owner Author

Genluo commented Sep 6, 2019

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Node Node相关
Projects
None yet
Development

No branches or pull requests

1 participant