產品 Rails 應用程序的不同緩存策略
簡介:Ruby on Rails 現在愈來愈多地被作為基本框架廣泛用於大中型可伸縮的復雜應用程序中。由 於 Ruby 是一種解釋型語言,所以要想使 Rails 隨您所願,需要使用很多不同的緩存策略。本文展示了 目前可用的一些緩存策略,包括我們為 ChangingThePresent.org 所使用的那些。
關於本系列
Rails 在開發人員中享有盛譽。Rails 一度備受矚目,是業界爭論的焦點。人們對它的評價也大 相徑庭:從一種高生產率技術到一個小玩意,從市場定位准確到宣傳過度。與很多新技術一樣,Rails 也 被毫無例外地被打上了 “未經驗證、可擴展性有限” 的標記。與 C 和 Java™ 語言不 同,Ruby 是解釋性的,且存在性能上的一些固有阻礙。
實際上,Internet 上的許多大型網站都 使用的是解釋性語言。這些網站均引入了類似 Ruby 所采用的相同的策略:即集群式的無共享的架構。此 外,緩存也是必需的。要獲得盡可能好的性能,許多站點都需要采用一種有效的緩存策略。Rails 開發人 員也開始跟隨其後。
幾個場景
首先,讓我先來帶您浏覽幾個 ChangingThePresent.org 中 的頁面吧。我將顯示站點中幾個需要緩存的地方。然後,再指出我們為其中每個地方所做出的選擇以及為 實現這些頁面所使用的代碼或策略。尤其會重點討論如下內容:
全靜態頁面
幾乎無變化的 全動態的頁面
動態頁面片段
應用程序數據
先來看看靜態頁面。幾乎每個站點都會 有靜態頁面,如圖 1 所示,其中還有我們的條款和條件。可以通過單擊 register 然後再選擇是否接受 用戶協議來浏覽相應頁面。對於 ChangingThePresent 而言,我們從此頁中刪除了所有動態內容以便 Apache 能夠對它進行緩存。按照我們 Apache 中配置的規則,這些內容永遠都不會由 Rails 服務器生成 。因此,我根本無需對其考慮 Rails 緩存。
圖 1. 用戶協議
接下來,再來看看 全動態頁面。理論上講,ChangingThePresent 可以有一些動態構建的頁面,但是這些頁面一般很少變化 。由於幾乎所有頁面都會顯示用戶是否登錄,因此我們並不怎麼關注這種緩存。
再下來,看看頁 面分段緩存。圖 2 中所示的主頁原來是完全靜態的,現在,有一些元素變成了動態的。每天,頁面都會 顯示一系列禮物,這些禮物有的是隨機選的,有的則由我們的管理員選定。請注意在標題為 “A Few of our Special Gifts for Mother's Day” 節下的那些禮物,同時也請注意在最右邊顯 示為 “login.” 的那個鏈接。此鏈接取決於用戶是否登錄。我們不能緩存整個頁。頁面每天 只能改變一次。
圖 2. 主頁
最後再考慮應用程 序。除非是在 15 年之前進行網絡沖浪,否則您現在遇到的有趣站點全部都是動態的。現代的應用程序大 都分層,而且可以通過在層間添加緩存來使這些分層更加有效。ChangingThePresent 在數據庫層采用了 一些緩存。接下來,我將深入討論不同類型的緩存,還會介紹我們為 ChangingThePresent 都采用了何種 緩存。
緩存靜態內容
除了圖像之外,有關緩存靜態數據的內容,可講的內容不多。由於我們的 網站是一個慈善性質的門戶網站,這意味著我們需要更多地關注用戶的感受,比如多加入一些圖像或視頻 。但我們的 Web 服務器 Mongrel 並不能很好地服務靜態數據,因此我們使用 Apache 來服務圖像內容。
我們現在正在著手轉向采用圖形加速器 Panther Express 來緩存最經常被使用的圖像以使其能夠 更快地被我們的客戶訪問到。要采取這種策略,我們將需要一個子域 images.changingThePresent.org。 Panther Express 直接在圖像的本地緩存中提供圖像服務,然後再向我們發送請求。由於 Panther 服務 並不知道我們何時會更改圖像,所以我們使用 HTTP 報頭來使其到期失效,如下所示:
HTTP 緩存 失效報頭
HTTP/1.1 200 OK Cache-Control: max-age=86400, must-revalidate Expires: Tues, 17 Apr 2007 11:43:51 GMT Last-Modified: Mon, 16 Apr 2007 11:43:51 GMT
注意這些不是 HTML 報頭。它們與 Web 頁面 內容獨立構建。Web 服務器將負責構建這些 HTTP 報頭。像這樣一篇有關 Rails 的文章系列若詳細介紹 Web 服務器配置,未免有點偏題,所以我將直接切入可用 Rails 框架進行控制的緩存內容這一主題(有 關 Web 服務器配置的更多內容,請參見 參考資料 中的相關鏈接)。
頁面緩存
如果動態頁面不經常更改,可以使用頁面級的緩存。比如,Blog 和公告牌使用的就 是這種緩存。通過頁面緩存,Rails 就可以用來構建動態 HTML 頁,並將此頁存儲在公共目錄,這樣,應 用程序服務器就可以像服務其他靜態頁面一樣來服務這個動態頁。
如果頁面已經被緩存,那麼就 不需要引入 Rails,頁面緩存是 Rails 內速度最快的一種緩存。在最底層,頁面緩存實際上在 Rails 中 非常容易實現。頁面和分段緩存二者均在控制器級別發生。您需要告知 Rails 如下內容:
想要緩 存哪些頁面?
當頁面內容更改時,您如何能在緩存中讓該頁面到期失效?
可以通過在控制 器類中使用 caches_page 指令來啟用頁面緩存。例如,若要在 about_us_controller 緩存 privacy_policy 和 user_agreement 頁面,可以輸入如下代碼:
清單 2. 啟用頁面緩存
class AboutController < ApplicationController caches_page :privacy_policy, :user_agreement end
讓頁面到期失效則可以通過 expire_page 指令來實現。若要在 Rails 調用 new_pages 動 作時使上述頁面到期失效,可以使用如下代碼:
清單 3. 使頁面失效
class AboutController < ApplicationController caches_page :privacy_policy, :user_agreement def new_pages expire_page :action => :privacy_policy expire_page :action => :user_agreement end end
另外,有幾個小問題需要注意,比如 URL。URL 不能依賴於 URL 參數。例如,應該使用 gifts/water/1 而非 gifts/water?page=1。在 routes.rb 中使用這類 URL 將非常容易。比如,我們的 頁面中總是有一個選項卡參數用來顯示哪個選項卡被當前選中。若要將此選項卡作為 URL 的一部分,我 們會有如下的路由規則:
清單 4. 選項卡的路由規則
map.connect 'member/:id/:tab', :controller => 'profiles', :action => 'show'
對於具有頁面參數的那些列表以及依賴於 URL 參數的其他頁面,也需要采用相 同的做法。此外,還需要考慮安全性問題。
如果頁面已經在緩存內,那麼就不會用到 Rails 框架 ,服務器並不能為您管理安全性。Web 服務器將更樂於在緩存內呈現任何頁面,而不管用戶是否對其擁有 查看的權限。所以,如果您很關心頁面可由誰查看,那麼就不要使用頁面緩存。
如果只是想緩存 簡單的靜態頁面,那麼了解上述內容就應該足夠了。只要內容簡單,實現起來就不難。
當想要緩 存更為復雜的內容時,就需要進行一些權衡取捨了。由於想要緩存的頁面高度動態,所以到期失效邏輯就 會變得更加復雜。要處理復雜的到期失效邏輯,將需要編寫和配置定制清理器(sweeper)。在某些控制 器擊發時,這些類會從緩存內刪除選定的元素。
多數定制清理器都會觀察某些模型對象,並根據 更改擊發邏輯來使一個或多個緩存頁面到期失效。清單 5 顯示了一種典型的緩存清理器。在此清理器中 ,開發人員可以定義一個活動記錄事件,比如 after_save。當此事件擊發時,清理器也會擊發,並可讓 緩存內的特定頁面到期失效。這個事例所顯示的到期失效基於 expire_page 方法。而很多嚴格的應用程 序大都直接使用 Ruby 優秀的文件系統實用工具來顯式地刪除所緩存的頁面。
清單 5. 一個典型 的觀察器
class CauseController < ApplicationController cache_sweeper :cause_sweeper ... class CauseSweeper < ActionController::Caching::Sweeper observe Cause def after_save(record) expire_page(:controller => 'causes', :action => 'show', :id => record.id) cause.nonprofits.each do |nonprofit| expire_page(:controller => 'nonprofits', :action => 'show', :id => nonprofit.id) end end end
現在,您可能會開始感覺到頁面緩存的些許缺點了:復雜性。您雖然可以很好地進行頁面級 的緩存,但固有的復雜性卻讓應用程序很難測試,進而會增加系統內出現 bug 的可能性。而且,如果頁 面針對每個用戶都會有所不同,或者希望緩存進行過身份驗證的頁面,那麼將需要使用頁面緩存之外的方 式。對於 ChangingThePresent,我們必須處理兩種情況,原因是我們必須基於用戶是否登錄來更改基本 布局上的鏈接。對於大多數頁面,我們甚至都不會考慮使用頁面級緩存。為了讓您能夠深入了解頁面級的 緩存,在本文的 參考資料 部分,我特意給出了到一系列有關頁面級緩存的優秀文章的鏈接。接下來,將 來深入探究另一種形式的整頁緩存 —— 動作緩存。
動作緩存
至此,您已經了解了頁面緩存的主要的優勢及主要的缺點:對於多數頁面檢索而言, 根本無需考慮使用 Rails。頁面緩存的優勢是速度快。缺點是缺少靈活性。如果想要基於應用程序內的條 件 — 例如,身份認證 — 來緩存整個頁面,那麼可以使用動作緩存。
動作緩存與頁 面緩存的工作方式大體相同,但在流程上稍有差別。Rails 在呈現動作前會實際調用控制器。如果由該動 作呈現的頁面已經存在於緩存內,那麼 Rails 就會在緩存內呈現頁面而不是重新加以呈現。由於現在使 用了 Rails,因此動作緩存的速度要比頁面緩存慢一些,但其優點還是很明顯的。幾乎所有的 Rails 認 證模式都會在控制器上使用 before 過濾器。動作緩存讓您能夠利用認證及控制器上的任何過濾器。
從語句構成的角度來看,動作緩存與頁面緩存也應該十分類似,只有指令不太一樣。清單 6 顯示 了如何使用 caches_action 指令。
清單 6. 啟用動作緩存
class AboutController < ApplicationController caches_action :secret_page, :secret_list end
緩存到期失效以及清理器的工作方式也應該相同。我們不使用動作緩存的原因與我們不 使用頁面緩存的原因是一樣的,但分段緩存對我們來說更重要一些。
緩存頁面分段
借助部 分緩存,可以緩存頁面的一部分,所緩存的內容很多時候都是布局之類的。要使用分段緩存,開發人員需 要先確定分段,方法是通過在 Web 頁面上直接放上 rhtml 指令來包圍一塊內容,如清單 7 所示。在 ChangingThePresent.org 上,我們使用分段緩存來緩存首頁和其他的幾頁。所有的這些頁均使用了數據 庫密集訪問而且大都是我們最受歡迎的頁面。
清單 7. 確定緩存分段
<% cache 'gifts_index' do %> <h3> Here, you can make the world a better place with a single gift. Donation gifts are also a wonderful way to honor friends and family. Just imagine what we can achieve together. </h3> <h2 class="lightBlue"><%= @event_title %></h2> <div id="homefeatureitems"> <% for gift in @event_gifts %> <%= render :partial => 'gifts/listable', :locals => { :gift => gift } %> <% end %> </div> ... <% end %>
在清單 7 中,cache 幫助程序標識所要緩存的分區。第一個參數是標識此緩 存分區的惟一名稱。第二個參數包含代碼塊 — 即第一個 do 和最後一個 end 之間的代碼 — 此代碼塊准確地確定了要緩存的 RHTML 分區。
我們的網站只有一個主頁,所以命名這個頁面非常 容易。在其他地方,我們使用一種特定的方法來決定此網頁的 URL 以便惟一標識緩存分段。例如,當我 們為特定的內容(比如世界和平或減少貧困)而進行代碼緩存時,我們需要使用清單 8 中的代碼。代碼 會為之尋找永久 url,也稱為 permalink。
清單 8. 通過 URL 標識緩存分段
<% cache @cause.permalink(params[:id]) do %>
通常,當緩存單獨頁面時,需要用清理器使 之過期失效。有時,使用簡單的基於時間的對象過期更為容易和簡潔。默認地,Rails 並不提供這類機制 ,但有一種插件名為 timed_fragment_cache 可以實現這一目的。借助這個插件,我可以指定超時,可以 在緩存了的內容中指定,也可以在為此頁提供了動態數據的控制器代碼中指定。例如,清單 9 所示的代 碼就可以為此頁面構建動態數據。when_fragment_expired 方法只有在相關的緩存分段過期時才會執行。 此方法接受參數,用來指定超時的時長,它還接受一個代碼塊,用來指定當內容過期時哪些內容需要重建 。我也可以選擇在 rhtml 頁面中指定超時和緩存方法,但我更願意使用基於控制器的方法。
清單 9. 基於時間的緩存到期
def index when_fragment_expired 'causes_list', 15.minutes.from_now do @causes = Cause.find_all_ordered end end
如果能夠容忍數據稍微有些陳舊,那麼使用定時的到期機制將可以極大地簡化緩存策略。對 於每個被緩存的元素,只需指定想要緩存的內容、可生成動態內容的任何控制器動作以及超時。與頁面緩 存類似,如果需要,也可以使用 expire_fragment :controller => controller, :action => action, :id => id 方法顯式讓內容到期。此方法的工作方式與緩存動作和緩存頁面的到期失效是一 樣的。接下來,我將介紹如何配置此後端。
Memcached
至此為止,我已經介紹了 Ruby on Rails 的頁面和分段緩存模型。看過了 API 之後,現在就可以定義緩存後的數據的去處了。默認地, Rails 將把緩存後的頁面放入文件系統。緩存後的頁面和動作都會進入公共目錄。可以配置緩存後的分段 的存儲位置。為此,需要用到內存存儲、文件系統(在所定義的目錄)、數據庫或稱為 memcached 的服 務。對於 ChangingThePresent.org,我們使用 memcached。
可以將 Memcached 想象為一個大型 的 hash 圖,這個圖可通過網絡獲得。基於內存的緩存速度快,而基於網絡的緩存的可伸縮性比較好。有 了插件支持,Rails 就可使用 memcached 來緩存分段和 ActiveRecord 模型。要使用它,需要安裝 memcached(更多信息,請參看 參考資料)並在 environment.rb(或其他的環境配置文件,比如 production.rb)對它進行配置。
清單 10. 配置緩存
config.action_controller.perform_caching = true memcache_options = { :c_threshold => 10_000, :compression => false, :debug => false, :readonly => false, :urlencode => false, :ttl => 300, :namespace => 'igprod', :disabled => false } CACHE = MemCache.new memcache_options
清單 10 顯示了一種典型的配置,其中第一行 config.action_controller.perform_caching = true 將啟用緩存。接下來的一行將准備緩存選項。注意 ,這裡的諸多選項是為了讓您可以獲得更多的調試數據、禁用緩存和定義該緩存的名稱空間。在 參考資 料 部分給出的 memcached 站點可以找到有關配置選項的更多信息。
模型緩存
我們使用的 最後一種緩存是基於模型的緩存。我們使用的是稱為 CachedModel 的緩存插件的一種定制版本。模型緩 存實際上是一種有限形式的數據庫緩存。緩存很容易按模型啟用。
要想讓模型使用緩存解決方案 ,只需擴展 CachedModel 類,而非擴展 ActiveRecord,如清單 11 所示。 CachedModel 擴展 ActiveRecord::Base。ActiveRecord 並非全對象關系型映射層。此框架極大地依賴於 SQL 來執行復雜的 特性,而且如果需要,用戶可以很容易降至 SQL。直接使用 SQL 會使緩存出問題,因為緩存層必須處理 完整的結果集,而不是單獨一個數據庫行。處理完整的結果集常常會問題不斷,而且如果沒有支持應用程 序的深層邏輯,這幾乎不太可能。正由於這個原因,CachedModel 的焦點才會放到緩存單個模型對象上, 並只加速返回單行結果的查詢。
清單 11. 使用 CachedModel
Class Cause < CachedModel
大多數 Rails 應用程序都會重復訪問多個條目,例如用戶對象。模型緩存在很多 情況下都可以明顯地使速度加快。對於 ChangingThePresent,我們剛剛開始加速基於模型的緩存。
結束語
Ruby 雖然是一門生產率極高的語言,但若從性能角度考慮,該語言解釋性的特性 讓它並不那麼理想。大多數主要的 Rails 應用程序都將會通過有效利用緩存來彌補某些不足。對於 ChangingThePresent.org,我們主要使用分段緩存,並通過控制器使用基於時間的方法來使緩存分段到期 失效。這種方式很適合我們的網站,即使其中有一些頁面會基於登錄進來的用戶有所變化。
我們 還研究了使用受 memcached 支撐的 CachedModel 類所能帶來的影響。雖然我們的研究還僅限於緩存對數 據庫性能所造成的影響,但早期的結果還是很有希望的。在 下一篇 文章中,我將介紹一些實用技巧,您 可以使用這些技巧來為另一個真實世界中的 Rails 示例進行數據庫優化。