Rails 的 cache 介紹一:cache stores

紅寶鐵軌客
Join to follow...
Follow/Unfollow Writer: 紅寶鐵軌客
By following, you’ll receive notifications when this author publishes new articles.
Don't wait! Sign up to follow this writer.
WriterShelf is a privacy-oriented writing platform. Unleash the power of your voice. It's free!
Sign up. Join WriterShelf now! Already a member. Login to WriterShelf.
寫程式中、折磨中、享受中 ......
1.88K   0  
·
2019/01/21
·
17 mins read


在 Rails ,最讓其他平台使用者攻擊的就是網站執行效率,效率這件事,有很多影響因素,像是 Ruby 的慢就是其中一個重要因素,但是要提高 Rails 的效率,Cache 就是其中很好的方法。

Rails 的 Cache 是建構在 Cache store 上的,Cache store 很好用,很多 gem 也依靠這功能運作,但是也很多細節,它的文件我覺得說得有些難懂,我覺得要搞懂 Rails 的 Cache 要先從底層開始,也就是:

Cache store

Cache store 就是用來存儲 cache 後資料的「店」,沒什麼了不起的,可以把它想像成一個倉庫,把要存的東西,給個標籤,write 進去就好,非常好用簡單,不過,Rails 一下就給你好幾種選擇,所以一開始,就有選擇的挑戰,你就必須要知道這些 store 的差異,這就比較麻煩了,讓我們先從最容易懂的開始:

Cache store: File store

將 cache 資料存成檔案,在 Rails 的官方文件說,預設的 cache store 就是 file store,但是,如果你是用 Rails 5.0+ 以上,你在開發(developement)時,卻可能是另外的 Memory store,或是 Null store (我們等一下介紹這些),這有點討厭,照理來說,開發環境最好是跟運轉的環境一樣,特別是 Memory store 還不支援多工環境,不過,file store 會將資料寫在硬碟目錄中,確實,如果開發環境有一堆亂七八糟資料,也是很討厭的。

依照文件上說,如果你沒有在config.cache_store 設定它的位置,他會寫到 applications root 的 tmp/cache 內。重點來了,原始的 Rails config/environments 中的 development.rb 跟 production.rb 都「沒有預設」,還有一些特別的設定!這,那,我怎麼知道是哪個? 所以首先,確定一下你的 cache store 在那裡,很簡單,到 rails c 中,執行:

Rails.application.config.cache_store

如果你都沒改 config 中設定,我猜你在

  • 開發環境中,會得到: => :null_store,
  • 在 production 環境下會得到:=> [:file_store, "/home/deploy/apps/urapp/xxx/xxx/tmp/cache/"]

如果你的開發環境真的是 null 或是不是跟 production 環境一樣,我的建議就是趕快把 Rails config 在 environments 中的 development.rb 有關於 Rails cache 的設定修改成 file_store,如下,也就是加上 config.cache_store = :file_store, "#{root}/tmp/cache/" 那行,並把原來的 config.cache_store remark :

# Enable/disable caching. By default caching is disabled. (I changed the mem-cache-dev.txt to caching-dev.txt, as rails dev:cache is actually toggle caching-dev.txt)
  #if Rails.root.join('tmp/mem-caching-dev.txt').exist?
  if Rails.root.join('tmp/caching-dev.txt').exist?
    config.action_controller.perform_caching = true

    #config.cache_store = :memory_store
    config.cache_store = :file_store, "#{root}/tmp/cache/"
    config.public_file_server.headers = {
      'Cache-Control' => 'public, max-age=172800'
    }
  else
    config.action_controller.perform_caching = false

    config.cache_store = :null_store
  end

在 Rails config 中,是用在 tmp 目錄裡有沒有 caching-dev.txt 這個檔案存在,來做開發環境中要不要使用 cache 的切換,他有一個對應的 rails 命令,可以很簡單的切換開發環境,只要在 OS 環境中,執行:

bin/rails dev:cache

你就換看到 cache 的狀態:

  • Development mode is now being cached.  或是,
  • Development mode is no longer being cached.

這個 rails 命令有一個好處,他除了會刪除或新增 caching-dev.txt 這個檔案,他還會 touch tmp/restart.txt,這會自動重啟你的 rails server,還蠻方便的。

為什麼我的 rails cache 開發環境沒有改變?

