提升服務效能、減輕 DB 負擔!(1): 在Server使用Low level caching
Cache 介紹、在 Rails 使用 Memcached 來做快取、Heroku Demo 實戰範例
最近碰到快取,以及一些資料庫效能的問題,從過程中學習也順便分享。Server 端主要是使用 Rails,資料庫是使用 PostgresQL。
目錄
快取是什麼?
快取(Cache)指的是一種高效能、快速的暫存空間,使用在硬體或軟體。讓我們可以把常用資料暫時存起來,下次需要它們時,能夠快速地取出來。
一般快取是以 Key-Value(鍵-值)的方式來存取,例如我把文章id 123
和 456
寫入 hot_articles
這個 key,下次去快取找 hot_articles
時,我就會得到123
,456
。
當使用 key 找到資料,稱為 Cache Hit;若該 key 無資料,稱為 Cache Miss。
資料存入快取時,我們通常都會手動設定暫存多久。過期後,快取資料就會清空,下次再存入新的內容。
Cache什麼時候會用?
Cache 可以用在很多種地方,例如在前端可以暫存網頁的內容,來增加載入速度;也可以使用在後端,如果有些從資料庫撈出來的資料,短時間不會有太大的變動,那我們可以透過存到 cache 裡面,來減輕資料庫的負擔。
以 Rails 為例,在前端有 Fragment Cache(快取住頁面中一段零碎內容)、Partial Cache(快取住局部渲染的頁面)等。大部分使用方式都可以在這裡找到。
Rails支援哪些快取儲存方式?
Rails支援以下方式:
- Memory Store
- File Store
- MemCached Store
- Redis Store
- Null Store
以下跟大家個別介紹這些類型。
而 Node.js 的大大們可以參考 memjs
以及 node-redis
。
Memory Store
最直接簡單,就是存到目前 Ruby Process 的記憶體裡面。但要注意的是
- 因為這個快取只屬於某個 Process,如果你開的伺服器是多 Multi-Process 的話會無法共用資料。
- 通常你Server的記憶體還會用在別的用途,例如上傳檔案等等,要小心不能壓縮到太多記憶體。通常這種快取空間我認為只能開小小的,除非你很有錢機器開超大,或是機器本身是 Memory Optimized 版本
File Store
直接存成硬碟某個檔案,預設是會存到本地端的tmp/cache/
裡面。
MemCached Store
使用 Memcached 伺服器來儲存。它是一個開源的鍵值儲存系統,簡單講他就是一個專門提供 memory 來做快取的伺服器。使用方法單純,這個是大家很常用的選擇。他是直接將資料以字串的形式儲存,沒有特定的資料型別。
此外他是揮發性的系統,也就是一切資料都是暫存的,無備份功能。
Memcached 有一個回收法則(eviction policy):若存滿時,會將最近最少被使用的資料(Least Recently Used Data)清除來挪出空間。
Redis Store
使用 Redis 來儲存。它是一個開源的鍵值儲存系統,他也是使用記憶體來存放資料,只不過 Redis 不完全是揮發性,他可以透過寫入硬碟,將資料備份存起來。
他支援多種資料型態:字串(String)、串列(List)、雜湊(Hash)、集合(Set)、有序集合(SortedSet)以及對應的資料型態操作。因為功能很多,它可以被當成
- 快取伺服器:資料都是存在記憶體中,存取速度快
- 資料庫:有支援的資料型態,還可以做 RDB 備份。
- 訊息代理:可以幫忙處理 MessageQueue(像是 Sidekiq 的非同步就是用此特性做的)、Pub/Sub(發布/訂閱架構,用來推播訊息)
此外,Redis支援六種不同的回收法則,有興趣可以瞄一下:
Null Store
這個是測試環境用的,叫做「根本沒快取」XD。它存在是為了要讓開發人員可以馬上看到程式碼造成的更動。本地接假的 Cache,production 接真的Cache。
存入跟取出資料時都不會真的有快取,每次都是使用新的資料。
講那麼多,我要選哪種?
我個人是建議使用 mem_cache_store 或是 redis_store。把快取的存取,與原本的 App Server 分開,這樣一來比較好管理、擴充。
至於是選擇 MemCache 還是 Redis,如果只要單純暫存,備份是非必要的話,使用 MemCache 即可。如果你的服務有一定規模,需要使用特定資料結構儲存增加 memory utilization,並且具有高度彈性,那就使用 Redis。關於他們兩個更細微的比較,網路上有很多文章可參考,例如這篇。
我大概整理了以下的資訊給你參考:
Rails快取使用:本地端
介紹完種類以後,就來跟大家分享如何在 Rails 內使用 MemCached 來暫存。而我們將要做的快取型態,是在後端暫存 ActiveRecord 查詢好的資料,這個動作稱為 Low Level Caching。
STEP1: 安裝本地的memcached服務
我們選用 memcached,直接用 brew 安裝:
brew install memcached
STEP2: 開啟本地的memcached服務
我們先用brew services list
檢查是否有開啟。
若沒有,我們用 brew services start memcached
來啟動它。
STEP3: 安裝dalli gem
MemCache 相對於我們的 RailsApp 是另一個服務,我們需要透過一個「轉接頭」來接上我們的 Rails,所以要安裝這個 gem。
# Gemfile
gem 'dalli'
再來記得要跑 bundle install
。
STEP4: 設定使用mem_cache_store
接著我們要在測試環境內,指定這個儲存方式。我們先找到 development 內的現有的設定(這是Rails 5.2+的預設設定):
我們就先不用他的設定。把action_controller.perform_caching
打開,並且設定為使用 mem_cache_store:
:mem_cache_store
後面,可放一或多個伺服器位址,而我們在本地開Memcached位置就是 localhost
,預設的 port 是 11211。
注意:dalli以前是用 :dalli_store
,但 2.7.11改成統一用 :mem_cache_store
STEP4: 開始使用Low Level Caching
設定完成後,我們就可以透過 Rails.cache 來存取它。我們可以透過幾個方法來存取快取資料。
- write(key, data, options)
- read(key)
- fetch(key, options, &block)
write/read
前兩個就是最基本的寫入、讀取。write 可以帶 expires_in
來指定暫存時間。
# 儲存
Rails.cache.write("number", 1, expires_in: 10.seconds)# 讀取
number = Rails.cache.read("number") # return 1
下圖你可以看到,我設定為10秒過期,時間一到東西就不見了。
fetch(key, options, &block)
這個是比較常用的,因為他同時包含 read 跟 write 的功能。
若不給 option 跟 block,他的作用就單純是讀取,這樣跟 read 其實是一樣的。
articles = Rails.cache.fetch('hot_articles')
而我們通常會一次給完 expires_in 跟 block。
artiles = Rails.cache.fetch('hot_articles', expires_in: 1.minute) do
Article.hot.to_a
end
這段程式碼首先會讀取 hot_articles
這個 key,如果有值就直接回傳。
要是沒有,就會從下方的 block 取出結果,除了回傳,也會放入hot_articles
的這個快取 key 中,同時設定1分鐘後過期。
如果1分鐘內,我再做一次一模一樣的 fetch
,就會發現他並沒有查詢SELECT * FROM "articles"
,因為我們的 cache 裡面還有熱門文章。
這樣一來每一分鐘內,我們都會從快取中取出資料,而不會每一次都查詢。
在Rails使用快取:Heroku
STEP1
首先我們也是要有一個 Memcached 機器,不過這次是在Heroku上面的。
到你的 Heroku 專案頁面,點擊 Resources,開一個叫做 Memcachier 的Memcached 的伺服器。
STEP 3
接著我們點進去 Memcachier > Settings 找連線資訊。
有了這些資訊後,把它們設定到 heroku 專案的環境變數裡。
heroku config:set MEMCACHIER_USERNAME=你的 MEMCACHIER_PASSWORD=你的 MEMCACHIER_SERVERS=你的
打開 config/production.rb
並且跟著heroku範例設定如下:
這個範例設定多了很多資訊呢!大概介紹一下:
我們前面有提到,要連線的機器可能是「一或多台」。比如說你有很多台Memcached機器,就可以將 MEMCACHIER_SERVERS
設定成 my_memcache_server1.com, my_memcache_server2.com
這串字,所以有個split來做分隔。
Options
此外除了剛剛的驗證連線資料,還有一些設定。例如 faileover
是說若現在連的那台壞掉,若還有其他台,就自動切換到其他台; socket_failure_delay
是說如果某次操作失敗,在重試之前會等多久。
各種 option 都可以在這裡找到。
基本上到這邊你就可以在 Heroku 上使用 Rails.cache 了。
使用情境範例:Heroku
我手邊有個專案,裡面有 Article
這個 model/controller,並且有一個熱門文章列表的功能。在我的認知下,熱門文章列表在一兩分鐘內不會差太多,就算用戶要互動也要點進去文章才行,所以我們可以在這邊做快取。
分成兩個 route 來方便比較差異:
至於重要的就在 controller 裡面了,為了好方便比較,我把它們寫成統一格式:
很明顯可以看到,這兩個差在 cached_hot 有做快取2分鐘。
至於 View 的部分隨意就好,只要記得把 @time
跟 @articles
印出來看看。
<!-- articles/hot.html.erb --><%= @time %>
<% @articles.each do |article|%>
<h3><%= article.title %></h3>
<% end %>
接著我們 git add、commit、push 到 heroku 上面。
檢視結果
我們打開 articles/hot
跟 articles/cached_hot
這兩個頁面來比較一下。
我是從 19:22 開始,這兩頁都重整,你會發現左邊沒Cache,所以一律是現在的時間。而右邊的 @time
跟@articles
都有成功被 cache 住2分鐘,所以會停在 19:22。下一次更新是 2 分鐘後。
時間我確定快取住了,我想要驗證下面的文章也是。這 2 分鐘內,我們來瘋狂重整右邊這頁,來看看他會不會仍然一直去查詢資料?
也就是成功快取住的意思。
小經驗談
工作上,我的手邊有一隻榜單的 API。會直接從 RDS 拉資料出來,撈的過程中有做 aggregation,該有 index 的都有 index,也有排除 N+1 問題。
剛開始還沒什麼人在看,時間久了越來越多人使用,甚至尖峰時期RDS差點要發 alert,必須想辦法緩和:
剛好當時我們本來就有使用 Memcached 伺服器跟 redis,所以就把這隻 API加上 cache,雖然用戶可能會覺得榜單多了一小段時間才更新,但為了資料庫這就是 trade off。
後來的時間也試著找了其他方式來處理這隻 API,比如說限制上榜人數、用config 依使用量來調整cache時間等等。此外有 cache 的時候,因為少了查詢所以 API 的速度也變快了許多(791ms->314ms)。
相信看到這邊,你多少都可以使用 cache 來增加服務的效能,同時減輕一點資料庫的負擔了。
感謝你看到這邊,諸君若有任何想法或建議歡迎交流學習。