Clarifications about Redis and Memcached

Salvatore Sanfilippo

關於 Redis 與 Memcached 的釐清

如果你認識我,你就會知道我不是那種把競爭產品視為壞事的人。我其實很樂見使用者擁有多種選擇,所以我很少會做像拿 Redis 與其他技術做比較這類的事。

然而,要讓使用者挑選到合適的解決方案,他們也必須獲得正確的資訊,這同樣是事實。

這篇文章的起因,是我讀到 Mike Perham(麥克·佩勒姆)發表的一篇部落格文章,你可能知道他是熱門函式庫 Sidekiq 的作者,而該函式庫恰好以 Redis 作為後端。因此,我完全不會認為麥克·佩勒姆是「反對」Redis 的人。然而,在他發表於以下網址的部落格文章中:http://www.mikeperham.com/2015/09/24/storing-data-with-redis/,他表示,就快取而言,「你大概應該改用 Memcached(而非 Redis)」。所以,麥克·佩勒姆確實真心相信 Redis 不適合做快取,而他是這樣論證自己的主張的:

  1. Memcached 是為快取而設計的。
  2. 它完全不執行任何磁碟 I/O。
  3. 它是多執行緒的,能透過多核心擴展來處理數十萬次請求。

我將逐一回應上述說法,稍後也會提供一些未被上述論點涵蓋、而在我看來對大多數快取使用者與使用情境更為重要的進一步資訊。

Memcached 是為快取而設計的:這一點我就不多談了,因為它稱不上是論點。我也可以說「Redis 是為快取而設計的」。所以在這方面,兩者完全相同,我們直接看下一點。

它完全不執行任何磁碟 I/O:在 Redis 中,如果你願意,你也可以完全停用磁碟 I/O,從而獲得純粹的記憶體內體驗。除此之外,如果你真的有需要,也可以在即將重新啟動時才持久化資料庫,例如使用「SHUTDOWN SAVE」。這裡的重點在於,即使你完全不使用 Redis 的持久化功能,它本身仍是一項附加價值。

它是多執行緒的:這點是事實,而我的目標之一就是讓 Redis 的 I/O 具備執行緒化能力(就像 memcached 那樣,基本上資料存取本身並非多執行緒的)。然而,Redis 尤其是搭配 pipelining 時,每個執行緒每秒就能處理相當驚人的請求量(在非常密集的 pipelining 下,五十萬次是常見的數字。若不使用 pipelining,則約為每秒 100,000 次操作)。在最單純的快取情境中,每個 Redis 執行個體都相同、皆以 master 角色運作、停用磁碟操作,且分片由客戶端負責,就像「memcached 分片模型」那樣,在單一系統上啟動多個 Redis 行程並非什麼糟糕的做法。一旦這麼做,你得到的便是一個 shared-nothing 的多執行緒架構,因此關鍵在於單一執行緒能處理多少操作。上次我測試時,Redis 在每個執行緒上的速度至少與 memcached 相當。隨著時間推移,實作會不斷變化,因此如今的優勢可能在任何一方,但我敢說兩者的效能會相當接近,因為它們都傾向於最大化可用的資源。Memcached 的多執行緒仍是一項優勢,因為它讓使用與管理變得更簡單,但我認為它並非關鍵所在。

還有更多。麥克·佩勒姆談到每秒操作次數時,卻沒有提及操作的「品質」。重點在於,在像 Redis 和 Memcached 這樣的系統中,相較於實際操作記憶體內資料結構,命令分派與 I/O 的成本才是主導因素。所以基本上,在 Redis 中執行一個簡單的 GET、一個 SET,或是一個像 ZRANK 這樣的複雜操作,成本是大同小異的。但從應用程式層面的角度來看,一個複雜操作所能完成的工作要多得多。或許,與其抓取五個快取的值,你只需要傳送一小段 Lua 腳本。因此,這兩個系統實際上的「可擴展性」具有多個面向,而你能達成什麼正是其中之一。

在麥克·佩勒姆的顧慮中,我認為唯一有道理的是多執行緒這一點,而如果我們把 Redis 視為 memcached 替代品的特殊情況,這個問題可以透過執行多個行程來解決,或者乾脆只執行一個,因為要讓單一執行緒在執行類似 memcached 的操作時達到飽和是非常、非常困難的。