如果你執行了 bin/rails dev:cache 但是進入 rails console 後,你再用 Rails.application.config.cache_store 來檢查你目前的 cache store,結果發現竟然還是 :null_store!這時,很有可能你的 config/environments/development.rb 中用來切換 cache 的檔名是錯的,它可能是「mem-caching-dev.txt」,你要將它改為 caching-dev.txt,我目前用的是 Rails 5.0.7,它的預設就是用 mem-caching-dev.txt 檔案來做 config cache 切換,可以確定的是,參考目前 Rails 的 github,是用 caching-dev.txt 為切換 cache 的檔名,這個檔名是對的,畢竟像我這樣要改變 cache store 的人可能不少,檔名用 mem-caching 是有誤導可能。

在 config 中,我們也看到 cache control 被設定到 max-age=172800,172800秒也就是兩天,也就是說 Response 的過期時間是 兩天,兩天內重讀同一頁網頁就會被 Cache 住,所以開發時要注意了。還有,rails c 一定要出去再進來,只 reload!是沒用的。  改好了這些後,這時,你再在 rails c 中執行 Rails.application.config.cache_store 你就應該會看到變成  file store ,這跟 production 環境中預設的是一樣了。 如果你的開發環境是用 puma,別用了,趕快改成 file store,Memory store 在多工的環境下,不能用!

好了,很高興了,可以來玩 cache store 了,這時如果你看了很多網路上的文章,在 rails c 下做以下的動作:

f = ActiveSupport::Cache::FileStore.new("file_cache_store")
f.write('a',"a123")
f.read('a')
=> "a123"

哈,你會發現 "file_cache_store" 是寫到 applications 的 root, 並不是寫到 tmp/cache 內,這好像跟文件說的不一樣。哈,你搞錯了,在 Rails 中你不需要再宣佈開一家「新的」cache 「店」啦,因爲 Rails 已經幫你開好了,所以,你只要用就好,常用的就是 read,write,fetch 來存讀資料:(不過這也意味著,你可以隨時開一家新店!)

irb> id=22 
=> 22
irb> Rails.cache.write("a#{id}","123")
=> true
irb> Rails.cache.read("a#{id}")
=> "123"

在上面的例子中,這筆 cache 資料被寫到 {applications root}/tmp/cache/062/620/a 中,中間的 062/620 子目錄是隨機產生的,檔名 a 可以確定就是 cache 的標簽 tag。

可是 read 跟 write 並不是 cache 使用上的大咖,fetch 才是!fetch 是個 ruby 指令,基本上,就是讀這個 hash,有就回,沒有時,可以指定回覆值,甚至執行一段指定程式,如下:

irb> h={a: 1, b: 2}
=> {:a=>1, :b=>2}
irb> h.fetch(:a)
=> 1
irb> h.fetch(:c, "default")
=> "default"
irb> h.fetch(:c) { h.each do |x,y| puts puts y end }
1
2
=> {:a=>1, :b=>2}
h.fetch(:c) { | key | "not found: #{key}"}
=> "not found: c"

Rails.cache.fetch 使用很頻繁,現知道 fetch 怎麼用就好。

使用 File store 就要記得定期清除老資料,不然,它就會一直留在硬碟中,直到,裝滿爆炸(disk full),你可以任意的刪除它,差別就是當這個標簽檔案被刪後,就讀不回原來的資料了,既然是 cache 的資料,所以當然沒差,再建立就有,檔案被刪後,Rails.cache.read 就會給你一個 nil。  

有沒有方法設定 rails cache 的 File store 大小呢?好像沒有,用類似 memcache 的設定,如:config.cache_store = :memory_store, { size: 64.megabytes } 不會動,所以,比較好的清除 cache 方式是設定過期(expired)時間,你可以每一個 cache 行為中設定,你也可以設定在 config 中,這樣所有的 cache 就會有一個過期時效:

config.cache_store = :file_store, "#{root}/tmp/cache/", { expires_in: 1.day }

如何清除 File store 所有資料呢?有三個比較常用的方法:

  • rake tmp:cache:clear # 刪除所有在 applications 的 root 下 tmp/cache 的檔案。
  • Rails.cache.clear # 在 file store,就是刪除檔案,跟上面一樣,但是如果用其他的 store,它會依照不同來刪除。
  • Rails.cache.cleanup # 這是只清除「過期的」cache 內容。

使用上,file store 應該是最方便的,又支援多執行緒的 cache store 了,但是還是有一個很大的限制,就是它跟 Heroku 的 ephemeral filesystem 八字不合,基本上,Heroku 的檔案系統你可以想像成暫存檔,他是隨時會不見的,而且,Heroku 也會每天清掉檔案,如果你是用 Heroku 當 host,那就別想 file store 了。 另外,在 production server 上執行時,要確定是否真的有執行刪除,rake tmp:cache:clear 如果碰到檔案權限問題,就直接裝作沒事,但也沒刪除 cache,Rails.cache.clear 就比較好,會「當」給你看。

Cache store: Memory store

用記憶體存 cache,也就是將 cache 留在記憶體內,這在開發環境中應該是最方便好用的,也是目前 rails 在開發環境中的預設,你可以很簡單的在 config 中設定,如:

config.cache_store = :memory_store, { size: 64.megabytes }

只是,Rails 5 以後,因為新加入的 ActionCable 功能,已經將 Puma 作為 Rails 預設的伺服器,如果你是使用多功( multi-process)的 Phusion Passenger 或是 puma 的 clustered mode,那恭喜你,Memory store 在每一個多功序列中,無法共通,也是因為這原因,所以我前面在介紹 file store 時,就建議直接換店,不要用這個了,另外,Rails console 也因為記憶體不能分享,也不能讀,再加上,我個人喜歡開發環境與運轉越一致越好,眾此種種,我很建議大家就不要用這個 store 了。

Cache store: Memory cache store

這跟 Memory store 是完全不一樣的東西,只有都是用記憶體存資料是一樣的,Memeory cache store 是一個分散式的快取系統,很像是升級版的 memory store,他最大的不同是可以支援多功,據說,這是目前在運轉環境中,使用最廣的,說實話,他真的算是好用又方便。 Rails 的說明寫得好簡單,但是這東西一點都不簡單,想要知道更多什麼是 memeory cache?看這裡: memcached - a distributed memory object caching system: memcached,說故事:這玩意是由 Danga Interactive 很早很早以前(1998?)開發的,開源後,大家覺得實在太好用了,所以也就廣為流傳,只是如今,Danga 早就被賣了幾輪了,有員工還是保留著這家公司的原始連結( Danga 在此 ),各位讀者如果點開網站,下面就列出了當初開發的人現在到那裡去了,開枝散葉,都是高手啊! 

哎呦,你說,我就是想用,不想要知道這麼多啦,不行,沒那麼簡單,要用,就請看下去!

Memory cache store 很像網頁運作,有伺服器端、還有用戶端,你要先讓伺服器端跑起來,在 linux 上,一般都伺服器端已經預裝好了,要確定有沒有,就打:memcached -h,你就會知道有沒有裝,也會知道版本,memcached 的版本很重要,Rails 的預設用戶端 Dalli gem 要求 1.4 以上,版本不到,就要跟新,詳細的 memcached 安裝,每個作業系統都不一樣,以下是如何在 Mac 上安裝 memcached,其他作業系統就請自己找了。

How to Install and Configure Memcached Process/Server on Mac OS X? • Crunchify — Memcached is one of the widely used distributed memory object caching solution out there. I've been using it since last 3 years actively for number of Crunchify

確定有安裝了,簡單啟動 memcached 伺服器端的指令如下:

memcached -l 127.0.0.1 -p 11211 -m 512 -d

這樣,你的 memcached 伺服器端就已經在 127.0.0.1 上執行了,port # 是 11211(大家好像都是用這個 port),佔用 512MB,在背景執行,更多詳細的 memcached 設定,一樣,就打 memcached -h,就可以看到了。

再來就是用戶端了,最簡單知道用戶端能不能讀到 memcached 的資料方法就是:

telnet localhost 11211

連上後,打個 stats,就可以看到 memcached 的狀態了,哈哈哈,你現在知道什麼是 Memory cache store 了,他就是一個用戶端透過 tcpip 存取伺服器端資料的分散式快取系統。那 rails 的用戶端是什麼呢?總不能用 telnet 吧?

這其實是個好問題,照 Rails 的官方說明,是用已經內包的 dalli(達利) gem,但是各位如果查網站,很多的介紹寫的是用 memcache-client gem,會造成混淆,其實這是換過了,2010 年後,全部改成 Dalli,Rails 5 以後已經預設內裝了,早期的版本,還是要安裝,詳細的安裝方式,就看 gem 說明,寫得很清楚:

petergoldstein/dalli — High performance memcached client for Ruby. Contribute to petergoldstein/dalli development by creating an account on GitHub.
GitHub

這些都確定了,使用 Memeory cache store 就很簡單了,在 Rails config/environments 中的 development.rb 跟 production.rb 中加入:

config.cache_store = :mem_cache_store, "cache-1.example.com", "cache-2.example.com"

就完成了,注意一下,如果你是用多功( multi-process)的 Phusion Passenger 或是 puma 的 clustered mode,那就要另外裝上 gem 'connection_pool',同時也要指定 pool size。

config.cache_store = :mem_cache_store, "cache-1.example.com", "cache-2.example.com", { :pool_size => 5 }

加上 connection_pool 的原因是要避免多執行緒競爭(thread contention,很難翻譯⋯⋯),基本上,只要是多工環境,connection pool 都是要設定的,接下來介紹的 Redis 也是一樣,這是 connetion_pool 的說明:

mperham/connection_pool — Generic connection pooling for Ruby. Contribute to mperham/connection_pool development by creating an account on GitHub.
GitHub

Dalli gem 提供很多設定的選項,像是要不要壓縮資料,連線保留等等,比較特別的是,你可以設定資料存入是 raw,也就是不經過序列化(serialization),這樣可以省下一些容量,你還可以直接 increment 或是 decrement cached 的值,Dalli gem 有很多可以研究跟設定。

Memcached 一旦存滿的,就會自動刪除資料,如果這不是你想要的,你就需要使用 memcached -M 來觸發錯誤,駭客也有可能攻擊沒有登入設定的 memcached 伺服器端,這些是你要注意的。

懶得自己架設 memcached?沒關係,也有很多網路服務商提供這樣的服務,例如:Amazon ElastiCache。

Amazon ElastiCache – 記憶體內資料存放區和快取 — ~
Amazon Web Services, Inc.

簡單嗎?可以算是簡單了,要認知 cache 從來就不是一件簡單的事。。。

用 memcached 效率算是很好的,甚至比接下來要介紹的 Redis 更好,但是目前的趨勢是,大家都開始用 Redis 了,為什麼?最大的好處就是更容易管理跟 scale(擴展)cache 這東東。

Cache store: Redis cache store

Redis 跟 memcached 非常的像,有伺服器端、也一樣有用戶端,你一樣要先讓伺服器端跑起來,他跟 memcached 最像的就是當裝滿的時候,也會自動刪除資料,但是要注意的是,他的預設刪除法並不是依照 Rails 中的 expired 設定,他是走標準的 LRU(Least Recently Used),也就是「最近最少使用」,Redis 4.0 以後,有了一個新的 LFU (Least Frequently Used),這就蠻好用的,rails 手冊建議設定為(allkeys-lfu),詳細的資料看這裡:Using Redis as an LRU cache – Redis  

一般我們用 Redis 來當 cache store 時,設定的讀寫時間越短越好,建議是一秒以下,因爲如果 Redis 反應很慢,那還不如從新再做一頁內容,而且,Rails cache 讀取不會回應說讀不到,不管是 time out 還是沒資料,他就是回覆 nil,也就是沒有這個 cache,所以如果你的 redis 反應很慢,就只是會被當成沒 cache 就是了,還有,Rails 預設也不會自動續連,也就是如果連線斷了,那就沒了,一般不會發生,但是要注意就是了。

好啦,先來看怎麼裝簡易版的 redis 伺服器端,在 Mac 上:brew install redis,在 ubuntu 上:sudo apt-get install redis-server,再來就是要確定他有跑,Mac 上就執行:redis-server /usr/local/etc/redis.conf,這會跑在前台,你也可以看到 redis 的設定檔就在 /usr/local/etc/redis.conf 內,你可以好好研究要怎麼設定了,在 ubuntu 上,就跟一般 service 一樣:sudo service redis-server start/stop/status,要記得有執行就是了。

接下來就是要安裝 gem 了,如果你是用 rails 5.2 以前的版本,你就要先加上 gem 'redis-rails',5.2 版以後就內建了,一定要裝的就是 gem 'redis',你也可以考慮裝 gem 'hiredis',就把這一或兩個加入 gemfile 中,把 hiredis 想成是一個用 c 寫的加速器,會加快讀去速度,當然,你就不能用 JRuby 了,話說,現在還有人用 Java 嗎?(我只是開玩笑的,別打我),bundle install 後,你就可以用了,你如果已經在用 Rails 的 action cable,redis gem 一定已經裝好了,不過,建議你要將 action cable 的 redis server 跟 cache store 的 redis server 分開來,連 rails api 都強烈建議這樣做了.

ActiveSupport::Cache::RedisCacheStore: Redis cache store. Deployment note: Take care to use a *dedicated Redis cache* rather than pointing this at your existing Redis server.

要測試 Redis 的讀取,很簡單, Redis 有一個很可愛的 command line 介面:redis-cli,在命令頁中打入 redis-cli,然後就可以下個簡單的 info 指令,它就會告訴你很多 redis 的狀況,例如:要知道他用了你多少資源。 不喜歡用 command line?沒關係,因爲你已經裝了 redis gem,你的 rails concole 也可以讀取 redis 了:

