如何提升 RailS 應用的性能?
Is rails slow?
「鐵路很慢」,你也許聽過這個笑話,那么我們的 Rails 框架呢? 如果說 Rails 慢,那么如何提升 Rails APP 的性能就成了開發者們最關注的問題。
也許你聽說過很多提升 RoR APP 性能的方法,它們有難有易,我們需要在選擇其中最能幫助開發者脫離性能困境的。
這里列舉了幾種不同的提升 Rails 應用性能的方法。
1. 數據庫索引
你的 APP 被 DB 性能限制,優秀的數據庫索引可以在大型數據庫表中帶給你100倍的性能提升。然而并非所有 Rails 開發者都明白這一點有多重要。
添加 indexes 很容易:
class AddIndexToClientIndustry < ActiveRecord::Migration def change add_index :client_industries, :client_id end end
接下來就有無 Index 的情況做個對比。
有 Index 的情況:
CREATE INDEX addresses_addressable_id_addressable_type_idx ON addresses USING btree (addressable_id,addressable_type); t1 = Time.now c = Company.find(178389) a = c.addresses.first t2 = Time.now puts "---Operation took #{t2-t1} seconds---” Result with index: ---Operation took 0.012412 seconds---
沒有 Index 的情況:
DROP INDEX addresses_addressable_id_addressable_type_idx; t1 = Time.now c = Company.find(178389) a = c.addresses.first t2 = Time.now puts "---Operation took #{t2-t1} seconds---” Result without index: ---Operation took 0.378073 seconds---
0.378073 / 0.012412 = 30.46 沒有索引比有索引慢了30.46秒。
因此工程師可以在所有引用參數,或者其他經常查詢的參數中加入 Indexes。但是不能加太多, 因為每一個都會增加 DB Size 從而影響性能。
2. 數據庫查詢數量
RoR讓編程更快捷,但反過來也讓每條請求的數據庫查詢次數難以控制。舉個例子,如果每一個 Client 有一或多個 Industries。 我們想要顯示 Client List 和它們的 Primary Industries:
<% @clients.each do |client| %> <tr> <td><%= client.id %></td> <td><%= client.business_name %></td> <td><%= client.industries.first.name %></td> </tr> <% end %> # app/controllers/clients_controller.rb def index @clients = Client.all end
如果有50個 Clients, 則會有51條數據庫查詢:
Processing by ClientsController#index as HTML SELECT "clients".* FROM "clients" SELECT "industries".* FROM "industries" INNER JOIN "client_industries" ON "industries"."id" = "client_industries"."industry_id" WHERE "client_industries"."client_id" = 1 LIMIT 1 SELECT "industries".* FROM "industries" INNER JOIN "client_industries" ON "industries"."id" = "client_industries"."industry_id" WHERE "client_industries"."client_id" = 2 LIMIT 1 SELECT "industries".* FROM "industries" INNER JOIN "client_industries" ON "industries"."id" = "client_industries"."industry_id" WHERE "client_industries"."client_id" = 3 LIMIT 1 …
解決方案: Eager Loading
# app/controllers/clients_controller.rb def index @clients = Client.includes(:industries).all end
現在只有2至3條數據庫查詢而非51條:
Processing by ClientsController#index as HTML SELECT "clients".* FROM "clients" SELECT "client_industries".* FROM "client_industries" WHERE "client_industries"."client_id" IN (1, 2, 3) SELECT "industries".* FROM "industries" WHERE "industries"."id" IN (1, 5, 7, 8, 4)
3. 減少內存占用
- 只用真正需要的gem
- 使用時再加載對象
- 分批處理海量數據。
一個使用真實數據的例子——find_each:
Using find:
t1 = Time.now Company.where(:country_id=>1).find do |c| puts "do something!" if ['Mattski Test'].include?(c.common_name) end t2 = Time.now puts "---Operation took #{t2-t1} seconds---”
Result:
1 query, taking 46.65 seconds
Now using find_each:
t1 = Time.now Company.where(:country_id=>1).find_each do |c| puts "do something!" if ['Mattski Test'].include?(c.common_name) end t2 = Time.now puts "---Operation took #{t2-t1} seconds---"
Result:
100 queries, taking 15.53 seconds in total (3x faster)
也有查詢多了反而快的情況。
4. 使用緩存
緩存的使用對性能有巨大影響,首先確保數據模型正確,緩存可以幫你隱藏結構問題。
-
對象緩存 在使用對象緩存的情況下,應該把查詢方法的 include 去掉,避免關聯查詢無法利用緩存的現象。
-
查詢緩存 在不要求實時的情況下,對于統計類耗時查詢,那么可以使用 memcache-client 將查詢結果緩存到 memcached 里。
-
頁面局部緩存 對象緩存和查詢緩存都會降低數據庫訪問負載,但如果 RoR 的負載很高,就只能依靠頁面局部緩存了。
「web2.0網站比較常用使用頁面局部緩存,Rails 的頁面局部緩存有一個缺點,就是和頁面查詢結果對應的 Action 當中的查詢語句要放在 View 里面,否則每次 Action 里面的查詢還是會被執行,但是這樣做會破壞程序代碼良好的 MVC 結構。這種情況下,也可以采用另外一個 Cache 插件: better rails caching,在緩存頁面的同時可以緩存 Action 當中的查詢語句。」
5. 讓 web 請求更快
只有少量可用進程用于服務 web 請求,因此需要使 web 請求更快。理想情況下, web 進程一般在毫秒內完成,1至2秒算是慢的,10秒以上是非常慢的。如果你的 web 請求很慢,你的Rails APP 將無法支撐同一時間的大量用戶。
解決方案:使用后臺運行 對長時間運行的項目使用后臺運行諸如 delayed jobs, 從而釋放你的 web 進程來解決更多請求。
6. 性能監控
對 APP 進行性能監控從而便于發現哪部分運行的慢,甚至快速定位到問題所在,可以利用國內應用性能監控做的最好的 OneAPM 監控工具。
OneAPM for Ruby 能夠深入到所有 Ruby 應用內部完成應用性能管理和監控,包括代碼級別性能問題的可見性、性能瓶頸的快速識別與追溯、真實用戶體驗監控、服務器監控和端到端的應用性能管理。 追溯性能瓶頸至:性能表現差的 SQL 語句、第三方 API、Web Services、Caching Layers、后臺任務等。
圖為使用 OneAPM 進行監控的總覽頁面,在這里可以對請求在服務器端耗時有個初步印象。可以直觀的看到不同時間 web 事物、后臺任務、數據庫和外部服務的平響應時間、吞吐量、執行次數等指標,圖中 web 事物在15:41的時候響應時間出現峰值,響應速度較慢。
為了進一步確定問題所在,點進 web 事物界面可以進一步了解各慢事物響應時間占比,快速定位到api/medicines/index的響應時間較長。
點擊錯誤的請求地址,將會列出該錯誤的 URL、第一次和最后一次發生時間、錯誤發生次數、監測到錯誤的 Agent 名稱、錯誤信息和堆棧信息。
好的應用性能監控往往需要花大量的時間和精力實現,因此選擇優秀的第三方監控工具將極大地提高運維效率,這對提升 Rails APP 性能有極大幫助。
7. 使用內存數據庫
當查詢和排序都在內存中完成,數據庫將會運行的更快,而它們需要在磁盤上運行的時候就變得很慢。
解決方案:
- 限制 DB 的大小,保證它完全適合內存。
- 將不緊急的信息移出主要數據庫,移入次要數據庫或其他地方。
- 如果有大量存儲需求,考慮使用非關系型數據庫。
8. 更多性能建議:
- 對靜態文件使用內容分發網絡,例如使用 AWS CloudFront。
- 對需要1-2秒的加載項使用延遲加載。
- 使用服務導向架構,使一些進程在托管棧同步進行。
相信選擇一種或幾種適合的性能提升方法,可以使 RoR APP 更令用戶滿意。
備注:本文參考并翻譯了 Matt Kuklinski 在 slideshare 上關于 提升 Rails 性能所分享的部分內容。
本文系 OneAPM 工程師編譯整理。OneAPM 是應用性能管理領域的新興領軍企業,能幫助企業用戶和開發者輕松實現:緩慢的程序代碼和 SQL 語句的實時抓取。想閱讀更多技術文章,請訪問 OneAPM 官方博客。