Ruby社群應該去Rails化了

發表於2013-03-27

來源:robbin的自言自語

從Linkedin和Iron.io拋棄ruby說起

最近半年關於Ruby程式語言最負面的兩條新聞莫過於2012年10月的報導:Linkedin從ruby遷移到node.js,30臺伺服器減到3臺,以及2013年3月的報導:Iron.io從ruby遷移到Go,30臺伺服器減到2臺

node.js和Go都是最近兩年伺服器端高併發程式設計的熱門語言,Linkedin和Iron.io拋棄Ruby遷移之後,都獲得10倍以上的系統效能提升,效果非常好。當然這兩篇新聞報導引發的爭議也非常大,最大的爭議在於:原有Ruby編寫的應用是隨著業務經過長時間程式碼演化而成的,程式碼可維護性和架構都已經存在嚴重的問題,即使沿用Ruby on rails重寫,也會獲得巨大的效能提升,非程式語言遷移之功。

誠然,繼續沿用Ruby on rails重寫或者重構應用,效能可能會有一兩倍的提升,但無法彌合10倍以上的效能差距,難道說ruby真的如此不堪嗎?註定要被node.js或者Go所取代嗎?

Ruby的效能真的如此不堪嗎?

JGW Maxwell在2011年底做了一個Ruby Web框架的併發處理能力測試,還做了node.js的對比測試。用250個併發去做壓力測試,後端使用MongoDB資料庫,總共跑完10萬個請求,測試結果如下:

Web框架 併發模型 吞吐量
Rails 多程式 531 request/s
Sinatra 多程式 576 request/s
Sinatra::Synchrony 纖程 1692 request/s
Goliath 纖程 1924 request/s
Cramp Event IO 3516 request/s
node.js Event IO 3100 request/s

纖程IO模型的效能是傳統多程式模型的3-4倍,而Event IO則是多程式的6-7倍。值得一提的是Ruby的Event IO框架Cramp甚至效能超過了node.js。看來併發效能差的原因並不在Ruby。

如果說這僅僅只是測試,不能說明問題,那麼我再舉一個真實的應用資料。去年年底我和@黃志敏交流,得知他為公司最近開發的一個API Server使用了Ruby的纖程框架Goliath,線上資料:

  • VPS上總共使用了16個CPU核心,跑了16個單程式例項
  • 每個程式例項穩定消耗50MB記憶體
  • Web框架使用Goliath, URL分發是grape,資料庫訪問使用ActiveRecord,快取使用Redis
  • 應用吞吐量達到了1800 request/s

這個資料意味著一臺配備了4顆4核CPU,2G記憶體的伺服器,每天可以處理 1.5億次 web請求。由此可見,Ruby完全可以做到高併發IO的應用。問題主要不在ruby直譯器上,而在Rails框架上。更準確的說就是, ruby on rails作為一個full-stack的web開發框架,並不適合用來開發Linkedin和Iron.io的後臺web服務,從某種意義上來說,屬於rails的時代已經過去了

移動時代,Web服務將取代Web網站

隨著最近幾年智慧手機的迅速普及,如今來自智慧手機和移動裝置的總體Web訪問和服務請求量已經超過了傳統的PC,這意味著Web時代主流的Browser/Server的架構重新回到了Mobile Client/Server的架構。在B/S架構下,在伺服器端生成完整的HTML頁面,我們需要開發一個完整的Website;但在移動時代,伺服器端的功能大大簡化了,退化成了Web API呼叫介面提供者,而複雜的介面構造、互動和運算都是在移動客戶端完成的。

傳統的Website將越來越讓位於Web Service,移動客戶端無論是iOS,Android還是HTML5都通過API呼叫獲取伺服器端的json/xml格式的資料,無需伺服器端生成HTML頁面了。這種B/S架構重新往C/S架構的遷移,也意味著full-stack Web框架將越來越沒有用武之地了。

Web伺服器端併發常見的三種應用場景:

  • Website:傳統Web網站
  • Web Service:Web服務端提供API呼叫介面
  • real-time:Web實時推送
併發場景 業務邏輯 介面構造 資料格式 IO併發
Website 複雜,功能多 服務端組裝頁面 HTML頁面 很低
Web Service 簡單,功能少 客戶端組裝頁面 json/xml
real-time 單一,功能極少 客戶端實時響應 json/xml 極高

我們看Linkedin和Iron.io的案例,都是非常典型的Web Service的應用場景:Linkedin使用Rails開發了移動伺服器的API閘道器,而Iron.io用Rails開發了蒐集客戶裝置資料的後臺服務,這些都不是Rails最擅長的開發website的場景,所以最終Rails被拋棄,並不是一個很意外的結果。

Rails為何不適合做Web Service?

我發現了一個有意思的現象,最早的一批用Ruby開發Web Service服務的網站,都選擇了用Rails開發,而在最近幾年又不約而同拋棄Rails重寫Web服務框架。當初用Rails的原因很簡單,因為產品早期起步,不確定性很高,使用Rails快速開發,可以最大限度節約開發成本和時間。但為何當請求量變大以後,Rails不再適合了呢?

