《深入浅出Node.js》笔记

Node 简介

Node 是单线程的,基于事件驱动的,是基于事件循环进行执行的,还具有异步 I/O、跨平台等特点

单线程和多线程

单线程串行依次执行,单线程的最大好处是不用像多线程编程那样处处在意状态的同步问题,这里没有死锁,也没有线程上下文交换所带来的性能上的开销。

多线程并行完成,多线程的好处是可以有多条线程来处理不同的任务,可以充分利用 CPU,而无须像单线程那样需要等待一个任务完成后,才能继续下一个任务。

模块机制

Node 采用的是 CommonJS 规范,采用同步 require

为啥是同步 require ?

因为 Node 遵循 CommonJS 规范。当时模块加载的方案只有 CommonJS 和 AMD,
而考虑到 Node 的模块是来自本地文件系统,加载更快,所以选择了 CommonJS

  • CommonJS 是同步加载,同步执行
  • AMD 是异步加载,异步执行(前置依赖)
  • CMD 是异步加载,同步执行(就近依赖)
  • ES6 模块是异步加载,同步执行

CommonJS 对模块的定义分为三部分:模块引用模块定义模块标识

模块分为两类:

Node 自身提供的核心模块

核心模块在 Node 源码的编译过程中,被编译进了二进制执行文件。不需要完整文件定位和编译执行

用户编写的文件模块(包括 node_modules)

文件模块是在运行时动态加载,需要完整的路径分析、文件定位、编译执行

Node 对引入过的模块会进行缓存,缓存的是编译执行后的对象,模块加载会优先从缓存中加载
核心模块的缓存检查先与文件模块的缓存检查

模块加载执行的步骤:路径分析文件定位编译执行

路径分析

根据 require 的路径,区分核心模块、文件模块、自定义模块

核心模块的寻址

如果是核心模块,则直接返回该模块(如果不是第一次引入该模块,则会从缓存中引入)

(路径形式)文件模块的寻址

将 require 中的路径转换为真实路径,并以真实路径为索引,将编译执行后的结果存放到缓存中,以使下次加载时更快

(非路径)自定义模块的寻址

当前目录下的 node_modules 目录
父目录下的 node_modules 目录
父目录的父目录下的 node_modules 目录
逐级向上查找,一直到根目录下的 node_modules 目录

文件定位步骤

  • 文件扩展名分析

Node 会按 .js、.json、.node 的次序补足扩展名,依次尝试。在尝试过程中,调用 fs 模块同步阻塞式地判断文件是否存在。非 js 文件最好带上文件后缀,能够稍微优化文件定位的效率。

  • 目录分析和包

查找当前目录下的 package.json,通过 JSON.parse() 解析出包描述对象,从中取出 main 属性指定的文件名进行定位

模块编译

定位到具体的文件后,Node 会新建一个模块对象,然后根据路径载入并编译。

.js 文件,通过 fs 同步读取并编译
.node 文件,通过 dlopen() 方法加载最后编译生成的文件
.json 文件,通过 fs 同步读取,用 JSON.parse 解析
其余文件会被当做 js 文件处理

每个编译成功的模块都会将其文件路径作为索引缓存在 Module._cache 对象上

JavaScript 模块的编译

在编译过程中,Node 对获取的 JavaScript 文件内容做了头尾包装。
在头部添加了 (function (exports, require, module, filename, dirname) { \n
在尾部添加了 \n})

1
2
3
4
5
6
(function (exports, require, module, __filename, __dirname) {
var math = require('math')
exports.area = function (radius) {
return Math.PI * radius * radius;
}
})

包装后的代码会通过 runInThisContext() 方法执行(类似于eval)

exportsmodule.exports

exports 是 module.exports 的引用。exports 是通过形参传递的,在函数作用域内修改该值,并不会改变作用域之外的值。

C/C++ 模块的编译

Node 调用 process.dlopen() 方法进行加载和执行,不需要编译

JSON 文件的编译

Node 调用 fs 同步读取 JSON 文件内容之后,用 JSON.parse 解析得到对象,然后将它赋值给模块对象的 exports

核心模块

分为两部分:C/C++编写部分,存放在 Node 项目的 src文件夹;JavaScript 编写部分,存放在 Node 项目,存放在 lib 文件夹

在编译所有 C/C++ 文件之前,编译程序会将所有 JavaScript 模块文件编译为 C/C++ 代码

核心模块的引入过程

