NodeJs基于事件驅(qū)動的服務(wù)模型芝囤,采用單線程避免了不必要的內(nèi)存開銷和上下文切換的開銷似炎,但是同時也帶來了一些問題,比如單線程不能充分利用多核CPU資源悯姊,并且進程出現(xiàn)未捕獲的異常會導(dǎo)致進程直接退出羡藐。NodeJs提供了子進程和集群模塊,幫助我們使用NodeJs多進程來充分利用CPU資源和提高應(yīng)用的健壯性悯许。
相關(guān)文章
- 學(xué)習(xí)NodeJs多進程(一)
- 學(xué)習(xí)NodeJs多進程(二)
目錄
- 創(chuàng)建子進程
- 進程間通信
- 端口共同監(jiān)聽
- 多進程需要考慮的問題
創(chuàng)建子進程
NodeJs使用child_process
模塊來創(chuàng)建子進程仆嗦。基礎(chǔ)的兩個方法為child_process.spawn()
先壕、child_process.spawnSync()
瘩扼,前者異步地創(chuàng)建子進程谆甜,且不阻塞 Node.js 事件循環(huán);后者則以同步的方式提供等效功能集绰,但會阻止事件循環(huán)直到衍生的進程退出或終止规辱。由于child_process.spawnSync()
不常用,此處不做介紹栽燕。
child_process
模塊基于child_process.spawn()
方法實現(xiàn)了其他幾個創(chuàng)建子進程方法罕袋,簡要介紹如下:
-
child_process.spawn(command[, args][, options])
:根據(jù)命令創(chuàng)建子進程,返回子進程對象碍岔,可以在子進程對象上注冊事件 -
child_process.exec(command[, options][, callback])
:創(chuàng)建一個shell環(huán)境進程并在該shell中運行命令浴讯,UNIX上是 '/bin/sh',windows上是'cmd.exe'蔼啦,可通過options.shell
指定程序 -
child_process.execFile(file[, args][, options][, callback])
:類似于child_process.exec()
榆纽,不創(chuàng)建shell直接根據(jù)命令創(chuàng)建子進程 -
child_process.fork()
:創(chuàng)建一個新的 Node.js 進程,并通過建立 IPC 通信通道來調(diào)用指定的模塊捏肢,該通道允許在父進程與子進程之間發(fā)送消息奈籽。
基礎(chǔ)使用方式如下:
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}`)
})
執(zhí)行結(jié)果:
spawn stdout: v10.15.3
execFile stdout: v10.15.3
exec stdout: v10.15.3
由于child_process.exec()
和child_process.execFile()
是由child_process.spawn()
實現(xiàn)的,它們執(zhí)行返回的子進程對象和child_process.spawn()
一樣可以獲取子進程的stdout猛计、stderr唠摹,只不過以回調(diào)方法的方式寫法簡單一些。
child_process.fork()
只能創(chuàng)建一個node的子進程奉瘤,只要指定模塊即可。相比于其他方式創(chuàng)建子進程煮甥,該方式可以和子進程相互通信盗温,通信方式也很簡單,監(jiān)聽message
事件接收消息成肘,使用send()
方法發(fā)送消息卖局,使用方式如下:
// 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)
})
執(zhí)行parent.js
,結(jié)果如下:
Message from parent: hello world
Message from child: hello world
由以上四種方式創(chuàng)建子進程双霍,都能獲取到子進程對象ChildProcess
的實例砚偶,它提供了close
、disconnect
洒闸、error
染坯、exit
、message
等事件與子進程交互丘逸。
更多關(guān)于子進程的api单鹿,請閱讀官方文檔:http://nodejs.cn/api/child_process.html
進程間通信
由上節(jié)child_process.fork()
的示例可以看到肄渗,進程間通過監(jiān)聽message
事件接收消息霉旗,使用send()
方法發(fā)送消息划址,它們是基于IPC實現(xiàn)的呢岗。
IPC的全稱是Inter-Process Communication,即進程間通信儒喊。Node中實現(xiàn)IPC通道的是管道(pipe)技術(shù)镣奋,具體細節(jié)實現(xiàn)依賴系統(tǒng)底層。借用《深入淺出Node.js》中的圖來表示創(chuàng)建IPC管道的過程怀愧,如下:
當(dāng)父進程調(diào)用child_process.fork()
創(chuàng)建子進程的時候侨颈,先創(chuàng)建IPC管道并監(jiān)聽它,創(chuàng)建成功后再創(chuàng)建子進程掸驱,并把IPC管道的文件描述符通過環(huán)境變量傳遞給子進程肛搬,子進程啟動后根據(jù)IPC管道的文件描述符去連接IPC通道,連接成功后毕贼,父子進程就能通過IPC管道通信了温赔。
端口共同監(jiān)聽
常規(guī)情況下,啟動兩個node程序去監(jiān)聽同一個端口時鬼癣,后一個程序會提示端口已占用陶贼,那在多進程服務(wù)中如何只監(jiān)聽一個端口把請求分發(fā)給多個進程處理呢?其實上文用于消息傳遞的send()
方法的第二個參數(shù)支持傳遞句柄待秃,來看一個例子:
// 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
,效果如下:
可以看到多個進程監(jiān)聽了同一個端口3000章郁,并且多次訪問之后枉氮,真正處理請求的進程是不確定的∨看到這里聊替,想必會有以下疑問。
主進程將server對象傳到子進程了嗎?
其實這里傳遞的server對象的句柄培廓,子進程接受到server對象的句柄惹悄,獲得父進程server對象的信息,再重新創(chuàng)建server對象肩钠。對于調(diào)用者而言泣港,就像把server對象直接傳遞到了子進程,實際上send()
只有消息傳遞价匠。
為什么多進程監(jiān)聽同一端口不報錯?
在TCP端socket套接字監(jiān)聽端口有一個文件描述符当纱,單獨啟動多個進程時文件描述符不同,導(dǎo)致監(jiān)聽相同端口會報錯霞怀。NodeJs底層對每個端口監(jiān)聽都設(shè)置了標(biāo)識惫东,在父進程和子進程傳遞server對象的過程中,將標(biāo)識傳給了對方,因此通過標(biāo)識它們監(jiān)聽端口用的是同一個文件描述符廉沮。在網(wǎng)絡(luò)請求向服務(wù)器發(fā)送時颓遏,這些進程通過搶占為請求服務(wù)。
send()方法除了server對象還支持發(fā)送哪些對象?
要發(fā)送類似的對象滞时,需要有完整的發(fā)送與還原對象的過程叁幢。根據(jù)官方文檔描述,支持的對象如下:
-
net.Socket
TCP套接字 -
net.Server
TCP服務(wù)器 -
dgram.Socket
UDP套接字
多進程需要考慮的問題
- 多進程開發(fā)
根據(jù)上文介紹的子進程創(chuàng)建和進程間通信坪稽,如果讓開發(fā)者手動來處理父子進程是比較麻煩的事情曼玩。幸好NodeJs官方提供了cluster
模塊,讓多進程的使用變得很容易窒百。 - 負載均衡
多個進程間需要有一個策略來保證資源的合理分配黍判。Node默認提供的機制是采用操作系統(tǒng)的搶占式策略,但也需要根據(jù)實際系統(tǒng)的資源使用情況來考慮篙梢。 - 進程管理
為了程序的健壯性以及充分利用CPU資源顷帖,我們引入多進程,那么多進程的管理也是一個問題渤滞,比如某個子進程異常退出需要自動創(chuàng)建一個新的子進程贬墩、讓所有的子進程去搶占端口請求會造成性能浪費等。目前開源好用的進程管理工具有pandora
妄呕、pm2
可以幫助我們解決一些問題陶舞。 - 狀態(tài)共享
通常在多個應(yīng)用間需要有一些共享數(shù)據(jù),比如IM系統(tǒng)中記錄當(dāng)前在線的用戶绪励。常見的做法是通過第三方數(shù)據(jù)存儲來實現(xiàn)肿孵,比如redis
。
關(guān)于以上問題將在后面的文章中繼續(xù)學(xué)習(xí)探索疏魏。
總結(jié)
本文簡要介紹了子進程創(chuàng)建和進程間通信的基礎(chǔ)內(nèi)容颁井,在后面的文章中將深入學(xué)習(xí)多進程的管理。
本文參考資源如下: