系列目錄
- NodeJS與Django協同應用開發(0) node.js基礎知識
- NodeJS與Django協同應用開發(1)原型搭建
- NodeJS與Django協同應用開發(2)業務框架
- NodeJS與Django協同應用開發(3)測試與優化
- NodeJS與Django協同應用開發(4)部署
不管怎樣,當你想要分享一個東西的時候你就逃不開要解釋它是什么,它是做什么用的。所以作為基礎知識儲備,這里就介紹一下node.js以及其最重要的模塊之一Socket.io。
node.js
對于node.js最多的介紹就是:它是基于異步事件驅動的高性能非阻塞IO框架。
所謂非阻塞IO模型(non-blocking I/O model),講到這個恐怕得從最初的服務器網絡模型說起。
最初的服務器都是單進程的阻塞IO,所有的請求都在同一個進程里連續處理,如果當前請求耗費了較長的CPU時間,那么其他后續所有請求都要等待這個請求處理結束后才能處理,而在處理期間這個進程也會被掛起。所謂是一卡全都卡。
在這之后發展出的是多進程多線程的網絡模型。首先是簡單的多進程模型:對于每一個連接都分配一個進程。這種做法簡單粗暴得解決了一個請求阻塞導致全部請求都阻塞的問題,在請求量稍高但是不那么高的場景下完全足夠了。但是當請求量一旦多起來,就又暴露出2個缺點:
- 一個進程所需的系統資源是不容忽視的,但是相比處理單個請求來說又顯得殺雞用牛刀,高并發情況下系統資源極易耗盡。
- 同時能夠處理的進程數取決于CPU核數,進程越多就意味著更頻繁的進程調度,也就意味著更多的時間被花在了切換進程上,導致整個系統的性能下降。
于是又加入了多線程模型,讓一個進程可以處理多個請求,以求降低系統資源的消耗。雖然多線程對于上述2個問題都能有所改善,但是治標不治本。創建線程開銷小于創建進程,切換線程開銷也小于切換進程(原因在于切換進程涉及到換頁,涉及到代碼段換入換出,線程不涉及代碼段,只涉及數據段,也就是寄存器狀態和棧狀態),不過這只是把壓力閾值提高了一些而已,依然有一部分系統資源、CPU時間浪費在了非處請請求上。
到這里為止所說的都還是阻塞IO模型,也就是一個處理請求的單元從收到請求,處理請求,到返回結果之間都是線性的,任何一步因為任何原因阻塞了都會導致這個處理單元的阻塞,并被掛起。假如我們在一個請求的處理過程中需要查詢數據庫,或是依賴外部服務,而這些操作又有很大的延遲,那么在這段時間內處理單元就會被掛起,但由于這個請求沒有處理完,資源就不能拿出來處理其他的請求。所以哪怕服務負載特別高,資源的利用率也不高。所以就催生出了非阻塞IO模型。
在非阻塞IO模型里,最重要的一點就是任何操作都不會將當前處理單元掛起,從而能夠允許其執行之后的請求。這么做的好處在于更大化得利用了CPU時間,讓CPU時刻都在處理需要處理的事務。所以在這種模型下,多進程多線程就顯得沒那么必要,只要能夠完全利用CPU的核數,單線程也完全足夠,且性能還要優于之前的模型。這也是為什么現在的負載均衡器都建議按照CPU核數來部署應用。
舉個例子,假設我們只有一個進程(單線程),此時來了3個請求,第一個請求需要查詢數據庫,耗時500ms,第二個請求需要調用外部服務,耗時2000ms,第三個請求查詢系統時間,耗時5ms。
在普通的阻塞IO模型里,第一個請求來了之后先花500ms處理完,再花2000ms處理第二個請求,再花5ms處理第三個請求。也就是說第一個用戶500ms后得到了結果,第二個用戶卻用了2500ms得到結果,第三個用戶僅僅只要查一下時間卻用了2505ms才得到結果。
而在非阻塞IO模型里,同樣第一個請求過來后,查詢工作交給數據庫,緊接著就開始處理第二個請求。同樣任務交給外部服務后,就開始處理第三個請求。第三個請求很快就得到了結果,并返回給了用戶,此時才經過了5ms。在500ms的時間點,數據庫查詢工作結束,并返回給了第一個用戶,在2000ms的時間點,外部服務返回了結果,于是就返回給了第二個用戶。
下面這個表格就很清楚的展示了兩種模型的差異。
發起請求到獲得結果時間表 | 阻塞IO模型 | 非阻塞IO模型 |
---|---|---|
用戶1 | 500ms | 500ms |
用戶2 | 2500ms | 2000ms |
用戶3 | 2505ms | 5ms |
可以看出,非阻塞IO模型在請求量很多的時候,對于每一個用戶而言都有很好的體驗。
這一系列的演化,舉個例子,就好比有一個水庫需要排水,最初你只有一個水管(單進程)排水,突然有些水草把管子堵住了(當前請求阻塞),那在你把水草清理掉之前水都流不出來(所有請求阻塞)。后來你決定再多買一些水管(多進程),但是受財力限制你只能買100根(系統資源耗盡)。不過水草堵了之后依然需要人力來清理,但是依然財力所限你只能請4個工人(CPU核數),而工人大部分時間又花在了在水管間走來走去,而不是實際清理水草上(進程切換)。這時候有個公司過來找你說他們有種技術可以把25根管子包進一個大管子里,看上去你只用了4個水管排水(多線程),而且還比原來便宜(減少系統資源開銷)。但是水管依然會堵,工人依然要找究竟哪個小管子堵了(線程依然需要切換)。再后來又有個公司過來找你說有一套新型管子,能夠在水草堵住的時候自動切換到備用水管,并自動開始清理水草,永遠能夠讓水順利流出(非阻塞模型),價值是原來的一個工人和25根水管的總和。于是你非常開心的辭退了所有工人,賣了原先的所有水管,買了4套新型管子(最大化利用CPU核數)。此后就再也不用擔心水排不出了。
這就是非阻塞IO模型,而node.js的設計初衷就是搭建靈活的網絡應用,于是在此之上選用了JavaScript這一靈活的語言和異步事件驅動設計。
所謂事件驅動(event-driven),通俗來講就是發生了什么事,我們才做相應的處理。這里的事件可以是一個用戶操作,可以是一次內容變化,也可以是一次網絡請求,甚至可以是一次系統異常。凡是我們關心的,都可以成為事件,然后我們再做相應的處理。
而異步(asynchronous communication)的意思就是說,當一個調用到來時,并不等到有結果了再返回,而是直接返回,有結果了再通知調用方。
同步與異步、阻塞與非阻塞,看上去非常相似但本質是不同的。同步異步關心的是消息通信機制,阻塞非阻塞關心的是等待調用結果時的狀態。
以下內容引用自知乎用戶 盧毅 在問題怎樣理解阻塞非阻塞與同步異步的區別?中的回答:
[同步異步]
舉個通俗的例子:
你打電話問書店老板有沒有《分布式系統》這本書,如果是同步通信機制,書店老板會說,你稍等,”我查一下",然后開始查啊查,等查好了(可能是5秒,也可能是一天)告訴你結果(返回結果)。
而異步通信機制,書店老板直接告訴你我查一下啊,查好了打電話給你,然后直接掛電話了(不返回結果)。然后查好了,他會主動打電話給你。在這里老板通過“回電”這種方式來回調。
[阻塞非阻塞]
還是上面的例子,
你打電話問書店老板有沒有《分布式系統》這本書,你如果是阻塞式調用,你會一直把自己“掛起”,直到得到這本書有沒有的結果,如果是非阻塞式調用,你不管老板有沒有告訴你,你自己先一邊去玩了, 當然你也要偶爾過幾分鐘check一下老板有沒有返回結果。
在這里阻塞與非阻塞與是否同步異步無關。跟老板通過什么方式回答你結果無關。
而node.js在運行時是采用單線程架構的,又綜合了異步與非阻塞的優點,這就是為什么它還能保持高性能的原因。至于為什么node.js選用了JavaScript和異步事件驅動,那就不是這里論述的話題了,只能說它就是這么選了,它樂意。
題外話:
node.js選擇了JavaScript,所以就選擇了Google V8引擎,而原本V8是為了瀏覽器所設計的高性能js解釋器,因此并不具備多線程能力,所以node.js也只能是單線程架構。但我認為如果技術上能夠讓node.js支持多線程,性能并不會高于單線程。引入多線程就勢必引入鎖,在某些情況下加鎖的開銷是不能忽略的,所以不見得多線程就是好。不過單線程的node.js并不能100%發揮出多核CPU的能力,所以通過child_process.fork(),也就是現在的cluster模塊,允許node在多進程下編程。這樣更加能夠利用硬件資源來支撐高并發高訪問量。
Socket.io
相比node.js, socket.io就簡單得多了。這是一個用來構建實時web應用的JavaScript庫,對于瀏覽器的客戶端和后臺服務器端有兩套接口相似的庫,與node.js相同,它也是事件驅動的。
多數情況下socket.io是基于websocket協議的,但是對于不支持的瀏覽器它也能夠自動回退到flash或是長輪詢。
socket.io的好處就在于自動選擇合適的協議,簡單易上手的api。可以說node.js的熱度有相當一部分是由socket.io撐起來的。
對于socket.io也沒有那么多可以介紹的,各種特性實例代碼一看便知,所以這篇文章就先寫到這吧,后文會搭建出一套node.js+socket.io+Django+redis的原型,更詳細的內容可以移步那里。