美文网首页纵横研究院NodeJS技术专题社区
【原创】学习NodeJs多进程(一)

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

作者: 拂云枝 | 来源:发表于2019-03-31 19:05 被阅读95次

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

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

总结

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

本文参考资源如下

相关文章

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

    NodeJs基于事件驱动的服务模型,采用单线程避免了不必要的内存开销和上下文切换的开销,但是同时也带来了一些问题,...

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

    前文介绍了NodeJs子进程创建和进程间通信,但让开发者处理多进程的管理是比较麻烦的事情,通常开发者只希望关注业务...

  • nodejs进程

  • Nodejs child_process学习

    Nodejs子进程、多进程 我们都知道 Nodejs 是以单线程的模式运行的,但它使用的是事件驱动来处理并发,这样...

  • forever

    使用forever运行nodejs应用何为forever forever可以看做是一个nodejs的守护进程,能够...

  • node 多进程与session共享

    一、Node如何开启多进程 cluster是一个nodejs内置的模块,用于nodejs多核处理。cluster模...

  • 5. 进程管理

    NodeJS可以感知和控制自身进程的运行环境和状态,也可以创建子进程并与其协同工作,这使得NodeJS可以把多个程...

  • nodejs 多进程篇

    nodejs 多进程篇 子进程创建模块 child_process spawn() : 启动一个子进程用来执行命令...

  • pm2--一个nodejs进程管理工具

    pm2是针对nodejs的项目进程管理工具,可以方便管理多个nodejs进程。 常用命令: 启动 pm2 star...

  • Nodejs如何充分利用多核CPU

    Nodejs单进程带来的问题 大家都知道, nodejs是单进程单线程的,但是现在的服务器基本都是多核的,这导致了...

网友评论

    本文标题:【原创】学习NodeJs多进程(一)

    本文链接:https://www.haomeiwen.com/subject/ehfybqtx.html