這主要是因為Rails本身是一個full-stack的Web框架,所有的設計目標就是為了開發Website,所以Rails框架封裝過於厚重,對於需要更高效能更輕薄的Web Service應用場景來說,暴露出來了很多缺陷:

Rails呼叫堆疊過深,URL請求處理效能很差

Rails的設計目標是提供Web開發的 最佳實踐 ,所以無論你需要不需要,Rails預設提供了開發Website所有可能的元件,但其中絕大部分你可能一輩子都用不上。例如Rails專案預設新增了20個middleware,但其中10個都是可以去掉的,我們自己的專案當中手工刪除了這些middleware:

其中最誇張的是ActionDispatch::RequestIdmiddleware,只有在大型應用部署在群集環境下進行線上除錯才可能用到的功能,有什麼必要做成預設的功能呢? Rails的哲學是:提供最全的功能集給你,如果你用不到,你自己手工一個一個關閉掉 ,但是這樣帶來的結果就是預設帶了太多不必要的冗餘功能,造成效能損耗極大。

我們看一個Ruby web框架請求處理效能評測 ,這個評測不訪問資料庫,也不測試併發效能,主要是測試框架處理URL請求路由,渲染文字,返回結果的處理速度。

Web框架 處理速度
Rack 1570.43 request/s
Camping 1166.16 request/s
Sinatra 912.81 request/s
Padrino 648.68 request/s
Rails 291.27 request/s

Sinatra至少是Rails速度的3倍以上。

Rails載入的框架和依賴庫過多,記憶體消耗過度

Rails自身依賴庫非常多,造成的結果就是Rails應用持續執行以後記憶體消耗非常高。舉個例子:如果你用到了Rails的asset pipeline功能,那麼專案需要依賴一個JS引擎來編譯JS和CSS,預設會使用libv8這個庫。儘管只是編譯階段使用libv8,執行期並不需要它,但是仍然會載入libv8,這意味著你的每個ruby程式會多佔20MB記憶體。在我們其中一個大專案上,總共開了40個ruby程式,直接浪費了800MB記憶體。於是我們不得不在生產伺服器上安裝了node.js,替換了libv8。

此外,一旦其中某個依賴庫有記憶體洩露,整個應用也可能出現記憶體洩露,這種記憶體洩露是很討厭的事情,Rails如此肆無忌憚不加限制的使用第三方依賴庫也是一個潛在的隱患。

最後,Rails的Restful路由也是記憶體消耗大戶,它預設會生成全套的URL路由helpers,無論你實際是否使用到,造成的結果就是記憶體會消耗很多,而且URL路由請求的處理速度會很慢,以致於有第三方專門開發了外掛去關閉無用的路由。

我做了一個稍完整的案例比較,分別使用Sinatra, Padrino和Rails框架開發一個簡單的資料庫CRUD應用,資料庫訪問都是用ActiveRecord,在我的iMac電腦上,3個ruby應用單程式消耗的記憶體分別是:

Web框架 實體記憶體
Sinatra 45MB
Padrino 60MB
Rails 85MB

Rails傳統多程式模型的IO併發能力很低

Rails的多程式併發模型的IO併發能力很低,開多少個程式,就只能同時響應多少個併發請求,但Ruby程式的記憶體消耗是很大的,多程式排程的CPU開銷也很高,這決定了單臺伺服器上能開的程式數是非常有限的,一般不會超過30個。但是對於Web Service型別的應用,需要很高的IO併發處理能力,傳統Rails多程式很容易就會出現負載的瓶頸。

提高Web應用的IO併發能力,必須拋棄多程式模型,改用多執行緒模型,纖程模型或者事件驅動的併發程式設計模型。關於這個話題,我寫過一個ppt,請參考:Web併發程式設計模型的粗淺探討 ,這篇文章不展開了。總之,我個人更推薦使用Sinatra/Padrino編寫多執行緒的Web服務端應用,或者為了追求更高的併發效能,可以使用Goliath的纖程併發。

從Rails4.0開始,預設也開啟了多執行緒模式,也可以支援多執行緒方式執行Rails應用。但就目前來說,Rails使用多執行緒,還面臨一些相容性問題:大量的Rails外掛和程式碼不是執行緒安全的,在多執行緒模型下執行,會出現意想不到的bug;另外Rails的多執行緒應用尚未得到廣泛應用,可能會有潛在的bug:

我們嘗試在一個實際的生產系統上開啟Rails3.2的多執行緒模式執行,對程式碼和外掛都進行了相容性修改和仔細的程式碼審查。但實際跑下來發現,應用系統出現了隱蔽的記憶體洩露問題,Ruby程式記憶體會一直增長下去,直到伺服器記憶體佔滿,程式失去響應,這個bug至今未能找到原因。

