新型的服務端正在進入我們的視野,讓我們投入了關注的目光,例如近來的 NodeJS 算比較搶眼的一員。
之是以創造 NodeJS ,引用原作者 Ryan 之語,目标是為了可以更輕松地編寫具有可伸縮性的網絡程式。咋一想,這樣的目标作為網絡開發人員們何曾不想擁有。——于是看看Nodejs 是怎麼實作的。首先由淺入深說下簡單的概念:無論是複雜的業務邏輯,還是簡單的“HelloWorld”也罷,用戶端發送連結過來,Web 伺服器肯定要一一全單照收,不會拒“連結”于千裡之外。當中所說的性能名額即為我們日常會提到的——“并發(Concurrency)”。Web 伺服器是并發處理這些連結請求的。并發越高,伺服器性能越好——到最終,大概是要解決著名 “C10K 問題”。在處理并發的這個技術問題上,NodeJS 表現出來的,就是高并發、低消耗的佼佼者。
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsIiZpdmLzQTMxETN5QTOxkDOyEzXw8CX48CXxEDMxAjMvw1ckF2bsBXdvwFdl5mLuR2cj5Set1yZtl2Lc9CX6MHc0RHaiojIsJye.gif)
NodeJS 有一定性能優勢,也引發了我們技術人員的濃厚興趣。不免要問,NodeJS 是如何辦到的?NodeJS 為開源項目,如果不打算直接通過源碼了解,我們還是可以利用網上一點資訊去了解的。筆者收集了關于 NodeJS 的幾遍文章、部落格,略有心得,将它們想表達 NodeJS 的特點、缺點、相關原理、前期分析、選型等等各方的問題“共冶一爐”,說出個 NodeJS 初步分析的大概。
如果各位看官不太了解服務端的運作的話,我們稍微回顧一下請求 Request 這一環節的過程。現今多數的 Web 伺服器中,有一條新的連結就會申請一條線程來負責處理至到這個 Request 周期結束,接着執行其他流程。可以想象,成千上萬個連結便有成千上萬條線程(Thread-spawning)。每條線程姑且以堆棧 2MB 的消耗去計算,一條條線程它們的累加都是不小的數目。如何優化和改進本身就是一個大問題,此外,使用系統線程,必須考慮線程鎖的問題,否則造成堵塞主程序又是一個令人操心的難題。NodeJS 則通過基于事件的異步模型繞開了基于線程模型的所帶來的問題。NodeJS 使用 JavaScript 單線程(Single-threaded)輪詢事件,設計上比較簡單,高并發時,不僅根本性的減少了線程建立和切換的開銷(因而沒有吓人的消耗),而且由于沒有鎖,也不會造成程序阻塞。每當有連結發起到服務端之後,NodeJS 會分發 epoll、kqueue、dev/poll 或 select 指令通知作業系統,有新連結到達,應執行指定的回調函數(Callback)。每個連結從成本上說隻消耗一個堆(heap allocation)。
NodeJS 使用單線程就足以提供高速的并發能力?是的,實際上著名 Web 伺服器 nginx 也是基于單線程的。然而拜 C++ 所賜,NodeJS 之于 V8 運作時卻擁有多線程的運作環境。怎麼了解這點呢?就是一門腳本語言去代替相對複雜的網絡程式設計。NodeJS 帶有 JS 的名稱,即以 JS 為賣點,由此可見其簡單性浮出水面了——這确實也就是開發 NodeJS 的初衷之一,衆所周知,JS 是輕盈的,用來替 C++ 來跟開發人員打交道——再美好不過,但必須強調,JS 終究是編寫中間件的腳本語言,底層發揮作用的依然為功不可沒的 C++。為了實作這些設計目标,NodeJS 使用了 Google V8 并打包了其中的一些庫:
libev 實作了時間循環并封裝了底層使用的具體的技術(如 select,epoll 等)。
作者自己寫的 http-parser 等協定和其他等等。
其中 libev 正是實作多線程 NodeJS 的基礎(edit on 2010-9-12: Are you sure to say so??? 有什麼證據??)。JavaScript 仍舊發揮腳本語言的本色,一方面将 C++ 的複雜性屏蔽,一方面向程式員呈現優雅的 API。NodeJS 在适合一些較輕松的場合,例如一些分離器 Dispatcher、Request、BeansTalk、AMQP 消息應該沒有問題。但依據國外一些部落格文章分析就是,實際生産中可能會意外頻頻,發生一個錯誤就會挂起 NodeJS,是以單線程不太可靠或許是 NodeJS 一個先天的缺點。另外,編寫 NodeJS 的擴充仍需要出來高深的 C++,恐怕須完善好 C 與 JS 之間的接口層,編寫 NodeJS 擴充則才是我輩能力範圍内的。寫本文的時候,NodeJS 屬新生事物,無須諱言,筆者并沒有太多的一線經驗。話說回來,究竟實際上有多少的情景允許我們一邊計算,一邊做其他的事情而穩定無虞的呢?希望可以有待更多的觀察。
上述的幾點,的确提到了“基于線程模型” v.s “基于事件模型”之争,目的就在于,除了明晰分辨它們的利弊之外,還不能不回答這樣一個問題:既然“基于線程模型”消耗得那麼厲害,那麼為什麼現在這麼多的 Apaches、IIS 都運作得好好的?
基于事件的 Web 伺服器相對是比較新的概念,可以做到比較好的性能,因而受到推崇一點不意外,像 NodeJS 那樣的,——而傳統的基于線程的模型伺服器成熟程度高,況且仍不斷地發展,例如 Apache 的 PHP 會派生出很多的 OS 線程來解決并發的問題,若一個請求挂起了其所在的線程,可以保證其他的線程也不會受到影響,不會當機整個伺服器程序,顯得也比較合理。必須指出的是,像對于如何處理并發來選擇“基于線程模型” v.s “基于事件模型”這樣的讨論,業界一直存在,并不是說基于事件模型的一定優秀無敵,尚有許多一一斟酌讨論的地方,具體如何就不一一展開了。
那麼,NodeJS 的優勢到底在哪?
如某網站 pv 非常可觀,與使用者互動頻繁,那麼它的線路總是處于高峰,自然它的網絡進進出出肯定非常頻繁,勢必要求背景要趕快處理好前一個請求,以便接着有時間來處理一個請求,越快越好、越高效。好在,我們的請求大小都不是很大,通常幾十位元組(如 http://domain:80,一個 GET 操作,cookies 不大的話),控制線程在一個很小的機關,如此往返一個來回很快搞掂。那當然屬于 I/O 最簡單的情況了,稍為複雜的一些就是 POST 表單、檔案上傳等的任務。但好在不是每個連結皆如此,伺服器還可以吃得消久一點的連結。可是,這時候,來問題了——
話說 Web2.0 時興的元素,WebIM、WebGAME、Web 協作……無一不需求長連結為其服務的。長連結,或長輪詢,均是企圖突破現有 HTTP v1.1 連結模型,把無态(Stateless)的點對點連結變為人們理想的有态(Stateful),也就是 Request/Response 互不分離,總是線上有溝通着。實際情形 HTTP 并沒有提供這種的 API 或者說服務。目前我們大抵采用折衷的方法:打開一 HTML 頁面立刻發送服務端的 AJAX 請求,就算是沒有内容的請求都好,沒有關系,伺服器就千萬别像普通 AJAX 那樣接收請求,處理流程後就傳回 Repsonse,不要立刻傳回内容而是等待,換言之,就是保持連結。隻是在有消息發出的時候才傳回 Response 然後浏覽器渲染 Response 内容。例如,有好友發悄悄話給你,通過伺服器發送到你浏覽器上顯示,然後立刻發起新的請求,讓彼此之間的連結一直保持下去。
介紹前面的這麼多,無非想說明,用戶端與服務端一旦連結後,除非使用者關閉浏覽器,否則是不會斷開 keep-alive 連結的。這樣,對于同時維系着數十條或者數百條(聊天室)的 connection 的伺服器,一直非空閑,還要顧上各方面資源(CPU usage、consuming memory……),顯然不是一件容易事情,甚至如“開心網”那樣成千上萬筆 connection 場景就是對服務端極大的考驗,如果占用的線程不能得到迅速釋放,将會給伺服器帶來災難性的後果!
于是一些 Web Serever 開始認真考慮這點,在新版中提供适應長連結的場景,例如 Java 世界的 Jetty 就很早的時候提供了一個 JEE 容器的解決方案,與 Comet 的通訊協定對接上。每個 Server 的架構不一,然而如何改進和改進目标都有參考意義,但改進已是必然了,就要重新考慮 WebI/O,提供足夠快而穩定性能适應長連結的場景。明顯,不得不重新考慮服務端的設計了,然而,背後要考慮的事情就多了。總之,可以想象任務艱巨性,不僅要考慮前方 I/O 高并發,低響應時間的請求,還要考慮整套的服務供應者怎麼去資源調控,具體如負載平衡(Load Balancing)、動态 DNS 切換、DB 的叢集、多個檔案鏡像的問題,往往配合起來就有許多不可預料的問題發生。一個環節有問題真個系統的堵塞了。這一啟承轉合要處理好。
不是有 WebSocket 标準嗎?HTML5 的世界盡管在移動平台上很熱鬧,普通浏覽器更新卻覺得是另一回事。如果現在一下子都是支援 Web Socket 的浏覽器,那不用說準是皆大歡喜了,但事實和将來的預測表明 Web Socket 完全是另外一回事,咱和咱使用者面對的仍舊那些僵硬不化的 IE6……是以說在 Web Socket 不現實的今天,将善于“長連結”的 NodeJS 派上用場便有很充分的理由。
p.s:……包括用 Flash Socket 元件那些 hack 的都不算。
如果删除檔案成功,觸發 success 事件執行 addCallback() 所定義的回調函數;即是删除檔案失敗,産生 wait 的信号,直至 timeout 的時限,也不會阻塞其他 JS 代碼的執行。在 Node.js 的 API 中到處使用着事件的概念,包括許多方法都設有“同步”和“異步”的兩種方式供選擇,故是以我們不用擔心寫的代碼會阻塞 Node.js 的 I/O。
題外話:貌似 AJAX AIR in JS 呈現了也是一種異步調用方式(記得 SQL query 時文法相似)。
事件循環的 console 模拟圖
實際上,NodeJS 不是第一家标榜事件的 Web Server,早在 NodeJs 之前,在各種語言中都有事件的實作,不能不提的就是 nginx。不過使用 JavaScript 的還屬于頭一遭吧?過去幾年可以說是 JS 引擎發展的高峰期,就連最保守的微軟也要 IE9 把落後的 JS 解釋速度争回來,親愛的服務端方面卻又怎麼按耐的住呢?自然,革新速度後,JSVM 引入到 ServerSide 的工作更是一件順理成章的事。
話說回來基于事件理念的 Server。NodeJS 的 idea 最初啟發自 Ruby 的 EventMachine 和 Python 的 Twisted,将包括各種 I/O 操作定義在回調函數中,通過事件不斷輪詢任務清單來觸發那些 Callback,——并且 NodeJS 有創新的地方,就是提出新的思路來呈現事件機制。從原理上講,NodeJS 不僅僅是一個庫,而是嘗試利用語言機制來建構的事件模型。EventMachine 或 Twisted 卻不是這樣,它們都是在代碼開始和結束的時候插入回調函數來完成一個阻塞的調用,然後這個過程的啟用,就用:
而 NodeJS 沒有這種代碼順序的限制,可以在定義代碼之後再插入新的代碼,繼續參與事件。同時 NodeJS 也不會像 Twisted Python 那樣提供“延時線程(deferto thread)”,實際是堵塞代碼的“陷阱”。
盡管我們這裡說的事件模型好像比較簡單,但是許多的基礎設施對異步操作的支援的不足的,尤其普通使用者根本不會自己去建立業務事件。相關内容在介紹 NodeJS 的 Slide 有介紹(搜尋 jsconf.pdf),說明為什麼 NodeJS 出現之前沒有類似 NodeJS 的“物體”出現,同時也說明設計 NodeJS 要克服的難關。
2013-2-7 Edit:
同步的好處:同步流程是最天然的控制過程順序執行的方式,因為同步流程對結果的處理始終和前文保持在一個上下文内,是以同步流程對結果處理通常更為簡單,可以就近處理,是以可以很容易捕獲、處理異常。
異步流程在執行的過程中,可以釋放占用的線程等資源,避免阻塞,等到結果産生再重新擷取線程處理,是以,在此期間可以做更多額外的工作,例如結果記錄等等。這樣,異步流程可以等多次調用的結果出來後,再統一傳回一次結果集合,提高響應效率。
最後一點,談談 NodeJS 為什麼選擇 GoogleV8 的 JS 引擎而不是另一個著名引擎 SpiderMonkey 。抛開速度等的硬性名額不表,依然可能認為 SpiderMonkey 源碼仍比較複雜的緣故,不好把玩,既然這樣,人們自然就青睐 V8 了 。
本文介紹了一位 JS 愛好者對 NodeJS 以及背景初步感性的了解,沒有深刻的認識,竟也成文,看官們可作一定取舍(trade-off),将就來讀,或請積極獻言,一同讨論。
參考:
LouisSimoneau,Node.js is theNew Black
PaulQuerna, Drinking the Node.js Kool-Aid
<a target="_blank" href="http://www.infoq.com/cn/articles/nodejs-weakness-cpu-intensive-tasks">《 Node.js 軟肋之CPU密集型任務 》</a>
<a target="_blank" href="http://blog.csdn.net/yanghua_kobe/article/details/12145537">《了解 Node.js 的事件循環》</a>
<a target="_blank" href="http://www.infoq.com/cn/presentations/several-solutions-node.js-thread-defects"></a>