Rails 的 cache 介紹二:網頁 caching
喜歡作者的文章嗎?馬上按「關注」,當作者發佈新文章時,思書™就會 email 通知您。
思書是公開的寫作平台,創新的多筆名寫作方式,能用不同的筆名探索不同的寫作內容,無限寫作創意,如果您喜歡寫作分享,一定要來試試! 《 加入思書》
思書™是自由寫作平台,本文為作者之個人意見。
文章資訊
本文摘自:
Categories:
Tags:
Date:
Published: 2019/02/03 - Updated: 2019/10/04
Total: 9889 words
給本文個喜歡
或不
關於作者
很久以前就是個「寫程式的」,其實,什麼程式都不熟⋯⋯
就,這會一點點,那會一點點⋯⋯
看看作者的其他文章
看看思書的其他文章
前一篇文章介紹 cache store,如果你還未看,我建議先看,很多設定與選擇要做:
Rails 的 cache 介紹一:cache stores — 在 Rails ,最讓其他平台使用者攻擊的就是網站執行效率,效率這件事,有很多影響因素,像是 Ruby 的慢就是其中一個重要因素,但是...
Scrivinor 思書: 紅寶鐵軌客
知道了 Rails 的 cache stores 是什麼了後,當然就要知道怎麼用了,網頁 caching 最主要就是要讓 Rails 的網頁變快,越快越好,只是我們一般都不會把網頁效率寫就規格內,都是等到被客戶嫌的時候,才開始改,畢竟太多變數了,硬體軟體都會參一腳,硬體難改很貴,苦工就是我們寫程式的了,有沒有一個速度的準蛇呢?我看了一些網路討論,一般都會希望使用者的每一網頁動作能在一秒內完成,要做到這一點,東扣西扣,每一個網頁的 server responses 就會被希望能在 300ms 完成,算是一個參考吧。
要優化速度為什麼要用 cache?很簡單,因為 cache 應該是最簡單的速度優化方法,Ruby 本來就不快,最好的提速方法就是讓它跑越少行越好,所以,這就帶出了什麼是 Cache 的主要觀念了:
所以,cache 有兩大重點:
知道這樣大該就可以開始寫 cache 了,反正,一定一大堆問題,Rails guide 的 Cache 篇還是要讀的,不過很難讀,不能只靠這篇啦:Caching with Rails: An Overview — Ruby on Rails Guides。
在開始寫之前,還有一個問題,我們怎麼知道,減肥前跟減肥後,到底差在那裡?所以我們需要一個「秤」,最簡單的秤就是你的 Log,Rails 已經提供了一寫簡單的效率數據,你可以再你的 server log 中看到類似以下的資料:
Completed 200 OK in 1971ms (Views: 1490.5ms | ActiveRecord: 208.9ms)
這就是告訴你:你這一網頁總共花了快兩秒,view template(例如:index.html.erb) 花了快 1.5 秒產生,你的資料庫花了 0.2 秒,這數字並不是很準,ActiveRecord 的數字只有 DB 的讀寫時間,ActiveRecord 的轉換、lazy loading 等,應該都沒算入,不過,聊勝於無!要更準一些,也好讀一些嗎?來來來,裝個 profiling 工具!
Profiling 效率工具
你當然可以把「全部」的網站都 cache 起來,但是真的不用啦,只要找出那幾個最慢的部分,把它 cache 起來,基本上,就大功告成了,畢竟,打蛇打七寸,抓元兇就好,只是,誰是戰犯呢?這個抓戰犯的行為,就叫 profiling!
Rails 最強大的地方就是社群,有一個叫做 rack-mini-profiler 的 profiling gem 非常好用:
MiniProfiler/rack-mini-profiler — Profiler for your development and production Ruby rack apps. - MiniProfiler/rack-mini-profiler
GitHub
安裝也簡單,只要在 gemfile 裡面加上,記住,要加在 pg/sql gem 的後面,他會改 DB 設定:
gem 'rack-mini-profiler'
然後 bundle install,鐺鐺!你就會在開發環境的網頁左上角,看到一個像下圖的小標簽了:
是的,那個數字就是你這一頁網頁跑了多久,點一下它,你還可以看到分析,多方便啊!如果你還沒裝,我強烈的建議你一定要裝,這個 mini profiler 可一點都不迷你,只有顯示的很迷你,他有很多功能,也可以加很多外掛,功能強大,詳細的介紹與使用,就請自己看這個 gem 的說明吧,關於 mini profiler 中顯示的數字是什麼,以下這篇文章講得很清楚:rack-mini-profiler - the Secret Weapon of Ruby and Rails Speed。
Fragment Caching
我們先來介紹 Fragment(片段) Caching,事實上好像也只介紹這個,也假設你已經選用了 File Store 當 cache 的店(為什麼?因為這是初學者應該要用的,還是不知道為什麼?請看前篇),我們不介紹 action caching 跟 page caching 了,反正:一、Rails 4+ 以後也不支援這兩種東東;二、用的地方不多,除非你的網頁超級簡單,如果超簡單,那還要 cache 嗎?
絕大多數的動態網頁(就是在說 Rails)都是由一堆 Fragments 組成的,一般一個網頁都會分成什麼版頭、上面下面中間的,這就是 Fragment,當各個 Fragment 產生後的結果先存起來(就是 cache),下次,我們就檢查,「原來生產這些 Fragment 的變數」,也沒有改變,還是一樣時(沒改變),就直接讀 cache 的資料!有改變,就從新做一個 Fragment,就這樣,就可以用 Fragment Caching!
讀得懂嗎?一定有點昏昏的,什麼跟什麼嘛,這也是我寫這篇文章的主因,一堆文章都用講的,到底實作時,怎麼做?有那些要注意的?來,放碼!
這是在 Rails guide 上的說明程式舉例:
很清楚的說明了,你只要把要 cache 的內容,用第二行的:
cache do ... end
包起來,你的 cache 就完成了,超級簡單的,但是是這樣嗎?答案不是! 這第二行中間的 product 是一個很重要的關鍵,你必須了解,它叫做「key」,這種 cache 也就是你會常在 cache 行為中聽到的「key base」cache!這個 key 很重要,你可以有很多的方式來產生,最重要要懂得它的觀念,這個 key 要能夠獨特的代表你所 cache 的內容,也就是說,這個 key 以後,就是代表這段
cache do ... end
的內容,如果 key 的「值」沒改變,我們就不執行cache do ... end
所包起來的程式碼,直接讀取使用我們以前產生的 cache 內容,如果「值」改變了,我們就會執行cache do ... end
,重新產生 cache 內容。詳細的說明,可以看一下它的 Helper 文件說明: cache (ActionView::Helpers::CacheHelper) - APIdock: ,我們來看一下一些實務上常用的例子:用網址:
<% cache do %>
這最簡單,你什麼 key 都沒用,這時,Rails 會自作聰明的以你目前的 URL 內容,當成 key!注意看一下你的 log,你的 log 會有類似的下面這幾行:
註:如果你在 log 中沒看到這些,那你可能沒有在 config 中,設定要顯示:
config.action_controller.enable_fragment_cache_logging = true
,Rails 5.1 後,預設就是關閉的。你可以到目錄 tmp/cache/ 下,找到產生 cache 的內容,他在 file store 中會隨機產生的兩層數字 xxx/xxx 目錄下(我怎麼也找不到這兩層目錄的命名規則,好像就是隨機,但是,隨機要怎麼找?這對我是個謎!,有那位大師知道,請務必告知),檔名就是 views%2F0.0.0.0%3A3030%....,你可以打開這個檔案來看,哈,就是那一個 fragment 的 html 嘛,只是前面跟後面加了一些 cache 用的識別碼,是的,cache 就這麼簡單,它就是把要 cache 的這部分 html 存起來,如果 key 找得到,就用這個,找不到,就在產生一個。
檔名的組成也很有意義,它就是 "view" + URL + template digest 所產生,等等!什麼是 template digest?它是一個「值」,依照你的「整頁」網頁 template 內容,用 md5 計算後,自動產生的,目的是當你改了這個網頁的 template 時,rails 也才會知道要產生新的 cache 內容,要注意的是,他不只是計算 cache 內的 template 「值」,它會計算的是「整頁」的 template 內容及相關連的 template,會算一大堆,做這的目的是為確保當你改變網頁內的 template 時,cache 會即時更新,但是這也產生了要怎麼知道網頁相關聯(template dependencies)的問題,我們等一下會介紹。
如果你的這個網頁非常簡單,這個網址剛好就是獨特的代表你所要 cache 的內容,那這就是你要用的,超級簡單,只是,我想絕大多數都不會是這樣,因為我們在做的是 fragment cache,這樣做基本像是一個 page caching,rails 4.0 以後就不提供內建的 page caching,但是你如果有需要,還是有個以下的 gem 可以使用:
rails/actionpack-page_caching — Static page caching for Action Pack (removed from core in Rails 4.0) - rails/actionpack-page_caching
GitHub
用單一變數:
<% cache product do %>
<% cache product do %>
<%= render product %>
<% end %>
如果你的這部分的網頁就只是由單一個變數所產生,如上面的例子,就是只用到一個 active record 的 product 變數,這時,你的 key 就可以很簡單的用它來代表,這時,你的 log 會有類似的下面這幾行:
同樣的,你也可以到目錄 tmp/cache/ 下,找到相對應產生的 cache 內容,注意到,他的檔案名稱,也就是 key,變了,是的,他的檔案名稱變成由 active record 變數所產生了,他是由:"view" + class + 這個 active record 的 id + record 的 updated_at datetime stamp + 先前說的 template digest 所產生。
這時,cache 與否的判斷就只有依靠 product 這個 active record 的 ID 與 updated_at 來判斷了,我們知道,當我們跟改了任一個 active record,它的 update_at 就會記錄下改的時間,所以我們很確定,當我們跟新了這筆 product 的內容時,cache 一定會更新! 只是,如果你的fragment 內容不只是依靠這個 product 的值,還會依據別的變數改變,那不幸的,你的 fragment 內容就被 product 鎖死了,這筆 product 的內容沒改變時,它就會一直給你一樣的內容,為了要讓 cache 的內容能夠依其他變數重新 caching,更多的 caching 選項就來了。
用多個變數:
<% cache [I18n.locale.to_s, product] do %>
<% cache [I18n.locale.to_s, product] do %>
<%= render product %>
<% end %>
前面這個例子是用 array,你也可以自組 key 如:
<% cache "product-#{product.id}" do %>
,在很多的網頁實務裡,這才是大部分正常的使用狀況,這代表你的這部分的網頁就是由多個變數所產生,如上面的例子,就是用到一個 active record 的 product 變數,再加上一個多國語言識別的變數,這時,你的 key 就是由這兩個變數所組成,同樣的,這時,你的 log 會有類似的下面這幾行:注意到了沒,它的「key」現在多了一個語言的語言別變數值,同樣的,你也可以到目錄 tmp/cache/ 下,找到 cache 存的資料,這種用法應該是最常用的,有人因爲很煩,每次都要加 I18n.locale,乾脆寫了一個 gem(如下),它會自動加上 I18n.localte,要不要用就看你自己了,我是能少用一個 gem 就少用一個的人。
igorkasyanchuk/cache_with_locale — Contribute to igorkasyanchuk/cache_with_locale development by creating an account on GitHub.
GitHub
用指定的名稱:
<% cache ‘name_of_cache’ do %>
<% cache ‘name_of_cache’ do %>
<%= render product %>
<% end %>
這種用法很有趣,就是一個全手動的觀念,它最大的好處是因為 key 就是指定的那麼一個,所以不用管 expired 後的刪除清理,但是,你卻要自己管理何時要 refresh 這個 cache 的內容,你可以在 model 中用 before_destroy 跟 after_create/update 的 callbacks 來呼叫 expire_fragement,舉例如下:
這種用法 rails 並不建議,基本上就是一個全手動的 cache 管理方式,但是,就如同我一開始說的, cache 絕不是一件簡單的事,有一個手動的選項,總是很好的,手動的 fragment cache 管理還有幾個好用的功能,像是建立 key、確定 key 的存在、讀寫 fragment cache 等,如果你決定要手動做 fragment cache,一定要看一下他的文件:ActionController::Caching::Fragments,這篇文章也值得一看:Caching in Ruby on Rails 5.2。
Collection caching
就是指
render partial: xxx, collection: @collection
,這時使用 collection caching 很方便又效果好,但是只限於你是使用 partial collection rendering 的時候,也就是說,你只能在有render partial: xxx, collection: @collection
的形況下使用,render @collection, cached: true
這種是不能用的。 cache 使用很簡單,就在後面加一個 cached: true,如下:<%= render partial: 'products/product', collection: @products, cached: true %>
就這樣就好,夠簡單吧! 使用後,你在 log 中第一次 render 會看到:
多了一個 x/x cache hits 的顯示,第一次當然都不中,refresh 網頁後,第二次,就發現全中了!
很棒吧,這是效率很好的 cache,要多多使用!實務上,如果是 render partial with collection,就一定要用這個方法,Rails 內部有優化,效率很高,如果沒有 collection,就把整個 partial 包在一個 cache 內,那樣效果比較好,畢竟,partial 還是另外一個檔,讀取還是很花時間的,只是,世界常常就不會是完美的,render partial with collection 不支援過期 expires_in。 下面這篇文章有一些深入的效能數字比較,有興趣的可以看看:
Rails Collection Caching — In this article, we'll take a look at how Rails collection caching works and how we can use it to speed up large collection rendering.
AppSignal Blog
cache 一個 Query
<% cache query.cache_key do %>
可以 cache 一組資料嗎?在 Rails 的官方 guide 上沒說,但是可以,這是在 Rails 5 以後新增的功能,也是 Rails 社群建議後產生的,很有趣的一個功能,但是使用起來要小心,我們就來看以下這個簡單個例子,你一看就懂了:
乍看之下,跟以前單一變數長得幾乎一樣,只是這次變數內不是只有一筆資料了,而是一組 records,執行以上的程式後,會在 LOG 中看到以下的一個 Fragment key 產生:
views/category/en/categories/query-b179323bc4d3f0cf677118ed5fb76028-6-20191004093312714577/e651926d451a2da2815d43b775294a1f
views/category/en 我們已經介紹過了,重點在它的後面那串:
後面跟著的就是 Digest 的值了,這我們也介紹過了。
所以,如果 Query 字串沒變,找出來的資料也沒改變,這段 cache 就會直接用已經存好的 fragment 來取代,很方便吧,只是,這使用起來要小心,有一些情況下,這個 Fragment key 可能會遇到當資料已經改變時,產生的 key 卻是一樣的,以下這篇文章介紹的很清楚,我就不重複了,幾本上:
詳細就看這篇 Mohit Natoo 的大作,解釋得很清楚:
Caching result sets and collection in Rails 5 | BigBinary Blog — Rails 5 provides cache_key for ActiveRecord::Relation that can help in caching result of a collection of records. BigBinary Blog
cache_if
我們還可以在程式中設定,用 cache_if 或是 cache_unless 來決定 cache 與否,實務上,我覺得用處不大,但是也許在開發時,是個好幫手。 下面的例子就是只會在 production 中 cache:
template digest:網頁關聯(template dependencies)問題
好了,終於,我們要來談 template digest 了,你在開發 Cache 時,很有可能會看到一個 Couldn't find template for digesting: xxx 的錯誤訊息,但是好像又對 cache 沒有什麼大影響,這時,你就是遇到網頁相關聯(template dependencies)問題了。 話說,我想大家都知道什麼是 template 吧?這裡指的是 view template,rails 的預設 template 就是那個 xxxx.html.erb 東東~
記得我們在之前談過,cache 後的檔名(或是 key)的組成,是由 "view" + 指定的變數內容 + template digest 所構成,而這 template digest 是用 md5 來算這「整頁」網頁的 template 及 其相關連的 template 後,所得到的值。 好啦,問題來了,Rails 怎麼知道這頁網頁是由那些 template 所組成呢? Rails 想要很聰明,也算很很厲害啦,它會沿著這一頁網頁中的每一個 render 去找到每一個指到的 template,而且會一層一層的找下去,只是,有時候,特別是你如果有用 render helper,Rails 會找不到 render 所用的 template,這時我們就要去告訴 Rails 那一個才是我們用的 template了!以下我借用 rails guide 的例子:
可以找到 template 的 rendering:
不能找到 templete 的 rendering:
其中,像第一與第二行找不到時,你就要改寫成內含 template 位置的寫法(有一點不是很美了):
但是,第三行,就完全沒辦法改寫成內含 template 位置了,怎麼辦?這時,你就只能用很特別很怪的「註解指令」寫法來指定 template 的位置了⋯⋯ (真不美啊!)
<%# Template Dependency: todolists/todolist %>
<%= render_sortable_todolists @project.todolists %>
這
<%# Template Dependency: todolists/todolist %>
「註解指令」就是告訴 rails 你的 template 在那裡,這「# Template Dependency:」要照著打,多一字少一字都不行,很奇怪的問題解法,很不美。 這 「註解指令」還可以有鬼牌,如:<%# Template Dependency: events/* %>
,也還有一個更鬼怪的:<%# Template Collection: notification %>
,我真不知道何時會用到,就不介紹了。強迫 template digest 更新
你只要改變了 template 中的內容,包含改變 remark 內容,template digest 就會從新計算,這是一個很方便的 cache 更新方法,但是,有一個情況,就是當你使用 helper 時,你可能改寫了 helper ,讓它產生新的內容,但是這時,Rails 的 cache 是完全不會知道的,因為 template 並沒有改變啊,簡單的說,Rails cache 是不會檢查 helper 有沒有改變的,那怎麼辦?我們再是借用 rails guide 中的例子:
<%# Helper Dependency Updated: May 6, 2012 at 6pm %>
<%= some_helper_method(person) %>
這時你就要強迫 template digest 更新,最簡單的方法就是改 remark 的內容,這也是 rails 的建議方式,rails 是建議加一個日期在 remark 內,當你更新 helper 後,記得改一下日期,這樣就會跟新 template digest 了,只是,這樣做真的太太麻煩了,再說,如果你這個 helper 用得到處都是時,那要改多少次啊! 這在 Rails 的 cache 中真的沒解,我能想到的唯一方法就是讓 cache 過期!expired 它,這樣至少如果忘了,一段時間後,也會被更新!來,讓我們先說完 template digest 相關的事,等一下,我們就來看怎麼讓 cache expired。
註:當 log 中,出現 Couldn't find template for digesting: templates/template
你如果找了很久,都找不到你的那一個 render 裡有用到這個 view template 時:
關掉 template digest
有時候,如果你所要 cache 的內容與 template 無關,這時,你就可以關掉 template digest,很簡單,如下,加上一個 skip_digest: true 就好:
<% cache "no_template", skip_digest: true do %>
就這樣,這時你如果看你的 log ,就會發現,template digest 不見了!
Time Base! 設定過期時間的好處
設定過期時間的 cache 就叫做 time base cache,為什麼要設定過期時間?為什麼要設定這個 cache 的內容多久就過期?很多的 cache 的介紹不是都說不用管 cache 過期,大家都說 Rails cache 的好處就是你不用手動清除 Cache 的過期內容,連 DHH(Rails 創辦人) 在他的這篇介紹 key base cache expiration 的文章 中,也是這麼說的,其實,rails 的 作法是:舊的 cache 內容就讓它留著,交給 cache store 去負責清除它,但是他說的是那些可以設定店容量大小,而且裝滿後,會自動清理的店,像是 Memcached,不是 file store,file store cache 的內容可是會一直加到硬碟全滿,而且 file store 是預設在 production 環境中的 cache 店家,所以,在 file store 中,清除過期的 cache 內容是很重要的!
清理 file store 的 cache 就是一個很有趣的挑戰了,我的建議是:
怎麼樣設定過期時間呢?
config.cache_store = :file_store, "#{root}/tmp/cache/", { expires_in: 1.day }
<% cache(product, :expires_in => 1.day) do %>
那一種好?我覺得直接設在 config 中好,可以確保不會忘了設定,程式碼也乾淨一些,你可以在 development 環境中設短一些,我是設一個小時,production 環境就可以設的長一些。 你也可以兩個都設定,當兩者都設定時,所以 <2> 的每一筆分開設定就會蓋過 <1> 的設定,文件上沒寫,但是我驗證過了。
Dog-pile effect 狗堆效應
在高流量的環境中,使用 expires_in 是一項具有挑戰的事,有一個可能就是在高流量的環境中,一個 cache 剛過期,其他多個多功運行中的程式就同時建立了好多個新的 cache,Rails 有一個特有的 :race_condition_ttl 設定,專門使用在設有 expires_in 的情況下,用來防止這個問題,用法如下:
<% cache(product, :expires_in => 1.day, race_condition_ttl: 60.seconds do %>
以可以是在 config 中,這種現象也稱為狗堆效應(dog-pile effect ),你如果覺得這會發生的可能性太低了吧,可以看看以下這位苦主的發文:Memcached: How to avoid dog-pile effect – Askar Fuzaylov – Medium: Problem。
expires_at
有一個讓 cache 在指定的時間過期的方法也不錯,也就做一個 expires_at,只是 Rails 不提供這個功能,以下的文章有怎麼開發的介紹,基本上就是計算要過期的時差,如下,別忘了要用 round 來解決小數點誤差問題。
更詳細的介紹請看這篇:
Be aware of time operations when using cache with expires_in — In the last post I exposed some methods to calculate time difference in seconds, minutes and hours using Ruby on Rails. Today, we will… Medium
如何清除過期的 cache?
很簡單,就執行 Rails.cache.cleanup,可以把它寫成一個 rails 的 script 給 crontab 呼叫 rails runner 定期執行就好,你也可以在 rails console 中使用,這在開發中很有用,我甚至每個禮拜都回完全刪除所有的 cache 資料,畢竟,萬一搞到硬碟爆炸,整個 server 都完蛋。
不幸的是,你如果是用 Rails 5.2 以前,Rails.cache.cleanup 在 file store 時不能用,也就是說,你沒有辦法 expired 內容,你唯一的選項就是全刪,用:Rails.cache.clear,這很糟糕,但是,這也就是有時候,你一定要更新 rails 版本的原因......吧。 註:cleanup 在 Rails 5.2 以降,也許不能用 cleanup 刪除過期的 cache 內容,但是還是可以用來設定 cache 內容過期時間,所以當內容過期了,還是會令 cache 重新產生。
ActiveSupport::Cache::FileStore#cleanup does not remove expired entries from cache · Issue #30788 · rails/rails — Steps to reproduce # frozen_string_literal: true begin require... GitHub
Russian doll caching (俄羅斯娃娃 caching)
都看過俄羅斯娃娃吧,就是一個娃娃打開後,裡面還有一個,再打開,還有,聽說可以做到幾十個的,在 Rails 的 cache 中,我們也會遇到同樣的問題,我們來看以下的例子:
我們可以發現,當我們改變了 comment 的內容時,blog 並不知道,所以,最外面的這個 cache(@blog) 沒有變,因為這個 key 沒變,所以 blog 的 cache 並不會改變,這就是俄羅斯娃娃 caching 問題,為了解決這個問題,Rails guide 上的建議的解法是:
改 model:用 'touch' 指令在 model 中:
這樣的解法好不好,見仁見智,好處是簡單,不過你必須了解,touch 是一個不只用在 cache 的功能,他是一個 ActiveRecord 的功能,當你設定這個 model 的 touch 是 true 時,任何的更動這筆資料,會觸發:after_touch,after_commit 跟 after_rollback 的 callbacks。
不改 model:還有一個下面的解法是不用改 model 的,看起來比較直覺,是用 Maximum 及 map,效果一樣,我覺得也很好用,還是用前面這個 blog + comment 的例子來說明:
這樣,當任何一筆相關的 comment 被改變時,@blog.comments.maximum(:updated_at) 就會改變,或是,當相關的 comment 有新增刪除時,@blog.comments.map(&:id),也會改變,這兩個誰好,難說,還是要因時因地自己判斷,不過這個範例也很清楚的說明了如何正確的設定 Rails 的 cache key。
分享 cache 內容
cache 如果是同樣的 key 是可以在不同的網頁分享的,很棒吧,這很好用,以下我們來列舉一些常碰到的問題:
skip_digest: true
來關掉 template digest,才能分享,但是,後遺症就是當你改變 template 後,cache 就不會更新,兩難,怎麼用,你要自己判斷了,我的建議是:關掉 template digest 來分享,但是也設一個短一些的過期時間,這樣,自少時間到後,一定會跟新。Low-Level Caching
有時候,你需要「暫存」一些不是「view」的資料,這時,low-level caching 就是一個很好的選項,Rails cache 可以將任何資料以字串的方式儲存,最常用的方式就是用:
Rails.cache.fetch
,它是一個 ruby hash 的 fetch 指令 rails 加強版,rails guide 上有一個很棒的例子,它可以設定讀取時間,減少很慢的外部網站讀取,所以我們就用這個例子來介紹:在這個例子中,設定了隔 12 小時才會讀取跟新一次外部的網站的價格資料,既然是 cache 就要有一個 key,你可以自己取一個名稱,或是用 cache_key 還是 Rails 5.2 後提供的 cache_key_with_version,cache_key_with_version 會預設產生一個model id + updated_at 的 key,如:products/233-20140225082222765838000/competing_price。
cache_key 還是 cache_key_with_version 這兩個沒有什麼大差別,只是 cache_key_with_version 可以用:cache_version 來設定不用預設的 updated_at timestamp 來當作 version,會有更大的運用彈性,rails 6.0 後使用 cache_key 會出現 deprecated 的警告,也就是說 rails 希望 5.2 以後,都要用 cache_key_with_version 來清楚的指定是用那個 timestamp 來當版本 version 識別,是會清楚一些啦,但是很煩啊!以下是指定 timestamp 的做法:
以下是不指定 timestamp (用預設的 updated_at )
Rails 5.2 以後,你也可以把 versioning 關掉:ActiveRecord::Base.cache_versioning = false,6.0 後預設也是關掉,這樣, cache_key 就可以繼續使用了。 Cache_key 或是 cache_key_with_version 還有一個好用的地方就是用在 HTTP cache 中,我們來看看:
HTTP 條件取得 Conditional GETs
大家都知道瀏覽器與伺服器間,也有 cache 吧,好啦,這裡也不講太深,反正就是當瀏覽器來要求讀取資料時,如果以前有讀過,也還保有資料,就會提供一個以前資料的 ETag(實體標籤)與 Last-Modified(最後修改時間),伺服器端可以用這個來判斷網頁的資料有沒有改變,如果這兩個都沒有改變,就回應說:304 (Not Modified),這樣,伺服器端就不用再送一次資料,這比搞什麼 fragment cache 都快。
在 Rails 中,常用的是:if stale?(etag: @article, last_modified: @article.created_at),在 Rails guide 中,Etag 可以用 cache_key_with_version 來做。 我們還是用 Rails guide 中很棒的例子來說明:
還有一個更簡單的用法是:
if stale?(@product)
,Rails 會自動以 updated_at 跟cache_key_with_version
來產生 last_modified 跟 etag,就跟上面的一模一樣,省了很多字,如果你的 controller 沒有指定的 respond_to,也就是說是用預設的,這時,你更簡單,就用:fresh_when last_modified: @product.published_at.utc, etag: @product
,更省字了。 rails 還可以讓你個 HTTP cache 永遠:http_cache_forever(public: true)
,其中 public 設成 true 會讓 proxy 設成 2011-1-1 起,一百年不過期,不過這樣真的超級危險,你以後的網頁就完全不會跟新了,使用要很小心、非常的小心。強 Strong ETags vs. 弱弱 Weak ETags
ETags 是什麼?如前面所介紹的,Etag 是一個「值」,我們可以用像是 cache_key_with_version 來產生,它是使用在 http header 內,主要是用在當瀏覽器向 web server 要網頁時,判斷要不要重新讀取,如果 ETag 沒變,就用瀏覽器的 cache 網頁,如果變了,就重新讀取。 Rails 5+ 以後,預設從 strong ETags 改成 weak ETags,弱與強主要的差別是就是弱弱 Etags 由一個 W 開頭,以下的例子說明兩者比較的差別:
說實在的,這改變好像跟 rails 的開發沒什麼影響,也跟我們現在談的 cache 無關,大家以為可以跳過,只要知道基本上 Rails 5.0 後是用 weak ETags 就好,可是不然啊,如果你有用到 CDN,很多就只支援 Strong ETags,像是 Akamai,所以你一定要改用,用法如下:
還有更多的求知慾,可以看以下這篇:
Rails 5 switches from strong etags to weak etags | BigBinary Blog — Rails 5 switches from strong etags to weak etags
BigBinary Blog
清除 cache 資料
如同我在另一篇的 cache store 的介紹,不同的 store 要做不同的清除,選用 File Store 當 cache 的店就必須要定期清除,不然他就會一直增加、一直增加,直到,塞滿你的硬碟,清除 cache store 很簡單,有兩種方法:
這三個刪除法在文件上都註明不是每一種店都適用,我沒試過所有的,大家用時多注意、多測試吧,實務上,你可以把它寫到一個 ruby 程式,再用 crontab/rails runner 去做定期的刪除動作。
網頁效率真實量測
如果你開發的網站,有一個最大回應時間的目標值,開發就不是那麼簡單了,你必須要有很多的測量工具,你也不能再依靠 log 了,比較準確的方式就是用網頁量測工具(web benchmark),找一個你喜歡的,常用的有 apache benchmark,使用上也很簡單:
$ ab -t 2 -c 2 https://www.test.com/
以上的指令是說:-t 2 = 做多測兩秒;-c 2 = 同時兩個(多功 works 測試),以下就是一份產生的測試報告,以這個命令,可以看到這個目標網頁:完成了五次,沒有錯誤,平均一個 Request 969ms,快一秒。
由於網頁回應時間的測量與測試是一個大工程,絕不是一個簡單的事,我只是起個頭,不是這篇文章的目的,我們也就不再做深入的介紹了。
實際使用的建議:
與其說 Cache 是個技術,不如說 cache 是門藝術,實務上,cache 一旦加入後,一定或對開發造成一些困擾,我真的建議 cache 要到程式開發的差不多時,最後再來開發,但是,我們都知道,網站都是不斷的演進,新功能慢慢的一點一點的加入,永遠都沒有開發完的一天,所以,講了等於白講,不過,有一點真正有用的建議是,要先了解 cache,然後先在一些效能關鍵的地方運用,不要多,這會對你瞭解 cache 有很大的幫助,回過頭來,對你在設計每一個功能時,也會知道要如何考慮未來如何加入 cache。
cache 在 rails 中,有著三種形式,基本上,核心都圍繞著如何管理過期:
開發 cache 時,也不是就是三選一,更多的清況下是「混用」,Key based + Time based 混用是很基本的,每個應用都有它實務上的考量,我碰到最多的挑戰就是,到底,網頁上的資料要如何「即時」,這個即時是如何定義的,可以允許晚一個小時更新嗎?例如:一個購物網,如果要顯示「即時」的購買數量,如果看的人多、買的人很少,問題不大,但如果買的人很多,那等於每一次購物量改變,cache 的內容就必須過期重新產生了,再如果每個進來的人都會買,那 cache 根本就是增加 overhead,拖慢時間效能,但是,換個設計,如果網頁上沒有購買數量顯示,或是,只做一個「售完」顯示,或是允許購買數量顯示五分鐘前的「非即時」,那就算是高流量網站,也可以大大的提高網頁效能,所以,cache 的難就是它與設計跟應用妥協有者交互的影響,我能給的建議是,寫之前,多想,多溝通,太多的 cache 都是寫了等於白寫。
我實在很希望大家分享一些 cache 的經驗與故事,一定很精彩,期待啊!
最後,不要忘了要將 cache 目錄加到你的 deploy script 中,如果你是用 capistrano 也就是:
set :linked_dirs, %w{... tmp/cache ....}
,如果你沒加,那每次發佈 deploy 後,所有的 cache 內容就重新開始,當然,也許這就是你要的,每次 deploy 後,就重新開始!