總之Rails適合開發Website,但不太適合Web Service,而移動時代的發展趨勢就是:未來伺服器端會更多的使用Web Service而不是Website,這也意味著Rails將越來越不適合時代的發展

我們應該用什麼Ruby框架?

我一直覺得Ruby社群的很多開發者長期以來待在Rails的舒適區裡面,完全喪失了探索和嘗試其他東西的勇氣,其實在Rails的世界之外,Ruby社群的好東西還有很多很多。這裡簡單介紹3個Ruby輕量級框架,效能都遠遠超過Rails,很適合做Web Service:

  • SinatraSinatra本身也是Ruby社群非常流行和著名的輕量級Web框架,核心原始碼不超過1000行,文件只有1頁。對於Rails開發者來說,花了幾個小時,就可以快速使用Sinatra開發Web Service了。Sinatra對多執行緒支援的非常好,可以用rainbows來跑多執行緒Sinatra,IO併發處理能力很好。Github也是用它來提供開放API服務的。我自己寫了一個Sinatra的專案模版,如果你用Sinatra開發Web Service,可以參考。
  • PadrinoPadrino是一個基於Sinatra之上的輕量級Web框架,在Sinatra基礎之上提供了命名路由,模組化專案組織,頁面helpers和generators等等。Padrino是一個高度模仿Rails的框架,API的命名和Rails很像,Rails開發者花1-2天看看文件就可以快速上手開發了。Padrino相比Rails易學易用,多執行緒支援良好,效能比Rails好很多,開發Website推薦使用。我自己的網站也是用Padrino開發的,原始碼在:robbin_site
  • GoliathGoliath是一個Ruby的纖程開發框架,效能非常好,作者本身是在開發PostRank產品過程中開發的Goliath。PostRank是一個使用者社交行為實時跟蹤工具,需要很高的效能來支撐,PostRank被Google收購了,作者現在在Google工作。Goliath適合用來開發對效能非常敏感的Web Service或者real-time的應用,但使用Goliath有一些門檻,你不能使用普通的阻塞IO庫,必須使用作者封裝的一些纖程的庫。

總之,無論是Linkedin的移動API閘道器還是Iron.io的後臺任務系統,用Ruby來編寫,本身並不是問題,實踐也有大量案例證明使用Goliath或者Sinatra編寫高效能Web Service都是可行的。問題只是在於我們應該: Ruby off rails 了。

如何去Rails化

掌握一門程式語言實際上包括兩部分:

  • 程式語言語法以及核心類庫無論你用不用Rails,是否開發Web應用,這些都是必須牢固掌握的,即使你不用Rails了,這些知識和技能也不會過時。
  • 開發特定領域應用所需要的第三方類庫當你用Rails開發一個專案的時候,仍然需要依賴大量的第三方類庫,每當你在Gemfile裡面require一個類庫的時候,都意味著你付出了一定的學習成本。而Rails本身也不過就是幾個核心Gem包而已,具體來說最核心的就是ActiveRecord和ActionPack這兩個庫。

學習Rails無非意味著你花了時間熟悉ActiveRecord和ActionPack以及相關庫的功能而已,所謂去Rails化也僅僅只是放棄使用ActionPack,換一個更輕量級更簡單的URL路由處理器,例如換成Grape,Sinatra,Padrino或者Camping而已。這對一個長期使用Rails的Ruby開發者來說,應該是舉手之勞的事情。所以自己動手,根據實際應用場景挑選最合適的元件。例如ActionPack不太適合寫Web Service,那我換成Sinatra就行了,但是ActiveRecord照常用,這並不需要你付出多少學習成本,更不需要你放棄什麼。

為何不用node.js和Go?

有一點是毫無疑問的,node.js的V8引擎,Go的靜態編譯進去的GC,效能遠遠好於Ruby的虛擬機器,儘管在實際的應用中,未必會表現出來這麼明顯的差距。那麼,一個隨之而來的問題就是:為何不用node.js和Go呢?

每個程式設計師都有自己的傾向性,答案可能都不同。我在去年底花了很多時間瞭解node.js和Go,最終還是覺得用Ruby對我來說最合適:

  • 用Sinatra或者Goliath這樣的輕量級框架寫Web Service,效能已經足夠好了,特別是@黃志敏的案例證明,16核已經可以支撐每天1.5億次請求了,對我來說已經不太可能遇到超過這個負載量的應用了。而Ruby的開發效率,程式碼表達能力和可維護性對我來說還是很重要的。
  • node.js的Event IO程式設計風格在我看來是“反人類”的,極其變態的。用來寫程式碼上規模的應用,程式碼的可讀性和可維護性都很差。Event IO是很底層的技術,我很難理解為何不封裝成coroutine來使用。node.js只適合用來開發real-time型別的應用。
  • Go的主要問題在於現階段還不成熟:一方面Go自身還在演進當中;另一方面Go的類庫還是過於貧瘠了,用來開發專案還是需要自己寫很多東西的,感覺很不方便。加上Go是編譯型語言,開發過程當中反覆編譯也是挺麻煩的事情。

相關文章