這第九章也是個大重點
九、玩轉進程
Node在選型時決定在V8引擎之上構建,也就意味著它的模型與瀏覽器類似。
本章關于進程的介紹和討論將會解決如下兩個問題:
- 單進程單線程并非完美,如今CPU基本均是多核的,真正的服務器(非VPS)往往還有多個CPU。——如何充分利用多核CPU服務器?
- 由于Node執行在單線程上,一旦單線程上拋出的異常沒有捕獲,將會引起整個進程的崩潰。——如何保證進程的健壯性和穩定性?
9.1 服務模型的變遷
9.1.1 石器時代:同步
9.1.2 青銅時代:復制進程
9.1.3 白銀時代:多進程
9.1.4 黃金時代:事件驅動
9.2 多進程架構
面對單進程單線程對多核使用不足的問題,前人的經驗是啟動多進程即可。理想狀態下每個進程各自利用一個CPU,以此實現多核CPU的利用。
Node提供了 child_process 模塊,并且也提供了 child_process.fork()函數實現進程復制。
9.2.1 創建子進程
child_process 模塊提供了4個方法用于創建子進程。
spawn(); // 啟動一個子進程來執行命令。
exec(); // 啟動一個子進程來執行命令,與spawn()不同的是其接口不同,它有一個回調函數獲知子進程的狀況。
execFile(); // 啟動一個子進程來執行可執行文件。
fork(); // 與spawn()類似,不同點在于它創建的Node的子進程只需指定要執行的JavaScript文件模塊即可。
- spawn()、exec()、execFile()不同點:后兩者創建時可以指定timeout屬性設置超時時間,一旦創建的進程超過設定的時間將會被殺死。
這里的可執行文件是指可以直接執行的文件,如果是JavaScript文件通過execFile()運行,它的首行內容必須添加如下代碼:
#!/usr/bin/env node
盡管4種創建子進程的方式有些差別,但事實上后面三種方法都是spawn()的延伸應用。
9.2.2 進程間的通信
-
創建子進程后為了實現父子進程之間的通信,父與子之間會創建IPC通道,通過IPC通道父子進程之間才能通過
message
和send()
傳遞消息。 -
IPC(Inter-Process Communication)進程間的通信。實現進程間通信的技術也有很多,如:命名管道、匿名管道、socket、信號量、共享內存、消息隊列、Domain Socket等。Node中實現IPC通道的是管道(pipe)技術。在Node中管道只是個抽象層面的稱呼,具體實現由 libuv 提供,在Windows下有命名管道(named pipe)實現,*nix系統則采用Unix Domain Socket實現,表現在應用層上的進程間通信只有簡單的 message 事件和 send() 方法。
-
與網絡socket的行為比較類似,屬于雙向通信。不同的是他們在系統內核中就完成了進程間的通信,而不用經過實際的網絡層,非常高效。
-
注意:只有啟動的子進程是Node進程時,子進程才會根據環境變量去連接IPC通道,對于其他類型的子進程無法實現進程間的通信,除非其他進程也按約定去連接這個已經創建好的IPC通道。
9.2.3 句柄傳遞
send(message, [sendHandle])
方法除了能通過IPC發送數據外,還能發送句柄,第二個可選參數就是句柄。
什么是句柄?一種可以用來標識資源的引用,它的內部包含了指向對象的文件描述符。比如句柄可以用來標識一個服務器端socket對象、一個客戶端socket對象、一個UDP套接字、一個管道等。P252
發送句柄意味著什么?在前一個問題中,我們可以去掉代理這種方案,使主進程接收到 socket 請求后,將這個socket直接發送給工作進程,而不是重新與工作進程之間建立新的socket連接來轉發數據。文件描述符浪費的問題可以通過這樣的方式輕松解決。P253
1、句柄發送與還原
2、端口共同監聽
9.2.4 小結
至此,我們介紹了創建子進程、進程間通信的IPC通道實現、句柄在進程間的發送和還原、端口共用等細節。通過這些基礎技術,用child_process模塊在單機上搭建Node集群是件相當容易的事情。因此在多核CPU的環境下讓Node進程能夠充分利用資源不再是難題。
9.3 集群穩定之路
- 性能問題。
- 多個工作進程的存活狀態管理。
- 工作進程的平滑重啟。
- 配置或者靜態數據的動態重新載入。
- 其他細節。
9.3.1 進程事件 P258
9.3.2 自動重啟 P259
9.3.3 負載均衡 P264
9.3.4 狀態共享 P265
9.4 Cluster 模塊
前文介紹了child process模塊中的大多數細節,以及如何通過這個模塊構建強大的單機集群。如果熟知Node,也許你會驚訝為何遲遲不談cluster模塊。上述提及的問題,Node在v0.8版本時新增的cluster模塊就能解決。在v0.8版本之前,實現多進程架構必須通過child process來實現,要創建單機Node集群,由于有這么多細節需要處理,對普通工程師而言是一件相對較難的工作,于是v0.8時直接引入了cluter模塊,用以解決多核CPU的利用率問題,同時也提供了較完善的API,用以處理進程的健壯性問題。
P267
9.4.1 Cluster 工作原理
事實上cluster模塊就是child process和net模塊的組合應用。cluster啟動時,如同我們在9.2.3節里的代碼一樣,它會在內部啟動TCP服務器,在cluster.fork()子進程時,將這個TCP服務器端socket的文件描述符發送給工作進程。如果進程是通過cluster.fork()復制出來的,那么它的環境變量里就存在NODE_UNIOUE_ID如果作進中存在listen()偵聽網絡端口的調用,它將拿到該文件描述符,通過SO_REUSEADDR端口重用,從而實現多個子進程共享端口。對于普通方式啟動的進程,則不存在文件描述符傳遞共享等事情。
在cluster內部隱式創建TCP服務器的方式對使用者來說十分透明,但也正是這種方式使得它無法如直接使用child_process那樣靈活。在cluster模塊應用中,一個主進程只能管理一組工作進程,如下圖所示。( 書中P268)
對于自行通過child process來操作時,則可以更靈活地控制工作進程,甚至控制多組工作進程。其原因在于自行通過child process操作子進程時,可以隱式地創建多個TCP服務器使得子進程可以共享多個的服務器端socket。
9.4.2 Cluster 事件
對于健壯性處理,cluster模塊也暴露了相當多的事件。
- fork:復制一個工作進程后觸發該事件。
- online:復制好一個工作進程后,工作進程主動發送一條nline消息給主進程,主進程收到消息后,觸發該事件。
- listening:工作進程中調用listen()(共享了服務器端Socket)后,發送一條listening消息給主進程,主進程收到消息后,觸發該事件。
- disconnect:主進程和工作進程之間IPC通道斷開后會觸發該事件。
- exit:有工作進程退出時觸發該事件。
- setup:cluster.setupMaster()執行后觸發該事件。
這些事件大多跟child process模塊的事件相關,在進程間消息傳遞的基礎上完成的封裝這些事件對于增強應用的健壯性已經足夠了。
9.5 總結
盡管Node從單線程的角度來講它有夠脆弱的:既不能充分利用多核CPU資源,穩定性也無法得到保障。但是群體的力量是強大的,通過簡單的主從模式,就可以將應用的質量提升一個檔次。在實際的復雜業務中,我們可能要啟動很多子進程來處理任務,結構甚至遠比主從模式復雜,但是每個子進程應當是簡單到只做好一件事,然后通過進程間通信技術將它們連接起來即可。這符合Unix的設計理念,每個進程只做一件事,并做好一件事,將復雜分解為簡單,將簡單組合成強大。
盡管通過child_process模塊可以大幅提升Node的穩定性,但是一旦主進程出現問題所有子進程將會失去管理。在Node的進程管理之外,還需要用監聽進程數量或監聽日志的方式確保整個系統的穩定性,即使主進程出錯退出,也能及時得到監控警報,使得開發者可以及時處理故障。
個人心得:這章學得我腦殼痛,下次再重新學一遍然。