1. 程式人生 > 其它 >redis_09 _ 切片叢集:資料增多了,是該加記憶體還是加例項

redis_09 _ 切片叢集:資料增多了,是該加記憶體還是加例項

我曾遇到過這麼一個需求:要用Redis儲存5000萬個鍵值對,每個鍵值對大約是512B,為了能快速部署並對外提供服務,我們採用雲主機來執行Redis例項,那麼,該如何選擇雲主機的記憶體容量呢?

我粗略地計算了一下,這些鍵值對所佔的記憶體空間大約是25GB(5000萬*512B)。所以,當時,我想到的第一個方案就是:選擇一臺32GB記憶體的雲主機來部署Redis。因為32GB的記憶體能儲存所有資料,而且還留有7GB,可以保證系統的正常執行。同時,我還採用RDB對資料做持久化,以確保Redis例項故障後,還能從RDB恢復資料。

但是,在使用的過程中,我發現,Redis的響應有時會非常慢。後來,我們使用INFO命令檢視Redis的latest_fork_usec指標值(表示最近一次fork的耗時),結果顯示這個指標值特別高,快到秒級別了。

這跟Redis的持久化機制有關係。在使用RDB進行持久化時,Redis會fork子程序來完成,fork操作的用時和Redis的資料量是正相關的,而fork在執行時會阻塞主執行緒。資料量越大,fork操作造成的主執行緒阻塞的時間越長。所以,在使用RDB對25GB的資料進行持久化時,資料量較大,後臺執行的子程序在fork建立時阻塞了主執行緒,於是就導致Redis響應變慢了。

看來,第一個方案顯然是不可行的,我們必須要尋找其他的方案。這個時候,我們注意到了Redis的切片叢集。雖然組建切片叢集比較麻煩,但是它可以儲存大量資料,而且對Redis主執行緒的阻塞影響較小。

切片叢集,也叫分片叢集,就是指啟動多個Redis例項組成一個叢集,然後按照一定的規則,把收到的資料劃分成多份,每一份用一個例項來儲存。回到我們剛剛的場景中,如果把25GB的資料平均分成5份(當然,也可以不做均分),使用5個例項來儲存,每個例項只需要儲存5GB資料。如下圖所示:

那麼,在切片叢集中,例項在為5GB資料生成RDB時,資料量就小了很多,fork子程序一般不會給主執行緒帶來較長時間的阻塞。採用多個例項儲存資料切片後,我們既能儲存25GB資料,又避免了fork子程序阻塞主執行緒而導致的響應突然變慢。

在實際應用Redis時,隨著使用者或業務規模的擴充套件,儲存大量資料的情況通常是無法避免的。而切片叢集,就是一個非常好的解決方案。這節課,我們就來學習一下。

如何儲存更多資料?

在剛剛的案例裡,為了儲存大量資料,我們使用了大記憶體雲主機和切片叢集兩種方法。實際上,這兩種方法分別對應著Redis應對資料量增多的兩種方案:縱向擴充套件(scale up)和橫向擴充套件(scale out)。

  • 縱向擴充套件:升級單個Redis例項的資源配置,包括增加記憶體容量、增加磁碟容量、使用更高配置的CPU。就像下圖中,原來的例項記憶體是8GB,硬碟是50GB,縱向擴充套件後,記憶體增加到24GB,磁碟增加到150GB。
  • 橫向擴充套件:橫向增加當前Redis例項的個數,就像下圖中,原來使用1個8GB記憶體、50GB磁碟的例項,現在使用三個相同配置的例項。

那麼,這兩種方式的優缺點分別是什麼呢?

首先,縱向擴充套件的好處是,實施起來簡單、直接。不過,這個方案也面臨兩個潛在的問題。

第一個問題是,當使用RDB對資料進行持久化時,如果資料量增加,需要的記憶體也會增加,主執行緒fork子程序時就可能會阻塞(比如剛剛的例子中的情況)。不過,如果你不要求持久化儲存Redis資料,那麼,縱向擴充套件會是一個不錯的選擇。

