【原创】学习NodeJs多进程(一)

NodeJs基于事件驱动的服务模型,采用单线程避免了不必要的内存开销和上下文切换的开销,但是同时也带来了一些问题,比如单线程不能充分利用多核CPU资源,并且进程出现未捕获的异常会导致进程直接退出。NodeJs提供了子进程和集群模块,帮助我们使用NodeJs多进程来充分利用CPU资源和提高应用的健壮性。

相关文章

目录

  • 创建子进程
  • 进程间通信
  • 端口共同监听
  • 多进程需要考虑的问题

创建子进程

NodeJs使用child_process模块来创建子进程。基础的两个方法为child_process.spawn()child_process.spawnSync(),前者异步地创建子进程,且不阻塞 Node.js 事件循环;后者则以同步的方式提供等效功能,但会阻止事件循环直到衍生的进程退出或终止。由于child_process.spawnSync()不常用,此处不做介绍。

child_process模块基于child_process.spawn() 方法实现了其他几个创建子进程方法,简要介绍如下:

  • child_process.spawn(command[, args][, options]):根据命令创建子进程,返回子进程对象,可以在子进程对象上注册事件
  • child_process.exec(command[, options][, callback]):创建一个shell环境进程并在该shell中运行命令,UNIX上是 '/bin/sh',windows上是'cmd.exe',可通过options.shell指定程序
  • child_process.execFile(file[, args][, options][, callback]):类似于child_process.exec(),不创建shell直接根据命令创建子进程
  • child_process.fork():创建一个新的 Node.js 进程,并通过建立 IPC 通信通道来调用指定的模块,该通道允许在父进程与子进程之间发送消息。

基础使用方式如下:

const { spawn, exec, execFile } = require('child_process')
const path = require('path')

const child= spawn('node', ['--version'])
child.stdout.on('data', (data) => {
  console.log(`spawn stdout: ${data}`)
})

exec('node --version', (error, stdout, stderr) => {
  if (error) {
    throw error
  }
  console.log(`exec stdout: ${stdout}`)
})

execFile('node', ['--version'], (error, stdout, stderr) => {
  if (error) {
    throw error
  }
  console.log(`execFile stdout: ${stdout}`)
})

执行结果:

spawn stdout: v10.15.3
execFile stdout: v10.15.3
exec stdout: v10.15.3

由于child_process.exec()child_process.execFile()是由child_process.spawn()实现的,它们执行返回的子进程对象和child_process.spawn()一样可以获取子进程的stdout、stderr,只不过以回调方法的方式写法简单一些。

child_process.fork()只能创建一个node的子进程,只要指定模块即可。相比于其他方式创建子进程,该方式可以和子进程相互通信,通信方式也很简单,监听message事件接收消息,使用send()方法发送消息,使用方式如下:

// parent.js
const { fork } = require('child_process')
const path = require('path')
const child = fork(path.resolve(__dirname, './child.js'))
child.on('message', function (msg) {
  console.log('Message from child: ', msg)
})
child.send('hello world')

// child.js
process.on('message', function (msg) {
  console.log('Message from parent:', msg)
  process.send(msg)
})

执行parent.js,结果如下:

Message from parent: hello world
Message from child:  hello world

由以上四种方式创建子进程,都能获取到子进程对象ChildProcess
的实例,它提供了closedisconnecterrorexitmessage等事件与子进程交互。

更多关于子进程的api,请阅读官方文档:http://nodejs.cn/api/child_process.html

进程间通信

由上节child_process.fork()的示例可以看到,进程间通过监听message事件接收消息,使用send()方法发送消息,它们是基于IPC实现的。

IPC的全称是Inter-Process Communication,即进程间通信。Node中实现IPC通道的是管道(pipe)技术,具体细节实现依赖系统底层。借用《深入浅出Node.js》中的图来表示创建IPC管道的过程,如下:

创建IPC管道的示意图

当父进程调用child_process.fork()创建子进程的时候,先创建IPC管道并监听它,创建成功后再创建子进程,并把IPC管道的文件描述符通过环境变量传递给子进程,子进程启动后根据IPC管道的文件描述符去连接IPC通道,连接成功后,父子进程就能通过IPC管道通信了。

端口共同监听

常规情况下,启动两个node程序去监听同一个端口时,后一个程序会提示端口已占用,那在多进程服务中如何只监听一个端口把请求分发给多个进程处理呢?其实上文用于消息传递的send()方法的第二个参数支持传递句柄,来看一个例子:

// parent.js
const { fork } = require('child_process')
const path = require('path')

const child1 = fork(path.resolve(__dirname, './child.js'))
const child2 = fork(path.resolve(__dirname, './child.js'))

const server = require('net').createServer()

server.on('connection', (socket) => {
  socket.end('handle by parent')
})

server.listen(3000, () => {
  child1.send('server', server)
  child2.send('server', server)
})

// child.js
process.on('message', function (msg, server) {
  if (msg === 'server') {
    server.on('connection', (socket) => {
      socket.end(`handle by child ${process.pid}`)
    })
  }
})

运行parent.js后,多次访问http://127.0.0.1:3000,效果如下:

多进程监听同一端口执行结果

可以看到多个进程监听了同一个端口3000,并且多次访问之后,真正处理请求的进程是不确定的。看到这里,想必会有以下疑问。

主进程将server对象传到子进程了吗?

其实这里传递的server对象的句柄,子进程接受到server对象的句柄,获得父进程server对象的信息,再重新创建server对象。对于调用者而言,就像把server对象直接传递到了子进程,实际上send()只有消息传递。

为什么多进程监听同一端口不报错?

在TCP端socket套接字监听端口有一个文件描述符,单独启动多个进程时文件描述符不同,导致监听相同端口会报错。NodeJs底层对每个端口监听都设置了标识,在父进程和子进程传递server对象的过程中,将标识传给了对方,因此通过标识它们监听端口用的是同一个文件描述符。在网络请求向服务器发送时,这些进程通过抢占为请求服务。

send()方法除了server对象还支持发送哪些对象?

要发送类似的对象,需要有完整的发送与还原对象的过程。根据官方文档描述,支持的对象如下:

  • net.Socket TCP套接字
  • net.Server TCP服务器
  • dgram.Socket UDP套接字

多进程需要考虑的问题

  • 多进程开发
    根据上文介绍的子进程创建和进程间通信,如果让开发者手动来处理父子进程是比较麻烦的事情。幸好NodeJs官方提供了cluster模块,让多进程的使用变得很容易。
  • 负载均衡
    多个进程间需要有一个策略来保证资源的合理分配。Node默认提供的机制是采用操作系统的抢占式策略,但也需要根据实际系统的资源使用情况来考虑。
  • 进程管理
    为了程序的健壮性以及充分利用CPU资源,我们引入多进程,那么多进程的管理也是一个问题,比如某个子进程异常退出需要自动创建一个新的子进程、让所有的子进程去抢占端口请求会造成性能浪费等。目前开源好用的进程管理工具有pandorapm2可以帮助我们解决一些问题。
  • 状态共享
    通常在多个应用间需要有一些共享数据,比如IM系统中记录当前在线的用户。常见的做法是通过第三方数据存储来实现,比如redis

关于以上问题将在后面的文章中继续学习探索。

总结

本文简要介绍了子进程创建和进程间通信的基础内容,在后面的文章中将深入学习多进程的管理。

本文参考资源如下

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