主頁 > 知識庫 > 對優(yōu)化Ruby on Rails性能的一些辦法的探究

對優(yōu)化Ruby on Rails性能的一些辦法的探究

熱門標(biāo)簽:徐州網(wǎng)絡(luò)外呼系統(tǒng)哪個好 高德地圖標(biāo)注客服 常德電銷平臺外呼系統(tǒng)軟件價格 湖州u友防封電銷卡 地圖標(biāo)注賺錢項目注冊 電銷機(jī)器人廠商代理 白銀外呼paas系統(tǒng) 百度地圖標(biāo)注自定義圖片 滴滴外呼系統(tǒng)

1.導(dǎo)致你的 Rails 應(yīng)用變慢無非以下兩個原因:

  1. 在不應(yīng)該將 Ruby and Rails 作為首選的地方使用 Ruby and Rails。(用 Ruby and Rails 做了不擅長做的工作)
  2. 過度的消耗內(nèi)存導(dǎo)致需要利用大量的時間進(jìn)行垃圾回收。

Rails 是個令人愉快的框架,而且 Ruby 也是一個簡潔而優(yōu)雅的語言。但是如果它被濫用,那會相當(dāng)?shù)挠绊懶阅?。有很多工作并不適合用 Ruby and Rails,你最好使用其它的工具,比如,數(shù)據(jù)庫在大數(shù)據(jù)處理上優(yōu)勢明顯,R 語言特別適合做統(tǒng)計學(xué)相關(guān)的工作。

內(nèi)存問題是導(dǎo)致諸多 Ruby 應(yīng)用變慢的首要原因。Rails 性能優(yōu)化的 80-20 法則是這樣的:80% 的提速是源自于對內(nèi)存的優(yōu)化,剩下的 20% 屬于其它因素。為什么內(nèi)存消耗如此重要呢?因為你分配的內(nèi)存越多,Ruby GC(Ruby 的垃圾回收機(jī)制)需要做的工作也就越多。Rails 就已經(jīng)占用了很大的內(nèi)存了,而且平均每個應(yīng)用剛剛啟動后都要占用將近 100M 的內(nèi)存。如果你不注意內(nèi)存的控制,你的程序內(nèi)存增長超過 1G 是很有可能的。需要回收這么多的內(nèi)存,難怪程序執(zhí)行的大部分時間都被 GC 占用了。

2 我們?nèi)绾问挂粋€ Rails 應(yīng)用運(yùn)行更快?

有三種方法可以讓你的應(yīng)用更快:擴(kuò)容、緩存和代碼優(yōu)化。

擴(kuò)容在如今很容易實(shí)現(xiàn)。Heroku 基本上就是為你做這個的,而 Hirefire 則讓這一過程更加的自動化。其它的托管環(huán)境提供了類似的解決方案??傊梢缘脑捘阌盟褪橇?。但是請牢記擴(kuò)容并不是一顆改善性能的銀彈。如果你的應(yīng)用只需在 5 分鐘內(nèi)響應(yīng)一個請求,擴(kuò)容就沒有什么用。還有就是用 Heroku + Hirefire 幾乎很容易導(dǎo)致你的銀行賬戶透支。我已經(jīng)見識過 Hirefire 把我一個應(yīng)用的擴(kuò)容至 36 個實(shí)體,讓我為此支付了 $3100。我立馬就手動吧實(shí)例減容到了 2 個, 并且對代碼進(jìn)行了優(yōu)化.

Rails 緩存也很容易實(shí)施。Rails 4 中的塊緩存非常不錯。Rails 文檔 是有關(guān)緩存知識的優(yōu)秀資料。不過同擴(kuò)容相比,緩存并不能成為性能問題的終極解決方案。如果你的代碼無法理想的運(yùn)行,那么你將發(fā)現(xiàn)自己會把越來越多的資源耗費(fèi)在緩存上,直到緩存再也不能帶來速度的提升。

讓你的 Rails 應(yīng)用更快的唯一可靠的方式就是代碼優(yōu)化。在 Rails 的場景中這就是內(nèi)存優(yōu)化。而理所當(dāng)然的是,如果你接受了我的建議,并且避免把 Rails 用于它的設(shè)計能力范圍之外,你就會有更少的代碼要優(yōu)化。

2.1 避免內(nèi)存密集型Rails特性

Rails 一些特性花費(fèi)很多內(nèi)存導(dǎo)致額外的垃圾收集。列表如下。

