如何使用 memcached 做快取

關於 memcached,雖然可以找到一些基本
,DK大神也有點到一些進階議題,不過最近看了 Using memcached PDF 之後才真正學到了不少實戰技巧跟如何設計快取的概念。以下是一些零散的筆記:

裝好之後,基本的啟動方式是

* memcached -l 127.0.0.1 -P 11211 -m 128 -d for deamon
* memcached -l 127.0.0.1 -P 11211 -m 128 -vv for development debug

memcached 是一套 Name-Value Pair(NVP) 分散式記憶體快取系統,Key 的長度被限制在 250 characters,儲存的資料不能超過 1 megabyte。如果資料會超過 1mb,可以考慮使用壓縮工具,例如在 Rails 2.1 裡就內建了 ActiveSupport::Cache::CompressedMemCacheStore。

除了 memcached server,不同程式語言都有自己的 memcache client library 工具提供更方便的介面。一個基本的任務就是使用 Hashing algorithm 根據 Key 來決定該去存取一台 memcached server (如果有超過一台的 memcached server 的話)。Ruby 的 client 目前檯面上有幾套 1. memcache-client 2. fiveruns fork 版 memcache-client,針對 Hasing 的部份用C改寫了 3. 使用 libmemcached 的超快 memcached

有趣的是,不需要 client library,memcached 是可以直接 telnet 127.0.0.1 11211 的。telnet 之後打 stats 可以得到一些統計資料,除了目前共有多少筆資料跟共用多少空間之外,重要的有 cmd_get 跟 cmd_hits,就可以得出 cache hit ratio,這個數字應該努力到九成以上。另外還有你的 cmd_set 應該超過 cmd_get,

其他的 memcached 標準操作有 SET (新增或是更新一個值)、ADD(只有在該key不存在時,才會新增快取資料成功)、REPLACE(只有在該key已經存在時,才會更新資料成功)、GET(拿快取資料)。

