
Is rails slow?
「鐵路很慢」,你也許聽過這個(gè)笑話,那么我們的 Rails 框架呢?
如果說 Rails 慢,那么如何提升 Rails APP 的性能就成了開發(fā)者們最關(guān)注的問題。
也許你聽說過很多提升 RoR APP 性能的方法,它們有難有易,我們需要在選擇其中最能幫助開發(fā)者脫離性能困境的。
這里列舉了幾種不同的提升 Rails 應(yīng)用性能的方法。
1. 數(shù)據(jù)庫索引
你的 APP 被 DB 性能限制,優(yōu)秀的數(shù)據(jù)庫索引可以在大型數(shù)據(jù)庫表中帶給你100倍的性能提升。然而并非所有 Rails 開發(fā)者都明白這一點(diǎn)有多重要。
添加 indexes 很容易:
class AddIndexToClientIndustry < ActiveRecord::Migration
def change
add_index :client_industries, :client_id
end
end
接下來就有無 Index 的情況做個(gè)對比。
有 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秒。
因此工程師可以在所有引用參數(shù),或者其他經(jīng)常查詢的參數(shù)中加入 Indexes。但是不能加太多, 因?yàn)槊恳粋€(gè)都會增加 DB Size 從而影響性能。
2. 數(shù)據(jù)庫查詢數(shù)量
RoR讓編程更快捷,但反過來也讓每條請求的數(shù)據(jù)庫查詢次數(shù)難以控制。舉個(gè)例子,如果每一個(gè) Client 有一或多個(gè) 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個(gè) Clients, 則會有51條數(shù)據(jù)庫查詢:
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
現(xiàn)在只有2至3條數(shù)據(jù)庫查詢而非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. 減少內(nèi)存占用
- 只用真正需要的gem
- 使用時(shí)再加載對象
- 分批處理海量數(shù)據(jù)。
一個(gè)使用真實(shí)數(shù)據(jù)的例子——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. 使用緩存
緩存的使用對性能有巨大影響,首先確保數(shù)據(jù)模型正確,緩存可以幫你隱藏結(jié)構(gòu)問題。
對象緩存
在使用對象緩存的情況下,應(yīng)該把查詢方法的 include 去掉,避免關(guān)聯(lián)查詢無法利用緩存的現(xiàn)象。查詢緩存
在不要求實(shí)時(shí)的情況下,對于統(tǒng)計(jì)類耗時(shí)查詢,那么可以使用 memcache-client 將查詢結(jié)果緩存到 memcached 里。頁面局部緩存
對象緩存和查詢緩存都會降低數(shù)據(jù)庫訪問負(fù)載,但如果 RoR 的負(fù)載很高,就只能依靠頁面局部緩存了。
「web2.0網(wǎng)站比較常用使用頁面局部緩存,Rails 的頁面局部緩存有一個(gè)缺點(diǎn),就是和頁面查詢結(jié)果對應(yīng)的 Action 當(dāng)中的查詢語句要放在 View 里面,否則每次 Action 里面的查詢還是會被執(zhí)行,但是這樣做會破壞程序代碼良好的 MVC 結(jié)構(gòu)。這種情況下,也可以采用另外一個(gè) Cache 插件: better rails caching,在緩存頁面的同時(shí)可以緩存 Action 當(dāng)中的查詢語句。」
5. 讓 web 請求更快
只有少量可用進(jìn)程用于服務(wù) web 請求,因此需要使 web 請求更快。理想情況下, web 進(jìn)程一般在毫秒內(nèi)完成,1至2秒算是慢的,10秒以上是非常慢的。如果你的 web 請求很慢,你的Rails APP 將無法支撐同一時(shí)間的大量用戶。
解決方案:使用后臺運(yùn)行
對長時(shí)間運(yùn)行的項(xiàng)目使用后臺運(yùn)行諸如 delayed jobs, 從而釋放你的 web 進(jìn)程來解決更多請求。
6. 性能監(jiān)控
對 APP 進(jìn)行性能監(jiān)控從而便于發(fā)現(xiàn)哪部分運(yùn)行的慢,甚至快速定位到問題所在,可以利用國內(nèi)應(yīng)用性能監(jiān)控做的最好的 OneAPM 監(jiān)控工具。
OneAPM for Ruby 能夠深入到所有 Ruby 應(yīng)用內(nèi)部完成應(yīng)用性能管理和監(jiān)控,包括代碼級別性能問題的可見性、性能瓶頸的快速識別與追溯、真實(shí)用戶體驗(yàn)監(jiān)控、服務(wù)器監(jiān)控和端到端的應(yīng)用性能管理。 追溯性能瓶頸至:性能表現(xiàn)差的 SQL 語句、第三方 API、Web Services、Caching Layers、后臺任務(wù)等。