2.1.1 序列化程序

序列化程序是從數(shù)據(jù)庫讀取的字符串表現(xiàn)為 Ruby 數(shù)據(jù)類型的實(shí)用方法。

class Smth  ActiveRecord::Base
 serialize :data, JSON
end
Smth.find(...).data
Smth.find(...).data = { ... }

它要消耗更多的內(nèi)存去有效的序列化,你自己看:

class Smth  ActiveRecord::Base
 def data
 JSON.parse(read_attribute(:data))
 end
 def data=(value)
 write_attribute(:data, value.to_json)
 end
end

這將只要 2 倍的內(nèi)存開銷。有些人,包括我自己,看到 Rails 的 JSON 序列化程序內(nèi)存泄漏,大約每個請求 10% 的數(shù)據(jù)量。我不明白這背后的原因。我也不知道是否有一個可復(fù)制的情況。如果你有經(jīng)驗,或者知道怎么減少內(nèi)存,請告訴我。

2.1.2 活動記錄

很容易與 ActiveRecord 操縱數(shù)據(jù)。但是 ActiveRecord 本質(zhì)是包裝了你的數(shù)據(jù)。如果你有 1g 的表數(shù)據(jù),ActiveRecord 表示將要花費(fèi) 2g,在某些情況下更多。是的,90% 的情況,你獲得了額外的便利。但是有的時候你并不需要,比如,批量更新可以減少 ActiveRecord 開銷。下面的代碼,即不會實(shí)例化任何模型,也不會運(yùn)行驗證和回調(diào)。

Book.where('title LIKE ?', '%Rails%').update_all(author: 'David')
后面的場景它只是執(zhí)行 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 字符串回調(diào)

Rails 回調(diào)像之前/之后的保存,之前/之后的動作,以及大量的使用。但是你寫的這種方式可能影響你的性能。這里有 3 種方式你可以寫,比如:在保存之前回調(diào):

before_save :update_status
before_save do |model|
model.update_status
end
before_save “self.update_status”

前兩種方式能夠很好的運(yùn)行,但是第三種不可以。為什么呢?因為執(zhí)行 Rails 回調(diào)需要存儲執(zhí)行上下文(變量,常量,全局實(shí)例等等)就是在回調(diào)的時候。如果你的應(yīng)用很大,你最終在內(nèi)存里復(fù)制了大量的數(shù)據(jù)。因為回調(diào)在任何時候都可以執(zhí)行,內(nèi)存在你程序結(jié)束之前不可以回收。

有象征,回調(diào)在每個請求為我節(jié)省了 0.6 秒。

2.2 寫更少的 Ruby

這是我最喜歡的一步。我的大學(xué)計算機(jī)科學(xué)類教授喜歡說,最好的代碼是不存在的。有時候做好手頭的任務(wù)需要其它的工具。最常用的是數(shù)據(jù)庫。為什么呢?因為 Ruby 不善于處理大數(shù)據(jù)集。非常非常的糟糕。記住,Ruby 占用非常大的內(nèi)存。所以舉個例子,處理 1G 的數(shù)據(jù)你可能需要 3G 的或者更多的內(nèi)存。它將要花費(fèi)幾十秒的時間去垃圾回收這 3G。好的數(shù)據(jù)庫可以一秒處理這些數(shù)據(jù)。讓我來舉一些例子。

2.2.1 屬性預(yù)加載

有時候反規(guī)范化模型的屬性從另外一個數(shù)據(jù)庫獲取。比如,想象我們正在構(gòu)建一個 TODO 列表,包括任務(wù)。每個任務(wù)可以有一個或者幾個標(biāo)簽標(biāo)記。規(guī)范化數(shù)據(jù)模型是這樣的:

  • Tasks
  •  id
  •  name
  • Tags
  •  id
  •  name
  • Tasks_Tags
  •  tag_id
  •  task_id

加載任務(wù)以及它們的 Rails 標(biāo)簽,你會這樣做:

這段代碼有問題,它為每個標(biāo)簽創(chuàng)建了對象,花費(fèi)很多內(nèi)存??蛇x擇的解決方案,將標(biāo)簽在數(shù)據(jù)庫預(yù)加載。

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

這只需要內(nèi)存存儲額外一列,有一個數(shù)組標(biāo)簽。難怪快 3 倍。

2.2.2 數(shù)據(jù)集合

