Node.js创建多进程实践

Node.js如何创建多进程

这里我们在主进程cluster.isMaster中根据系统CPU的总核数require('os').cpus()创建多个工作进程cluster.fork()。在各工作进程即子进程中创建HTTP服务器,监听同一端口号8090并返回响应。具体实现如下图所示:

image.png

通过上面的处理逻辑,我发现在主进程中只是执行了创建子进程的动作,并没有创建服务器的动作。那么主进程的服务器是如何创建的呢?由于服务器创建的动作是在子进程中执行的,因此主进程是否就离不开子进程的交互了。

Q:主进程在cluster模式下如何创建服务器?

关于集群,你应该知道的事儿

在集群模式下,主进程的服务器会接受到请求然后发送给子进程。而主进程服务器的创建当然和子进程密切相关了。下面详细分析一下:


image.png

子进程在cluster._getServer函数中向已建立的IPC通道发送内部消息message,该消息包含serverQuery信息,同时包含act: 'queryServer'字段,等待服务器响应后继续执行回调函数modifyHandle。

主进程internal/cluster/master.js中会监听message。

function onmessage(message, handle) {
  const worker = this;

  if (message.act === 'online')
    online(worker);
  else if (message.act === 'queryServer')
    queryServer(worker, message);
  else if (message.act === 'listening')
    listening(worker, message);
  else if (message.act === 'exitedAfterDisconnect')
    exitedAfterDisconnect(worker, message);
  else if (message.act === 'close')
    close(worker, message);
}

主进程接收到子进程发送到内部消息,会根据act:'queryServer'执行对应queryServer()方法,完成服务器到创建,同时发送回复消息给子进程,子进程执行回调函数modifyHandle,继续接下来到操作。

Q:为什么可以通过cluster.isMaster判断是主进程还是子进程呢?

这里就需要查看Node.js的具体实现了。我们可以发现在Node.js的cluster模块中只有一行处理代码,如下所示:

const childOrMaster = 'NODE_UNIQUE_ID' in process.env ? 'child' : 'master';
module.exports = require(`internal/cluster/${childOrMaster}`);

其中NODE_UNIQUE_ID变量默认是没有的,所以默认创建的是主进程。而变量NODE_UNIQUE_ID是在主进程fork子进程时传递进去的参数,因此采用cluster.fork()创建的子进程是一定包含NODE_UNIQUE_ID的,具体流程如下图所示:

cluster.isMaster区分主进程和子进程.png

??这里需要指出的是,必须通过cluster.fork创建的子进程才有NODE_UNIQUE_ID变量,如果通过child_process.fork的子进程,在不传递环境变量的情况下是没有NODE_UNIQUE_ID的。因此,当你在child_process.fork的子进程中执行cluster.isMaster判断时,返回 true。

Q:如何做到多个子进程共同监听一个端口号的?

我们都知道,同一个端口号是不能同时被多个进程监听的,如果有两个进程同时对一个端口进行监听,Node.js会直接抛出一个异常(Error: listen EADDRINUSE)。

但是如果使用代理模式同时监听多个端口,让master进程监听8090端口,收到请求时,再将请求分发给不同服务,而且master进程还能做适当的负载均衡。

首先我们先启动项目,查看系统端口占用情况,以便后期分析:

  1. 启动项目,但是不发起任何请求,此时应该只有主进程在运行。


    image.png
  2. 发起请求,主进程开始分配任务给工作进程执行。


    image.png

    通过上图,可以发现主进程监听8090端口,并且对请求进行分配转发到各工作进程。这就是Master-Worker模式,又称主从模式。是典型的分布式架构中用于并行处理业务的模式,具备较好的可伸缩性(很好的处理并发情况)和稳定性(一个进程挂掉不会影响其它进程)。

主进程不负责具体的业务处理,而是负责调度和管理工作进程,它是趋向于稳定的。而工作进程负责具体的业务处理。

Q:主进程对请求进行分配,是否做了负载均衡

对于这个问题,我们在服务上线后通过日志进行打印分析,统计各工作进程被调用次数,分析该??槭欠褚咽迪指涸鼐?。

  1. 方案一:在app.js中创建全局变量global.works = [ ];在每次请求的时候将当前使用的工作进程id添加到全局数组中,并进行统计分析。
    image.png

    该方案存在问题:由于每个子进程是单独创建到服务实例 http.createServer(app); 。。。全局变量global.works每次会被重置,因此没有只能看当当次请求所使用当进程情况。

通过fork()复制的进程都是一个独立的进程,每个进程中有着独立而全新的V8实例。

  1. 方案二:在主进程中创建全局变量,并监听包含notifyRequest的消息对子进程的调用进行统计分析。
    app.js process.send({cmd:'notifyRequest'});//记录子进程调用次数使用 返回notifyRequest消息
image.png

监控结果展示:


image.png

根据监控结果展示,发现Node.js的集群模式已经实现了负载均衡。

参考:

http://nodejs.cn/api/cluster.html#cluster_event_message