不過,這時,你還要面對第二個問題:縱向擴充套件會受到硬體和成本的限制。這很容易理解,畢竟,把記憶體從32GB擴充套件到64GB還算容易,但是,要想擴充到1TB,就會面臨硬體容量和成本上的限制了。

與縱向擴充套件相比,橫向擴充套件是一個擴充套件性更好的方案。這是因為,要想儲存更多的資料,採用這種方案的話,只用增加Redis的例項個數就行了,不用擔心單個例項的硬體和成本限制。在面向百萬、千萬級別的使用者規模時,橫向擴充套件的Redis切片叢集會是一個非常好的選擇

不過,在只使用單個例項的時候,資料存在哪兒,客戶端訪問哪兒,都是非常明確的,但是,切片叢集不可避免地涉及到多個例項的分散式管理問題。要想把切片叢集用起來,我們就需要解決兩大問題:

  • 資料切片後,在多個例項之間如何分佈?
  • 客戶端怎麼確定想要訪問的資料在哪個例項上?

接下來,我們就一個個地解決。

資料切片和例項的對應分佈關係

在切片叢集中,資料需要分佈在不同例項上,那麼,資料和例項之間如何對應呢?這就和接下來我要講的Redis Cluster方案有關了。不過,我們要先弄明白切片叢集和Redis Cluster的聯絡與區別。

實際上,切片叢集是一種儲存大量資料的通用機制,這個機制可以有不同的實現方案。在Redis 3.0之前,官方並沒有針對切片叢集提供具體的方案。從3.0開始,官方提供了一個名為Redis Cluster的方案,用於實現切片叢集。Redis Cluster方案中就規定了資料和例項的對應規則。

具體來說,Redis Cluster方案採用雜湊槽(Hash Slot,接下來我會直接稱之為Slot),來處理資料和例項之間的對映關係。在Redis Cluster方案中,一個切片叢集共有16384個雜湊槽,這些雜湊槽類似於資料分割槽,每個鍵值對都會根據它的key,被對映到一個雜湊槽中。

具體的對映過程分為兩大步:首先根據鍵值對的key,按照CRC16演算法計算一個16 bit的值;然後,再用這個16bit值對16384取模,得到0~16383範圍內的模數,每個模數代表一個相應編號的雜湊槽。關於CRC16演算法,不是這節課的重點,你簡單看下連結中的資料就可以了。

那麼,這些雜湊槽又是如何被對映到具體的Redis例項上的呢?

我們在部署Redis Cluster方案時,可以使用cluster create命令建立叢集,此時,Redis會自動把這些槽平均分佈在叢集例項上。例如,如果叢集中有N個例項,那麼,每個例項上的槽個數為16384/N個。

當然, 我們也可以使用cluster meet命令手動建立例項間的連線,形成叢集,再使用cluster addslots命令,指定每個例項上的雜湊槽個數。

舉個例子,假設叢集中不同Redis例項的記憶體大小配置不一,如果把雜湊槽均分在各個例項上,在儲存相同數量的鍵值對時,和記憶體大的例項相比,記憶體小的例項就會有更大的容量壓力。遇到這種情況時,你可以根據不同例項的資源配置情況,使用cluster addslots命令手動分配雜湊槽。

為了便於你理解,我畫一張示意圖來解釋一下,資料、雜湊槽、例項這三者的對映分佈情況。

示意圖中的切片叢集一共有3個例項,同時假設有5個雜湊槽,我們首先可以通過下面的命令手動分配雜湊槽:例項1儲存雜湊槽0和1,例項2儲存雜湊槽2和3,例項3儲存雜湊槽4。

redis-cli -h 172.16.19.3 –p 6379 cluster addslots 0,1
redis-cli -h 172.16.19.4 –p 6379 cluster addslots 2,3
redis-cli -h 172.16.19.5 –p 6379 cluster addslots 4