我所說的數(shù)據(jù)集合任何代碼去總結(jié)或者分析數(shù)據(jù)。這些操作可以簡單的總結(jié),或者一些更復(fù)雜的。以小組排名為例。假設(shè)我們有一個員工,部門,工資的數(shù)據(jù)集,我們要計算員工的工資在一個部門的排名。

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 的數(shù)據(jù)程序在 4.02 秒內(nèi)完成。替代 Postgres 查詢,使用 window 函數(shù)做同樣的工作在 1.1 秒內(nèi)超過 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 倍加速已經(jīng)令人印象深刻,有時候你得到更多,到 20 倍。從我自己經(jīng)驗舉個例子。我有一個三維 OLAP 多維數(shù)據(jù)集與 600k 數(shù)據(jù)行。我的程序做了切片和聚合。在 Ruby 中,它花費(fèi)了 1G 的內(nèi)存大約 90 秒完成。等價的 SQL 查詢在 5 內(nèi)完成。

2.3 優(yōu)化 Unicorn

如果你正在使用Unicorn,那么以下的優(yōu)化技巧將會適用。Unicorn 是 Rails 框架中最快的 web 服務(wù)器。但是你仍然可以讓它更運(yùn)行得快一點(diǎn)。

2.3.1 預(yù)載入 App 應(yīng)用

Unicorn 可以在創(chuàng)建新的 worker 進(jìn)程前,預(yù)載入 Rails 應(yīng)用。這樣有兩個好處。第一,主線程可以通過寫入時復(fù)制的友好GC機(jī)制(Ruby 2.0以上),共享內(nèi)存的數(shù)據(jù)。操作系統(tǒng)會透明的復(fù)制這些數(shù)據(jù),以防被worker修改。第二,預(yù)載入減少了worker進(jìn)程啟動的時間。Rails worker進(jìn)程重啟是很常見的(稍后將進(jìn)一步闡述),所以worker重啟的速度越快,我們就可以得到更好的性能。

若需要開啟應(yīng)用的預(yù)載入,只需要在unicorn的配置文件中添加一行:

preload_app true
2.3.2 在 Request 請求間的 GC

請謹(jǐn)記,GC 的處理時間最大會占到應(yīng)用時間的50%。這個還不是唯一的問題。GC 通常是不可預(yù)知的,并且會在你不想它運(yùn)行的時候觸發(fā)運(yùn)行。那么,你該怎么處理?

首先我們會想到,如果完全禁用 GC 會怎么樣?這個似乎是個很糟糕的想法。你的應(yīng)用很可能很快就占滿 1G 的內(nèi)存,而你還未能及時發(fā)現(xiàn)。如果你服務(wù)器還同時運(yùn)行著幾個 worker,那么你的應(yīng)用將很快會出現(xiàn)內(nèi)存不足,即使你的應(yīng)用是在自托管的服務(wù)器。更不用說只有 512M 內(nèi)存限制的 Heroku。

其實(shí)我們有更好的辦法。那么如果我們無法回避GC,我們可以嘗試讓GC運(yùn)行的時間點(diǎn)盡量的確定,并且在閑時運(yùn)行。例如,在兩個request之間,運(yùn)行GC。這個很容易通過配置Unicorn實(shí)現(xiàn)。

對于Ruby 2.1以前的版本,有一個unicorn模塊叫做OobGC:

require 'unicorn/oob_gc'
 use(Unicorn::OobGC, 1) # "1" 表示"強(qiáng)制GC在1個request后運(yùn)行"

