Node.js真的無所不能?那些不適用的應用領域分析
原創Node.js是一個服務器端JavaScript解釋器,底層采用的還是libevent;它的目標是幫助程序員構建高度可伸縮的應用程序,目前對Node.js 的采用狀況,Node.js 官方站點有一些羅列,但是相當不完整。如果你自己公司用到,也可以在 github 上提交自己的 pull-request 來更新這個文檔。
https://github.com/joyent/node/wiki/Projects,-Applications,-and-Companies-Using-Node
其實到今天為止,很少有哪些大的互聯網公司是和 Node.js 無關的。LinkedIn,Yahho,Paypal, eBay, Walmart 都在將既有的系統向 Node.js 遷移(https://www.quora.com/Node-js/What-companies-are-using-Node-js-in-production 翻墻看)。國內的淘寶、網易、百度等也都有很多項目運行在 Node.js 之上。
2011 年我開始接觸 Node.js 的時候,npmjs.org 上只有不到 3,000 個 Node.js 的 packages,今天(2014-3-2)則有 61,897 個,這個數字還在快速增長中。
下面有兩個鏈接,第一個是在講 Walmart 這幾年為什么以及如何遷移到 Node.js 上;第二個則為 eBay 是如何從 Node.js 的懷疑者轉變為采用者。
《Announcing ql.io》 這篇文章的最后一段,列出了 eBay 為什么選擇 Node.js。
每天都有幾百個新的 packages 被發布到 npm 上,小到幾行代碼,大到萬行代碼的 Framework。一天有7百萬次的包下載(安裝到某臺電腦上),對于單一開發框架的社區來說,用沸騰的海洋來形容并不過分。
以下應用領域和程序員不適合選擇Node.js:
- 計算密集型應用。Javascript 的計算性能是很難和 C 語言代碼相比的。當然,也有反例:http://onlinevillage.blogspot.jp/2011/03/is-javascript-is-faster-than-c.html,只不過不具有典型性。
- 需要精密控制內存的分配和釋放的場景,如果用 Node.js 實現 Redis 數據庫,雖然程序會簡單不少,但是 JVM 對內存數據結構的精密控制能力是比不了用 C 語言純手工打造的。
- 大量且需要頻繁通過 C Binding 調用 C library 的情況。這種場景下,往返參數的 Marshal/Unmarshal 的成本可能會大于 C Library 帶來的性能提升。
- 實時性要求很高的場景,例如:交換機或者工控機器人。這是因為所有通過垃圾回收機制來管理內存的系統都有可能在 GC 過程中產生停頓,從而影響響應速度,而且很難優化。
- 需要單一進程控制大內存的場景:v8 引擎的設計限制,在 32-bit下有 1GB 最大堆尺寸的限制,在 64-bit下是1.7GB。當然,由于 node.js buffer 的分配不是在 v8 的堆上,因此可以超過此限制。這個限制可以通過向 v8 引擎傳遞max_old_space_size 參數來超越,但是也會帶來 GC 的性能退化。這一問題在幾乎所有 GC Based 的系統下都存在。
- 不關心系統吞吐率或者不需要異步調用的場景:例如,自動化腳本,這些腳本不需要關心多用戶并發訪問的性能消耗。用 Python 這樣的“膠水”語言寫起來會更簡單。
- 某些非通用場景:例如 nginx 對于靜態 web server 或者 反向代理的場景是特別設計的,這些場景中 nginx 的性能比 Node.js 要好。
- 強類型強迫癥:有些 Java 或者 .NET 過來的程序員會認為只有強類型語言和嚴格定義的類型系統是專業化的象征,構造這樣的系統是架構師的使命,而動態語言只是玩具,只能用來做 Demo 或者前端開發。
- 團隊成員難以理解或者接受函數式編程:Javascript 本質上更像函數式語言,有些程序員在理解和使用閉包、高階函數等概念時總是不能習慣,這個問題在國內的開發團隊中還挺普遍的。
- 回調式編程的不習慣:Node.js 的異步IO 大量依賴回調。回調讓程序的執行出現了兩條路徑,出現故障時調用棧也很難理解。這對習慣了同步編程的程序員來說一開始確實是個坎。async, Q promise 等 package 可以緩解這個問題(在 ES6 的Generator 普及之前),不過這也帶來了更陡峭的學習曲線。一般情況下,需要半年到一年的習慣過程,當然前提是多看,多寫。隨著越來越多的經驗分享,這個過程也在不斷地縮短。
除此之外的領域,或者沒有上述問題的,都都可以享受到 Node.js 帶來的生產力提升和穩定的性能保障。
性能的爭議
不同開發環境間的性能對比從來都是有爭議的話題。我只能說,當開發 Web 或 網絡環境下的應用時,Node.js 靠以下幾個方面來避免出現不必要的性能問題:
- Chrome V8,一個可靠的優秀的虛擬機(hidden classes 和 inline caching),讓 Javascript 的運行速度進入了第一陣營(C++, Java, .NET)。
- 異步 IO 大大降低了線程數量,莫名其妙的死鎖和等待的概率被降低了很多。大部分場景不用去考慮并發和同步鎖,犯錯誤的機會少。而在 Python 中,異步 IO 并不是標準,并沒有被貫穿到所有 Package 中,因此應用程序也就很難獲得一致的性能保障。
- 非常輕巧的“內核”。Node.js 的模塊分為 Core Modules 和 Userland 兩部分。Core Modules 非常精簡,只包括 TCP, HTTP, DNS, File System, child processes 和其他一些模塊,這些網絡庫還只有異步版本。相對地,在 Userland 中卻有著海量的 Packages。開發應用的時候,我們根據應用的需求來組合 Userland 的 Packages,使得我們的應用程序有機會在一個很低的資源消耗水平下運行(在《Announcing ql.io》中指出,一臺開發服務器就可以支持 12 萬活躍連接,平均每個連接消耗 2k 內存)。事實上,我開發的 WebSockte 應用在 Raspberry Pi 下都可以支持幾百并發長連接(WebSocket)。和那些動輒上萬個類的企業開發框架相比,這是一個巨大的優勢。這種方式降低了出現問題的概率、查找問題的成本以及減少部署成本。
對 Javascript 的絕對性能的追求一直沒有停頓(例如, Mozilla 的 asm.js )。而 Node.js 則在絕對性能的基礎上,確保應用程序可以獲得穩定和可預測的性能保障( Benchmark 和實際的應用運行往往是兩回事)。
Node.js繼承了JavaScript 的靈活性,優秀的JS庫應當如何選擇
可以在 npmjs.org 或者 google 上搜索關鍵詞。如果類似的返回很多,則看其被其他 package 依賴的數量有多少。上 github 上查看 starred 和 forks 的數量,讀 issues。
如果是“名人”(substack, visionmedia (TJ Holowaychuk), dominictarr, rvagg 等)寫的 Packages 自然會被加分。
最后是把 Git Repo. Clone 或者 Fork 下來, 閱讀且注釋他們的源代碼。這個過程也可以發現很多他們依賴的其他 Packages。這是一個蠻享受的過程,可以學到很多新知識和新的用法。
還有一些亂槍打鳥的方法:
- 在 Tweeter 上關注 @nodenpm,所有在 npm 上發布或者更新的 packages 都會在該 handle 上發布出來。在你的碎片時間沒事可以刷刷這個,當然你需要 APN 翻墻。
- 關注一些推薦和評論賬號:@dailyjs,@echojs 等。
- Changelog 會提供不錯的開源信息匯總,其中包括 Node.js、Javascript 和 npm 欄目。
- Hacker News 則不會讓你忽略軟件行業的一些“大事”或者新概念。
一個項目開始前的研究階段,我大約會瀏覽幾十個 Packages,精讀其中的5 ~ 10個。開發過程中則根據需要還會不斷地發現和精讀一些,這些都被我計入了項目的成本。
"自由選擇,自己負責",在這個龐大的開發社區了不要指望有人能告訴你“標準答案”。每個人面臨的問題域和知識背景都不一樣,堅持多看,多試,多思考,享受獲得新知識的過程比獲得“標準答案”更重要。
在眾多的成熟開發框架下為什么需要Node.js
在每一個特定的問題域,大家總是在嘗試找到最優解。這個過程是沒有終結的,就想最終也會有其他框架代替 Node.js 一樣。
今天的 Web,是無數相互連接的 Web Services 組成的,這些連接的本質是異步的。Node.js 天生異步的特性和這個場景的匹配度相對其他開發框架要更高,因此實現起來也更自然。
除此之外,Node.js 的設計基本原則遵循了 《Unix 的編程藝術》,參見 Isaac Z. Schlueter (前任 Node.js 的Gatekeeper,目前負責 npm 的商業化) 的Blog: Unix Philosophy and Node.js。
npm 和 stream 就是上述哲學的產物。
npm
npm 是 Node.js 的包管理系統。包管理系統不是新東西,但是和 npm 的那些前輩和表兄弟不同的是:
- npm 直接集成在 Node.js 中,無需單獨安裝,發布,安裝 packages 非常簡單。
- npmjs.org 提供一個統一的入口,你可以看到每個 package 被哪些 packages 所依賴,你也可以一目了然地看到它依賴了誰,以及最近的下載次數。結合到 github 上的更新情況,基本上對一個 package 的基本情況你都能了解到。
- 約定俗成的發布規范:一個 git repo. 讓你可以直接找到源代碼;README.md 提供簡要的說明讓消費者能盡快用起來。
對于開發者來說,每一個 package 就是一個 "micro service",是最小重用單元。大部分的 package 只有幾百行代碼,甚至有些只有幾行代碼。這樣的重用粒度是在其他社區難以想象的。
在 Node.js 的應用的開發過程中,編寫 “一口尺寸”(bite-size)的 module 是推薦的編程方式。這也很方便你把這些小 module 封裝為 package 分享到社區當中,而不用擔心泄露“企業機密”。
npm 是每一個 Noder 的 "home",也是每一個 Node.js 應用的系統架構的一部分。
#p#
Stream
如果說,npm 提供了“開發時重用”的機制,那么 stream 的則提供了“運行時”不同組件之間的“重用”機制。stream 概念和 unix 中的 stream 對應,應用中的每一個 component 則對應 unix 的 filter。下面舉一個實際的例子:
在某個應用中,我需要一個 API Server,它的客戶端包括 Web Browser,iOS App., 以及網絡中的其他 Server。Web Browser 和我們的 API Server 的通信基于 SockJS(當然你也可以選擇 SocketIO,或者 Faye 等),它為瀏覽器兼容提供了適當的 “Fallback” 方案;對于 iOS App.來說,由于不需要考慮瀏覽器兼容,則采用基于標準的 RFC 6455 的純 WebSocket 通信協議,這樣實現起來更簡單;而對于其他 Server 來說,局域網內則用 TCP,互聯網上則用 TLS 來保證傳輸安全。
我在 Node.js 上是這么實現的:
- 利用 donde 構建一個通信無關的 RPC Server 來提供 API 服務。
- 用 Node.js Core Modules 中的 tcp, tls 創建 TCP/TLS Server 并監聽,用第三方的 SockJS 和 websocket-stream 分別創建 SockJS 和 WebSocket 的 Server 并監聽。
- 當 Client 連接到不同的端口,在 Server 上就會創建基于該協議的 Commnucation Stream,然后創建一個新的 dnode 實例,得到一個 dnode 的 Stream。最后將 Commnucation Stream 和 dnode Stream 像接水管一樣接到一起即可。
- net.createServer(function(connStream){
- dnodeStream = dnode({ func1: function(){} });
- connStream.pipe(dnodeStream).pipe connStream;
- });
考慮到在不穩定的網絡環境下的自動重連需求,也可以添加 reconnect。
不算你自己 RPC API 的實現邏輯,支持這么多的通信協議的 Server 框架只需要百十行代碼,還加上了一定程度的異常處理。
tcp, tls, SockJS,或者 reconnect 的開發者并不能確定“消費者”是如何使用這些 Package 的,但是大家都支持 Stream 的接口,則讓自己的 Package 能夠被運用到更多的場景。
進一步,我們也可以多路復用一個底層的 Stream。我們把上面的例子再擴展一下:
在既有的通信連接上(connStream),除了提供 RPC API 之外,還需要添加分布式的狀態同步功能,例如:通過 Scuttlebutt,完成 Client 與 Server 或 Server 與 Server 之間的常量數據自動同步,而不用為這些功能設計新的 RPC API。通過 mux-demux ,可以復用既有的網絡通信 Stream(tcp, SockJS, Web Socket...),避免建立不必要的網絡連接。
Stream 是 Node.js 的核心概念之一,其接口和工作方式被廣泛地采用,為不同組件在運行時相互通信提供了最基本的支持。
在 Node.js 中,如何使用 Stream 可以用一本書的容量來描述,不是因為 Stream 的概念有多復雜,而是因為其組合方式非常豐富。
小結
三年前接觸 Node.js,并且學習和采用,主要原因是因為 Node.js 在解決當今網絡應用的問題時,提供了高性能、高可靠和低功耗的方法。高性能、高可靠和低功耗,不是在于 Node.js 做了什么,而是在于 Node.js 不做什么。Node.js 和 Javascript 的概念,在 Java 或者其他開發框架中都能找到對應的概念。但是 Node.js 僅保留了它認為最重要的部分作為 Core Modules,其他都讓給了 User Land,這才是高性能、高可靠和低功耗的最本質的保障。
隨著 Node.js 這三年的發展,今天使我浸淫其中的理由已經不是之前的那些特點了。
npm 建立了一個“人人為我,我為人人”社區,無論你是一個入門級的 Noder,還是一個多年的老兵,都在自覺或不自覺地從這個社區吸取營養,也在不斷地回饋社區。在使用 npm 的過程中,你會很自然地發現,將自己的應用切割為盡量小的 Modules,發布為公有的 Packages,配上一個簡單扼要的 README.md,反而是最有效率的系統架構方式。
Node.js 所遵循的 Unix 設計哲學,又提供了最簡單有效的復用規范。簡單有效,才會被大家自覺采用,采用得越多,重用的可能性就更大。以Express 4.0 ( MEAN 架構中的那個 'E' )為例,這么一個流行的 MVC Web Framework的核心代碼只有 2,600 多行(不算測試,中間件和例子,但是包括注釋)。
npm 和 github 一起,為今天的軟件生產提供了新的生產關系,這也是當前 Node.js 超越其他社區的根本原因。不是單純的性能,也不僅僅是因為動態語言,甚至不是因為大量熟悉 Javascript 的前端程序員(和后端程序員相比,由于缺少系統性的思維,前端 Javascript 程序員掌握 Node.js 未必有多少優勢 ),而是以更加便捷的分享式開發為基礎的生產關系實實在在地提升了軟件生產力。
Node.js的駕馭能力
如果“復雜的后端程序” 等于 “龐大的繼承樹”,“強類型安全”,“精細的異常定義和處理”,那么 Node.js 當然無法駕馭。因為 Node.js 和 Java, .NET 相比,是一顆獨立的“科技樹”。原型繼承、函數式編程、模塊系統、回調...,這些概念和編程方式對習慣了 Java 以及 .NET 的程序員來說不僅僅是不熟悉,甚至一開始會產生“不舒服的”感覺。
從我的體驗來看(Basic->C->VB->Delphi->.NET->Node.js),這種不舒服更多地來自于之前對嚴謹的類型系統的信仰。原本所謂的“架構師”,承擔著整個應用或項目的類型系統的建設任務,對任何破壞類型一致性的行為都會自然而然的產生抵觸情緒。要想掌握 Node.js,最好的方法是先從 Java, .NET 這顆“科技樹”上爬下來,清空自己,然后重新爬 Node.js 這棵樹。
程序員從 Java, .NET 可以學到面向對象和泛型這些重用手段,而在 Node.js 的世界中,當你接觸到大量來自于完全不同背景的程序員所編寫的 Packages 的時候,你也會意識到,不是每樣東西都是“類”,重用也不一定都基于繼承。雖然有人試圖在 Node.js 中克隆之前自己熟悉的類型系統,但是更多的程序員則在不斷嘗試更優雅、簡單的編寫方式。在 Node.js 的開發過程中,沒有所謂的“最佳實踐”,類似的問題總會有人嘗試不同的解決方法。對于一個勤于思考和反思的程序員,這是一個充滿樂趣的過程。反之,如果你的團隊是由缺少獨立思考或者獨立解決問題的程序員組成的,那么 Node.js 確實不適合。你需要用強類型語言搭好一個受限的框架,然后讓體力型的隊友去填空。
我們公司只有兩個程序員,一個負責 iOS 開發,而我負責“復雜”后端程序和 Web Browser 開發。如果用 Java 或者 .NET 來開發,完成同樣的功能需要至少三倍以上的人力。
Node.js能否統一前后端
完全統一既不可能,也沒必要。再說這個所謂的“統一”與其放到 Node.js 腦袋上,不如送給 Javascript,因為 Javascript 用到的場景太多了。讓我們看幾個事實:
- Node.js 讓我們可以用 Javascript 寫后臺程序
- Node.js 讓我們寫 Web Browser 前端:一方面,可以通過 Grunt 或者其他持續集成工具生成可發布的前端靜態網站內容(例如: Bootstrap); 另一方面,也可以通過 Browserify 在前端代碼中使用 Node.js 的 Modules,讓前后臺代碼使用統一的代碼基(例如:domready。很多 Node.js 的 Modules 本身就經過了瀏覽器兼容測試,可以同時運行在兩端)。
- 用 Node.js 開發桌面應用,例如:https://github.com/rogerwang/node-webkit/wiki/List-of-apps-and-companies-using-node-webkit,列出了基于 node-webkit 的桌面應用列表。
- 即使在性能受限的移動設備中,我們也可以通過 Javascript Binding 將一部分應用邏輯用 Javascript 來實現,而 UI 的渲染還是 Native 的方式。這在很多游戲中已經被采用(http://www.zhihu.com/question/21130385)。只要用到 Javascript,或者說,隨著 Javascript 代碼基的擴大,npm based 的包管理方式就會通過 Browserify 的方式被慢慢引入。
除了第二個問題提到的那些不適合 Node.js 的地方,其他領域想徹底不碰 Node.js 是很難的。
Node.js發展方向
如果你的老板不讓你碰 Node.js,你需要讓他支付青春損失費。開個玩笑:)
投資在 Node.js 不會吃虧。
基于 Browserify 的貢獻,前后臺一致的代碼基正在成為現實(在我的一個項目中已經如此,Web Client 通過 Node.js 的 stream 和后端傳遞數據 )。你可以看到在前端使用原本為后臺寫的Module,或者用寫后端程序的方法寫前端代碼,例如:domready。瀏覽器中無需運行一個完整的 Node.js,只要打包好需要的 Modules 下載到瀏覽器執行即可。
在問題 5 中,大家已經看到 Node.js 在分布式計算領域的應用能力。 在問題 8 中,大家可以看到 Node.js 在客戶端開發中所扮演的角色。
傳統的數據庫這一領域也在發生變化。通用的數據庫系統在未來會慢慢“失寵”,“樂高積木”化的存儲服務會流行起來。Hackers 們圍繞著 LevelUp構建自己的存儲引擎,從 key/value,到Graph DB;從基于 B 樹的一維索引到基于 R-Tree 的多維索引;從能夠在瀏覽器中運行的嵌入數據庫到支持成千上萬訪問者,高可用的數據庫系統;從支持兩階段提交的 Transaction 到,到支持實時增量的 Map-Reduce。在我的一個項目中已經開始采用這種方法,為特定的存儲和查詢需求構建特定的存儲服務。這在以前是不可想象的,但是現在,也就是一個程序員的工作吧。
我是一個自己寫程序的“產品經理”。每年有8個月是集中開發的時間,剩下的時間則是負責產品設計。關注的方向從 Web、Mobile App. 到后端系統。Node.js 給我提供了無數塊“樂高積木”,讓我可以拼裝自己的玩具,這是很快樂的體驗的過程。遺憾的就是時間不夠多,有那么多東西沒時間去了解,去體會其他人的奇思妙想。小公司也是對成本極度敏感的,如果沒有 Node.js 很多東西連想都不敢想。