微信公众号:[前端一锅煮]
一点技术、一点思考。
问题或倡议,请公众号留言。
Node.js 主线程是单线程的,如果咱们应用 node app.js 形式运行,就启动了一个过程,只能在一个 CPU 中进行运算,无奈应用服务器的多核 CPU。为了解决这个问题,咱们能够应用多过程散发策略,即主过程接管所有申请,而后通过肯定的负载平衡策略散发到不同的 Node.js 子过程中。
这一实现有 2 种不同的计划:
- 主过程监听一个端口,子过程不监听端口,主过程通过负载平衡技术散发申请到子过程;
- 主过程和子过程别离监听不同端口,通过主过程散发申请到子过程。
第一种计划多个 Node 过程去监听同一个端口,益处是过程间通信绝对简略、缩小了端口的资源节约,然而这个时候就要保障服务过程的稳定性,特地是对 Master 过程稳定性要求会更高,编码也会简单。
第二种计划存在的问题是占用多个端口,造成资源节约,因为多个实例是独立运行的,过程间通信不太好做,益处是稳定性高,各实例之间无影响。
在 Node.js 中自带的 Cluster 模块应用的是第一种计划。
cluster 模式
cluster 模式其实就是一个主过程和多个子过程,从而造成一个集群的概念。咱们先来看看 cluster 模式的利用例子。
先实现一个简略的 app.js,代码如下:
const http = require('http');
const cluster = require('cluster');
const instances = 2; // 启动过程数量
if (cluster.isMaster) {for (let i = 0; i < instances; i++) { // 应用 cluster.fork 创立子过程
cluster.fork();}
} else {
// 创立 http 服务,简略返回
const server = http.createServer((req, res) => {res.write(`hello world, start with cluster ${process.pid}`);
res.end();});
// 启动服务
server.listen(8000, () => {console.log('server start http://127.0.0.1:8000');
});
console.log(`Worker ${process.pid} started`);
}
首先判断是否为主过程:
- 如果是则应用 cluster.fork 创立子过程;
- 如果不是则为子过程 require 具体的 app.js。
而后运行上面命令启动服务。
node cluster.js
启动胜利后,再关上另外一个命令行窗口,屡次运行以下命令:
curl "http://127.0.0.1:8000/"
能够看到如下输入:
hello world, start with cluster 8553
hello world, start with cluster 8552
hello world, start with cluster 8553
hello world, start with cluster 8552
前面的过程 ID 是比拟有法则的随机数,有时候输入 8553,有时候输入 8552,8553 和 8552 就是下面 fork 进去的两个子过程,上面咱们来看下为什么是这样的。
原理
首先咱们须要搞清楚两个问题:
- Node.js 的 cluster 是如何做到多个过程监听一个端口的;
- Node.js 是如何进行负载平衡申请散发的。
主过程判断
在 cluster 模式中存在 master 和 worker 的概念,master 是主过程,worker 是子过程。
判断主过程还是子过程的逻辑在源码中,如下:
'use strict';
const childOrPrimary = 'NODE_UNIQUE_ID' in process.env ? 'child' : 'primary';
module.exports = require(`internal/cluster/${childOrPrimary}`);
通过过程环境变量设置来判断:
- 如果没有设置则为 master 过程;
- 如果有设置则为子过程。
因而第一次调用 cluster 模块是 master 过程,而后都是子过程。
多过程端口问题
运行下面的 app.js,胜利开启了 1 个 Master 过程、2 个 Worker 过程。
因为端口只有一个 8000,所以咱们要来看下它是由哪些过程监听的。
lsof -i:8000
node 8551 qianduanyiguozhu 23u IPv6 0xb5e3cbb6deb4d65d 0t0 TCP *:irdmi (LISTEN)
当初咱们晓得了,8000 端口它并不是被所有的过程监听,仅受到 Master 过程监听。上面让咱们再看一个信息。
ps -ef | grep 8551
501 8552 8551 0 5:53 下午 ?? 0:00.10
501 8553 8551 0 5:53 下午 ?? 0:00.10
这个分明展现了 Worker 与 Master 的关系,Master 通过 cluster.fork() 这个办法创立的,怎么实现过程间端口共享呢?
在后面的例子中,多个 woker 中创立的 server 监听了同个端口 8000。通常来说,多个过程监听同个端口,零碎会报错。为什么咱们的例子没问题呢?
机密在于,net 模块中,对 server.listen() 办法做了非凡解决。依据以后过程是 master 过程,还是 worker 过程:
- master 过程:在该端口上失常监听申请。(没做非凡解决)
- worker 过程:创立 server 实例。而后通过 send 办法,向 master 过程发送音讯,让 master 过程也创立 server 实例,并在该端口上监听申请。当申请进来时,master 过程将申请转发给 worker 过程的 server 实例。
总结就是:端口只会被主过程绑定监听一次。master 过程监听特定端口,并通过负载平衡技术将客户申请转发给各 worker 过程。
负载平衡原理
Node.js cluster 模块应用的是奴才过程形式,那么它是如何进行负载平衡解决的呢,这里次要波及两个模块。
- round_robin_handle.js(非 Windows 平台利用模式),这是一个轮询解决模式,也就是轮询调度分发给闲暇的子过程,解决实现后回到 worker 闲暇池子中,这里要留神的就是如果绑定过就会复用该子过程,如果没有则会从新判断,这里能够通过下面的 app.js 代码来测试,用浏览器去拜访,你会发现每次调用的子过程 ID 都会不变。
- shared_handle.js(Windows 平台利用模式),通过将文件描述符、端口等信息传递给子过程,子过程通过信息创立相应的 SocketHandle / ServerHandle,而后进行相应的端口绑定和监听、解决申请。