Redis 在許多公司中被廣泛用作緩存。單個 Redis 執行個體可以處理數百萬個讀/寫 qps,因為所有操作都在記憶體中。除了作為類似于memcached的簡單記憶體内緩存外,Redis還提供了許多其他功能。例如,在許多情況下,Redis 也可以用作持久存儲,因為 Redis 具有内置的 AOF(僅追加檔案)機制,可以啟用該機制以持久化對磁盤的每個寫入操作。AOF有兩種模式,可以在設定Redis時進行配置。
- 同步 AOF:同步 AOF 意味着每次寫入都需要在傳回到用戶端(也稱為。“阻止”寫入)。在這種模式下,Redis 不再是純粹的“記憶體内”存儲。同步 AOF 可確定持久性,但需要犧牲高寫入延遲。在我們的生産環境中,我們觀察到,平均而言,同步 AOF 對于單個寫入操作需要 ~1 秒。
- 異步 AOF:在此模式下,寫入是非阻塞的,這意味着 Redis 仍然可以被視為用戶端的“記憶體中”,隻是記憶體内資料在背景定期持久化到磁盤上,這對用戶端是透明的。此模式提供非常低的寫入延遲,因為它仍然是用戶端的記憶體,但需要權衡資料丢失。如果 Redis 節點發生故障或重新啟動,則寫入此 Redis 節點但尚未持久儲存到磁盤的資料将永久丢失。
Redis叢集
當資料量、讀/寫qps超過單個Redis執行個體的容量時,我們需要使用 Redis 叢集來存儲 kv 資料。在叢集中,密鑰分發到多個執行個體。通常,有三種方法可以将密鑰分片到多個執行個體:
用戶端分片
假設我們有一個廣告排名引擎,它本質上是一種推薦服務,可以從廣告資料庫中調用相關廣告,并為每個ad_request對廣告進行排名。排名引擎需要檢索每個廣告的實時出價才能執行廣告競價。由于業務對延遲的高度敏感性,所有廣告的實時出價都會被計算并預加載到 Redis 叢集中。廣告排名引擎在其二進制檔案中内置了 Redis 用戶端,以便從 Redis 讀取。
在用戶端分片中,Redis 用戶端包含分片和路由邏輯。換句話說,這是一個非常厚的用戶端。優點是此體系結構不依賴于任何中間件。隻有兩方:Redis 用戶端和 redis 節點。
使用集中式代理(也稱為中間件)的伺服器端分片
中間件用作代理。來自廣告排名引擎的請求将命中代理。代理包含分片和路由邏輯,用于确定通路哪個 Redis 執行個體來檢索資料。在這種情況下,redis 用戶端可以是一個非常瘦的用戶端,因為它不再包含分片或路由邏輯。
去中心化伺服器端分片
去中心化分片是官方 Redis 叢集中實際使用的。叢集中的每個節點都維護着“路由表”的本地副本,并通過gossip協定不斷更新其路由表。換句話說,不再有集中式代理。相反,每個 Redis 節點都包含充當代理所需的完整資訊集。
請求可以命中任何 Redis 節點。每個節點都知道叢集中的所有其他節點,包括網絡位址、存儲的密鑰等。處理請求的節點将首先檢查自身或其他節點是否具有請求的資料,如果資料存儲在其他地方,則将請求重定向到相應的節點。
分片算法
一緻的哈希
一緻哈希是一種經典的分片算法。本質上,所有事物,包括 Redis 叢集中的每個節點和每個資料記錄的鍵,都映射到 0 ~2³²-1(或 0 ~2⁶⁴-1)的哈希環。當用戶端想要檢索 k-v 記錄時,它首先計算密鑰的哈希,然後在哈希環上順時針行走以查找下一個 Redis 節點,并從該節點擷取密鑰。
添加/删除節點時,一緻哈希可確定隻有相鄰節點需要遷移資料,而叢集中的大多數其他節點不受影響。好處是叢集總體上保持穩定,這意味着服務堆棧延遲峰值的原因更少。
不過,一緻的哈希有一個很大的缺點:分片不平衡或偏度。通常,緩存群集可能隻有數十到數百個節點。在極端情況下,假設隻有 2 個節點,則可能大多數鍵和關聯值位于節點 A 上,而隻有少量位于節點 B 上。即使對于最初具有平衡分片的叢集,随着系統的發展,例如添加/删除節點、密鑰過期、添加新密鑰等,叢集最終也會出現不平衡的分片。
為了最大化分片平衡,常用的方法包括:
- 介紹微分片的概念。例如,每個實體節點都映射到 n 個虛拟節點。如果群集有 m 個節點,則總共會有 m*n 個虛拟節點。虛拟節點越多,資料分布在統計上就越平衡。
- 定期重新分片。例如,每月重新計算每個分片上的密鑰配置設定,并在所有節點上執行全局資料遷移,以使叢集恢複到平衡分片狀态。
Redis 叢集中使用的哈希槽分片
Redis 叢集沒有使用上面描述的一緻哈希。相反,使用哈希槽。密鑰空間中的所有鍵都按照公式散列到整數範圍 0 ~ 16383 中。每個節點負責存儲一組插槽,以及該插槽的關聯 k-v 對。slot = CRC16(key) & 16383
本質上,哈希槽是另一層抽象。它将資料記錄(KV 對)和節點解耦。每個節點隻需要知道應該在其上存儲哪些插槽。插槽編碼為位數組。此數組的大小位元組。換句話說,一個 2048 位元組的數組完全包含節點的資訊,以确定 16384 個插槽中的特定插槽是否存儲在其中。例如,對于插槽 1,節點隻需要檢查第二個位是否為 1(因為插槽索引從 0 16384/8 = 2048
擁有一層哈希槽也使新節點的添加/删除變得非常簡單。假設我們有一個由 3 個節點 A、B 和 C 組成的叢集。每個節點将存儲一系列插槽:
節點 A:插槽 0–5460
節點 B:插槽 5461–10922
節點 C:插槽 10923–16383
- 假設我們需要向群集添加新的節點 D。在這種情況下,來自 A、B 和 C 的一些插槽将被移動到 D.即插槽及其關聯的鍵和值是不可分割的,并且在跨節點移動時顯示為原子單元。
- 同樣,當我們從叢集中删除節點 A 時,隻有 A 中的插槽應該遷移到 B 和 C。然後可以安全地删除節點 A。
總之,将哈希槽從一個節點移動到另一個節點不需要群集停止操作、添加和删除節點或更改節點持有的哈希槽百分比,不需要任何群集停機時間。
分片架構
原生 Redis 叢集
原生叢集的實作方式與上述算法完全相同。叢集中內建了路由/分片、叢集拓撲中繼資料、執行個體健康監控等所有功能。沒有其他依賴項。執行個體使用八卦互相通信。
本機叢集通常可以支援 ~300 - 400 個執行個體。每個執行個體可以處理 80K 讀取 QPS,叢集總共可以處理 20-3000 萬個 QPS。
但是,如果需要處理更高的 QPS,那麼在超過 400 個執行個體時添加更多 Redis 執行個體不再是一個好主意。原因是 Gossip 協定的資源占用也随着叢集中執行個體數量的增加而迅速增加。當系統需要進一步擴充時,其他體系結構的使用範圍會更廣泛。
Twemproxy + 原生 Redis 叢集
下圖顯示了Twemproxy在多個Redis執行個體環境中工作的基本架構。
Twemproxy 是上述使用集中式代理的伺服器端分片的一個例子。Twemproxy是由Twitter開源的。Twemproxy 可以接受來自多個用戶端服務的請求,并将請求定向到底層 Redis 節點,等待響應,然後直接響應用戶端。Twemproxy還支援一組有用的功能:
- 自動删除失敗的 Redis 節點
- 支援标簽。例如,如果我們想確定一組鍵被哈希到同一個 Redis 節點執行個體,我們可以為這些鍵配置設定相同的主題标簽。
- 支援多種雜湊演算法。
Twemproxy 可以自定義以使用原生 Redis 叢集,如圖所示:
在這種情況下,Twemproxy 不再處理路由/分片,而是用于存儲中繼資料,例如進階 Redis 叢集拓撲、通路控制清單,并可用于監控衆所周知會導緻原生 Redis 叢集出現問題的熱鍵、大鍵等。分片/路由仍由底層原生 Redis 叢集處理。換句話說,Twemproxy 節點保持與所有 Redis 節點的連接配接,并且可以向任何底層 Redis 節點發送請求,并且 Redis 節點将在需要時處理到叢集中另一個 Redis 節點的重新路由。
AWS ElasticCache就是以這種方式建構的。ElasticCache 由一個代理伺服器和一個支援主副本複制的 Redis 叢集組成,其中主叢集主要用于寫入,副本用于讀取。
使用 Codis 進行集中式分片
Codis引入了群體的概念。每個組包含一個 Redis 主節點(Redis 主節點)和 1 到多個 Redis 副本節點(Redis 從節點)。如果主節點死亡,則可以将副本節點提升為新的主節點。
Codis還使用了預分片機制,類似于原生Redis叢集中使用的哈希槽。所有密鑰都分布到 1024 個插槽,這意味着總共可以有多達 1024個組。路由資訊(即中繼資料)存儲在強一緻性資料存儲中,例如 Etcd 或 Zookeeper。
每個 Redis 組映射到一個插槽範圍,例如 0~127。映射資訊保留在 Zookeeper 中。在請求處理路徑中,首先使用 計算哈希槽,然後代理使用 Zookeeper 中的slot_id檢索 Redis 組的位址。crc32(key) % 1024
Codis 與 Twemproxy + Redis 叢集之間有兩個主要差別:
- 在Codis中,代理是無狀态的。它不再存儲有關底層 Redis 節點的任何狀态。相反,所有這些中繼資料的存儲都委托給Zookeeper。由于所有的路由/分片功能都是由代理和 Zookeeper 處理的,每個 Redis 節點隻存儲 KV 對,不需要通過八卦與其他 Redis 節點通信。
- 相反,當使用 Twemproxy + Redis 叢集時,此類中繼資料存儲在原生 Redis 叢集中,即在每個 Redis 節點上。
熱鍵和大鍵問題
熱鍵問題
熱鍵非常常見,特别是對于基于内容的服務,例如Youtube,抖音,Twitter等。一組密鑰(有時是單個密鑰)可能吸引了大部分使用者流量。例如,在抖音上,前5-10%的内容産生了90%的流量。
熱鍵對 Redis 叢集的影響在于,流量可能隻集中在幾個 Redis 執行個體上,有時隻集中在一個存儲熱鍵的 Redis 執行個體上。是以,這些不幸的執行個體會變得高度過載,而叢集中的大多數其他執行個體負載非常輕。簡而言之,熱鍵可能會破壞擁有叢集的目的。
主要有三種方法通常用于處理緩存中的熱鍵:
- 在用戶端使用本地緩存。當熱鍵緩存在 Redis 用戶端時,伺服器端根本沒有流量來請求帶有熱鍵的記錄。LFU(最不常用)通常在用戶端緩存中用作逐出算法。當緩存達到容量時,LFU 會逐出最不常用的密鑰,進而確定熱密鑰保留在緩存中。但是,由于用戶端緩存本質上是另一層緩存,是以遠端緩存(Redis 叢集)将存在緩存一緻性問題。
- 人為地将熱鍵“分片”為許多鍵。我們可以在熱鍵後附加或預置一個随機數。假設我們有一個熱鍵字元串(Redis 大多使用字元串作為鍵,而不是整數),那麼在 Redis 端,我們将首先通過字首 1-100 來生成 100 個鍵,這給了我們,,, ...,,換句話說,熱鍵被人為複制了 100 次。它們将映射到多個哈希槽,并将存儲在許多不同的 Redis 執行個體上。然後在查詢時,每當用戶端請求此熱鍵時,我們都會在熱鍵前面附加一個 [1, 100] 範圍内的随機數。可以在代理伺服器上完成預置邏輯。通過這種方式,熱鍵流量将分發到叢集中的更多執行個體,而不是集中在幾個甚至單個執行個體上。xoxogossipgirlxoxogossipgirl1xoxogossipgirl2xoxogossipgirl3xoxogossipgirl100xoxogossipgirlxoxogossipgirl
- 單獨讀取和寫入。對于内容應用,通常熱鍵的讀取QPS非常高,而熱鍵的寫入QPS隻是平均水準。還記得我們之前提到的小組嗎?我們可以配置 Redis 組,以便主執行個體僅接收寫入流量,而其他跟随執行個體處理讀取流量。我們可以有一個寫入副本,但隻讀副本需要的數量。
大鍵問題
大鍵表示 KV 對中的值需要大于平均記憶體空間來存儲。大鍵可能與熱鍵相關聯,也可能并不總是與熱鍵相關聯。例如,在抖音上,熱門圖檔通常會有更多的評論。假設在我們的 Redis 中,鍵是content_id,值是所有評論的清單。在這種情況下,熱鍵也是一個大鍵。
大鍵的存在會導緻偏斜。存儲大鍵的執行個體将具有異常高的記憶體使用率,并且可能會因 OOM 而失敗。這種故障很容易導緻 Domino 效應,并使整個叢集癱瘓。特别是對于具有不太智能的自動故障轉移的叢集,OOM-ed 執行個體的大鍵可能會遷移到另一個執行個體,并快速 OOM 并終止該執行個體。然後遷移到另一個執行個體,也殺死它,直到整個叢集關閉。
如何減輕大鍵的影響?你可以猜到,隻需“分片”鍵!
- 對于簡單的鍵值類型,我們可以稍微重新設計一下資料模式。Redis 支援具有鍵-子鍵-值的哈希結構。換句話說,假設值是一個包含數百個字段的結構,我們可以将其劃分為多個,其中這裡的每個值都是早期大值結構的小分片,例如少于 10 個字段。Redis 原生支援修改指令,無需更改vanillacommand 中的
key-giantValueStructsubkey-smallValueStructhget/hsetsmallValueStructgiantValueStructget/set
- 如果使用 Redis Hash、Redis Set 後,仍然有大鍵怎麼辦?我們可以添加另一層哈希桶來進一步分片鍵子鍵。例如,對于普通的 Redis 哈希,我們确實
hget(hashKey, subkey)
hset(hashKey, subkey, value)
要對大鍵進行分片,類似于熱鍵問題中的選項 2,我們可以做
newHashKey = hashKey + (hash(field) % 10000);
hset(newHashKey, subkey, value);
hget(newHashKey, subkey);
- 如果您預計會出現較大的關鍵問題,要避免使用 Zset,因為 Zset 包含排名的分數資訊。
好了,有關Redis 叢集的分片算法和架構,就介紹到這裡,如果大家有什麼好的想法,可以在評論區留言,共同交流。