Clarifications about Redis and Memcached

Salvatore Sanfilippo

關於 Redis 與 Memcached 的釐清

原文由 Salvatore Sanfilippo 發布,訂閱此部落格

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

但要能選對解決方案,使用者也必須獲得正確的資訊,這點同樣是事實。

這篇文章的起因,是我讀了 Mike Perham 發表的一篇部落格文章,你可能知道他是熱門函式庫 Sidekiq 的作者,而 Sidekiq 剛好就是以 Redis 作為後端。所以我完全不會把 Mike 看成是那種「反對」Redis 的人。然而,在他發表於 http://www.mikeperham.com/2015/09/24/storing-data-with-redis/ 的那篇文章中,他提到,就快取而言,「你大概還是該用 Memcached 而不是 [Redis]」。所以,Mike 真的是打從心裡認為 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 的話,大約是每秒十萬次操作)。在那種最單純的快取情境下——每個 Redis 實例都一樣、都作為 master 運作、關掉磁碟操作、而分片由客戶端負責,就像「memcached 分片模型」那樣——在同一台機器上跑多個 Redis 行程並不算糟。一旦這麼做,你得到的其實就是一個 shared-nothing 的多執行緒架構,這時關鍵就在於單一執行緒能處理多少操作。上次我測試時,單執行緒的 Redis 至少和 memcached 一樣快。實作會隨時間改變,所以現在誰快一點很難說,但我敢說兩者的效能應該很接近,因為它們都傾向把可用資源用到極限。Memcached 的多執行緒仍然是一項優勢,因為它讓使用與管理更簡單,但我認為這並非關鍵所在。

還有更多。Mike 只談每秒操作數,卻沒有提到操作的*品質*。重點在於,像 Redis 和 Memcached 這樣的系統,指令分派與 I/O 的成本,遠比實際去碰記憶體中的資料結構來得高。所以基本上,在 Redis 裡執行一個簡單的 GET、一個 SET,或是一個像 ZRANK 這樣複雜的操作,成本是差不多的。但從應用程式層的角度來看,一個複雜操作能完成的工作要多得多。也許你本來要抓五個快取值,現在只要送一個小小的 Lua 腳本就能搞定。所以,這兩套系統真正的「可擴展性」有很多面向,而你能完成多少事,就是其中之一。

在 Mike 提出的顧慮中,我認為唯一站得住腳的就是多執行緒這點,而如果我們把 Redis 當成 memcached 替代品這個特例來看,這個問題可以透過執行多個行程來解決,或者乾脆只跑一個就行了——因為要用類似 memcached 的操作去塞爆單一執行緒,是非常非常困難的事。

真正的差異

現在來談談兩套系統之間*真正*的差異。

記憶體使用效率

這是 Memcached 過去比 Redis 好的地方。在一個被設計成單純字串對字串字典的系統裡,要更有效率地利用記憶體相對簡單。這個差距並沒有很誇張,而且我大概有五年沒再比較過了,但以前確實是看得出差別的。

不過,如果我們考慮的是長時間運行行程的記憶體使用效率,情況就有點不同了。請看下一節。

但話說回來,要真正評估記憶體使用效率,你也得把 Redis 對小型聚合值所做的特殊編碼考慮進去,它們非常省記憶體。舉例來說,由小整數組成的集合,在內部會以 8、16、32 或 64 位元整數的陣列來表示,而且因為是有序的,要檢查某個元素是否存在時可以用二分搜尋,以對數時間完成。

同樣的情況也發生在你用 hash 來表示物件、而不是用 JSON 的時候。所以,真正的記憶體使用效率,必須針對具體的使用情境來評估。

Redis LRU 與 Slab Allocator

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

此外,Redis 的 LRU 最近已經大幅改進,現在已經非常接近真正的 LRU。更多資訊可以在這裡找到:http://redis.io/topics/lru-cache。據我理解,memcached 的 LRU 仍會依照其 slab allocator 來過期,所以有時行為可能與真正的 LRU 相去甚遠,不過這點我也想聽聽專家們的看法。如果你想測試 Redis 的 LRU,現在可以用新版 Redis 中 redis-cli 提供的 LRU 測試模式來做。

智慧快取

如果你想把 Redis 拿來當快取,卻用得像 memcached 一樣,那你就真的錯過重點了。在我看來,這是 Mike 那篇文章中最大的誤解。越來越多人轉向 Redis,正是因為他們發現可以用更有用的方式來表示快取資料。想保留最新的 N 筆資料?用 capped list。想做一個快取的熱門程度排行?用 sorted set,以此類推。

持久化與複寫

如果你需要這些功能,它們就是非常重要的資產。舉例來說,用這種模式要擴展龐大的讀取負載就非常簡單。搭配持久化來重新啟動、能夠隨時間對快取做快照等等,也是同樣的道理。但當然,也完全有那種這兩項功能都無關緊要的使用情境。我想說的是,即使是「純快取」的使用情境中,也存在著持久化與複寫很重要的情況。

可觀察性

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

Lua 腳本

我認為 Lua 腳本在許多快取使用情境中提供了極大的幫助。舉例來說,如果你有一筆快取的 JSON 資料,透過一個 Lua 指令,你就可以只擷取其中一個欄位並回傳給客戶端,而不需要傳輸整份資料(概念上,你也可以直接用 Redis hash 來表示物件,達到同樣的效果)。

結論

Memcached 是一套很棒的軟體,我把它的原始碼讀過好幾遍,它在我們這個產業中是一場革命,你應該好好評估對你而言它是不是比 Redis 更好的選擇。不過,事情還是得就事論事,這些年來讀到 Mike 那篇以及許多類似的報告,最終讓我有點惱火,所以我決定來分享我的觀點。如果你發現任何與事實不符的地方,請告訴我,我會以「EDIT」段落的形式更新這篇文章。

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

留言