Category Archives: Performance

Ruby on Rails Performance 最佳實務

這是我 2/26 在中研院 OSSF 工作坊和 3/23 在 Ruby Tuesday 所演講的題目之二。

關於 Performance,有四個最好不要做的事情:

  • 過早最佳化
  • 亂猜哪裡慢
  • 到處做快取
  • 與 Framework 過不去,硬要 Hack 它

然後有五條關於 Performance 的須知:

  • 演算法的改良永遠勝過硬擠程式碼
  • 一般來說,程式碼維護性的重要性勝過效能
  • 只最佳化最需要的部份 (80/20法則)
  • 測量再測量,最佳化前、最佳化後都要測量
  • 必須與程式碼彈性做平衡。有時候做了最佳化之後(例如快取),會犧牲掉一些彈性。

其他就看投影片吧。這份投影片主要還是關注在 Web application layer,也就是 Ruby 程式語言和 Ruby on Rails 上。有點小可惜的是篇幅和準備時間所限,關於 Front-end performance、NoSQL: Key-value stores 和 HTTP Caching 這三個主題是我覺得可以多講一點的東西。

有了效能,也許你會想進一步知道如何做 Scaling? 不同於 Performance 探討單一台 application server 可以服務多少 requests,Scaling 要探討的問題是如何擴展架構到多台伺服器上來服務更多流量。當然 Performance 會是做 Scaling 的一個重要的因素,不過 Scaling 需要考量的地方又更多了,包括:

  • Asynchrony Processing (Message queue)
  • Partition Component using SOA
  • HTTP Reverse Proxy Caching
  • Distributed Filesystem/Database

其中 Asynchrony Processing 和 SOA 的部份,我在 Distributes Ruby and Rails 的演講有分享過。

xdite 也有針對 Scaling Rails Site 這個主題寫了一系列的 Scaling Rails Site: Reading Material 來介紹: #1, #2, #3, #4, #5

如何有效率地分頁?

Efficient Pagination Using MySQL 是一份針對分頁這件事情如何更快的 PDF。

傳統的分頁方式,正如 will_paginate plugin 所做的:顯示所有頁數跟連結。有個不 scale 的問題是,它用了 OFFSET 跟 COUNT,當資料量越來越多,分頁的速度就會越來越慢 :(


 SELECT count(*) FROM messages
 SELECT * FROM messages ORDER BY id DESC LIMIT 0, 20

這裡提出的解決方案如同 Twitter 所採用的,只讓使用者點下一頁:

不要顯示頁數、不要顯示總數量,可以搭配使用 Ajax 換下一頁。如此就可避免使用到 OFFSET 只用 LIMIT,也減少了一次 COUNT query。


page1 = SELECT * FROM messages LIMIT 10 WHERE id > 0 ASC id
page2_min_id = page1.last.id
page2 = SELECT * FROM messages LIMIT 10 WHERE id > page2_min_id ASC id

當然,有個缺點就是使用者沒辦法知道總共有多少資料了。

MerbCamp 2008

身歷其境的看了兩天的 MerbCamp,主辦單位提供的 Webcast 非常流暢,幾乎沒有 delay。(怎麼會這麼厲害呢?! 我連 youtube 或 Y! Live 沒有不 delay 的啊…orz),讓我在家就可以參加地球另一端的研討會,真是棒極了。(雖然英文聽的哩哩落落,不過還是很爽。It’s free!)

主辦單位也在 IRC 上開了 #merbcamp 頻道,講者們似乎也都在上面,隨時都有一些 link 丟出來可以看看,有什麼問題也可在上面提出(例如: webcast 沒聲音或沒影像等轉撥問題反應)、講者QA時間也可以從 IRC 上面問,主辦單位會幫你問講者,非常有參與性。

以下整理自我在twitter的簡短 Merb 心得:

  • Merb router’s defer_to method is awesome!! # keep routing logic in the route, even authentication.
  • merb slice use 100% public API of merb core, not like rails engine broke with each rails upgrade. :)
  • MerbAuth’s “strategy” looks good to support multiple login methods(password, open id…etc) in the same app.
  • Although merb is ORM-agnostic and JavaScript library agnostic, but everybody loves DataMapper and JQuery ;)
  • Merb team will not implement RJS. RJS sucks!
  • It seems that Merb::Plugins is very different from rails plugins: Merb has hooks and public/stable API ;)
  • Merb really hates monkeypatching and alias_method_chain.
  • Rails has no public/private/plugin API, every method is API. That’s why rails developer must use monkeypatching and alias_method_chain.
  • Ruby is not slow! merb is faster than PHP (frameworks). see benchmark.

也有人做了詳細的筆記:MerbCamp Day1, MerbCamp Day2,最後一場 Keynote 的 Merb Performance Benchmarks 特別有意思:Merb without template 與 pure PHP 平分秋色,Merb with template 就大勝 CodeIgniter 和 CakePHP 這兩套 PHP web framework 了….. XD 找了一下這幾套的 benchmarks 比較,發現連 Rails 還不算太慢麻,比 Zend Framework 跟 CakePHP 都還快上不少,所以比較起來應該是 Static >> PHP > Merb >> CodeIgniter > Rails >> Zend Framework >> CakePHP。

投影片大部分在 slideshare 上可以下載的到。同時間 Merb 也將發佈 1.0 RC (即 0.9.9) 版本,所有 API 都將定下來(Merb team 非常強調保證他們的 public API will be stable and backwards compatibility for 1.x,我想大家都被 Rails 一升級就有 plugin 會爛掉非常感冒),相信在過不久就會有越來越多的文件可以看(目前還很缺啊!),是個不錯的進場時機。

最佳化 ActiveRecord SQL 查詢

Update(2008/9/5) 補充 named_scope 也可以用在 :select,感謝 tsechingho++

要看 AciveRecord 產生的 SQL 是什麼,除了可以直接 tail -f log/development.log 之外,也可以在 script/console 的情況下透過以下指令直接把 log 直接顯示出來:

ActiveRecord::Base.logger = Logger.new(STDOUT)
ActiveRecord::Base.clear_active_connections!

如此便可以好好在 console 的環境下實驗 ActiveRecord 囉。

我看到關於 ActiveRecord SQL 查詢最佳化方式有這幾種:

1. 使用 :select

只撈需要用到的欄位,特別是如果不需要用到的 text 或 binary 欄位請排除。

Event.find(:all, :select => "id, title, description")

搭配 named_scope 我們可以把常用的 :select 預先設定好,例如:

class User < ActiveRecord::Base named_scope :short, :select => "id, name, email"
end

User.short.find(:all)

2. 使用 :include

使用 :include 避免 N+1 次 queries 的問題。


@events = Event.find(:all, :include => [:group] )


@events.each do |e|
e.group.title
end

如果沒有加 :include 把相關的 groups 一起載入,在迴圈中就會產生 @event.size 次對 group 的個別 SQL 查詢,會非常傷。加了 :include 之後總共只需查詢兩次。

另外一個比較少人知道的是,在設定 Model associations 時,如果有很明顯的情境一定會順道載入二階 association model,可以設定 :include 在 has_many, belongs_to, has_one 上面,例如:


class User < ActiveRecord::Base has_one :foo, :include => [:bar]
end

如此便會在載入 @user.foo 的同時,也會提早載入 @user.foo.bar。

3. 資料庫索引

針對 foreign key 要加上資料庫索引 index。在 migration 上透過 add_index 就可以加上去了。

4. 特定情況下可用 :joins 取代 :include

在只需要用到 :conditions 而不需要載入該 model 的情況下,可以用 :joins 取代 :include。

Group.find(:all, :include => [ :group_memberships ], :conditions => [ "group_memberships.created_at > ?", Time.now - 30.days ] )

因為其中會載入 group_memberships model 只是為了加條件式,而沒有要撈出裡面的資料,所以可以改用 :joins,這其中的差異你看產生出的SQL就知道了 :)


Group.find(:all, :joins => [ :group_memberships ], :conditions => [ "group_memberships.created_at > ?", Time.now - 30.days ] )

5. 自己寫 SQL

ActiveRecord 可以直接寫 find_by_sql。

6. denormalization 逆正規化

當資料非常多,又要常常查詢其中計算的結果,這時可以考慮使用逆正規化的手法,將計算的結果也當做資料存起來。

一個最常見最基本的用法就是計算總數了,例如以下的例子可以解決需要常常查詢 @topic.posts.size 該篇主題有多少文章的情境:


class Topic < ActiveRecord::Base has_many :posts end class Posts < ActiveRecord::Base belongs_to :topic, :counter_cache => true
end

