- 新時期的Node.js入門
- 李鍇
- 3183字
- 2019-12-12 17:05:44
2.10 多進程服務
2.10.1 child_process模塊
我們現在已經知道了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實現的。
2.10.2 spawn
spawn方法的聲明格式如下:

spawn方法會使用指定的command來生成一個新進程,執行完對應的command后子進程會自動退出。
該命令返回一個child_process對象,這代表開發者可以通過監聽事件來獲得命令執行的結果。
代碼2.26 使用spwan來執行ls命令

其中spawn的第一個參數雖然是command,但實際接收的卻是一個file,代碼2.25可以在Linux或者Mac OSX上運行,這是由于ls命令也是以可執行文件形式存在的。
類似的,在Windows系統下我們可以試著使用dir命令來實現功能類似的代碼:

然而在Windows下執行上面代碼會出現形如Error: spawn dir ENOENT的錯誤。
原因就在于spawn實際接收的是一個文件名而非命令,正確的代碼如下:

這個問題的原因與操作系統本身有關,在Linux中,一般都是文件,命令行的命令也不例外,例如ls命令是一個名為ls的可執行文件;而在Windows中并沒有名為dir的可執行文件,需要通過cmd或者powershell之類的工具提供執行環境。
2.10.3 fork
在Linux環境下,創建一個新進程的本質是復制一個當前的進程,當用戶調用fork后,操作系統會先為這個新進程分配空間,然后將父進程的數據原樣復制一份過去,父進程和子進程只有少數值不同,例如進程標識符(PID)。
對于Node來說,父進程和子進程都有獨立的內存空間和獨立的V8實例,它們和父進程唯一的聯系是用來進程間通信的IPC Channel。
此外,Node中fork和POSIX系統調用的不同之處在于Node中的fork并不會復制父進程。
Node中的fork是上面提到的spawn的一種特例,前面也提到了Node中的fork并不會復制當前進程。多數情況下,fork接收的第一個參數是一個文件名,使用fork("xx.js")相當于在命令行下調用node xx.js,并且父進程和子進程之間可以通過process.send方法來進行通信。示例代碼如下:
代碼2.27 master.js——調用fork來創建一個子進程

代碼2.28 worker.js代碼

fork內部會通過spawn調用process.executePath,即Node的可執行文件地址(例如/Users/likai/.nvm/versions/node/v6.9.4/bin/node)來生成一個Node實例,然后再用這個實例來執行fork方法的modulePath參數。
2.10.4 exec和execFile
如果我們開發一種系統,那么對于不同的模塊可能會用到不同的技術來實現,例如Web服務器使用Node,然后再使用Java的消息隊列提供發布訂閱服務,這種情況下通常使用進程間通信的方式來實現。
但有時開發者不希望使用這么復雜的方式,或者要調用的干脆是一個黑盒系統,即無法通過修改源碼來進行來實現進程間通信,這時候往往采用折中的方式,例如通過shell來調用目標服務,然后再拿到對應的輸出。
筆者曾經做過一個項目,后臺用一個Spark集群來進行數據的分析,然后將結果繪成圖表展示給用戶,當時一種備選方案就是采用B/S架構并使用Node來做Web服務器,當用戶單擊頁面上的元素時,Node將其轉換為Spark集群中的命令,這個過程就是使用Node調用Shell來完成的。
1.Shell簡介
Shell其實很簡單,在控制臺輸入cd ~/desktop,然后回車,這就是最簡單的shell命令,把這行命令寫在文本里就是一個shell腳本。
例如:

在Linux或者Mac OSX下可以使用命令:

來執行這個腳本,效果跟直接輸入命令:

是一樣的。
2.execFile方法
child_process提供了一個execFile方法,它的聲明如下:

說明:
- 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則提供了標準輸出和錯誤輸出,使得子進程的狀態可以被獲取。
2.10.5 各方法之間的比較
1.spawn和execfile
為了更好地說明,我們先寫一段簡單的C語言代碼,并將其命名為example.c:

使用gcc編譯該文件:

生成名為example的可執行文件,然后將這個可執行文件放到系統環境變量中(編輯~/.bash_profile),然后打開控制臺,輸入example,看到最后輸出"Hello World"。
確保這個可執行文件在任意路徑下都能訪問。
我們分別用spawn和execfile來調用example文件。
首先是spawn。
代碼2.29 使用spwan來調用

程序輸出:

程序正確打印出了Hello World,此外還可以看到example最后的return 5會被作為子進程結束的code被返回。
然后是execfile。
代碼2.30 使用execFile來調用

同樣打印出Hello World,可見除了調用形式不同,二者相差不大。
2.execFile和spawn
在子進程的信息交互方面,spawn使用了流式處理的方式,當子進程產生數據時,主進程可以通過監聽事件來獲取消息;而exec是將所有返回的信息放在stdout里面一次性返回的,也就是該方法的maxBuffer參數,當子進程的輸出超過這個大小時,會產生一個錯誤。
此外,spawn有一個名為shell的參數,下面是該參數在文檔中的定義:

其類型為一個布爾值或者字符串,如果這個值被設置為true,就會啟動一個shell來執行命令,這個shell在UNIX上是bin/sh,在Windows上則是cmd.exe。
3.exec和execfile
exec在內部也是通過調用execFile來實現的,我們可以從源碼中驗證這一點,在早期的Node源碼中,exec命令會根據當前環境來初始化一個shell,例如cmd.exe或者/bin/sh,然后在shell中調用作為參數的命令。
代碼2.31 Node V0.10.0源碼/lib/child_process.js

通常execFile的效率要高于exec,這是因為execFile沒有啟動一個shell,而是直接調用spawn來實現的。
2.10.6 進程間通信
前面介紹的幾個用于創建進程的方法,都是屬于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方法,用于進程間通信,該方法的聲明如下:

通過send方法發送的消息,可以通過監聽message事件來獲取。
代碼2.32 父進程向子進程發送消息

代碼2.33 子進程接收父進程消息

send方法的第一個參數類型通常為一個json對象或者原始類型,第二個參數是一個句柄,該句柄可以是一個net.Socket或者net.Server對象。下面是一個例子:
代碼2.34 父進程發送一個Socket對象

代碼2.35 子進程接收socket對象

2.10.7 Cluster
前面已經介紹了child process的使用,child_process的一個重要使用場景是創建多進程服務來保證服務穩定運行。
為了統一Node創建多進程服務的方式,Node在0.6之后的版本中增加了Cluster模塊,Cluster可以看作是做了封裝的child_Process模塊。
Cluster模塊的一個顯著優點是可以共享同一個socket連接,這代表可以使用Cluster模塊實現簡單的負載均衡。
代碼2.36 Cluster的簡單例子

上面是使用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句柄發送給子進程。
- INSTANT Sencha Touch
- Production Ready OpenStack:Recipes for Successful Environments
- Java加密與解密的藝術
- Serverless架構
- Python時間序列預測
- Oracle從入門到精通(第5版)
- PySide 6/PyQt 6快速開發與實戰
- Learning OpenStack Networking(Neutron)(Second Edition)
- Visual FoxPro程序設計習題集及實驗指導(第四版)
- 深入淺出React和Redux
- Django 3.0入門與實踐
- SignalR:Real-time Application Development(Second Edition)
- Python Projects for Kids
- Java EE 8 and Angular
- Get Your Hands Dirty on Clean Architecture