Ruby 社區(qū)應(yīng)該去 Rails 化了
從Linkedin和Iron.io拋棄ruby說起
最近半年關(guān)于Ruby編程語言最負(fù)面的兩條新聞莫過于2012年10月的報(bào)導(dǎo):Linkedin從ruby遷移到node.js,30臺(tái)服務(wù)器減到3臺(tái),以及2013年3月的報(bào)導(dǎo):Iron.io從ruby遷移到Go,30臺(tái)服務(wù)器減到2臺(tái)
node.js和Go都是最近兩年服務(wù)器端高并發(fā)編程的熱門語言,Linkedin和Iron.io拋棄Ruby遷移之后,都獲得10倍以上的系統(tǒng) 性能提升,效果非常好。當(dāng)然這兩篇新聞報(bào)導(dǎo)引發(fā)的爭(zhēng)議也非常大,最大的爭(zhēng)議在于:原有Ruby編寫的應(yīng)用是隨著業(yè)務(wù)經(jīng)過長(zhǎng)時(shí)間代碼演化而成的,代碼可維護(hù) 性和架構(gòu)都已經(jīng)存在嚴(yán)重的問題,即使沿用Ruby on rails重寫,也會(huì)獲得巨大的性能提升,非編程語言遷移之功。
誠(chéng)然,繼續(xù)沿用Ruby on rails重寫或者重構(gòu)應(yīng)用,性能可能會(huì)有一兩倍的提升,但無法彌合10倍以上的性能差距,難道說ruby真的如此不堪嗎?注定要被node.js或者Go所取代嗎?
Ruby的性能真的如此不堪嗎?
JGW Maxwell在2011年底做了一個(gè)Ruby Web框架的并發(fā)處理能力測(cè)試,還做了node.js的對(duì)比測(cè)試。用250個(gè)并發(fā)去做壓力測(cè)試,后端使用MongoDB數(shù)據(jù)庫(kù),總共跑完10萬個(gè)請(qǐng)求,測(cè)試結(jié)果如下:
纖程IO模型的性能是傳統(tǒng)多進(jìn)程模型的3-4倍,而Event IO則是多進(jìn)程的6-7倍。值得一提的是Ruby的Event IO框架Cramp甚至性能超過了node.js??磥聿l(fā)性能差的原因并不在Ruby。
如果說這僅僅只是測(cè)試,不能說明問題,那么我再舉一個(gè)真實(shí)的應(yīng)用數(shù)據(jù)。去年年底我和@黃志敏交流,得知他為公司最近開發(fā)的一個(gè)API Server使用了Ruby的纖程框架Goliath,線上數(shù)據(jù):
·VPS上總共使用了16個(gè)CPU內(nèi)核,跑了16個(gè)單進(jìn)程實(shí)例
·每個(gè)進(jìn)程實(shí)例穩(wěn)定消耗50MB內(nèi)存
·Web框架使用Goliath, URL分發(fā)是grape,數(shù)據(jù)庫(kù)訪問使用ActiveRecord,緩存使用Redis
·應(yīng)用吞吐量達(dá)到了1800 request/s
這個(gè)數(shù)據(jù)意味著一臺(tái)配備了4顆4核CPU,2G內(nèi)存的服務(wù)器,每天可以處理 1.5億次 web請(qǐng)求。由此可見,Ruby完全可以做到高并發(fā)IO的應(yīng)用。問題主要不在ruby解釋器上,而在Rails框架上。更準(zhǔn)確的說就是, ruby on rails作為一個(gè)full-stack的web開發(fā)框架,并不適合用來開發(fā)Linkedin和Iron.io的后臺(tái)web服務(wù),從某種意義上來說,屬于rails的時(shí)代已經(jīng)過去了
移動(dòng)時(shí)代,Web服務(wù)將取代Web網(wǎng)站
隨著最近幾年智能手機(jī)的迅速普及,如今來自智能手機(jī)和移動(dòng)設(shè)備的總體Web訪問和服務(wù)請(qǐng)求量已經(jīng)超過了傳統(tǒng)的PC,這意味著Web時(shí)代主流的 Browser/Server的架構(gòu)重新回到了Mobile Client/Server的架構(gòu)。在B/S架構(gòu)下,在服務(wù)器端生成完整的HTML頁(yè)面,我們需要開發(fā)一個(gè)完整的Website;但在移動(dòng)時(shí)代,服務(wù)器端 的功能大大簡(jiǎn)化了,退化成了Web API調(diào)用接口提供者,而復(fù)雜的界面構(gòu)造、交互和運(yùn)算都是在移動(dòng)客戶端完成的。
傳統(tǒng)的Website將越來越讓位于Web Service,移動(dòng)客戶端無論是iOS,Android還是HTML5都通過API調(diào)用獲取服務(wù)器端的json/xml格式的數(shù)據(jù),無需服務(wù)器端生成 HTML頁(yè)面了。這種B/S架構(gòu)重新往C/S架構(gòu)的遷移,也意味著full-stack Web框架將越來越?jīng)]有用武之地了。
Web服務(wù)器端并發(fā)常見的三種應(yīng)用場(chǎng)景:
·Website:傳統(tǒng)Web網(wǎng)站
·Web Service:Web服務(wù)端提供API調(diào)用接口
·real-time:Web實(shí)時(shí)推送
我們看Linkedin和Iron.io的案例,都是非常典型的Web Service的應(yīng)用場(chǎng)景:Linkedin使用Rails開發(fā)了移動(dòng)服務(wù)器的API網(wǎng)關(guān),而Iron.io用Rails開發(fā)了搜集客戶設(shè)備數(shù)據(jù)的后臺(tái)服 務(wù),這些都不是Rails最擅長(zhǎng)的開發(fā)website的場(chǎng)景,所以最終Rails被拋棄,并不是一個(gè)很意外的結(jié)果。
Rails為何不適合做Web Service?
我發(fā)現(xiàn)了一個(gè)有意思的現(xiàn)象,最早的一批用Ruby開發(fā)Web Service服務(wù)的網(wǎng)站,都選擇了用Rails開發(fā),而在最近幾年又不約而同拋棄Rails重寫Web服務(wù)框架。當(dāng)初用Rails的原因很簡(jiǎn)單,因?yàn)楫a(chǎn) 品早期起步,不確定性很高,使用Rails快速開發(fā),可以最大限度節(jié)約開發(fā)成本和時(shí)間。但為何當(dāng)請(qǐng)求量變大以后,Rails不再適合了呢?
這主要是因?yàn)镽ails本身是一個(gè)full-stack的Web框架,所有的設(shè)計(jì)目標(biāo)就是為了開發(fā)Website,所以Rails框架封裝過于厚重,對(duì)于需要更高性能更輕薄的Web Service應(yīng)用場(chǎng)景來說,暴露出來了很多缺陷:
Rails調(diào)用堆棧過深,URL請(qǐng)求處理性能很差
Rails的設(shè)計(jì)目標(biāo)是提供Web開發(fā)的 最佳實(shí)踐 ,所以無論你需要不需要,Rails默認(rèn)提供了開發(fā)Website所有可能的組件,但其中絕大部分你可能一輩子都用不上。例如Rails項(xiàng)目默認(rèn)添加了20個(gè)middleware,但其中10個(gè)都是可以去掉的,我們自己的項(xiàng)目當(dāng)中手工刪除了這些middleware:
- config.middleware.delete 'Rack::Lock' # 多線程加鎖,多進(jìn)程模式下無意義
- config.middleware.delete 'Rack::Runtime' # 記錄X-Runtime(方便客戶端查看執(zhí)行時(shí)間)
- config.middleware.delete 'ActionDispatch::RequestId' # 記錄X-Request-Id(方便查看請(qǐng)求在群集中的哪臺(tái)執(zhí)行)
- config.middleware.delete 'ActionDispatch::RemoteIp' # IP SpoofAttack
- config.middleware.delete 'ActionDispatch::Callbacks' # 在請(qǐng)求前后設(shè)置callback
- config.middleware.delete 'ActionDispatch::Head' # 如果是HEAD請(qǐng)求,按照GET請(qǐng)求執(zhí)行,但是不返回body
- config.middleware.delete 'Rack::ConditionalGet' # HTTP客戶端緩存才會(huì)使用
- config.middleware.delete 'Rack::ETag' # HTTP客戶端緩存才會(huì)使用
- config.middleware.delete 'ActionDispatch::BestStandardsSupport' # 設(shè)置X-UA-Compatible, 在nginx上設(shè)置
其中最夸張的是ActionDispatch::RequestIdmiddleware,只有在大型應(yīng)用部署在群集環(huán)境下進(jìn)行線上調(diào)試才可能用到的功能,有什么必要做成默認(rèn)的功能呢? Rails的哲學(xué)是:提供最全的功能集給你,如果你用不到,你自己手工一個(gè)一個(gè)關(guān)閉掉 ,但是這樣帶來的結(jié)果就是默認(rèn)帶了太多不必要的冗余功能,造成性能損耗極大。
我們看一個(gè)Ruby web框架請(qǐng)求處理性能評(píng)測(cè) ,這個(gè)評(píng)測(cè)不訪問數(shù)據(jù)庫(kù),也不測(cè)試并發(fā)性能,主要是測(cè)試框架處理URL請(qǐng)求路由,渲染文本,返回結(jié)果的處理速度。
Sinatra至少是Rails速度的3倍以上。
#p#
Rails加載的框架和依賴庫(kù)過多,內(nèi)存消耗過度
Rails自身依賴庫(kù)非常多,造成的結(jié)果就是Rails應(yīng)用持續(xù)運(yùn)行以后內(nèi)存消耗非常高。舉個(gè)例子:如果你用到了Rails的asset pipeline功能,那么項(xiàng)目需要依賴一個(gè)JS引擎來編譯JS和CSS,默認(rèn)會(huì)使用libv8這個(gè)庫(kù)。盡管只是編譯階段使用libv8,運(yùn)行期并不需要 它,但是仍然會(huì)加載libv8,這意味著你的每個(gè)ruby進(jìn)程會(huì)多占20MB內(nèi)存。在我們其中一個(gè)大項(xiàng)目上,總共開了40個(gè)ruby進(jìn)程,直接浪費(fèi)了 800MB內(nèi)存。于是我們不得不在生產(chǎn)服務(wù)器上安裝了node.js,替換了libv8。
此外,一旦其中某個(gè)依賴庫(kù)有內(nèi)存泄露,整個(gè)應(yīng)用也可能出現(xiàn)內(nèi)存泄露,這種內(nèi)存泄露是很討厭的事情,Rails如此肆無忌憚不加限制的使用第三方依賴庫(kù)也是一個(gè)潛在的隱患。
最后,Rails的Restful路由也是內(nèi)存消耗大戶,它默認(rèn)會(huì)生成全套的URL路由helpers,無論你實(shí)際是否使用到,造成的結(jié)果就是內(nèi)存會(huì)消耗很多,而且URL路由請(qǐng)求的處理速度會(huì)很慢,以致于有第三方專門開發(fā)了插件去關(guān)閉無用的路由。
我做了一個(gè)稍完整的案例比較,分別使用Sinatra, Padrino和Rails框架開發(fā)一個(gè)簡(jiǎn)單的數(shù)據(jù)庫(kù)CRUD應(yīng)用,數(shù)據(jù)庫(kù)訪問都是用ActiveRecord,在我的iMac電腦上,3個(gè)ruby應(yīng)用單進(jìn)程消耗的內(nèi)存分別是:
Rails傳統(tǒng)多進(jìn)程模型的IO并發(fā)能力很低
Rails的多進(jìn)程并發(fā)模型的IO并發(fā)能力很低,開多少個(gè)進(jìn)程,就只能同時(shí)響應(yīng)多少個(gè)并發(fā)請(qǐng)求,但Ruby進(jìn)程的內(nèi)存消耗是很大的,多進(jìn)程調(diào)度的 CPU開銷也很高,這決定了單臺(tái)服務(wù)器上能開的進(jìn)程數(shù)是非常有限的,一般不會(huì)超過30個(gè)。但是對(duì)于Web Service類型的應(yīng)用,需要很高的IO并發(fā)處理能力,傳統(tǒng)Rails多進(jìn)程很容易就會(huì)出現(xiàn)負(fù)載的瓶頸。
提高Web應(yīng)用的IO并發(fā)能力,必須拋棄多進(jìn)程模型,改用多線程模型,纖程模型或者事件驅(qū)動(dòng)的并發(fā)編程模型。關(guān)于這個(gè)話題,我寫過一個(gè)ppt,請(qǐng)參考:Web并發(fā)編程模型的粗淺探討 ,這篇文章不展開了??傊覀€(gè)人更推薦使用Sinatra/Padrino編寫多線程的Web服務(wù)端應(yīng)用,或者為了追求更高的并發(fā)性能,可以使用Goliath的纖程并發(fā)。
從Rails4.0開始,默認(rèn)也開啟了多線程模式,也可以支持多線程方式運(yùn)行Rails應(yīng)用。但就目前來說,Rails使用多線程,還面臨一些兼容 性問題:大量的Rails插件和代碼不是線程安全的,在多線程模型下運(yùn)行,會(huì)出現(xiàn)意想不到的bug;另外Rails的多線程應(yīng)用尚未得到廣泛應(yīng)用,可能會(huì) 有潛在的bug:
我們嘗試在一個(gè)實(shí)際的生產(chǎn)系統(tǒng)上打開Rails3.2的多線程模式運(yùn)行,對(duì)代碼和插件都進(jìn)行了兼容性修改和仔細(xì)的代碼審查。但實(shí)際跑下來發(fā)現(xiàn),應(yīng)用 系統(tǒng)出現(xiàn)了隱蔽的內(nèi)存泄露問題,Ruby進(jìn)程內(nèi)存會(huì)一直增長(zhǎng)下去,直到服務(wù)器內(nèi)存占滿,進(jìn)程失去響應(yīng),這個(gè)bug至今未能找到原因。
總之Rails適合開發(fā)Website,但不太適合Web Service,而移動(dòng)時(shí)代的發(fā)展趨勢(shì)就是:未來服務(wù)器端會(huì)更多的使用Web Service而不是Website,這也意味著Rails將越來越不適合時(shí)代的發(fā)展
我們應(yīng)該用什么Ruby框架?
我一直覺得Ruby社區(qū)的很多開發(fā)者長(zhǎng)期以來待在Rails的舒適區(qū)里面,完全喪失了探索和嘗試其他東西的勇氣,其實(shí)在Rails的世界之 外,Ruby社區(qū)的好東西還有很多很多。這里簡(jiǎn)單介紹3個(gè)Ruby輕量級(jí)框架,性能都遠(yuǎn)遠(yuǎn)超過Rails,很適合做Web Service:
Sinatra本身也是Ruby社區(qū)非常流行和著名的輕量級(jí)Web框架,核心源代碼不超過1000行,文檔只有1頁(yè)。對(duì)于Rails開發(fā)者來說,花了幾個(gè)小時(shí),就可以快速使用Sinatra開發(fā)Web Service了。Sinatra對(duì)多線程支持的非常好,可以用rainbows來跑多線程Sinatra,IO并發(fā)處理能力很好。Github也是用它來提供開放API服務(wù)的。我自己寫了一個(gè)Sinatra的項(xiàng)目模版,如果你用Sinatra開發(fā)Web Service,可以參考。
Padrino是一個(gè)基于Sinatra之上的輕量級(jí)Web框架,在Sinatra基礎(chǔ)之上提供了命名路由,模塊化項(xiàng)目組織,頁(yè)面helpers和 generators等等。Padrino是一個(gè)高度模仿Rails的框架,API的命名和Rails很像,Rails開發(fā)者花1-2天看看文檔就可以快 速上手開發(fā)了。Padrino相比Rails易學(xué)易用,多線程支持良好,性能比Rails好很多,開發(fā)Website推薦使用。我自己的網(wǎng)站也是用 Padrino開發(fā)的,源代碼在:robbin_site
Goliath是一個(gè)Ruby的纖程開發(fā)框架,性能非常好,作者本身是在開發(fā)PostRank產(chǎn)品過程中開發(fā)的Goliath。PostRank是 一個(gè)用戶社交行為實(shí)時(shí)跟蹤工具,需要很高的性能來支撐,PostRank被Google收購(gòu)了,作者現(xiàn)在在Google工作。Goliath適合用來開發(fā) 對(duì)性能非常敏感的Web Service或者real-time的應(yīng)用,但使用Goliath有一些門檻,你不能使用普通的阻塞IO庫(kù),必須使用作者封裝的一些纖程的庫(kù)。
總之,無論是Linkedin的移動(dòng)API網(wǎng)關(guān)還是Iron.io的后臺(tái)任務(wù)系統(tǒng),用Ruby來編寫,本身并不是問題,實(shí)踐也有大量案例證明使用Goliath或者Sinatra編寫高性能Web Service都是可行的。問題只是在于我們應(yīng)該: Ruby off rails 了。
如何去Rails化
掌握一門編程語言實(shí)際上包括兩部分:
·編程語言語法以及核心類庫(kù)
無論你用不用Rails,是否開發(fā)Web應(yīng)用,這些都是必須牢固掌握的,即使你不用Rails了,這些知識(shí)和技能也不會(huì)過時(shí)。
·開發(fā)特定領(lǐng)域應(yīng)用所需要的第三方類庫(kù)
當(dāng)你用Rails開發(fā)一個(gè)項(xiàng)目的時(shí)候,仍然需要依賴大量的第三方類庫(kù),每當(dāng)你在Gemfile里面require一個(gè)類庫(kù)的時(shí)候,都意味著你付出了 一定的學(xué)習(xí)成本。而Rails本身也不過就是幾個(gè)核心Gem包而已,具體來說最核心的就是ActiveRecord和ActionPack這兩個(gè)庫(kù)。
學(xué)習(xí)Rails無非意味著你花了時(shí)間熟悉ActiveRecord和ActionPack以及相關(guān)庫(kù)的功能而已,所謂去Rails化也僅僅只是放棄 使用ActionPack,換一個(gè)更輕量級(jí)更簡(jiǎn)單的URL路由處理器,例如換成Grape,Sinatra,Padrino或者Camping而已。這對(duì) 一個(gè)長(zhǎng)期使用Rails的Ruby開發(fā)者來說,應(yīng)該是舉手之勞的事情。所以自己動(dòng)手,根據(jù)實(shí)際應(yīng)用場(chǎng)景挑選最合適的組件。例如ActionPack不太適 合寫Web Service,那我換成Sinatra就行了,但是ActiveRecord照常用,這并不需要你付出多少學(xué)習(xí)成本,更不需要你放棄什么。
為何不用node.js和Go?
有一點(diǎn)是毫無疑問的,node.js的V8引擎,Go的靜態(tài)編譯進(jìn)去的GC,性能遠(yuǎn)遠(yuǎn)好于Ruby的虛擬機(jī),盡管在實(shí)際的應(yīng)用中,未必會(huì)表現(xiàn)出來這么明顯的差距。那么,一個(gè)隨之而來的問題就是:為何不用node.js和Go呢?
每個(gè)程序員都有自己的傾向性,答案可能都不同。我在去年底花了很多時(shí)間了解node.js和Go,最終還是覺得用Ruby對(duì)我來說最合適:
·用Sinatra或者Goliath這樣的輕量級(jí)框架寫Web Service,性能已經(jīng)足夠好了,特別是@黃志敏的案例證明,16核已經(jīng)可以支撐每天1.5億次請(qǐng)求了,對(duì)我來說已經(jīng)不太可能遇到超過這個(gè)負(fù)載量的應(yīng)用了。而Ruby的開發(fā)效率,代碼表達(dá)能力和可維護(hù)性對(duì)我來說還是很重要的。
·node.js的Event IO編程風(fēng)格在我看來是“反人類”的,極其變態(tài)的。用來寫代碼上規(guī)模的應(yīng)用,代碼的可讀性和可維護(hù)性都很差。Event IO是很底層的技術(shù),我很難理解為何不封裝成coroutine來使用。node.js只適合用來開發(fā)real-time類型的應(yīng)用。
·Go的主要問題在于現(xiàn)階段還不成熟:一方面Go自身還在演進(jìn)當(dāng)中;另一方面Go的類庫(kù)還是過于貧瘠了,用來開發(fā)項(xiàng)目還是需要自己寫很多東西的,感覺很不方便。加上Go是編譯型語言,開發(fā)過程當(dāng)中反復(fù)編譯也是挺麻煩的事情。