在考慮使用 memcached 前,要知道它不是你系統中唯一的 cache,HTML 的整頁快取應該用 Web server、純 SQL query result 可以用 MySQL 內建 Query Cache,設定很簡單,效果很不錯 (Cache Performance Comparison。我自己的心得是很多時候你想要快取的物件其實並不是一個 SQL query 就可以搞定的,而是多個 SQL query 才計算出來,這時候去做純快取 SQL query result 我個人覺得也許不是很有意義,Rails 這部份就有人實做了query_memcached。。

另外要注意的是 memcached 並不是 persistent data store,只要一關掉 memcached server,裡面的資料就會通通不見,如果要拿來儲存 session authentication 資料要特別小心。

寫 memcached 程式的第一個問題是找出什麼資料需要快取? 一個常見的問題是我該快取 HTML fragment 還是純資料結構? 如果你操作介面只有一處用到,我們可以只快取 HTML fragment 即可,不然其實規模稍大的網站其實兩者都可以快取起來。

撰寫使用 memcached 程式的基本模式就是,先查看有沒有 key-value,有就把快取資料讀出來,沒有就運算結果後存到 memcached sever。這部份算是簡單的。真正困難的事情有兩件:一是清除過期的快取資料(expire),二是Key的命名。

命名的一個慣用的格式是 ObjectName:ObjectType:Key 或是該 SQL statement,但為了 Security 避免被人猜到 Key 和避免超過 255 bytes 的 key 長度限制,建議你將 Key hash 過。當然 Security 的最好解決方式是要有防火牆,因為只要連的上 Port 11211,有 Key 就可以讀的到快取資料,memcached server 本身是沒有任何認證機制的。

有兩種方式清除快取資料:一是在新增/刪除資料時,順便刪掉這個快取 key-value,這樣下次 request 來時便會重新 快取。二是在有更新的時候直接重設快取資料(Reset)。要注意的是如果您有不同的程式會直接更新資料庫(也就是不只是透過主應用程式,還有別的背景程式),就會有可能 memcachd 裡面的資料沒有被更新到,解法有 1. 清空所有資料 2. 有一隻程式可以重建 memcached 裡面的資料 3. 統一用一套知道 memcached 機制的介面操作

如果站很大,race condition 就會是個效能問題了,同時有多個 request 同時去更新同一份快取資料。雖然改用上述的方法二在有更新的時候直接重設(Reset)快取資料可以改善這個問題,不過如果資料根本還不在快取裡的話問題還是存在。也因此我們需要一個 lock 機制,概念是先檢查有沒有 lock entry,有的話先等一下,然後再抓。沒有的話就更新,然後刪除 lock entry。

實做招數有二,如果你的 client library 支援 ADD 操作(也就是如果該 key 已經存在的話,操作會失敗),就可以先用 ADD key:lock 決定是不是有別的 process 在用,沒有的話就 SET key,最後 DELETE delete key:lock。

如果 client library 只支援 GET 操作,只好先 GET key:lock,沒有值就 SET key:lock (有的話表示已經被鎖住,就先稍等),然後再設定真正的快取資料 SET key,最後 DELETE key:lock。這樣會比方法一多一個操作就是了。這的方法的範例程式碼可以去PDF的 Source Code下載,在 /LinuxBasics/memcachedUpdateCalendarOfEvents-3.pl。(well, it’s perl code)

如果不採用主動更新快取資料的方式,也可以直接設定過期時間(expire time),這取決於你有多常讀取/更新。越常更新 expire time 就越短。這種方式特別適合例如首頁每隔幾分鐘才換,而且沒有要求快取資料一定要跟資料庫裡面完全一致,例如在五分鐘內雖有很多更新資料,但前台只需要每隔五分鐘重組頁面即可。

為了徹底解決使用 expire time 方式仍會有的 race condition 問題,有一招是 proactive cache refill:我們另外紀錄一個比 expire 週期還短的 refresh time,如果 refresh time 到了,我們就先更新快取資料,這樣無論何時都不會有 client 要不到資料的情形,自然也就不會發生 rece condition。這招大絕的範例在 /LinuxBasics/memcachedRollingCalendarOfEvents.pl。

在更新跟查詢都很極端頻繁的情況下,也可以考慮用另外的程式專門去撈資料庫執行更新快取,這樣主應用程式只需要處理拿快取資料即可(唯一會去資料庫撈可能只有第一次資料不存在時)。

另一個議題是:如果群集跟個別資料都快取會有的重複現象,例如我們如果同時快取了 People.find(:all) 跟 Person.find(1),這時就重複存了 Person.find(1)。作者是建議群集的部份只當作指標(pointer)來使用,也就是群集只快取了所有 Person 的 ID 而不是完整資料。畢竟單項比較好重複使用(reuse),而且在處理更新快取資料時也比較方便,不會發生個別資料更新了,但是群集忘了沒有處理。

話說雖然 memcached 有提供 ADD/REPLACE 操作(根據Key存在或不存在,會導致儲存快取不一定成功的操作),但是這樣你就必須處理 error code 的情況,作者認為“快取”的基本原理是不回應(noreply):你不應該關心為什麼你的資料不在快取裡 (事實上就算你的 memcached server 都關掉,你的程式應該也要可以正常執行,只是比較慢而已),也許可能是 expire time 到了? 記憶體用完了? 被刪除了? 或是根本不存在? 這些都不重要。雖然有些例外,但基本上應該 SET 操作就可以應付絕大多數的情況。

參與討論

10 則留言

  1. 我看了幾篇文章, 裏面有提到 32bit OS 的限制, memory 只能用到 2G, 是否我裝了 win2008 server 64bit 版本, 就可以使用超過 2G 的 memory?

    ps. 我目前安裝的是 1.2.6 的 32bit memcacheD

  2. 自動引用通知: memcached info « My life

發佈留言

發表迴響