我們會在 Topic model 新增一個欄位 posts_count,然後 :counter_cache 就會幫我們在新增刪除 Post 時,自動更新所屬 Topic 的 posts_count 欄位資料。這時只要打 @topic.posts.size 就會直接回傳 posts_count,而無需每次都再發一個 SQL query 去 count 有多少筆 posts。

其他逆正規化的方式也包括了統計結算報表等,我們把需要複雜計算的的結果(可以透過非同步的機制如 cron 定期去觸發) 存到 Report model,這樣使用者直接撈 Report 的結果即可。

使用 httperf 做網站效能分析

Update(2008/7/8): 推薦Topfunky’s bong搭配使用,非常簡單。

做網站效能調校,一件重要的事情就是量測(measure)。調之前測一次,調之後再測一次才知有沒有藥效。

“Server 回應 requests 的速度有多快?” 就是一個最基本的問題。這技巧也叫做黑箱分析(Block-box analytic)。Advanced Rails 一書中給了幾點特別要注意的事項:

  1. 有台 front end server (Apache,Nginx等)先測試 static files,這個數據是一個上限。Rails 不會比這更快了。
  2. 找近一點的地方測,減少網路 latency 的變異性
  3. 不要用 server 同一台測試,雖然 CPU 不太會是問題,但是 I/O 可能會有影響。

觀測的數據除了平均,更要看標準差跟計算信賴區間,因為除了平均之外,低變異也很重要。基本的統計學告訴我們,相差平均一個標準差可以涵蓋 68% 的資料,相差兩個標準差就可以涵蓋 95% 的資料。因此我們可以算出 95% 的信賴區間,也就是 95% 發出的 requests 中,可以在幾秒到幾秒內回應。基本的統計知識是做 analytic 必備,Zed Shaw 為此還寫了篇Programmers Need To Learn Statistics Or I Will Kill Them All

在 Mac 上透過 MacPorts 安裝 httperf 很簡單,輸入 `sudo port install httperf` 即可。

httperf 的用法

基本用法是指定 Server、Port、URL和總共要發出多少個 requests:

httperf --server project1.local --port 3000 --uri /events --num-conns 1000

其中最重要的 output 資訊就是 Reply rate 有平均和標準差的那一行。要注意的是 httperf 是每五秒抓一次樣本(sample),根據 httperf 的建議是希望至少有 30 個樣本數才能得到準確的標準差,因此當 sample 數太少的時候,要記得把 –num-conns 往上加。

另一種算壓力測試,也就是去測試“Server 每秒可以承受多少 requests?”。請再加上 –rate 跟 –hog 參數:


httperf --server project1.local --port 3000 --uri /events --num-conns 5000 --rate 500 --hog

逐步把 rate 往上調,直到 server 超過極限時,就會開始少 replies (有 requests 被 drop off 了) 並出現 Errors。

Rails 的相關設定

有幾項設定可以增加更多相對比較數據:

  1. 透過 config.action_controller.perform_caching = false 的設定可以在 production mode 也把所有 caching Page, Action, Fragment 暫時關掉
  2. 暫時註解掉 before_filter 中的權限檢查
  3. 透過 session :off 關掉 session

本文參考資料

  1. Advanced Rails Chap.6 Performance
  2. Peepcode httperf screencast

Rails Front-End 優化

Update(2008/3/24): Yahoo 有份投影片值得一看:

因為 Registrano hosting 在國外的關係,先天速度就慢了一個太平洋。所以如何讓網頁能夠快點 loading 完並且 display 出來變得非常重要且感受明顯。我們可以使用 Firefox 的 YSlow 或 safari web inspector 來檢測實際下載的瓶頸在哪裡。教材則有 O’Reilly High Performance Web Sites 一書值得一讀,裡面的 guideline 有十四點如下:

  1. Make Fewer HTTP Requests
  2. Use a Content Delivery Network
  3. Add an Expires Header
  4. Gzip Components
  5. Put Stylesheets at the Top
  6. Put Scripts at the Bottom
  7. Avoid CSS Expressions
  8. Make JavaScript and CSS External
  9. Reduce DNS Lookups
  10. Minify JavaScript
  11. Avoid Redirects
  12. Remove Duplicates Scripts
  13. Configure ETags
  14. Make Ajax Cacheable

Continue reading Rails Front-End 優化