NodeJS是基於chrome瀏覽器的V8
引擎構建的,也就說明它的模型與瀏覽器是相似的。咱們的javascript會運行在單個進程的單個線程上。這樣有一個好處:javascript
狀態單一java
沒有鎖算法
不須要線程間同步chrome
減小系統上下文的切換數據庫
有效提升單核CPU的使用率瀏覽器
可是V8引擎的單進程單線程並非完美的結構,現現在CPU基本上都是多核的。真正的服務器每每有好幾個CPU(像咱們的線上物理機有12個核),因此,這就將拋出NodeJS實際應用中的第一個問題:「如何充分利用多核CPU服務器?」服務器
另外,因爲Node執行在單線程上,一旦單線程出現未捕獲的異常,就會形成這個進程crash。因此就遇到了第二個問題:「如何保證進程的健壯性和穩定性?」網絡
從嚴格意義上來說,Node其實並非真正的單線程架構,由於Node自身還有I/O
線程存在(網絡I/O、磁盤I/O),這些I/O線程是由更底層的libuv
處理,這部分線程對於JavaScript開發者來講是透明的。JavaScript代碼永遠運行在V8上,是單線程的。因此表面上來看NodeJS是單線程的。多線程
這類服務器是最先出現的,其執行模型是同步的(基於read或select I/O模型
),它的服務模式是一次只能處理一個請求,其餘的請求都須要按照順序依次等待接受處理。這就意味着除了當前的請求被處理以外,剩下的請求都是處於阻塞等待的狀態。
因此,它的處理能力特別的低下。
假如服務器每次響應請求處理的時間爲N秒,那麼這類服務器的QPS爲1/N
。架構
爲了解決上面的同步單進程服務器沒法處理的併發問題,這類服務器經過進程的複製
同時服務更多的請求和用戶。一個請求須要一個進程來服務,也就是100個請求就須要100個進程來進行服務,這須要很大的代價。由於在進程的複製總會複製進程內部的狀態,對於每一個鏈接都進行這樣的複製的話,相同的狀態會在內存中存在不少份,形成浪費。同時這個過程會由於複製不少個進程影響進行的啓動時間。並且服務器的進程數量也是有上限的。
因此,這個模型並無實質上解決併發問題。
假如這類服務器的進程數上限爲M,每一個請求處理的時間爲N秒,那麼這類服務器的QPS爲M*1/N
。
爲了解決進程複製中的資源浪費問題,多線程被引入了服務模型
,從一個進程處理一個請求改成一個線程處理一個請求。線程相對於進程的開銷要小許多,並且線程之間能夠共享數據。此外能夠利用線程池來減小建立和銷燬線程的開銷。可是多線程所面臨的併發問題只能說比多進程好點而已,由於每一個線程須要必定內存來存放本身的堆棧。另一個CPU核心只能處理一件事,系統是經過將CPU切分爲時間片的方法來讓線程能夠均勻地使用CPU資源,在系統切換線程的過程當中也會進行線程的上下文切換(切換爲當前線程的堆棧),當線程數量過多時進行上下文切換會很是耗費時間。
因此在大的併發量下,多線程結構仍是沒法作到強大的伸縮性。大名鼎鼎的Apache
服務器就是採用了這樣的架構,因此出現了著名的C10K
問題。
咱們忽略系統進行線程的上下文切換的開銷,假如這類服務器能夠建立M個進程,一個進程可使用L個線程,每一個請求處理的時間爲N秒,那麼它的QPS爲M*L/N
。
爲了解決C10K以及解決更高併發的問題,基於epoll(效率最高的I/O事件通知機制)
的事件驅動模型出現了。採用單線程避免了沒必要要的內存開銷和上下文切換開銷。
不過這種基於事件的服務器模型存在的文章剛開始提出兩個問題:「CPU的利用率和健壯性」。
另外,全部的請求處理都在單線程上進行,影響事件驅動服務模型性能的只有CPU的計算能力,它的上限決定了這類服務器的性能上限,但它不受多進程多線程模式中資源上限的影響,可伸縮性比前二者都高。若是能夠解決多核CPU的利用問題,那麼帶來的性能提高是很是高的。
面對單進程單線程對多核使用率不高的問題,按照以前的經驗,每一個進程各使用一個CPU便可,以此實現多核CPU的利用。Node提供了child_process
模塊,而且也提供了fork()
方法來實現進程的複製(只要是進程複製,都須要必定的資源和時間。Node複製進程須要不小於10M的內存和不小於30ms的時間
)。
這樣的解決方案就是*nix
系統上最經典的Master-Worker模式
,又稱爲主從模式
。這種典型並行處理業務模式的分佈式架構具有較好的可伸縮性(可伸縮性其實是和並行算法以及並行計算機體系結構放在一塊兒討論的。某個算法在某個機器上的可擴放性反映該算法是否能有效利用不斷增長的CPU。
)和穩定性。主進程不負責具體的業務處理,而是負責調度和管理工做進程,工做進程負責具體的業務處理,因此,工做進程的穩定性是開發人員須要關注的。
經過fork()複製的進程都是一個獨立
的進程,這個進程中有着獨立而全新的V8實例。雖然Node提供了fork()
用來複制進程使每一個CPU內核都使用上,可是依然要記住fork()
進程代價是很大的。好在Node經過事件驅動在單個線程上能夠處理大併發的請求。
注意:這裏啓動多個進程只是爲了充分將CPU資源利用起來,而不是爲了解決併發問題。
建立一個子進程來執行命令
建立一個子進程來執行命令,和spawn()不一樣的是方法參數不一樣,它能夠傳入回調函數來獲取子進程的狀態
啓動一個子進程來執行指定文件。注意,該文件的頂部必須聲明SHEBANG符號(#!)用來指定進程類型。
和spawn()相似,不一樣點在於它建立Node的子進程只須要執定要執行的JavaScript文件模塊便可。
注意:後面的3種方法都是spawn()的延伸應用。
在Master-Worker
模式中,要實現主進程管理和調度工做進程的功能,須要主進程和工做進程之間的通訊。它們經過消息來傳遞內容,而不是共享文件或直接操做相關資源,這是比較輕量和無依賴的作法。
經過fork()或者其餘的API建立子進程以後,爲了實現父子進程之間的通訊,父進程與子進程之間將會建立IPC通道。經過IPC通道,父子進程之間才能夠傳遞消息。
IPC全稱 Inter-Process Communication
,也就是進程間通訊。進程間通訊的目的是爲了讓不一樣的進程可以互相訪問資源並進行協調工做。Node中的IPC建立和實現過程以下:
父進程在實際建立子進程以前,會先建立IPC通道並監聽它,而後才真正建立出子進程。子進程在啓動的過程當中會去連接這個已存在的IPC通道,從而完成了父子進程之間的鏈接。
建立好進程之間的IPC以後,若是僅僅只用來發送一些簡單數據,顯然不夠咱們的實際使用。
理想狀況下,無論服務啓動了多少個進程都應該通過同一個Master進程來進行控制和調度。因此全部請求都應該先通過同一個端口,而後經過Master進程交由具體的Worker進程處理。
讓每一個進程監聽不一樣的端口,其中主進程監聽主端口(80端口),主進程對外接受全部的網絡請求,再將這些請求代理到不一樣的端口進程上。
經過代理,能夠避免端口不能重複監聽的問題,也能夠在代理進程作適當的負載均衡,這樣每一個子進程均可以均衡的處理服務。
因爲進程每接受到一個鏈接,將會用到一個文件描述符,所以代理模式鏈接工做進程的過程須要用到兩個文件描述符。操做系統的文件描述符是有限的。因此這種方案影響了系統的擴展能力
Nodejs提供了進程間發送句柄的功能。有了這個功能咱們能夠不使用代理模式
方案,使主進程接受到socket請求以後,將這個socket對象直接轉發給工做進程,而不是從新遇工做進程之間建立新的socket鏈接來轉發數據。這樣的話,文件描述符浪費的問題能夠輕輕鬆解決。
在程序設計中,句柄(handle)是一種特殊的智能指針。當一個應用程序要引用其餘系統(如數據庫、操做系統)所管理的內存塊或對象時,就要使用句柄。
這樣全部的請求都是由子進程處理了。整個過程當中,服務的過程發生了一次改變以下圖:
主進程發送完句柄並關閉監聽以後,就成了下圖的機構。
多個應用監聽相同的端口時,文件描述符同一時間只能被某個進程所用,也就是網絡請求發送的服務器端時,只有一個幸運的進程可以搶到鏈接,只有它能爲這個請求進行服務。因此這些進程服務是搶佔式
的。
至此,以此介紹了建立子進程、進程間通訊的IPC通道實現、句柄在進程間的發送和使用原理、端口共用等細節。經過這些基礎技術,在多核的CPU服務器上,讓Node進程可以充分利用資源不是難題。
搭建好了集羣,充分利用了多核CPU的資源,可是在迎接大量的客戶端請求以前,還有不少穩定性的問題亟待解決。
工做進程存活狀態管理
工做進程平滑重啓
工做進程限量重啓
工做進程性能問題
工做進程負載均衡
工做進程狀態共享
這些問題下次再寫。。。