Nodejs cluster 模組深入探究

欲休發表於2017-08-16
### 由表及裡 HTTP伺服器用於響應來自客戶端的請求,當客戶端請求數逐漸增大時服務端的處理機制有多種,如tomcat的多執行緒、nginx的事件迴圈等。而對於node而言,由於其也採用事件迴圈和非同步I/O機制,因此在高I/O併發的場景下效能非常好,但是由於單個node程式僅僅利用單核cpu,因此為了更好利用系統資源就需要fork多個node程式執行HTTP伺服器邏輯,所以node內建模組提供了child_process和cluster模組。 利用childprocess模組,我們可以執行shell命令,可以fork子程式執行程式碼,也可以直接執行二進位制檔案;利用cluster模組,使用node封裝好的API、IPC通道和排程機可以非常簡單的建立包括一個master程式下HTTP代理伺服器 + 多個worker程式多個HTTP應用伺服器的架構,並提供兩種排程子程式演算法。本文主要針對cluster模組講述node是如何實現簡介高效的服務叢集建立和排程的。那麼就從程式碼進入本文的主題: code1

主程式建立多個子程式,同時接受子程式傳來的訊息,迴圈輸出處理請求的數量; 子程式建立http伺服器,偵聽8000埠並返回響應。 泛泛的大道理誰都瞭解,可是這套程式碼如何執行在主程式和子程式中呢?父程式如何向子程式傳遞客戶端的請求?多個子程式共同偵聽8000埠,會不會造成埠reuse error?每個伺服器程式最大可有效支援多少併發量?主程式下的代理伺服器如何排程請求? 這些問題,如果不深入進去便永遠只停留在寫應用程式碼的層面,而且不瞭解cluster叢集建立的多程式與使用child_process建立的程式叢集的區別,也寫不出符合業務的最優程式碼,因此,深入cluster還是有必要的。 ## cluster與net cluster模組與net模組息息相關,而net模組又和底層socket有聯絡,至於socket則涉及到了系統核心,這樣便由表及裡的瞭解了node對底層的一些優化配置,這是我們的思路。介紹前,筆者仔細研讀了node的js層模組實現,在基於自身理解的基礎上詮釋上節程式碼的實現流程,力圖做到清晰、易懂,如果有某些紕漏也歡迎讀者指出,只有在互相交流中才能收穫更多。 ### 一套程式碼,多次執行 很多人對code1程式碼如何在主程式和子程式執行感到疑惑,怎樣通過_cluster.isMaster判斷語句內的程式碼是在主程式執行,而其他程式碼在子程式執行呢? 其實只要你深入到了node原始碼層面,這個問題很容易作答。cluster模組的程式碼只有一句:

只需要判斷當前程式有沒有環境變數“NODE_UNIQUE_ID”就可知道當前程式是否是主程式;而變數“NODE_UNIQUE_ID”則是在主程式fork子程式時傳遞進去的引數,因此採用cluster.fork建立的子程式是一定包含“NODE_UNIQUE_ID”的。 這裡需要指出的是,必須通過cluster.fork建立的子程式才有NODE_UNIQUE_ID變數,如果通過child_process.fork的子程式,在不傳遞環境變數的情況下是沒有NODE_UNIQUE_ID的。因此,當你在child_process.fork的子程式中執行cluster.isMaster判斷時,返回 true。 ### 主程式與伺服器 code1中,並沒有在cluster.isMaster的條件語句中建立伺服器,也沒有提供伺服器相關的路徑、埠和fd,那麼主程式中是否存在TCP伺服器,有的話到底是什麼時候怎麼建立的? 相信大家在學習nodejs時閱讀的各種書籍都介紹過在叢集模式下,主程式的伺服器會接受到請求然後傳送給子程式,那麼問題就來到主程式的伺服器到底是如何建立呢?主程式伺服器的建立離不開與子程式的互動,畢竟與建立伺服器相關的資訊全在子程式的程式碼中。 當子程式執行

時,http模組會呼叫net模組(確切的說,http.Server繼承net.Server),建立net.Server物件,同時偵聽埠。建立net.Server例項,呼叫建構函式返回。建立的net.Server例項呼叫listen(8000),等待accpet連線。那麼,子程式如何傳遞伺服器相關資訊給主程式呢?答案就在listen函式中。我保證,net.Server.prototype.listen函式絕沒有表面上看起來的那麼簡單,它涉及到了許多IPC通訊和相容性處理,可以說HTTP伺服器建立的所有邏輯都在listen函式中。 > 延伸下,在學習linux下的socket程式設計時,服務端的邏輯依次是執行socket(),bind(),listen()和accept(),在接收到客戶端連線時執行read(),write()呼叫完成TCP層的通訊。那麼,對應到node的net模組好像只有listen()階段,這是不是很難對應socket的四個階段呢?其實不然,node的net模組把“bind,listen”操作全部寫入了net.Server.prototype.listen中,清晰的對應底層socket和TCP三次握手,而向上層使用者只暴露簡單的listen介面。 code2