irb> re = Redis.new
irb> re.info
=> {"redis_version"=>"3.2.0",.......
irb> re.set("test", "test_value")
=> "OK"
irb> re.get("test")
=> "test_value"

如上,1~3 行是讀取 redis 的狀況,你也已經在 4~7 中行看到怎麼從 rails 中去讀寫 redis 了,很簡單對吧!

設定 redis 成 cache store 看這裡,在 Rails config/environments 中的 development.rb 跟 production.rb 中加入以下,要記得改啊,這是我從 rails guide 中直接抄出來的,畢竟,這裡面可是很多學問的。

cache_servers = %w(redis://cache-01:6379/0 redis://cache-02:6379/0)
config.cache_store = :redis_cache_store, { url: cache_servers,
 
  connect_timeout: 30,  # Defaults to 20 seconds
  read_timeout:    0.2, # Defaults to 1 second
  write_timeout:   0.2, # Defaults to 1 second
 
  error_handler: -> (method:, returning:, exception:) {
    # Report errors to Sentry as warnings
    Raven.capture_exception exception, level: 'warning',
      tags: { method: method, returning: returning }
  }
}

基本上到此,你就可以用了。 

Redis 會自動將記憶體中的資料存入硬碟中,所以,大部分的情況下,你重開機資料也不會掉,當你在開發環境時,如果有必要刪除 redis 的 cache ,就用:Rails.cache.delete

Redis 是目前 rails cache 的當紅炸子雞,我的建議是,當網站只有一個主機,流量也不大時,就用 file store 就好了啦,當成長到有需要時,特別是需要兩個以上的主機時,就改用 redis,他一樣有很多網路服務商提供這樣的服務,所以你可以不用自己架設,看你的需求了。

再來就是「沒有」cache store!

Cache store: Null cache store

這是很方便的開發更測試環境設定,事實上,你如果是剛開始用 cache,你目前的開發環境應該就是 nullstore。 

Rails config/environments 中的 development.rb 跟 production.rb 中加入以下:

config.cache_store = :null_store

這樣,你不管 rails.cache 怎麼讀寫,都是等於沒有 cache hit (中獎)!

介紹完 cache store 了,別忘了,萬事還是要以 rails guide 為準:

Caching with Rails: An Overview — Ruby on Rails Guides

那一個「店」效率好?

這絕對是大家最想要問的問題,標準答案一定是:「沒有一定答案啦,要看應用。」其實不然,如果是老派的電腦程式設計師,一定知道,如果以 IO 的讀寫效率來說:記憶體 》硬碟 》網路,所以 memory store 一定會快於 file store,最慢的一定是 redis 跟 memcached,但是我們還是要考慮你的 server 數目,因為 memory 跟 file store 就只能支援單一 server(file system sharing 就是網路了),所以結論就是:

  • 單一 server 的網站:
    • 只使用單執行緒 web server 時:用 memory store,
    • 使用多執行緒 web server 時:用 file store,
  • 多個 server 的網站:
    • Memcached 或是 Redis 看你喜好了,我個人覺得 Redis 有優勢,但是因為網路原生性的「慢」,你要避免:
      • cache 太小的內容,如果原來只要 30ms 以下,cache 應該會更慢,
      • cache store server 跟 web server 的連線反應速度要很高,使用遠端服務應該不是個好選項,

有一篇文章支援我的論點,各位有空可以看看,他寫的 cache 說明很清楚!是很棒的文章。

Speed Up Your Rails App by 66% - The Complete Guide to Rails Caching: Caching in a Rails app is a little bit like that one friend you sometimes have around for dinner, but should really have around more often.

Rails cache 的使用

寫到這裡,我才發現,這篇真的太長了,所以我就把 Rails cache 的使用寫在另一篇了:

Rails 的 cache 介紹二:網頁 caching — 前一篇文章介紹 cache store,知道了 Rails 的 cache stores 是什麼了後,當然就要知道怎麼用了,網頁 ca...
Scrivinor 思書: 紅寶鐵軌客


WriterShelf™ is a unique multiple pen name blogging and forum platform. Protect relationships and your privacy. Take your writing in new directions. ** Join WriterShelf**
WriterShelf™ is an open writing platform. The views, information and opinions in this article are those of the author.


Article info

This article is part of:
分類於:
標籤:
日期:
創作於:2019/01/21,最後更新於:2019/02/05。
合計:4855字


Share this article:
About the Author

很久以前就是個「寫程式的」,其實,什麼程式都不熟⋯⋯
就,這會一點點,那會一點點⋯⋯




Join the discussion now!
Don't wait! Sign up to join the discussion.
WriterShelf is a privacy-oriented writing platform. Unleash the power of your voice. It's free!
Sign up. Join WriterShelf now! Already a member. Login to WriterShelf.