在叢集執行的過程中,key1和key2計算完CRC16值後,對雜湊槽總個數5取模,再根據各自的模數結果,就可以被對映到對應的例項1和例項3上了。

另外,我再給你一個小提醒,在手動分配雜湊槽時,需要把16384個槽都分配完,否則Redis叢集無法正常工作

好了,通過雜湊槽,切片叢集就實現了資料到雜湊槽、雜湊槽再到例項的分配。但是,即使例項有了雜湊槽的對映資訊,客戶端又是怎麼知道要訪問的資料在哪個例項上呢?接下來,我就來和你聊聊。

客戶端如何定位資料?

在定位鍵值對資料時,它所處的雜湊槽是可以通過計算得到的,這個計算可以在客戶端傳送請求時來執行。但是,要進一步定位到例項,還需要知道雜湊槽分佈在哪個例項上。

一般來說,客戶端和叢集例項建立連線後,例項就會把雜湊槽的分配資訊發給客戶端。但是,在叢集剛剛建立的時候,每個例項只知道自己被分配了哪些雜湊槽,是不知道其他例項擁有的雜湊槽資訊的。

那麼,客戶端為什麼可以在訪問任何一個例項時,都能獲得所有的雜湊槽資訊呢?這是因為,Redis例項會把自己的雜湊槽資訊發給和它相連線的其它例項,來完成雜湊槽分配資訊的擴散。當例項之間相互連線後,每個例項就有所有雜湊槽的對映關係了。

客戶端收到雜湊槽資訊後,會把雜湊槽資訊快取在本地。當客戶端請求鍵值對時,會先計算鍵所對應的雜湊槽,然後就可以給相應的例項傳送請求了。

但是,在叢集中,例項和雜湊槽的對應關係並不是一成不變的,最常見的變化有兩個:

  • 在叢集中,例項有新增或刪除,Redis需要重新分配雜湊槽;
  • 為了負載均衡,Redis需要把雜湊槽在所有例項上重新分佈一遍。

此時,例項之間還可以通過相互傳遞訊息,獲得最新的雜湊槽分配資訊,但是,客戶端是無法主動感知這些變化的。這就會導致,它快取的分配資訊和最新的分配資訊就不一致了,那該怎麼辦呢?

Redis Cluster方案提供了一種重定向機制,所謂的“重定向”,就是指,客戶端給一個例項傳送資料讀寫操作時,這個例項上並沒有相應的資料,客戶端要再給一個新例項傳送操作命令。

那客戶端又是怎麼知道重定向時的新例項的訪問地址呢?當客戶端把一個鍵值對的操作請求發給一個例項時,如果這個例項上並沒有這個鍵值對對映的雜湊槽,那麼,這個例項就會給客戶端返回下面的MOVED命令響應結果,這個結果中就包含了新例項的訪問地址。

GET hello:key
(error) MOVED 13320 172.16.19.5:6379

其中,MOVED命令表示,客戶端請求的鍵值對所在的雜湊槽13320,實際是在172.16.19.5這個例項上。通過返回的MOVED命令,就相當於把雜湊槽所在的新例項的資訊告訴給客戶端了。這樣一來,客戶端就可以直接和172.16.19.5連線,併發送操作請求了。

我畫一張圖來說明一下,MOVED重定向命令的使用方法。可以看到,由於負載均衡,Slot 2中的資料已經從例項2遷移到了例項3,但是,客戶端快取仍然記錄著“Slot 2在例項2”的資訊,所以會給例項2傳送命令。例項2給客戶端返回一條MOVED命令,把Slot 2的最新位置(也就是在例項3上),返回給客戶端,客戶端就會再次向例項3傳送請求,同時還會更新本地快取,把Slot 2與例項的對應關係更新過來。