http://nodejs.cn/api/child_process.html

Q1:为什么方案二能统计到所有进程调度到情况?

  1. 在app.js中使用
app.use((req, res, next)=>{
  process.send({cmd:'notifyRequest'});//记录子进程调用次数使用
  console.log(`工作进程${cluster.worker.id} 正在端口${cluster.worker.process.pid}运行`);
  next();
})

是为了在每次请求(app.use()匹配了所有/路由)的时候发送特定信息给各进程。

  1. 在主进程中创建全局变量global.workers = [];//子进程调用次数统计数组
  2. 各个工作进程中监听message消息,只要有进程接受到请求信息就将当前进程的idpush到全局变量中,并对全局变量中的信息进行统计分析。

如果Node.js进程是通过进程间通信产生的,那么,process.send()方法可以用来给父进程发送消息。 接收到的消息被视为父进程的ChildProcess对象上的一个'message'事件。

如果Node.js进程不是通过进程间通信产生的, process.send() 会是undefined。

所以说主进程和各工作进程之间是通过消息传递内容,而不是共享或直接操作相关资源。??通过fork()或者其它API创建子进程后,为实现父子进程之间的通信,父进程和子进程之间会创建IPC通道(通过IPC通道,父子进程之间才能通过message和send()传递消息)。

Q:负载均衡是如何实现的?

Node.js在实现负载均衡上有至少两种处理方式:

  1. 抢占式策略
  2. Round-Robin 轮叫调度

由于单个Node程序仅仅利用单核CPU,因此为了更好利用系统资源就需要fork多个Node进程来执行HTTP服务器逻辑,所以Node内建??樘峁┝?code>child_process和cluster???。

Q: child_processcluster??榈那?/h3>
  • 利用child_process??椋颐强梢灾葱衧hell命令,可以fork子进程执行代码,也可以直接执行二进制文件;
  • 利用cluster???,使用node封装好的API、IPC通道和调度机可以非常简单的创建包括一个master进程下HTTP代理服务器 + 多个worker进程多个HTTP应用服务器的架构,并提供两种调度子进程算法。

Q:多进程之间的共享Session实现。

背景描述:在项目接入cluster??槭迪侄嘟檀砗?,发现项目启动后,会出现请求异常(Session丢失)导致页面空白。


image.png

分析发现是因为在进入系统后,会有多个请求,各请求可能被转发到不同到工作进程(不同的进程是不同的实例),因此会出现请求中携带的Session丢失,导致异常。查看解决方案发现,Express模块提供了express-session??椋杀4鎠ession。

var express = require('express');
var cookieParser = require('cookie-parser');
var session = require('express-session');
var RedisStore = require('connect-redis')(session);
app.use(session({
    'secret': '12345',
    'name': 'fecarApp', //这里的name值得是cookie的name,默认cookie的name是:connect.sid
    'cookie': { maxAge: 8000000 }, //设置maxAge是80000ms,即80s后session和相应的cookie失效过期
    'resave': false,
    'saveUninitialized': true,
    'store': new RedisStore(options),
    genid: function (req) {
        // 如果没有 ticket 就随机生成
        if (!req.query.ticket) return uid(24)
        // 如果有 ticket 就把 ticket MD5加密返回
        return MD5(req.query.ticket)
    }
}));

参考:
https://www.cnblogs.com/chenchenluo/p/4197181.html

Node.jsos模块获取CPU信息

require('os').cpus();返回一个对象数组,如下图所示,包含所安装的每个 CPU/内核的信息。

require('os').cpus().png

require('os').cpus().length;返回是总核数(总核数 = 物理CPU个数 X 每颗物理CPU的核数)。

拿我本机来说,查看系统配置发现核总数为2(物理CPU数目)。使用如上代码查看发现是4(核总数),说明是双核CPU。

image.png
image.png
# 总核数 = 物理CPU个数 X 每颗物理CPU的核数 
# 总逻辑CPU数 = 物理CPU个数 X 每颗物理CPU的核数 X 超线程数

# 查看物理CPU个数
cat /proc/cpuinfo| grep "physical id"| sort| uniq| wc -l

# 查看每个物理CPU中core的个数(即核数)
cat /proc/cpuinfo| grep "cpu cores"| uniq

# 查看逻辑CPU的个数
cat /proc/cpuinfo| grep "processor"| wc -l

参考

https://www.cnblogs.com/zmxmumu/p/6179503.html

https://blog.csdn.net/feijiges/article/details/76860372

https://segmentfault.com/a/1190000016169207

https://www.cnblogs.com/emanlee/p/3587571.html

Node.js采取cluster??榇唇ǘ嘟毯笪薹舻魇阅J?/a>

  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,128评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,316评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事?!?“怎么了?”我有些...
    开封第一讲书人阅读 159,737评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,283评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,384评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,458评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,467评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,251评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,688评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,980评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,155评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,818评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,492评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,142评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,382评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,020评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,044评论 2 352