require(‘os’) => NativeModule.require(‘os’) => process.binding(‘os’) => get_builtin_moduel(‘node_os’) => NODE_MODULE(node_os, reg_func)

JavaScript 核心模块主要职责

  • 作为 C/C++ 内建模块的封装层和桥接层,供文件模块调用
  • 纯粹的功能模块

总结

Node require 寻址

  • 分析路径,判断引入的模块时核心模块还是文件模块,
  • 如果是核心模块,则直接返回该模块(如果不是第一次引入该模块,则会从缓存中引入)
  • 如果是文件模块(以’./‘、’/‘、’../‘等引入),则首先会根据该模块的父模块,确定该模块的绝对路径,将该模块当做文件处理,依次查找 X,X.js,X.json、X.node,如果找到,则返回该文件,不再继续
  • 将该模块当做文件夹处理,则依次查找’X/package.json’,’X/index.js’,’X/index.json’,’X/index.node’,只要其中有一个存在,就返回该文件,不再继续执行
  • 如果该模块不带路径,根据该模块所在的父模块,确定该模块可能的安装目录。
    依次在每个目录中,将该模块当成文件名或目录名加载。

require 其实是调用了 Module._load 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
Module._load = function(request, parent, isMain) {
// 计算绝对路径
var filename = Module._resolveFilename(request, parent);
// 第一步:如果有缓存,取出缓存
var cachedModule = Module._cache[filename];
if (cachedModule) {
return cachedModule.exports;
// 第二步:是否为内置模块
if (NativeModule.exists(filename)) {
return NativeModule.require(filename);
}
// 第三步:生成模块实例,存入缓存
var module = new Module(filename, parent);
Module._cache[filename] = module;
// 第四步:加载模块
try {
module.load(filename);
hadException = false;
} finally {
if (hadException) {
delete Module._cache[filename];
}
}
// 第五步:输出模块的exports属性
return module.exports;
};

异步 I/O

多线程的代价在于创建线程和执行期线程上下文切换的开销较大。

单线程串行同步执行,会因阻塞 I/O 导致其他资源得不到最优的使用

Node 利用单线程,远离多线程死锁、状态同步等问题;利用异步 I/O ,让单线程远离阻塞,以更好利用 CPU。

Node 的异步 I/O 是 libuv 和线程池(或 IOCP)来实现的

异步I/O过程

由上图可知:当发起一个异步调用时,会封装一个请求对象,JavaScript层传入的参数和当前方法都会被封装在这个请求对象中,回调函数则被设置在这个对象的 oncomplete_sym 属性上,封装完成后,则将该请求对象推入线程池中等待执行。

当线程池中有可用线程时,则会执行请求对象的 I/O 操作,然后将执行完成的结果放在请求对象中,然后通知 IOCP 调用完成,I/O 观察者获取到调用结果,然后事件循环时会从 I/O观察者中取出可用的请求对象,然后从请求对象中取出回调函数和结果,调用执行。

请求对象是异步 I/O 过程中的重要中间产物,所有的状态都保存在这个对象中,包括送入线程池等待执行以及 I/O 操作完毕后的回调处理。

Node 异步 I/O 模型的基本要素是事件循环观察者请求对象I/O 线程池

process.nextTick VS setImmediate

两者十分类似,都是将回调函数延迟执行,但两者又有所不同:

process.nextTick 优先与 setImmediate。前者属于 idle 观察者,后者属于 check 观察者。在事件循环中,idle 观察者先于 I/O 观察者,I/O 观察者先于 check 观察者

process.nextTick 的回调函数保存在一个数组中,setImmediate 的结果是保存在链表中。

process.nextTick 在每轮循环中会将数组中的回调函数全部执行完,setImmediate 在每轮循环中执行链表中的一个回调函数。

异步编程

异步编程的多种方法,请查看另一篇文章

Node 自身提供的 events 模块就是发布/订阅模式的简单实现。Node 默认一个事件不可添加超过10个侦听器。(调用 emitter.setMaxListeners(0)取消这个限制)

内存控制

V8 的内存限制

V8 默认默认堆大小限制是:64位系统 1.4GB,32位系统 0.7GB,这是因为 V8 的内存管理机制一开始只运用于浏览器的应用场景,同时也是考虑到 V8 的垃圾回收效率

可以在程序启动时,添加参数来改变这个限制

1
2
3
node --max-old-space-size=1700 test.js
// 或者
node --max-new-space-size=1024 test.js

V8 的垃圾回收机制

V8 的垃圾回收策略主要基于分代式垃圾回收机制。

将内存分为新生代和老生代

新生代中的对象为存活时间较短的对象

老生代中的对象为存活时间较长或常驻内存的对象

在新生代中,主要通过 Scavenge 算法进行垃圾回收。

新生代中,将内存分为两个空间,一个 From 空间(处于使用状态),一个 To 空间(处于闲置状态)

当开始进行垃圾回收时,检查 From 空间中的存活对象,存活对象会被复制到 To 空间中,而非存活对象将会被释放。完成复制后,From 空间和 To 空间的角色进行对换。

如果一个对象经过多次复制仍然存活,它将会被认为生命周期较长的对象,会晋升到老生代

对象晋升的两个条件:对象经历过 Scavenge 回收、To 空间的内存占用比(25%)超过限制

在老生代中,主要采用了 Mark-Sweep(标记清除) 和 Mark-Compact (标记整理)相结合的方式进行垃圾回收

在老生代中,遍历所有对象,并标记存活对象。随后清除没有被标记的对象。

进行标记清除后,会产生内存空间不连续的状态,因为采用 Mark-Compact 算法,将存活对象往一端移动,移动完成后,直接清理边界外的内存。

内存泄漏

通常,造成内存泄漏的原因有如下几个:

缓存

队列消费不及时

作用域未释放

采用 node-heapdump、node-memwatch 等对内存泄漏进行排查

大内存应用

通过 Node 的内置模块 stream 模块处理大文件。

理解 Buffer

Buffer 是一个典型的 JavaScript 与 C++ 结合的模块,主要用于处理二进制数据。

Buffer 类的实例类似于整数数组,但 Buffer 的大小是固定的、且在 V8 堆外分配物理内存。 Buffer 的大小在被创建时确定,且无法调整。

new Buffer() 等方法已被废弃。通过 Buffer.from(), Buffer.alloc(), and Buffer.allocUnsafe() 等方法创建 Buffer

网络编程

Node 提供了 net、dgram、http、https 这4个模块,分别用于处理 TCP、UDP、HTTP、HTTPS。

TCP

传输控制协议,在 OSI 模型中属于传输层。

OSI 七层模型:应用层、表示层、会话层、传输层、网络层、链路层、物理层

TCP 三次握手(建立连接)

发送端发送 SYN 报文,服务端接收后,向发送端回传 SYN/ACK 报文,发送端最后发送 ACK 报文

TCP 四次挥手(关闭连接)

发送端发送 FIN 报文,接收端接收到后,立即返回 ACK 报文,等待接收端数据全部发送完成后,再向发送端发送 FIN 报文,最后发送端发送 ACK 报文。

为什么客户端最后还要等待2MSL?

  • 保证客户端发送的最后一个ACK报文能够到达服务器,因为这个ACK报文可能丢失,站在服务器的角度看来,我已经发送了FIN+ACK报文请求断开了,客户端还没有给我回应,应该是我发送的请求断开报文它没有收到,于是服务器又会重新发送一次,而客户端就能在这个2MSL时间段内收到这个重传的报文,接着给出回应报文,并且会重启2MSL计时器。
  • 防止类似与“三次握手”中提到了的“已经失效的连接请求报文段”出现在本连接中

为什么建立连接是三次握手,关闭连接却是四次挥手呢?

建立连接时,服务器处于 Listen 状态,收到建立连接的请求(SYN报文)后,把 ACK 和 SYN 放在一个报文里发送给客户端。

关闭连接时,接收到客户端 FIN 报文,表示客户端不再发送数据,但仍然可以接收数据,因此,服务端会立即返回 ACK 报文,通知客户己方已接收到关闭请求,但服务端可能还有数据没发送完。最后会再发一个 FIN 报文给客户端。

参考:TCP 三次握手和四次握手详解

TCP 滑动窗口

窗口是缓存的一部分,用来暂时存放字节流。发送方和接收方各有一个窗口,接收方通过 TCP 报文段(ACK报文)中的窗口字段告诉发送方自己的窗口大小,发送方根据这个值和其它信息设置自己的窗口大小。

发送窗口内的字节都允许被发送,接收窗口内的字节都允许被接收。如果发送窗口左部的字节已经发送并且收到了确认,那么就将发送窗口向右滑动一定距离,直到左部第一个字节不是已发送并且已确认的状态;接收窗口的滑动类似,接收窗口左部字节已经发送确认并交付主机,就向右滑动接收窗口。

接收窗口只会对窗口内最后一个按序到达的字节进行确认,例如接收窗口已经收到的字节为 {31, 33, 34},其中 {31} 按序到达,而 {32, 33} 就不是,因此只对字节 31 进行确认。发送方得到一个字节的确认之后,就知道这个字节之前的所有字节都已经被接收。

TCP 流量控制

流量控制是为了控制发送方发送速率,保证接收方来得及接收。

接收方发送的确认报文中的窗口字段可以用来控制发送方窗口大小,从而影响发送方的发送速率。将窗口字段设置为 0,则发送方不能发送数据。

TCP 拥塞控制

如果网络出现拥塞,分组将会丢失,此时发送方会继续重传,从而导致网络拥塞程度更高。因此当出现拥塞时,应当控制发送方的速率。这一点和流量控制很像,但是出发点不同。流量控制是为了让接收方能来得及接受,而拥塞控制是为了降低整个网络的拥塞程度。

TCP 主要通过四种算法来进行拥塞控制:慢启动、拥塞避免、快重传、快恢复。

发送方需要维护一个叫做拥塞窗口(cwnd)的状态变量,注意拥塞窗口与发送方窗口的区别:拥塞窗口只是一个状态变量,实际决定发送方能发送多少数据的是发送方窗口。

  • 慢启动与拥塞避免

发送的最初执行慢启动,令 cwnd=1,发送方只能发送 1 个报文段;当收到确认后,将 cwnd 加倍,因此之后发送方能够发送的报文段数量为:2、4、8 …

注意到慢启动每个轮次都将 cwnd 加倍,这样会让 cwnd 增长速度非常快,从而使得发送方发送的速度增长速度过快,网络拥塞的可能也就更高。设置一个慢启动门限 ssthresh,当 cwnd >= ssthresh 时,进入拥塞避免,每个轮次只将 cwnd 加 1。

如果出现了超时,则令 ssthresh = cwnd/2,然后重新执行慢启动。

  • 快重传和快恢复

在接收方,要求每次接收到报文段都应该发送对已收到有序报文段的确认(ACK报文)

在发送方,如果收到三个重复确认,那么可以确认下一个报文段丢失。此时执行快重传,立即重传下一个报文段。

在这种情况下,只是丢失个别报文段,而不是网络拥塞,因此执行快恢复,令 ssthresh = cwnd/2 ,cwnd = ssthresh,注意到此时直接进入拥塞避免。

TCP 优化

TCP 针对网络中的小数据包有一定的优化策略:Nagle 算法。会将小数据包存放到缓存区,缓存区数据达到一定数量或一定时间后,才将将其发出。这也就是 TCP 的粘包现象。

其他优化:

  • 启用 TFO (TCP快速打开)
  • 增大TCP的初始拥塞窗口
  • 慢启动重启
  • 消除不必要的数据传输
  • 实现数据的短距离传输(CDN)
  • 复用 TCP 链接(Nginx 负载均衡,有新连接时,检测是否有空闲连接,有则复用这个连接,没有则新建连接)

详细了解:TCP 优化

UDP

用户数据包协议,也属于传输层。UDP 与 TCP 最大的不同是 UDP 不是面向连接的。

TCP VS UDP

协议 连接性 双工性 可靠性 有序性 有界性 拥塞控制 传输速度 量级 头部大小
TCP 面向连接
(Connection oriented)
全双工(1:1) 可靠
(重传机制)
有序
(通过SYN排序)
无, 有粘包情况 20~60字节
UDP 无连接
(Connection less)
n:m 不可靠
(丢包后数据丢失)
无序 有消息边界, 无粘包 8字节

参考:ElemeFE 的 node-interview

HTTP

详细请看 HTTP

WebSocket

Node 与 WebSocket 的完美配合

WebSocket 客户端基于事件的编程模型与 Node 中自定义事件相差无几

WebSocket 实现了客户端与服务端之间的长连接,而 Node 事件驱动的方式十分擅长与大量的客户端保持高并发连接

此外,WebSocket 还具有以下特点

客户端与服务端只建立一个 TCP 连接,可以使用更少的连接

WebSocket 服务器端可以推送数据到客户端,这远比 HTTP 请求响应模式更灵活、更高效

有更轻量级的协议头、减少数据传送量

WebSocket 最早是作为 HTML5 重要特性而出现的。WebSocket 是全新的网络协议,握手部分是由 HTTP 完成的,但并不是基于 HTTP 实现。

WebSocket 协议主要分为两个部分:握手和数据传输。

客户端发起请求,请求头包含 Upgrade 字段和 Connection 字段

Upgrade: websocket

Connection: Upgrade

表示请求服务器升级协议为 WebSocket,其中 Sec-WebSocket-Key 用于安全校验

如果成功,则服务端返回 101,表示允许客户端切换协议

WebSocket 传输数据是数据帧协议的,会将数据封装成一帧或多帧数据,然后逐帧发送。

为了安全考虑,客户端需要对发送的数据帧进行掩码处理,服务器一旦收到无掩码帧,连接将关闭。服务端发送到客户端的数据则无须做掩码处理。同理,客户端如果接收到带掩码的数据帧,连接也将关闭。

对 WebSocket 感兴趣的话,还可以看看这篇文章 WebSocket基本原理和心跳机制

网络服务与安全

Node 在网络安全上提供了 crypto、tls、https 模块。其中 crypto 主要用于加密解密,tls 和 https 是运用于网络。

TLS/SSL

TLS/SSL 是一个公钥/私钥的结构,是一个非对称的结构。
Node 底层采用的是 openssl 实现 TLS/SSL 的,为此要生成公钥和私钥可通过 openssl 完成。

TLS/SSL 引入数字证书(CA)来进一步认证。

HTTPS

HTTPS实质上是添加了加密(SSL和TLS)和认证(数字证书)机制的HTTP

HTTPS采用共享密钥加密和公开密钥加密的混合加密方式

共享密钥加密:又叫对称加密,是指加密、解密都用同一个密钥

公开密钥加密:又叫非对称加密,对外发布公钥,对数据进行加密,然后通过私钥对数据进行解密

HTTPS的加密方式是:对传输的报文数据采用共享密钥进行加密,而共享密钥加密的密钥又被公开密钥进行加密

这样的好处是,避免了公共密钥加密需要耗费大量的CPU和内存资源,传输较慢的问题,同时又能够确保传输的数据能够安全传输

构建 Web 应用

玩转进程

Node 是运行在单个进程的单个线程上。这带来的好处是:程序状态是单一的,在没有多线程的情况下没有锁、线程同步问题,操作系统在调度时也因为较少上下文的切换,可以很好地提高 CPU 的使用率

基本概念

进程和线程都是一个时间段的描述,是CPU工作时间段的描述

进程:CPU 分配资源的最小单位。每个应用至少有一个进程,每个进程都有自己的独立地址空间。进程之间的通信需要以通信的方式(IPC)进行,进程之间相互不影响。

线程:CPU 调度资源的最小单位。每个进程至少有一个线程,线程之间共享同一个进程的数据,一个线程挂掉,意味着整个进程都挂掉了。

死锁、活锁和饥饿

死锁:指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成一种相互等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

活锁:执行的任务没有被阻塞,由于某些条件没有满足,导致一直重复尝试。在单一实体中,执行一个任务,任务失败,继续尝试执行这个任务。在协同中,两个或多个进程相互“礼让”,导致都无法使用资源。

饥饿:某个进程一直等待资源的释放,但资源却被后面一个又一个进程占用,导致了该进程一直无法请求到资源,处于饥饿状态。

多进程架构

基于事件的 Node 服务模型存在两个问题:CPU 的利用率和进程的健壮性。

为此,Node 提供了 child_process 模块,实现了多进程架构,能够充分利用 CPU。

Node 在多进程架构中,采用主从模式,进程分为主进程和工作进程。

主进程不负责具体业务的处理,而是负责调度或管理工作进程。工作进程则负责具体的业务处理。

进程间通信(IPC)

进程间通信的目的是为了让不同的进程能够互相访问资源并进行协调工作。

实现进程间通信的技术有:命名管道、匿名管道、socket、信号量、共享内存、消息队列、Domain Socket 等。

Node 中实现 IPC 通道的是管道(pipe)技术,具体实现由 libuv 提供,在 windows 下由命名管道实现,nix 系统则采用 *Unix Domain Socket 实现。

父进程在实际创建子进程之前,会创建 IPC 通道并监听它,然后才创建出子进程,并通过环境变量( NODE_CHANNEL_FD )告诉子进程这个 IPC 通道的文件描述符。

子进程在启动过程中,根据文件描述符去链接这个已存在的 IPC 通道,从而完成与父进程之间的连接。

句柄传递

如何让服务器都监听到相同的端口呢?

通常的做法是,各个进程监听不同的端口,主进程监听主端口,主进程对外接收所有的网络请求,然后将这些请求代理到不同端口的子进程上。通过代理,可以避免端口不能重复监听的问题,甚至可以在代理进程上做适当的负载均衡。但是由于进程每接收到一个连接,将会用掉一个文件描述符,代理进程的方案会浪费掉一倍数量的文件描述符,而操作系统的文件描述符是有限的,这样会影响系统的扩展能力。

句柄传递的方案因此诞生。句柄是一种可以用来标识资源的引用,它的内部包含了指向对象的文件描述符

比如句柄可以用来标识一个服务端 socket 对象、一个客户端 socket 对象、一个 UDP 套接字、一个管道等。

发送句柄:主进程接收到 socket 请求后,将这个 socket 直接发送给工作进程,而不是重新与工作进程之间建立新的 socket 连接来转发数据。文件描述符浪费的问题可以通过这样的方式解决。

master.js

1
2
3
4
5
6
7
8
var child = require('child_process').fork('child.js')
var server = require('net').createServer();
server.listen(1337, function () {
child.send('server', server) // 发送句柄 server
server.close()
})

child.js

1
2
3
4
5
6
7
8
9
10
11
12
13
var http = require('http')
var server = http.createServer(function (req, res) {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('handle by child, pid is ' + process.pid + '\n')
})
process.on('message', function (m, tcp) { // 接收句柄 tcp server
if (m == 'server') {
tcp.on('connection', function (socket) {
server.emit('connection', socket)
})
}
})

句柄发送与还原

示意图

send() 方法将消息发送到 IPC 管道前,将消息组装成两个对象,一个参数是 handle, 另一个是 message

1
2
3
4
5
message = {
cmd: 'NODE_HANDLE',
type: 'net.Server',
msg: message
}

发送到 IPC 管道中的实际上是要发送的句柄文件描述符,文件描述符实际上是一个整数值。这个 message 对象在写入到 IPC 管道时通过 JSON.stringify() 进行序列化。最终发送到 IPC 通道中的信息都是字符串。send() 方法不能发送任意对象。

连接了 IPC 通道的子进程可以读取父进程发来的消息,将字符串通过 JSON.parse() 解析还原为对象后,才触发 message 事件将消息传递给应用层使用。

集群稳定之路

多个工作进程的存活状态管理

工作进程的平滑重启

配置或静态数据的动态重新载入

进程事件

message 事件:接收消息

error 事件:当子进程无法被复制创建、无法被杀死、无法发送消息时触发该事件

exit 事件:子进程退出时触发该事件。子进程正常退出,事件的第一个参数为退出码,否则为 null 。如果进程通过 kill() 方法被杀死的,会得到第二个参数,它表示杀死进程时的信号

close 事件:在子进程的标准输输出流中止时触发该事件,参数与 exit 相同

disconnect 事件:在父进程或子进程中调用 disconnect() 方法时触发该事件,在调用该方法时,将关闭监听 IPC 通道

自动重启

  • 主进程监听子进程的 exit 事件,然后重新启动一个工作进程来继续服务。
  • 子进程退出时,向主进程发送一个自杀信号,主进程接收到信号后,立即创建一个新的工作进程。从 exit 事件的处理函数中转移到 message 事件的处理函数中。

负载均衡

Node 默认提供的机制是采用操作系统的抢占式策略。进程对到来的请求进行争抢,哪个进程抢到哪个进程服务

Node 还有一种新的策略,Round-Robin,又叫轮叫调度。轮叫调度的工作方式是由主进程接受连接,将其依次分发给工作进程

状态共享

Node 进程中不宜存放过多数据,会加重垃圾回收的负担,影响性能。可采用第三方数据存储。数据发送改变时,可通过定时轮询和主动通知两种方法来更新数据。

Cluster 模块

Cluster 模块是 child_process 和 net 模块的组合应用。Cluster 有以下事件:

fork: 复制一个工作进程后触发

online:复制好一个工作进程后,工作进程主动发送一条 online 消息给主进程,主进程接收后,触发该事件

listening:工作进程中调用 listen() (共享了服务端 Socket )后,发送一条 listening 消息给主进程,主进程接收后触发该事件

disconnect:主进程与工作进程之间的 IPC 通道断开后触发

exit:有工作进程退出时触发

setup:cluster.setupMaster() 执行后触发该事件

坚持原创技术分享,您的支持将鼓励我继续创作!