由於本文只探究cluster模式下HTTP伺服器的相關內容,因此我們只關注有關TCP伺服器部分,其他的Pipe(domain socket)服務不考慮。 listen函式可以偵聽埠、路徑和指定的fd,因此在listen函式的實現中判斷各種引數的情況,我們最為關心的就是偵聽埠的情況,在成功進入條件語句後發現所有的情況最後都執行了listenInCluster函式而返回,因此有必要繼續探究。 code3

listenInCluster函式傳入了各種引數,如server例項、ip、port、ip型別(IPv6和IPv4)、backlog(底層服務端socket處理請求的最大佇列)、fd等,它們不是必須傳入,比如建立一個TCP伺服器,就僅僅需要一個port即可。 簡化後的listenInCluster函式很簡單,cluster模組判斷當前程式為主程式時,執行_listen2函式;否則,在子程式中執行cluster._getServer函式,同時像函式傳遞serverQuery物件,即建立伺服器需要的相關資訊。 因此,我們可以大膽假設,子程式在cluster._getServer函式中向主程式傳送了建立伺服器所需要的資料,即serverQuery。實際上也確實如此: code4

子程式在該函式中向已建立的IPC通道傳送內部訊息message,該訊息包含之前提到的serverQuery資訊,同時包含act: ‘queryServer’欄位,等待服務端響應後繼續執行回撥函式modifyHandle。 主程式接收到子程式傳送的內部訊息,會根據act: ‘queryServer’執行對應queryServer方法,完成伺服器的建立,同時傳送回覆訊息給子程式,子程式執行回撥函式modifyHandle,繼續接下來的操作。 至此,針對主程式在cluster模式下如何建立伺服器的流程已完全走通,主要的邏輯是在子程式伺服器的listen過程中實現。 ### net模組與socket 上節提到了node中建立伺服器無法與socket建立對應的問題,本節就該問題做進一步解釋。在net.Server.prototype.listen函式中呼叫了listenInCluster函式,listenInCluster會在主程式或者子程式的回撥函式中呼叫_listen2函式,對應底層服務端socket建立階段的正是在這裡。

通過createServerHandle函式建立控制程式碼(控制程式碼可理解為使用者空間的socket),同時給屬性onconnection賦值,最後偵聽埠,設定backlog。 那麼,socket處理請求過程“socket(),bind()”步驟就是在createServerHandle完成。

在createServerHandle中,我們看到了如何建立socket(createServerHandle在底層利用node自己封裝的類庫建立TCP handle),也看到了bind繫結ip和地址,那麼node的net模組如何接收客戶端請求呢? 必須深入c++模組才能瞭解node是如何實現在c++層面呼叫js層設定的onconnection回撥屬性,v8引擎提供了c++和js層的型別轉換和介面透出,在c++的tcp_wrap中:

我們關注uvlisten函式,它是libuv封裝後的函式,傳入了**handle,backlog和OnConnection回撥函式,其中handle_為node呼叫libuv介面建立的socket封裝,OnConnection函式為socket接收客戶端連線時執行的操作。我們可能會猜測在js層設定的onconnction函式最終會在OnConnection中呼叫,於是進一步深入探查node的connection_wrap c++模組:

過濾掉多餘資訊便於分析。當新的客戶端連線到來時,libuv呼叫OnConnection,在該函式內執行uv_accept接收連線,最後將js層的回撥函式onconnection[通過env->onconnection_string()獲取js的回撥]和接收到的客戶端socket封裝傳入MakeCallback中。其中,argv陣列的第一項為錯誤資訊,第二項為已連線的clientSocket封裝,最後在MakeCallback中執行js層的onconnection函式,該函式的引數正是argv陣列傳入的資料,“錯誤程式碼和clientSocket封裝”。 js層的onconnection回撥

這樣,node在C++層呼叫js層的onconnection函式,構建node層的socket物件,並觸發connection事件,完成底層socket與node net模組的連線與請求打通。 至此,我們打通了socket連線建立過程與net模組(js層)的流程的互動,這種封裝讓開發者在不需要查閱底層介面和資料結構的情況下,僅使用node提供的http模組就可以快速開發一個應用伺服器,將目光聚集在業務邏輯中。 > backlog是已連線但未進行accept處理的socket佇列大小。在linux 2.2以前,backlog大小包括了半連線狀態和全連線狀態兩種佇列大小。linux 2.2以後,分離為兩個backlog來分別限制半連線SYN_RCVD狀態的未完成連線佇列大小跟全連線ESTABLISHED狀態的已完成連線佇列大小。這裡的半連線狀態,即在三次握手中,服務端接收到客戶端SYN報文後併傳送SYN+ACK報文後的狀態,此時服務端等待客戶端的ACK,全連線狀態即服務端和客戶端完成三次握手後的狀態。backlog並非越大越好,當等待accept佇列過長,服務端無法及時處理排隊的socket,會造成客戶端或者前端伺服器如nignx的連線超時錯誤,出現“error: Broken Pipe”**。因此,node預設在socket層設定backlog預設值為511,這是因為nginx和redis預設設定的backlog值也為此,儘量避免上述錯誤。 ###

打賞支援我寫出更多好文章,謝謝!

打賞作者

打賞支援我寫出更多好文章,謝謝!

Nodejs cluster 模組深入探究

相關文章