如何提高 Ruby On Rails 效能
1 Introduction簡介
大家總是說 Rails 好慢啊,這差不多已經成為 Ruby and Rails 社群裡的一個老生常談的問題了。然而實際上這個說法並不正確。只要正確使用 Rails,把你的應用執行速度提升 10 倍並不困難。那麼如何優化你的應用呢,我們來了解下面的內容。
1.1 優化一個 Rails app 的步驟
導致你的 Rails 應用變慢無非以下兩個原因:
- 在不應該將 Ruby and Rails 作為首選的地方使用 Ruby and Rails。(用 Ruby and Rails 做了不擅長做的工作)
- 過度的消耗記憶體導致需要利用大量的時間進行垃圾回收。
Rails 是個令人愉快的框架,而且 Ruby 也是一個簡潔而優雅的語言。但是如果它被濫用,那會相當的影響效能。有很多工作並不適合用 Ruby and Rails,你最好使用其它的工具,比如,資料庫在大資料處理上優勢明顯,R 語言特別適合做統計學相關的工作。
記憶體問題是導致諸多 Ruby 應用變慢的首要原因。Rails 效能優化的 80-20 法則是這樣的:80% 的提速是源自於對記憶體的優化,剩下的 20% 屬於其它因素。為什麼記憶體消耗如此重要呢?因為你分配的記憶體越多,Ruby GC(Ruby 的垃圾回收機制)需要做的工作也就越多。Rails 就已經佔用了很大的記憶體了,而且平均每個應用剛剛啟動後都要佔用將近 100M 的記憶體。如果你不注意記憶體的控制,你的程式記憶體增長超過 1G 是很有可能的。需要回收這麼多的記憶體,難怪程式執行的大部分時間都被 GC 佔用了。
2 我們如何使一個 Rails 應用執行更快?
有三種方法可以讓你的應用更快:擴容、快取和程式碼優化。
擴容在如今很容易實現。Heroku 基本上就是為你做這個的,而 Hirefire 則讓這一過程更加的自動化。你可以在這個瞭解到更多有關自動擴容的內容。其它的託管環境提供了類似的解決方案。總之,可以的話你用它就是了。但是請牢記擴容並不是一顆改善效能的銀彈。如果你的應用只需在 5 分鐘內響應一個請求,擴容就沒有什麼用。還有就是用 Heroku + Hirefire 幾乎很容易導致你的銀行賬戶透支。我已經見識過 Hirefire 把我一個應用的擴容至 36 個實體,讓我為此支付了 $3100。我立馬就手動吧例項減容到了 2 個, 並且對程式碼進行了優化.
Rails 快取也很容易實施。Rails 4 中的塊快取非常不錯。Rails 文件 是有關快取知識的優秀資料。另外還有一篇 Cheyne Wallace 有關 Rails 效能的文章 也值得一讀。如今設定 Memcached 也簡單。不過同擴容相比,快取並不能成為效能問題的終極解決方案。如果你的程式碼無法理想的執行,那麼你將發現自己會把越來越多的資源耗費在快取上,直到快取再也不能帶來速度的提升。
讓你的 Rails 應用更快的唯一可靠的方式就是程式碼優化。在 Rails 的場景中這就是記憶體優化。而理所當然的是,如果你接受了我的建議,並且避免把 Rails 用於它的設計能力範圍之外,你就會有更少的程式碼要優化。
2.1 避免記憶體密集型Rails特性
Rails 一些特性花費很多記憶體導致額外的垃圾收集。列表如下。
2.1.1 序列化程式
序列化程式是從資料庫讀取的字串表現為 Ruby 資料型別的實用方法。
class Smth < ActiveRecord::Base serialize :data, JSON end Smth.find(...).data Smth.find(...).data = { ... } But convenience comes with 3x memory overhead. If you store 100M in data column, expect to allocate 300M just to read it from the database.
它要消耗更多的記憶體去有效的序列化,你自己看:
class Smth < ActiveRecord::Base def data JSON.parse(read_attribute(:data)) end def data=(value) write_attribute(:data, value.to_json) end end
這將只要 2 倍的記憶體開銷。有些人,包括我自己,看到 Rails 的 JSON 序列化程式記憶體洩漏,大約每個請求 10% 的資料量。我不明白這背後的原因。我也不知道是否有一個可複製的情況。如果你有經驗,或者知道怎麼減少記憶體,請告訴我。
2.1.2 活動記錄
很容易與 ActiveRecord 操縱資料。但是 ActiveRecord 本質是包裝了你的資料。如果你有 1g 的表資料,ActiveRecord 表示將要花費 2g,在某些情況下更多。是的,90% 的情況,你獲得了額外的便利。但是有的時候你並不需要,比如,批量更新可以減少 ActiveRecord 開銷。下面的程式碼,即不會例項化任何模型,也不會執行驗證和回撥。
Book.where('title LIKE ?', '%Rails%').update_all(author: 'David')
後面的場景它只是執行 SQL 更新語句。
update books set author = 'David' where title LIKE '%Rails%' Another example is iteration over a large dataset. Sometimes you need only the data. No typecasting, no updates. This snippet just runs the query and avoids ActiveRecord altogether: result = ActiveRecord::Base.execute 'select * from books' result.each do |row| # do something with row.values_at('col1', 'col2') end
2.1.3 字串回撥
Rails 回撥像之前/之後的儲存,之前/之後的動作,以及大量的使用。但是你寫的這種方式可能影響你的效能。這裡有 3 種方式你可以寫,比如:在儲存之前回撥:
before_save :update_status
before_save do |model|
model.update_status
end
before_save “self.update_status”
前兩種方式能夠很好的執行,但是第三種不可以。為什麼呢?因為執行 Rails 回撥需要儲存執行上下文(變數,常量,全域性例項等等)就是在回撥的時候。如果你的應用很大,你最終在記憶體裡複製了大量的資料。因為回撥在任何時候都可以執行,記憶體在你程式結束之前不可以回收。
有象徵,回撥在每個請求為我節省了 0.6 秒。
2.2 寫更少的 Ruby
這是我最喜歡的一步。我的大學電腦科學類教授喜歡說,最好的程式碼是不存在的。有時候做好手頭的任務需要其它的工具。最常用的是資料庫。為什麼呢?因為 Ruby 不善於處理大資料集。非常非常的糟糕。記住,Ruby 佔用非常大的記憶體。所以舉個例子,處理 1G 的資料你可能需要 3G 的或者更多的記憶體。它將要花費幾十秒的時間去垃圾回收這 3G。好的資料庫可以一秒處理這些資料。讓我來舉一些例子。
2.2.1 屬性預載入
有時候反規範化模型的屬性從另外一個資料庫獲取。比如,想象我們正在構建一個 TODO 列表,包括任務。每個任務可以有一個或者幾個標籤標記。規範化資料模型是這樣的:
Tasks id name Tags id name Tasks_Tags tag_id task_id
載入任務以及它們的 Rails 標籤,你會這樣做:
tasks = Task.find(:all, :include => :tags) > 0.058 sec
這段程式碼有問題,它為每個標籤建立了物件,花費很多記憶體。可選擇的解決方案,將標籤在資料庫預載入。
tasks = Task.select <<-END *, array( select tags.name from tags inner join tasks_tags on (tags.id = tasks_tags.tag_id) where tasks_tags.task_id=tasks.id ) as tag_names END > 0.018 sec
這隻需要記憶體儲存額外一列,有一個陣列標籤。難怪快 3 倍。
2.2.2 資料集合
我所說的資料集合任何程式碼去總結或者分析資料。這些操作可以簡單的總結,或者一些更復雜的。以小組排名為例。假設我們有一個員工,部門,工資的資料集,我們要計算員工的工資在一個部門的排名。
SELECT * FROM empsalary; depname | empno | salary -----------+-------+------- develop | 6 | 6000 develop | 7 | 4500 develop | 5 | 4200 personnel | 2 | 3900 personnel | 4 | 3500 sales | 1 | 5000 sales | 3 | 4800
你可以用 Ruby 計算排名:
salaries = Empsalary.all salaries.sort_by! { |s| [s.depname, s.salary] } key, counter = nil, nil salaries.each do |s| if s.depname != key key, counter = s.depname, 0 end counter += 1 s.rank = counter end
Empsalary 表裡 100K 的資料程式在 4.02 秒內完成。替代 Postgres 查詢,使用 window 函式做同樣的工作在 1.1 秒內超過 4 倍。
SELECT depname, empno, salary, rank() OVER (PARTITION BY depname ORDER BY salary DESC) FROM empsalary; depname | empno | salary | rank -----------+-------+--------+------ develop | 6 | 6000 | 1 develop | 7 | 4500 | 2 develop | 5 | 4200 | 3 personnel | 2 | 3900 | 1 personnel | 4 | 3500 | 2 sales | 1 | 5000 | 1 sales | 3 | 4800 | 2
4 倍加速已經令人印象深刻,有時候你得到更多,到 20 倍。從我自己經驗舉個例子。我有一個三維 OLAP 多維資料集與 600k 資料行。我的程式做了切片和聚合。在 Ruby 中,它花費了 1G 的記憶體大約 90 秒完成。等價的 SQL 查詢在 5 內完成。
2.3 優化 Unicorn
如果你正在使用Unicorn,那麼以下的優化技巧將會適用。Unicorn 是 Rails 框架中最快的 web 伺服器。但是你仍然可以讓它更執行得快一點。
2.3.1 預載入 App 應用
Unicorn 可以在建立新的 worker 程式前,預載入 Rails 應用。這樣有兩個好處。第一,主執行緒可以通過寫入時複製的友好GC機制(Ruby 2.0以上),共享記憶體的資料。作業系統會透明的複製這些資料,以防被worker修改。第二,預載入減少了worker程式啟動的時間。Rails worker程式重啟是很常見的(稍後將進一步闡述),所以worker重啟的速度越快,我們就可以得到更好的效能。
若需要開啟應用的預載入,只需要在unicorn的配置檔案中新增一行:
preload_app true
2.3.2 在 Request 請求間的 GC
請謹記,GC 的處理時間最大會佔到應用時間的50%。這個還不是唯一的問題。GC 通常是不可預知的,並且會在你不想它執行的時候觸發執行。那麼,你該怎麼處理?
首先我們會想到,如果完全禁用 GC 會怎麼樣?這個似乎是個很糟糕的想法。你的應用很可能很快就佔滿 1G 的記憶體,而你還未能及時發現。如果你伺服器還同時執行著幾個 worker,那麼你的應用將很快會出現記憶體不足,即使你的應用是在自託管的伺服器。更不用說只有 512M 記憶體限制的 Heroku。
其實我們有更好的辦法。那麼如果我們無法迴避GC,我們可以嘗試讓GC執行的時間點儘量的確定,並且在閒時執行。例如,在兩個request之間,執行GC。這個很容易通過配置Unicorn實現。
對於Ruby 2.1以前的版本,有一個unicorn模組叫做OobGC:
require 'unicorn/oob_gc' use(Unicorn::OobGC, 1) # "1" 表示"強制GC在1個request後執行"
對於Ruby 2.1及以後的版本,最好使用gctools(https://github.com/tmm1/gctools):
require 'gctools/oobgc' use(GC::OOB::UnicornMiddleware)
但在request之間執行GC也有一些注意事項。最重要的是,這種優化技術是可感知的。也就是說,使用者會明顯感覺到效能的提升。但是伺服器需要做更多的工作。不同於在需要時才執行GC,這種技術需要伺服器頻繁的執行GC. 所以,你要確定你的伺服器有足夠的資源來執行GC,並且在其他worker正在執行GC的過程中,有足夠的worker來處理使用者的請求。
2.4 有限的增長
我已經給你展示了一些應用會佔用1G記憶體的例子。如果你的記憶體是足夠的,那麼佔用這麼一大塊記憶體並不是個大問題。但是Ruby可能不會把這塊記憶體返還給作業系統。接下來讓我來闡述一下為什麼。
Ruby通過兩個堆來分配記憶體。所有Ruby的物件在儲存在Ruby自己的堆當中。每個物件佔用40位元組(64位作業系統中)。當物件需要更多記憶體的時候,它就會在作業系統的堆中分配記憶體。當物件被垃圾回收並釋放後,被佔用的作業系統中的堆的記憶體將會返還給作業系統,但是Ruby自有的堆當中佔用的記憶體只會簡單的標記為free可用,並不會返還給作業系統。
這意味著,Ruby的堆只會增加不會減少。想象一下,如果你從資料庫讀取了1百萬行記錄,每行10個列。那麼你需要至少分配1千萬個物件來儲存這些資料。通常Ruby worker在啟動後佔用100M記憶體。為了適應這麼多資料,worker需要額外增加400M的記憶體(1千萬個物件,每個物件佔用40個位元組)。即使這些物件最後被收回,這個worker仍然使用著500M的記憶體。
這裡需要宣告, Ruby GC可以減少這個堆的大小。但是我在實戰中還沒發現有這個功能。因為在生產環境中,觸發堆減少的條件很少會出現。
如果你的worker只能增長,最明顯的解決辦法就是每當它的記憶體佔用太多的時候,就重啟該worker。某些託管的服務會這麼做,例如Heroku。讓我們來看看其他方法來實現這個功能。
2.4.1 內部記憶體控制
Trust in God, but lock your car 相信上帝,但別忘了鎖車。(寓意:大部分外國人都有宗教信仰,相信上帝是萬能的,但是日常生活中,誰能指望上帝能幫助自己呢。信仰是信仰,但是有困難的時候 還是要靠自己。)。有兩個途徑可以讓你的應用實現自我記憶體限制。我管他們做,Kind(友好)和hard(強制).
Kind 友好記憶體限制是在每個請求後強制記憶體大小。如果worker佔用的記憶體過大,那麼該worker就會結束,並且unicorn會建立一個新的worker。這就是為什麼我管它做“kind”。它不會導致你的應用中斷。
獲取程式的記憶體大小,使用 RSS 度量在 Linux 和 MacOS 或者 OS gem 在 windows 上。我來展示下在 Unicorn 配置檔案裡怎麼實現這個限制:
class Unicorn::HttpServer KIND_MEMORY_LIMIT_RSS = 150 #MB alias process_client_orig process_client undef_method :process_client def process_client(client) process_client_orig(client) rss = `ps -o rss= -p #{Process.pid}`.chomp.to_i / 1024 exit if rss > KIND_MEMORY_LIMIT_RSS end end
硬碟記憶體限制是通過詢問作業系統去殺你的工作程式,如果它增長很多。在 Unix 上你可以叫 setrlimit 去設定 RSSx 限制。據我所知,這種只在 Linux 上有效。MacOS 實現被打破了。我會感激任何新的資訊。
這個片段來自 Unicorn 硬碟限制的配置檔案:
after_fork do |server, worker| worker.set_memory_limits end class Unicorn::Worker HARD_MEMORY_LIMIT_RSS = 600 #MB def set_memory_limits Process.setrlimit(Process::RLIMIT_AS, HARD_MEMORY_LIMIT * 1024 * 1024) end end
2.4.2 外部記憶體控制
自動控制沒有從偶爾的 OMM(記憶體不足)拯救你。通常你應該設定一些外部工具。在 Heroku 上,沒有必要因為它們有自己的監控。但是如果你是自託管,使用 monit,god 是一個很好的主意,或者其它的監視解決方案。
2.5 優化 Ruby GC
在某些情況下,你可以調整 Ruby GC 來改善其效能。我想說,這些 GC 調優變得越來越不重要,Ruby 2.1 的預設設定,後來已經對大多數人有利。
GC 好的調優你需要知道它是怎麼工作的。這是一個獨立的主題,不屬於這編文章。要了解更多,徹底讀讀 Sam Saffron 的 揭祕 Ruby GC 這篇文章。在我即將到來的 Ruby 效能的一書,我挖到更深的 Ruby GC 細節。訂閱這個,當我完成這本書的 beta 版本會給你傳送一份郵件。
我的建議是最好不要改變 GC 的設定,除非你明確知道你想要做什麼,而且有足夠的理論知識知道如何提高效能。對於使用 Ruby 2.1 或之後的版本的使用者,這點尤為重要。
我知道只有一種場合 GC 優化確實能帶來效能的提升。那就是,當你要一次過載入大量的資料。你可以通過改變如下的環境變數來達到減少GC執行的頻率:RUBY_GC_HEAP_GROWTH_FACTOR,RUBY_GC_MALLOC_LIMIT,RUBY_GC_MALLOC_LIMIT_MAX,RUBY_GC_OLDMALLOC_LIMIT,和 RUBY_GC_OLDMALLOC_LIMIT。
請注意,這些變數只適用於 Ruby 2.1 及之後的版本。對於 2.1 之前的版本,可能缺少某一個變數,或者變數不是使用這個名字。
RUBY_GC_HEAP_GROWTH_FACTOR 預設值 1.8,它用於當 Ruby 的堆沒有足夠的空間來分配記憶體的時候,每次應該增加多少。當你需要使用大量的物件的時候,你希望堆的記憶體空間增長的快一點。在這種場合,你需要增加該因子的大小。
記憶體限制是用於定義當你需要向作業系統的堆申請空間的時候,GC 被觸發的頻率。Ruby 2.1 及之後的版本,預設的限額為:
New generation malloc limit RUBY_GC_MALLOC_LIMIT 16M Maximum new generation malloc limit RUBY_GC_MALLOC_LIMIT_MAX 32M Old generation malloc limit RUBY_GC_OLDMALLOC_LIMIT 16M Maximum old generation malloc limit RUBY_GC_OLDMALLOC_LIMIT_MAX 128M
讓我簡要的說明一下這些值的意義。通過設定以上的值,每次新物件分配 16M 到 32M 之間,並且舊物件每佔用 16M 到 128M 之間的時候 (“舊物件” 的意思是,該物件至少被垃圾回收呼叫過一次), Ruby 將執行 GC。Ruby 會根據你的記憶體模式,動態的調整當前的限額值。
所以,當你只有少數幾個物件,卻佔用了大量的記憶體(例如讀取一個很大的檔案到字串物件中),你可以增加該限額,以減少 GC 被觸發的頻率。請記住,要同時增加 4 個限額值,而且最好是該預設值的倍數。
我的建議是可能和其他人的建議不一樣。對我可能合適,但對於你卻未必。這些文章將介紹,哪些對 Twitter 適用,而哪些對 Discourse 適用。
2.6 Profile
有時候,這些建議未必就是通用。你需要弄清楚你的問題。這時候,你就要使用 profiler。Ruby-Prof 是每個 Ruby 使用者都會使用的工具。
想知道更多關於 profiling 的知識, 請閱讀 Chris Heald’s 和我的關於在 Rails 中 使用ruby-prof 的文章。還有一些也許有點過時的關於 memory profiling 的建議.
2.7 編寫效能測試用例
最後,提高 Rails 效能的技巧中,雖然不是最重要的,就是確認應用的效能不會因你修改了程式碼而導致效能再次下降。Rails 3.x 有一個附帶了一個 效能測試和 profiling 框架 的功能。對於 Rails 4, 你可以通過 rails-perftest gem 使用相同的框架。
3 總結感言
對於一篇文章中,對於如何提高 Ruby 和 Rails 的效能,要面面俱到,確實不可能。所以,在這之後,我會通過寫一本書來總結我的經驗。如果你覺得我的建議有用,請登記 mailinglist ,當我準備好了該書的預覽版之後,將會第一時間通知你。現在,讓我們一起來動手,讓 Rails 應用跑得更快一些吧!
相關文章
- rails on ruby,ruby on rails 之程式碼塊(二)AI
- ruby on rails 小技巧AI
- Ruby on Rails Ping ++ 支付AI
- Ruby on Rails 生成指定版本的 Rails 專案AI
- ruby on rails筆記和理解AI筆記
- Ruby on Rails中的MVC架構是如何工作的AIMVC架構
- Ruby on rails專案中 引入BootstrapAIboot
- Ruby on Rails Mountable vs. Full EngineAI
- Rails並不是用Ruby編寫的AI
- 從NodeJS切換到Ruby on Rails - nikodunkNodeJSAI
- 強大的Rails/Ruby開發工具:JetBrains RubyMine 2023 for macAIMac
- 【Ruby on Rails全棧課程】2.7 塊(Block)和迭代器AI全棧BloC
- 肖威洞察 | Ruby on Rails 的快速學習的體系框架;AI框架
- 如何提高前端效能——字型篇前端
- 如何提高python程式的效能Python
- 幽默:Ruby on Rails團隊提出Rail治理名單引發爭議AI
- 開發新手最容易犯的50個 Ruby on Rails 錯誤(1)AI
- 如何提高Linux伺服器效能Linux伺服器
- 伺服器的效能如何提高伺服器
- 如何提高 Locust 的壓測效能
- 幽默:Ruby on Rails建立者DHH質疑無伺服器和微服務AI伺服器微服務
- [譯] 如何使用 Rails HelperAI
- Ruby on Rails 動態渲染遠端程式碼執行漏洞 (CVE-2016-0752)AI
- 從底層去認識 ruby 的load,require,gems,bundler,以及rails中的autoloadingUIAI
- Gbase8d如何提高查詢效能?
- 如何提高你的 React 應用的效能React
- Go1.13 defer 的效能是如何提高的?Go
- 如何使用 Set 來提高程式碼的效能
- 雙埠SRAM如何提高系統的整體效能
- [Ruby Summit 2018 話題分享] 模組化的 Rails,微服務以外的另一種選擇MITAI微服務
- 【棧】RailsAI
- 從0使用Ruby on Rails打造企業級RESTful API專案實戰之我的雲音樂AIRESTAPI
- java中如何將巢狀迴圈效能提高500倍Java巢狀
- 如何在Rails應用程式中使用Kafka?AIKafka
- [Ruby]format xml with RubyORMXML
- 從Rails到Clojure再到Java,最後回到RailsAIJava
- 透過 CancellationToken 提高 Web 效能Web
- 怎樣提高insert的效能
- Java物件重用如何降低延遲並提高效能 - MinborgJava物件