圖為使用 OneAPM 進(jìn)行監(jiān)控的總覽頁面,在這里可以對請求在服務(wù)器端耗時(shí)有個(gè)初步印象。可以直觀的看到不同時(shí)間 web 事物、后臺任務(wù)、數(shù)據(jù)庫和外部服務(wù)的平響應(yīng)時(shí)間、吞吐量、執(zhí)行次數(shù)等指標(biāo),圖中 web 事物在15:41的時(shí)候響應(yīng)時(shí)間出現(xiàn)峰值,響應(yīng)速度較慢。

為了進(jìn)一步確定問題所在,點(diǎn)進(jìn) web 事物界面可以進(jìn)一步了解各慢事物響應(yīng)時(shí)間占比,快速定位到 api/medicines/index
的響應(yīng)時(shí)間較長。

點(diǎn)擊錯(cuò)誤的請求地址,將會列出該錯(cuò)誤的 URL、第一次和最后一次發(fā)生時(shí)間、錯(cuò)誤發(fā)生次數(shù)、監(jiān)測到錯(cuò)誤的 Agent 名稱、錯(cuò)誤信息和堆棧信息。
好的應(yīng)用性能監(jiān)控往往需要花大量的時(shí)間和精力實(shí)現(xiàn),因此選擇優(yōu)秀的第三方監(jiān)控工具將極大地提高運(yùn)維效率,這對提升 Rails APP 性能有極大幫助。
7. 使用內(nèi)存數(shù)據(jù)庫
當(dāng)查詢和排序都在內(nèi)存中完成,數(shù)據(jù)庫將會運(yùn)行的更快,而它們需要在磁盤上運(yùn)行的時(shí)候就變得很慢。
解決方案:
- 限制 DB 的大小,保證它完全適合內(nèi)存。
- 將不緊急的信息移出主要數(shù)據(jù)庫,移入次要數(shù)據(jù)庫或其他地方。
- 如果有大量存儲需求,考慮使用非關(guān)系型數(shù)據(jù)庫。
8. 更多性能建議:
- 對靜態(tài)文件使用內(nèi)容分發(fā)網(wǎng)絡(luò),例如使用 AWS CloudFront。
- 對需要1-2秒的加載項(xiàng)使用延遲加載。
- 使用服務(wù)導(dǎo)向架構(gòu),使一些進(jìn)程在托管棧同步進(jìn)行。
相信選擇一種或幾種適合的性能提升方法,可以使 RoR APP 更令用戶滿意。
備注:本文參考并翻譯了 Matt Kuklinski 在 slideshare 上關(guān)于 提升 Rails 性能所分享的部分內(nèi)容。
本文系 OneAPM 工程師編譯整理。OneAPM 是應(yīng)用性能管理領(lǐng)域的新興領(lǐng)軍企業(yè),能幫助企業(yè)用戶和開發(fā)者輕松實(shí)現(xiàn):緩慢的程序代碼和 SQL 語句的實(shí)時(shí)抓取。想閱讀更多技術(shù)文章,請?jiān)L問 OneAPM 官方博客。