對于Ruby 2.1及以后的版本,最好使用gctools(https://github.com/tmm1/gctools):

require 'gctools/oobgc'
use(GC::OOB::UnicornMiddleware)

但在request之間運(yùn)行GC也有一些注意事項。最重要的是,這種優(yōu)化技術(shù)是可感知的。也就是說,用戶會明顯感覺到性能的提升。但是服務(wù)器需要做更多的工作。不同于在需要時才運(yùn)行GC,這種技術(shù)需要服務(wù)器頻繁的運(yùn)行GC. 所以,你要確定你的服務(wù)器有足夠的資源來運(yùn)行GC,并且在其他worker正在運(yùn)行GC的過程中,有足夠的worker來處理用戶的請求。

2.4 有限的增長

我已經(jīng)給你展示了一些應(yīng)用會占用1G內(nèi)存的例子。如果你的內(nèi)存是足夠的,那么占用這么一大塊內(nèi)存并不是個大問題。但是Ruby可能不會把這塊內(nèi)存返還給操作系統(tǒng)。接下來讓我來闡述一下為什么。

Ruby通過兩個堆來分配內(nèi)存。所有Ruby的對象在存儲在Ruby自己的堆當(dāng)中。每個對象占用40字節(jié)(64位操作系統(tǒng)中)。當(dāng)對象需要更多內(nèi)存的時候,它就會在操作系統(tǒng)的堆中分配內(nèi)存。當(dāng)對象被垃圾回收并釋放后,被占用的操作系統(tǒng)中的堆的內(nèi)存將會返還給操作系統(tǒng),但是Ruby自有的堆當(dāng)中占用的內(nèi)存只會簡單的標(biāo)記為free可用,并不會返還給操作系統(tǒng)。

這意味著,Ruby的堆只會增加不會減少。想象一下,如果你從數(shù)據(jù)庫讀取了1百萬行記錄,每行10個列。那么你需要至少分配1千萬個對象來存儲這些數(shù)據(jù)。通常Ruby worker在啟動后占用100M內(nèi)存。為了適應(yīng)這么多數(shù)據(jù),worker需要額外增加400M的內(nèi)存(1千萬個對象,每個對象占用40個字節(jié))。即使這些對象最后被收回,這個worker仍然使用著500M的內(nèi)存。

這里需要聲明, Ruby GC可以減少這個堆的大小。但是我在實(shí)戰(zhàn)中還沒發(fā)現(xiàn)有這個功能。因為在生產(chǎn)環(huán)境中,觸發(fā)堆減少的條件很少會出現(xiàn)。

如果你的worker只能增長,最明顯的解決辦法就是每當(dāng)它的內(nèi)存占用太多的時候,就重啟該worker。某些托管的服務(wù)會這么做,例如Heroku。讓我們來看看其他方法來實(shí)現(xiàn)這個功能。

2.4.1 內(nèi)部內(nèi)存控制

Trust in God, but lock your car 相信上帝,但別忘了鎖車。(寓意:大部分外國人都有宗教信仰,相信上帝是萬能的,但是日常生活中,誰能指望上帝能幫助自己呢。信仰是信仰,但是有困難的時候 還是要靠自己。)。有兩個途徑可以讓你的應(yīng)用實(shí)現(xiàn)自我內(nèi)存限制。我管他們做,Kind(友好)和hard(強(qiáng)制).

Kind 友好內(nèi)存限制是在每個請求后強(qiáng)制內(nèi)存大小。如果worker占用的內(nèi)存過大,那么該worker就會結(jié)束,并且unicorn會創(chuàng)建一個新的worker。這就是為什么我管它做“kind”。它不會導(dǎo)致你的應(yīng)用中斷。

獲取進(jìn)程的內(nèi)存大小,使用 RSS 度量在 Linux 和 MacOS 或者 OS gem 在 windows 上。我來展示下在 Unicorn 配置文件里怎么實(shí)現(xiàn)這個限制:

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

硬盤內(nèi)存限制是通過詢問操作系統(tǒng)去殺你的工作進(jìn)程,如果它增長很多。在 Unix 上你可以叫 setrlimit 去設(shè)置 RSSx 限制。據(jù)我所知,這種只在 Linux 上有效。MacOS 實(shí)現(xiàn)被打破了。我會感激任何新的信息。

這個片段來自 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 外部內(nèi)存控制

自動控制沒有從偶爾的 OMM(內(nèi)存不足)拯救你。通常你應(yīng)該設(shè)置一些外部工具。在 Heroku 上,沒有必要因為它們有自己的監(jiān)控。但是如果你是自托管,使用 monit,god 是一個很好的主意,或者其它的監(jiān)視解決方案。

2.5 優(yōu)化 Ruby GC

在某些情況下,你可以調(diào)整 Ruby GC 來改善其性能。我想說,這些 GC 調(diào)優(yōu)變得越來越不重要,Ruby 2.1 的默認(rèn)設(shè)置,后來已經(jīng)對大多數(shù)人有利。

我的建議是最好不要改變 GC 的設(shè)置,除非你明確知道你想要做什么,而且有足夠的理論知識知道如何提高性能。對于使用 Ruby 2.1 或之后的版本的用戶,這點(diǎn)尤為重要。

我知道只有一種場合 GC 優(yōu)化確實(shí)能帶來性能的提升。那就是,當(dāng)你要一次過載入大量的數(shù)據(jù)。你可以通過改變?nèi)缦碌沫h(huán)境變量來達(dá)到減少GC運(yùn)行的頻率: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 默認(rèn)值 1.8,它用于當(dāng) Ruby 的堆沒有足夠的空間來分配內(nèi)存的時候,每次應(yīng)該增加多少。當(dāng)你需要使用大量的對象的時候,你希望堆的內(nèi)存空間增長的快一點(diǎn)。在這種場合,你需要增加該因子的大小。

