您好,登錄后才能下訂單哦!
這篇文章主要介紹“Node的多進程服務如何實現”的相關知識,小編通過實際案例向大家展示操作過程,操作方法簡單快捷,實用性強,希望這篇“Node的多進程服務如何實現”文章能幫助大家解決問題。
我們現在已經知道了Node
是單線程運行的,這表示潛在的錯誤有可能導致線程崩潰,然后進程也會隨著退出,無法做到企業追求的穩定性;另一方面,單進程也無法充分多核CPU,這是對硬件本身的浪費。Node
社區本身也意識到了這一問題,于是從0.1版本就提供了child_process
模塊,用來提供多進程的支持。
child_process
模塊中包括了很多創建子進程的方法,包括fork
、spawn
、exec
、execFile
等等。它們的定義如下:
child_process.exec(command[, options][, callback])
child_process.spawn(command[, args][, options])
child_process.fork(modulePath[, args][, options])
child_process.execFile(file[, args][, options][, callback])
在這4個API中以spawn
最為基礎,因為其他三個API或多或少都是借助spawn
實現的。
spawn
方法的聲明格式如下:
child_process.spawn(command[, args][, options])
spawn
方法會使用指定的command
來生成一個新進程,執行完對應的command
后子進程會自動退出。
該命令返回一個child_process
對象,這代表開發者可以通過監聽事件來獲得命令執行的結果。
下面我們使用spwan
來執行ls
命令:
const spawn = require('child_process').spawn; const ls = spawn('ls', ['-1h', '/usr']); ls.stdout.on('data', (data) => { console.log('stdout: ', daata.toString()); }); ls.stderr.on('data', (data) => { console.log('stderr: ', daata.toString()); }); ls.on('close', (code) => { console.log('child process exited with code', code); });
其中spawn
的第一個參數雖然是command
,但實際接收的卻是一個file
,可以在Linux或者Mac OSX上運行,這是由于ls
命令也是以可執行文件形式存在的。
類似的,在Windows系統下我們可以試著使用dir
命令來實現功能類似的代碼:
const spawn = require('child_process').spawn; const ls = spawn('dir'); ls.stdout.on('data', (data) => { console.log('stdout: ', daata.toString()); });
然而在Windows下執行上面代碼會出現形如Error:spawn dir ENOENT
的錯誤。
原因就在于spawn
實際接收的是一個文件名而非命令,正確的代碼如下:
const spawn = require('child_process').spawn; const ls = spawn('powershell', ['dir']); ls.stdout.on('data', (data) => { console.log('stdout: ', daata.toString()); });
這個問題的原因與操作系統本身有關,在Linux中,一般都是文件,命令行的命令也不例外,例如ls
命令是一個名為ls
的可執行文件;而在Windows中并沒有名為dir
的可執行文件,需要通過cmd
或者powershell
之類的工具提供執行環境。
在Linux環境下,創建一個新進程的本質是復制一個當前的進程,當用戶調用 fork
后,操作系統會先為這個新進程分配空間,然后將父進程的數據原樣復制一份過去,父進程和子進程只有少數值不同,例如進程標識符(PD)。
對于 Node 來說,父進程和子進程都有獨立的內存空間和獨立的 V8 實例,它們和父進程唯一的聯系是用來進程間通信的 IPC Channel。
此外,Node中fork
和 POSIX 系統調用的不同之處在于Node中的fork
并不會復制父進程。
Node中的fork
是上面提到的spawn
的一種特例,前面也提到了Node中的fork
并不會復制當前進程。多數情況下,fork
接收的第一個參數是一個文件名,使用fork("xx.js")
相當于在命令行下調用node xx.js
,并且父進程和子進程之間可以通過process.send
方法來進行通信。
下面我們來看一個簡單的栗子:
// master.js 調用 fork 來創建一個子進程 const child_process = require('child_process'); const worker = child_process.fork('worker.js', ['args1']); worker.on('exit', () => { console.log('child process exit'); }); worker.send({ msg: 'hello child' }); worker.on('message', msg => { console.log('from child: ', msg); }); // worker.js const begin = process.argv[2]; console.log('I am worker ' + begin); process.on('message', msg => { console.log('from parent ', msg); process.exit(); }); process.send({ msg: 'hello parent' });
fork
內部會通過spawn
調用process.executePath
,即Node
的可執行文件地址來生成一個Node
實例,然后再用這個實例來執行fork
方法的modulePath
參數。
輸出結果為:
I am worker args1
from parent { msg: 'hello child' }
from child: { msg: 'hello parent' }
child process exit
如果我們開發一種系統,那么對于不同的模塊可能會用到不同的技術來實現,例如 Web服務器使用 Node ,然后再使用 Java 的消息隊列提供發布訂閱服務,這種情況下通常使用進程間通信的方式來實現。
但有時開發者不希望使用這么復雜的方式,或者要調用的干脆是一個黑盒系統,即無法通過修改源碼來進行來實現進程間通信,這時候往往采用折中的方式,例如通過 shell 來調用目標服務,然后再拿到對應的輸出。
child_process
提供了一個execFile
方法,它的聲明如下:
child_process.execFile(file, args, options, callback)
說明:
file {String}
要運行的程序的文件名
args {Array}
字符串參數列表
options {Object}
cwd {String}
子進程的當前工作目錄
env {Object}
環境變量鍵值對
encoding {String}
編碼(默認為 'utf8'
)
timeout {Number}
超時(默認為 0)
maxBuffer {Number}
緩沖區大小(默認為 200*1024)
killSignal {String}
結束信號(默認為'SIGTERM'
)
callback {Function}
進程結束時回調并帶上輸出
error {Error}
stdout {Buffer}
stderr {Buffer}
返回:ChildProcess
對象
可以看出,execfile
和spawn
在形式上的主要區別在于execfile
提供了一個回調函數,通過這個回調函數可以獲得子進程的標準輸出/錯誤流。
使用 shell 進行跨進程調用長久以來被認為是不穩定的,這大概源于人們對控制臺不友好的交互體驗的恐懼(輸入命令后,很可能長時間看不到一個輸出,盡管后臺可能在一直運算,但在用戶看來和死機無異)。
在 Linux下執行exec
命令后,原有進程會被替換成新的進程,進而失去對新進程的控制,這代表著新進程的狀態也沒辦法獲取了,此外還有 shell 本身運行出現錯誤,或者因為各種原因出現長時間卡頓甚至失去響應等情況。
Node.js 提供了比較好的解決方案,timeout
解決了長時間卡頓的問題,stdout
和stderr
則提供了標準輸出和錯誤輸出,使得子進程的狀態可以被獲取。
為了更好地說明,我們先寫一段簡單的 C 語言代碼,并將其命名為 example.c
:
#include<stdio.h> int main() { printf("%s", "Hello World!"); return 5; }
使用 gcc
編譯該文件:
gcc example.c -o example
生成名為example
的可執行文件,然后將這個可執行文件放到系統環境變量中,然后打開控制臺,輸入example
,看到最后輸出"Hello World"
。
確保這個可執行文件在任意路徑下都能訪問。
我們分別用spawn
和execfile
來調用example
文件。
首先是spawn
。
const spawn = require('child_process').spawn; const ls = spawn('example'); ls.stdout.on('data', (data) => { console.log('stdout: ', daata.toString()); }); ls.stderr.on('data', (data) => { console.log('stderr: ', daata.toString()); }); ls.on('close', (code) => { console.log('child process exited with code', code); });
程序輸出:
stdout: Hello World!
child process exited with code 5
程序正確打印出了Hello World
,此外還可以看到example
最后的return 5
會被作為子進程結束的code
被返回。
然后是execFile
。
const exec = require('child_process').exec; const child = exec('example', (error, stdout, stderr) => { if (error) { throw error; } console.log(stdout); });
同樣打印出Hello World
,可見除了調用形式不同,二者相差不大。
在子進程的信息交互方面,spawn
使用了流式處理的方式,當子進程產生數據時,主進程可以通過監聽事件來獲取消息;而exec
是將所有返回的信息放在stdout
里面一次性返回的,也就是該方法的maxBuffer
參數,當子進程的輸出超過這個大小時,會產生一個錯誤。
此外,spawn
有一個名為shell
的參數:
其類型為一個布爾值或者字符串,如果這個值被設置為true
,,就會啟動一個 shell 來執行命令,這個 shell 在 UNIX上是 bin/sh,,在Windows上則是cmd.exe。
exec
在內部也是通過調用execFile
來實現的,我們可以從源碼中驗證這一點,在早期的Node源碼中,exec
命令會根據當前環境來初始化一個 shell,,例如 cmd.exe 或者 bin/sh,然后在shell中調用作為參數的命令。
通常execFile
的效率要高于exec
,這是因為execFile
沒有啟動一個 shell,而是直接調用 spawn
來實現的。
前面介紹的幾個用于創建進程的方法,都是屬于child_process
的類方法,此外childProcess
類繼承了EventEmitter
,在childProcess
中引入事件給進程間通信帶來很大的便利。
childProcess
中定義了如下事件。
Event:'close'
:進程的輸入輸出流關閉時會觸發該事件。
Event:'disconnect'
:通常childProcess.disconnect
調用后會觸發這一事件。
Event:'exit'
:進程退出時觸發。
Event:'message'
:調用child_process.send
會觸發這一事件
Event:'error'
:該事件的觸發分為幾種情況:
該進程無法創建子進程。
該進程無法通過kill
方法關閉。
無法發送消息給子進程。
Event:'error'
事件無法保證一定會被觸發,因為可能會遇到一些極端情況,例如服務器斷電等。
上面也提到,childProcess
模塊定義了send
方法,用于進程間通信,該方法的聲明如下:
child.send(message[, sendHandle[, options]][, callback])
通過send
方法發送的消息,可以通過監聽message
事件來獲取。
// master.js 父進程向子進程發送消息 const child_process = require('child_process'); const worker = child_process.fork('worker.js', ['args1']); worker.on('exit', () => { console.log('child process exit'); }); worker.send({ msg: 'hello child' }); worker.on('message', msg => { console.log('from child: ', msg); }); // worker.js 子進程接收父進程消息 const begin = process.argv[2]; console.log('I am worker ' + begin); process.on('message', msg => { console.log('from parent ', msg); process.exit(); }); process.send({ msg: 'hello parent' });
send
方法的第一個參數類型通常為一個json
對象或者原始類型,第二個參數是一個句柄,該句柄可以是一個net.Socket
或者net.Server
對象。下面是一個例子:
//master.js 父進程發送一個 Socket 對象 const child = require('child_process').fork('worker.js'); // Open up the server object and send the handle. const server = require('net').createServer(); server.on('connection', socket => { socket.end('handled by parent'); }); server.listen(1337, () => { child.send('server', server); }); //worker.js 子進程接收 Socket 對象 process.on('message', (m, server) => { if (m === 'server') { server.on('connection', socket => { socket.end('handled by child'); }); } });
前面已經介紹了child_process
的使用,child_process
的一個重要使用場景是創建多進程服務來保證服務穩定運行。
為了統一 Node 創建多進程服務的方式,Node 在之后的版本中增加了Cluster
模塊,Cluster
可以看作是做了封裝的child_Process
模塊。
Cluster
模塊的一個顯著優點是可以共享同一個socket
連接,這代表可以使用Cluster
模塊實現簡單的負載均衡。
下面是Cluster
的簡單栗子:
const cluster = require('cluster'); const http = require('http'); const numCPUs = require('os').cpus().length; if (cluster.isMaster) { console.log('Master process id is', process.pid); // Fork workers. for (let i = 0; i < numCPUs; i++) { cluster.fork(); } cluster.on('exit', (worker, code, signal) => { console.log('worker process died, id ', worker.process.pid); }); } else { // Worker 可以共享同一個 TCP 連接 // 這里的例子是一個 http 服務器 http.createServer((req, res) => { res.writeHead(200); res.end('hello world\n'); }).listen(8000); console.log('Worker started, process id', process.pid); }
上面是使用Cluster
模塊的一個簡單的例子,為了充分利用多核CPU,先調用OS
模塊的cpus()
方法來獲得CPU的核心數,假設主機裝有兩個 CPU,每個CPU有4個核,那么總核數就是8。
在上面的代碼中,Cluster
模塊調用fork
方法來創建子進程,該方法和child_process
中的fork
是同一個方法。
Cluster
模塊采用的是經典的主從模型,由master
進程來管理所有的子進程,可以使用cluster.isMaster
屬性判斷當前進程是master
還是worker
,其中主進程不負責具體的任務處理,其主要工作是負責調度和管理,上面的代碼中,所有的子進程都監聽8000端口。
通常情況下,如果多個 Node 進程監聽同一個端口時會出現Error: listen EADDRINUS
的錯誤,而Cluster
模塊能夠讓多個子進程監聽同一個端口的原因是master
進程內部啟動了一個 TCP 服務器,而真正監聽端口的只有這個服務器,當來自前端的請求觸發服務器的connection
事件后,master
會將對應的socket
句柄發送給子進程。
關于“Node的多進程服務如何實現”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識,可以關注億速云行業資訊頻道,小編每天都會為大家更新不同的知識點。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。