需要注意的是,在上圖中,當客戶端給例項2傳送命令時,Slot 2中的資料已經全部遷移到了例項3。在實際應用時,如果Slot 2中的資料比較多,就可能會出現一種情況:客戶端向例項2傳送請求,但此時,Slot 2中的資料只有一部分遷移到了例項3,還有部分資料沒有遷移。在這種遷移部分完成的情況下,客戶端就會收到一條ASK報錯資訊,如下所示:

GET hello:key
(error) ASK 13320 172.16.19.5:6379

這個結果中的ASK命令就表示,客戶端請求的鍵值對所在的雜湊槽13320,在172.16.19.5這個例項上,但是這個雜湊槽正在遷移。此時,客戶端需要先給172.16.19.5這個例項傳送一個ASKING命令。這個命令的意思是,讓這個例項允許執行客戶端接下來發送的命令。然後,客戶端再向這個例項傳送GET命令,以讀取資料。

看起來好像有點複雜,我再借助圖片來解釋一下。

在下圖中,Slot 2正在從例項2往例項3遷移,key1和key2已經遷移過去,key3和key4還在例項2。客戶端向例項2請求key2後,就會收到例項2返回的ASK命令。

ASK命令表示兩層含義:第一,表明Slot資料還在遷移中;第二,ASK命令把客戶端所請求資料的最新例項地址返回給客戶端,此時,客戶端需要給例項3傳送ASKING命令,然後再發送操作命令。

和MOVED命令不同,ASK命令並不會更新客戶端快取的雜湊槽分配資訊。所以,在上圖中,如果客戶端再次請求Slot 2中的資料,它還是會給例項2傳送請求。這也就是說,ASK命令的作用只是讓客戶端能給新例項傳送一次請求,而不像MOVED命令那樣,會更改本地快取,讓後續所有命令都發往新例項。

小結

這節課,我們學習了切片叢集在儲存大量資料方面的優勢,以及基於雜湊槽的資料分佈機制和客戶端定位鍵值對的方法。

在應對資料量擴容時,雖然增加記憶體這種縱向擴充套件的方法簡單直接,但是會造成資料庫的記憶體過大,導致效能變慢。Redis切片叢集提供了橫向擴充套件的模式,也就是使用多個例項,並給每個例項配置一定數量的雜湊槽,資料可以通過鍵的雜湊值對映到雜湊槽,再通過雜湊槽分散儲存到不同的例項上。這樣做的好處是擴充套件性好,不管有多少資料,切片叢集都能應對。

另外,叢集的例項增減,或者是為了實現負載均衡而進行的資料重新分佈,會導致雜湊槽和例項的對映關係發生變化,客戶端傳送請求時,會收到命令執行報錯資訊。瞭解了MOVED和ASK命令,你就不會為這類報錯而頭疼了。

我剛剛說過,在Redis 3.0 之前,Redis官方並沒有提供切片叢集方案,但是,其實當時業界已經有了一些切片叢集的方案,例如基於客戶端分割槽的ShardedJedis,基於代理的Codis、Twemproxy等。這些方案的應用早於Redis Cluster方案,在支撐的叢集例項規模、叢集穩定性、客戶端友好性方面也都有著各自的優勢,我會在後面的課程中,專門和你聊聊這些方案的實現機制,以及實踐經驗。這樣一來,當你再碰到業務發展帶來的資料量巨大的難題時,就可以根據這些方案的特點,選擇合適的方案實現切片叢集,以應對業務需求了。

每課一問

按照慣例,給你提一個小問題:Redis Cluster方案通過雜湊槽的方式把鍵值對分配到不同的例項上,這個過程需要對鍵值對的key做CRC計算,然後再和雜湊槽做對映,這樣做有什麼好處嗎?如果用一個表直接把鍵值對和例項的對應關係記錄下來(例如鍵值對1在例項2上,鍵值對2在例項1上),這樣就不用計算key和雜湊槽的對應關係了,只用查表就行了,Redis為什麼不這麼做呢?

歡迎你在留言區暢所欲言,如果你覺得有收穫,也希望你能幫我把今天的內容分享給你的朋友,幫助更多人解決切片叢集的問題。