內(nèi)存限制是用于定義當(dāng)你需要向操作系統(tǒng)的堆申請空間的時候,GC 被觸發(fā)的頻率。Ruby 2.1 及之后的版本,默認(rèn)的限額為:

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

讓我簡要的說明一下這些值的意義。通過設(shè)置以上的值,每次新對象分配 16M 到 32M 之間,并且舊對象每占用 16M 到 128M 之間的時候 (“舊對象” 的意思是,該對象至少被垃圾回收調(diào)用過一次), Ruby 將運(yùn)行 GC。Ruby 會根據(jù)你的內(nèi)存模式,動態(tài)的調(diào)整當(dāng)前的限額值。

所以,當(dāng)你只有少數(shù)幾個對象,卻占用了大量的內(nèi)存(例如讀取一個很大的文件到字符串對象中),你可以增加該限額,以減少 GC 被觸發(fā)的頻率。請記住,要同時增加 4 個限額值,而且最好是該默認(rèn)值的倍數(shù)。

我的建議是可能和其他人的建議不一樣。對我可能合適,但對于你卻未必。這些文章將介紹,哪些對 Twitter 適用,而哪些對 Discourse 適用。

2.6 Profile

有時候,這些建議未必就是通用。你需要弄清楚你的問題。這時候,你就要使用 profiler。Ruby-Prof 是每個 Ruby 用戶都會使用的工具。

想知道更多關(guān)于 profiling 的知識, 請閱讀 Chris Heald's 和我的關(guān)于在 Rails 中 使用ruby-prof 的文章。還有一些也許有點(diǎn)過時的關(guān)于 memory profiling 的建議.

2.7 編寫性能測試用例

最后,提高 Rails 性能的技巧中,雖然不是最重要的,就是確認(rèn)應(yīng)用的性能不會因你修改了代碼而導(dǎo)致性能再次下降。

3 總結(jié)感言

對于一篇文章中,對于如何提高 Ruby 和 Rails 的性能,要面面俱到,確實(shí)不可能。所以,在這之后,我會通過寫一本書來總結(jié)我的經(jīng)驗。如果你覺得我的建議有用,請登記 mailinglist ,當(dāng)我準(zhǔn)備好了該書的預(yù)覽版之后,將會第一時間通知你?,F(xiàn)在,讓我們一起來動手,讓 Rails 應(yīng)用跑得更快一些吧!

您可能感興趣的文章:
  • ruby on rails 代碼技巧
  • 在阿里云 (aliyun) 服務(wù)器上搭建Ruby On Rails環(huán)境
  • Windows下Ruby on Rails開發(fā)環(huán)境安裝配置圖文教程
  • win7安裝ruby on rails開發(fā)環(huán)境
  • 舉例理解Ruby on Rails的頁面緩存機(jī)制
  • 在Docker中自動化部署Ruby on Rails的教程
  • 詳解Ruby on Rails中的Cucumber使用
  • Ruby on Rails基礎(chǔ)之新建項目

標(biāo)簽:張家界 遼寧 梧州 荊門 三沙 公主嶺 永州 普洱

巨人網(wǎng)絡(luò)通訊聲明:本文標(biāo)題《對優(yōu)化Ruby on Rails性能的一些辦法的探究》,本文關(guān)鍵詞  對,優(yōu)化,Ruby,Rails,性能,的,;如發(fā)現(xiàn)本文內(nèi)容存在版權(quán)問題,煩請?zhí)峁┫嚓P(guān)信息告之我們,我們將及時溝通與處理。本站內(nèi)容系統(tǒng)采集于網(wǎng)絡(luò),涉及言論、版權(quán)與本站無關(guān)。
  • 相關(guān)文章
  • 下面列出與本文章《對優(yōu)化Ruby on Rails性能的一些辦法的探究》相關(guān)的同類信息!
  • 本頁收集關(guān)于對優(yōu)化Ruby on Rails性能的一些辦法的探究的相關(guān)信息資訊供網(wǎng)民參考!
  • 推薦文章