真正的差異

現在是時候來談談這兩個系統之間「真正的」差異了。

記憶體效率

這是 Memcached 過去優於 Redis 的地方。在一個被設計用來表示單純字串對字串字典的系統中,要更有效率地利用記憶體相對簡單。這項差異並非非常巨大,而且我大概已有五年沒再去驗證,但過去確實是顯而易見的。

然而,若我們考量長時間運行行程的記憶體效率,情況就有些不同了。請參閱下一節。

但話說回來,要真正評估記憶體效率,你也應該把 Redis 中經過特殊編碼的小型聚合值非常節省記憶體這點納入考量。例如,小整數的集合在內部是以 8、16、32 或 64 位元整數的陣列來表示,而且由於它們是有序的,當你想檢查某個元素是否存在時,可以用二分搜尋法以對數時間進行存取。

同樣的情況也發生在你使用 hashes 來表示物件、而非依賴 JSON 的時候。因此,真正的記憶體效率必須根據實際的使用情境來評估。

Redis LRU 與 Slab allocator(Slab 分配器)

從記憶體使用率的角度來看,Memcached 並非完美。如果你剛好有一個會隨著時間大幅改變快取值大小的應用程式,你很可能會遇到嚴重的碎片化,而唯一的解決辦法就是重新啟動。從這個角度來看,Redis 要確定性得多。

此外,Redis 的 LRU 最近已大幅改進,現在已非常接近真實的 LRU。更多資訊可參考:http://redis.io/topics/lru-cache。就我的理解,memcached 的 LRU 仍會依據其 Slab allocator 來過期,因此有時其行為可能與真正的 LRU 相去甚遠,不過我很樂意聽聽專家們對此的看法。如果你想測試 Redis 的 LRU,現在可以使用新版 Redis 中提供的 redis-cli LRU 測試模式來進行。

智慧快取

如果你想把 Redis 用於快取,卻以類似 memcached 的方式來使用,那你真的錯失了重點。在我看來,這是麥克·佩勒姆那篇部落格文章中最大的錯誤。越來越多人轉向 Redis,正是因為他們發現可以用更有用的方式來表示快取資料。想保留某個東西的最新 N 筆資料嗎?使用 capped list。想取得一個已快取的人氣指數嗎?使用 sorted set,諸如此類。

持久化與複寫

如果你需要這些功能,它們就是非常重要的資產。舉例來說,使用這種模型要擴展以應對龐大的讀取負載就非常簡單。具備持久化的重新啟動、能夠隨時間擷取快取快照等能力也是如此。不過,在某些使用情境中,這兩項功能完全無關緊要也是完全合理的。我在這裡想說的是,確實存在一些「純快取」的使用情境中,持久化與複寫是很重要的。

可觀測性

Redis 具有非常、非常高的可觀測性。它針對大量內部指標提供詳細的報告,你可以 SCAN 整個資料集、觀察物件的過期情況、調整 LRU 演算法、為客戶端命名並在 CLIENT LIST 中查看、使用「MONITOR」來除錯你的應用程式,以及許多其他進階功能。我認為這是一項優勢。

Lua 腳本

我認為 Lua 腳本在許多快取使用情境中能提供極大的幫助。舉例來說,如果你有一個已快取的 JSON blob,透過一個 Lua 命令,你就能擷取單一欄位並僅將其回傳給客戶端,而無需傳輸全部內容(從概念上來說,你也可以直接使用 Redis hashes 來表示物件以達到同樣的效果)。

結論

Memcached 是一套很棒的軟體,我曾多次閱讀它的原始碼,它在我們的產業中是一場革命,你應該檢視對你而言它相較於 Redis 是否是更好的選擇。然而,事情必須就其本質來評估,最終,讀到麥克·佩勒姆的報告以及這些年來非常類似的報告,讓我感到有些惱火。因此,我決定向你們展示我的觀點。如果你發現任何與事實不符之處,請告訴我,我會據此以「EDIT」段落來更新這篇部落格文章。

原文由 Salvatore Sanfilippo 發布

本文章由 muse-spark-1.2-contributor 進行翻譯