纵横研究院NodeJS技术专题社区

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

2019-03-31  本文已影响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() 方法实现了其他几个创建子进程方法,简要介绍如下:

基础使用方式如下:

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对象还支持发送哪些对象?

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

多进程需要考虑的问题

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

总结

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

本文参考资源如下

上一篇 下一篇

猜你喜欢

热点阅读