提升服務效能、減輕 DB 負擔!(1): 在Server使用Low level caching

Cache 介紹、在 Rails 使用 Memcached 來做快取、Heroku Demo 實戰範例

袁浩 Harry Yuan
13 min readNov 27, 2020

最近碰到快取,以及一些資料庫效能的問題,從過程中學習也順便分享。Server 端主要是使用 Rails,資料庫是使用 PostgresQL。

目錄

  1. 快取是什麼?
  2. 快取什麼時候會用?
  3. Rails支援哪些快取儲存方式?
  4. 在Rails使用快取:本地端
  5. 在Rails使用快取:Heroku
  6. 使用情境範例:Heroku
  7. 小經驗談

快取是什麼?

快取(Cache)指的是一種高效能、快速的暫存空間,使用在硬體或軟體。讓我們可以把常用資料暫時存起來,下次需要它們時,能夠快速地取出來。

一般快取是以 Key-Value(鍵-值)的方式來存取,例如我把文章id 123456寫入 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 的記憶體裡面。但要注意的是

  1. 因為這個快取只屬於某個 Process,如果你開的伺服器是多 Multi-Process 的話會無法共用資料
  2. 通常你Server的記憶體還會用在別的用途,例如上傳檔案等等,要小心不能壓縮到太多記憶體。通常這種快取空間我認為只能開小小的,除非你很有錢機器開超大,或是機器本身是 Memory Optimized 版本

File Store

直接存成硬碟某個檔案,預設是會存到本地端的tmp/cache/ 裡面。

MemCached Store

使用 Memcached 伺服器來儲存。它是一個開源的鍵值儲存系統,簡單講他就是一個專門提供 memory 來做快取的伺服器。使用方法單純,這個是大家很常用的選擇。他是直接將資料以字串的形式儲存,沒有特定的資料型別。

此外他是揮發性的系統,也就是一切資料都是暫存的,無備份功能。

Memcached 有一個回收法則(eviction policy):若存滿時,會將最近最少被使用的資料(Least Recently Used Data)清除來挪出空間。

Redis Store

Redis

使用 Redis 來儲存。它是一個開源的鍵值儲存系統,他也是使用記憶體來存放資料,只不過 Redis 不完全是揮發性,他可以透過寫入硬碟,將資料備份存起來。

他支援多種資料型態:字串(String)、串列(List)、雜湊(Hash)、集合(Set)、有序集合(SortedSet)以及對應的資料型態操作。因為功能很多,它可以被當成

  1. 快取伺服器:資料都是存在記憶體中,存取速度快
  2. 資料庫:有支援的資料型態,還可以做 RDB 備份。
  3. 訊息代理:可以幫忙處理 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 來存取它我們可以透過幾個方法來存取快取資料。

  1. write(key, data, options)
  2. read(key)
  3. 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 裡面還有熱門文章。

第一次fetch
1分鐘內進行第二次fetch

這樣一來每一分鐘內,我們都會從快取中取出資料,而不會每一次都查詢。

在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/hotarticles/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 來增加服務的效能,同時減輕一點資料庫的負擔了。

感謝你看到這邊,諸君若有任何想法或建議歡迎交流學習。

--

--

袁浩 Harry Yuan

Software Engineer | Ruby on Rails 喜歡學習前後端技術。希望文章白話到阿嬤都看得懂。