Redis Cluster 3.0

郭冰冰發表於2020-12-12

目錄

一:叢集架構

二:Client 請求重定向

三:叢集節點通訊

四:叢集高可用&主備切換

五:衡量分散式系統指標(在此指叢集)

可用性

可擴充套件性

一致性

六:叢集不可用條件

七:擴容和縮容

擴容

縮容

八:目前市面上redis叢集的架構

直連cluster(直連架構)

Codis架構(代理架構)

Twemproxy架構(代理架構)

九:分散式架構擴充

分散式系統中的資料分佈方式

hash分割槽(該資料分佈就是redis cluster3.0的資料分佈實現,一種非常常見的資料分佈方式)

range分割槽

資料量分割槽

副本的資料分佈

一致性hash

本地化計算資料分佈

工作中常見的應用場景

十:思考



一:叢集架構

直接上redis cluster架構圖(理解叢集的重點在於理解其分散式架構以及其中分散式原理)

之前講過哨兵,在此直接丟擲一個觀點:redis cluster = replication + sentinal

官方3.0叢集擁有以下特點:

1、所有的redis節點彼此互聯(PING-PONG機制),內部使用二進位制協議優化傳輸速度和頻寬(gossip通訊)。

2、節點的fail是通過叢集中超過半數的節點檢測失效時才生效。

3、客戶端與redis節點直連,不需要中間proxy層.客戶端不需要連線叢集所有節點,連線叢集中任何一個可用節點即可。

4、redis-cluster把所有的物理節點對映到[0-16383]slot上(不一定是平均分配),cluster 負責維護node<->slot<->value。

5、Redis叢集預分好16384個桶,當需要在 Redis 叢集中放置一個 key-value 時,根據 CRC16(key) mod 16384的值,決定將一個key放到哪個桶中。

6.去中心化,去中心化指的上述圖片中節點平等無主次之分,中心化和去中心化也是分散式思想中一個非常重要的理論。

7.生產環境中,每個藍色節點下面會掛1~2個slave,便於高可用,寫資料是通過master寫入,非同步同步到slave節點之上。

8.叢集至少3節點,過半可用,raft選舉。

9.沒有沒有沒有使用哨兵,但是還是使用哨兵的那一套原理實現高可用。

二:Client 請求重定向

首先,在此丟擲來一個問題,client怎麼才能知道我的這個請求該打在哪個節點上面?

假設我們有三臺節點,分別為a,b,c,其中a負責0~5000的slot,b負責5001~10000slot,c負責10001~16383的slot。

如果一個請求的key所對應的slot是6000,隸屬於節點是b管轄,client怎麼才能知道我的請求應該打在b節點上面,在此涉及到client請求的重定向,首先client在剛一開始並不知道這個請求該打在b節點上,client會隨機打在a,b,c其中的一個節點上,如果打在b節點上,那麼b節點發現這個key所對應的slot是資料自己管轄範圍,直接處理掉,如果隨機打在了a節點上,a發現這個key對應的slot不是自己負責的,會像client返回一個moved錯誤,該資訊中包括了該key負責的節點ip資訊,然後client通過該ip把請求重定向到該節點上。

在此,我們再丟擲來一個問題,a節點怎麼知道這個key所對應的slot隸屬於b管轄?帶著這個問題,我們去看叢集中的節點通訊。

三:叢集節點通訊

還是直接先丟擲來一個問題:redis叢集有沒有後設資料節點,比如:hdfs叢集有namenode後設資料管理,kafka有zookeeper後設資料管理,什麼是後設資料,好比叢集中的節點資訊,叢集中節點所負責的slot。

redis通過gossip協議去進行資料的交換,如節點所負責的slot,節點對應的ip和port,該交換是陸陸續續的,不是集中式的管理,通過這種手段也是可以讓節點中互相知道彼此的資料資訊,通過這種資訊交換方式,當然這種方式也有一定的弊端,一是同步時間不確定,二是gossip協議在叢集過大會資訊交換傳遞壓力,這也是redis cluster為什麼把slot數量設定成16384的原因,到現在,我們可以解答上面提出來的“a節點怎麼知道這個key所對應的slot隸屬於b管轄?”的問題。

使用後設資料節點有什麼弊端?

1.後設資料節點容易形成單點,需要做高可用,否則,一旦後設資料節點掛掉,會直接導致該叢集不可用。

2.後設資料節點資料比較大的時候,會對後設資料節點造成壓力,比如hdfs的namenode會在檔案快過小導致後設資料龐大的時候對後設資料節點產生大的壓力,hdfs官方也已經在優化namenode壓力過大的導致的問題了。

四:叢集高可用&主備切換

之前說過redis叢集使用redis哨兵去做叢集的高可用,官方的3.0 cluster並沒有使用哨兵去做高可用的主備切換。

回看我們一開始給出來的redis的架構圖,圖中藍色的節點都是主節點,該節點下面在生產環境中會有若干個slave節點,當master掛掉之後,slave切換成master繼續提供服務。

說一下master選舉流程:和哨兵選舉幾乎是一摸一樣,名副其實的換湯不換藥,只不過原本由哨兵的選舉變成了叢集中的master節點。

還是有主觀當機和客觀當機不過不再由哨兵去管理,而是叢集中的節點,和哨兵幾乎一致,不再重複。

確定客觀當機後開始master的選舉,流程如下:

一:檢查slave和master斷開的時間,超時的沒有資格成為master節點。

二:優先順序(slave priority)越高,越優先成為master

三:其次是offset越大的,就是同步資料最多的,最先成為master

四:再其次就是runid最小的(最先啟動的)成為master。

依然是raft選舉,其實好多分散式系統的設計都離不開raft,redis/etcd/zookeeper 其核心原理都是raft,設計raft目的就是為了簡化paxos的難以理解,更方便學術界和工業界的使用,附上raft漢化論文,還是比較容易理解的,之所以比paxos容易理解是因為作者把raft分成了leader選舉,日誌複製,安全,成員變更幾個模組。

注:哨兵其實就是raft中的leader選舉模組的一個實現。

五:衡量分散式系統指標(在此指叢集)

衡量分散式系統指標還是離不開cap理論,還是要圍繞著cap去衡量。

可用性

叢集擁有主備切換功能,當master掛掉之後,由的slave充當新的master,繼續提供服務,master掛掉後,無slave,該節點管轄的的slot資料失效。

可擴充套件性

可擴充套件性指的當使用者量上來之後,伴隨而到的就是叢集規模的擴大,redis cluster中如果新增節點,就要考慮到新增節點帶來的資料遷移複雜度以及其間的遷移時候的叢集可用性,redis cluster做的比較簡單明瞭:當你新增一些例項的時候,只需要將一部分槽位遷移到新的例項即可。在遷移的過程中,客戶端會先去舊的例項上去查詢資料,因為遷移正在發生,如果對應的資料還在本機上,那麼直接返回,否則返回讓客戶端重定向到新的例項

一致性

不保證強一致性,因為非同步複製&網路分割槽可能存在資料丟失,在講哨兵時候也說過該問題。

六:叢集不可用條件

一):過半master掛掉(選舉就會有問題)

二):某一個master和下面的slave全部掛掉(請求可能會打到掛掉的機器)

七:擴容和縮容

擴容

縮容

八:目前市面上redis叢集的架構

直連cluster(直連架構)

官方預設的連線方式,缺點:存在 MOVED 和 ASK 轉向(請求重定向會增加 IO 開銷)

 

Codis架構(代理架構)

 

Twemproxy架構(代理架構)

推特開源,也是加了一個代理,Twemproxy是一種代理分片機制,由Twitter開源。Twemproxy作為代理,可接受來自多個程式的訪問,按照路由規則,轉發給後臺的各個Redis伺服器,再原路返回。

後端依賴於原生的 redis 節點與 redis-sentinel。

缺點:proxy存在單點,擴容問題

 

其實redis叢集分兩大類,就是直連和代理模式,試想下,我們可以通過代理做一些什麼操作?

1.大key統計。

2.熱key蒐集

3.路由控制,避免直連的moved/ask轉向。

九:分散式架構擴充

redis叢集是分散式的,redis僅僅是應用分散式理論的一個應用,瞭解分散式系統原理&設計是至關重要的,瞭解分散式原理,可以很快的掌握業界中常見的kafka,etcd,zookeeper,redis,hdfs,spark等基礎工具的設計原理,分散式原理都是通用的,對於redis叢集最主要的是資料的分佈方式,需要一種演算法去精確的判斷redis的key落在哪個具體節點上,並且考慮到節點的變更擴充套件是否容易,本文簡單說一下分散式系統中的資料分佈方式,後續大家如果對分散式原理感興趣,會專題去講解分散式系統原理。

分散式系統中的資料分佈方式

hash分割槽(該資料分佈就是redis cluster3.0的資料分佈實現,一種非常常見的資料分佈方式)

我們可以把hash分割槽想像成一個巨大的hash表,每一臺機器都是一個bucket,hash分割槽就是按照資料的某一緯度計算該緯度的hash值,通過計算出來的hash對節點取模計算判斷落在哪些節點上面

優點:理想情況下,hash方式可以達到理論上的資料均分,後設資料非常簡單,就是hash和機器數量取模的結果。

缺點:

(1)資料分佈可能存在資料傾斜,畢竟可能有些情況並不是很理想,redis叢集也存在這樣的問題

  (2) 橫向擴充套件比較繁瑣,涉及到大量的資料遷移,所以hash分佈方式往往如此涉及到成倍的增加 節點,這樣可以減少資料遷移的成本,實際只需要一半的資料被遷移即可。

應用舉例:如kafka的producer產出來的資料也是通過對key取hash然後對partition 數量取模運算定位到需要寫入的partition,如果沒有指定key,kafka會使用atomicinteger生成一個執行緒安全的數字,通過該數字和partition數量取模運算定位到partition,然後後續沒有key的會在此數字上呼叫atomicinteger 的 incrementandget去自增然後去計算。

range分割槽

比較簡單,如圖:

優點:根據業務需求緯度分,可以靈活的根據資料量的具體情況拆分原有資料區間, 拆分後的資料區間可以遷移到其他機器,一旦需要叢集完成負載均衡時,與雜湊方式相比非常靈活

缺點:規模龐大需要維護後設資料,或者還需要維護後設資料伺服器。

資料量分割槽

每個節點上的資料量大小是一致的,比如hdfs的檔案塊,預設是64m。

優點:可以達到解決均分,不涉及到資料傾斜,如spark/mr預設的資料計算都是基於hdfs的檔案塊去做task分配的,一般不涉及到由於資料傾斜帶來的資料計算木桶效應。

缺點:維護大量後設資料,如:hsfs要求檔案快不要過小,過小的話會涉及到大量的後設資料維護,會給namenode帶來很大的壓力。

副本的資料分佈

有三個資料段,o,p,q,在需要有副本的情況下,資料分佈如圖:

典型的例子是kafka的副本分割槽就是該設計的一個的實現。

優點:資料副本的分配可以保證節點在掛掉之後可以由其他節點提供服務。

缺點:如o副本所在機器全部掛掉之後就要考慮可用性和一致性的抉擇。

一致性hash

redis官方叢集出來之前,一致性hash就是redis叢集的一種實現方式,網上資料較多,一致性hash在此不再深入。

本地化計算資料分佈

說到mapreduce和spark大家應該都不陌生,這些是大資料計算工具,大資料就涉及到大量資料的分散式儲存,mr和spark都會把一任務split成一個個的小任務取機器節點上取執行,最後再reduce,在此涉及到移動資料和移動計算的一個問題,我們可以把task分配到資料所在機器相關的節點上(移動計算),也可以把資料移動到任務所在節點上(移動資料),很明顯資料過大的時候移動資料很明顯的網路瓶頸,所以我們通常會把計算移動到資料所在節點上,這是分散式一種很重要的思想“移動資料不如移動計算”。

工作中常見的應用場景

1.如mysql在做分表的時候,很常見的一種策略就是通過userid%表的數量(hash分割槽)去定位到該使用者的資料所在的表名字。

2.冷熱資料分離,如mysql兩張表,mydata, mydata_del,被刪除的資料會單獨被放在mydata_del裡面,長時間的業務迭代會造成資料庫中會有大量的冷資料(被刪除的),一是影響查詢效率,二是佔用資料庫大量的的磁碟空間,還有可能造成資料庫的擴容,該資料的分佈方式就是range分割槽,未被刪除的存放於mydata,刪除資料存放於mydata_del。

十:思考

Q:為什麼官方的3.0叢集規定只能有16384個槽位?

A:首先需要明白key針對槽位的分配方式為:redis使用crc16(key)% 16384 演算法計算出來key歸屬於哪一個槽位 , crc16演算法會產出65536個數字(2的16次方),在此為什麼要對16384取模運算,我們先討論如果不取模運算,假設擁有65536個槽位,redis叢集節點需要相互交換資訊,詳見標註的訊息體結構,一個char佔用1個位元組,如果槽數量為16384,那麼char陣列的大小為:16384➗1024➗8 = 2kb,

如果槽數量為65536,那麼char陣列的大小為:65536➗1024➗8 = 8kb,節點和節點之間的傳送節點的負責槽資訊量會是原來的4倍,浪費頻寬,作者不建議redis官方叢集節點數量大於1000,16384個槽對於1000個節點足夠使用了。沒必要搞到65536個。

所以得到結論:

(1):65536比16384更加浪費頻寬

(2):16384可以滿足不超過1000節點的叢集

(3):Redis主節點的配置資訊中,它所負責的雜湊槽是通過一張bitmap的形式來儲存的,在傳輸過程中,會對bitmap進行壓縮,但是如果bitmap的填充率slots / N很高的話(N表示節點數),bitmap的壓縮率就很低。如果節點數很少,而雜湊槽數量很多的話,bitmap的壓縮率就很低

#define CLUSTER_SLOTS 16384


typedef struct {
    char sig[4];        /* 簽名“RCmb”(Redis群集訊息匯流排) */
    uint32_t totlen;    /* 訊息的總長度 */
    uint16_t ver;       /* 協議版本,目前設定為1 */
    uint16_t port;      /* TCP基本埠號r. */
    uint16_t type;      /* 訊息型別 */
    uint16_t count;     /* 僅用於某種訊息 */
    uint64_t currentEpoch;  /* 相應於傳送節點的紀元 */
    uint64_t configEpoch;   /* 如果是主伺服器的配置紀元,或者如果它是從
                                                         伺服器則由其主伺服器通告的最後一個紀元 */
    uint64_t offset;    /* 如果節點是從屬節點,則節點是主節點或已處理的
                                                 複製偏移量時,主複製偏移量 */
    char sender[CLUSTER_NAMELEN]; /* 發件人節點的名稱 */
    unsigned char myslots[CLUSTER_SLOTS/8]; /* 傳送節點負責的槽資訊 */
    char slaveof[CLUSTER_NAMELEN]; /* 如果傳送節點是從節點,記錄對應主節點的nodeId */
    char myip[NET_IP_STR_LEN];    /* 發件人IP,如果不存在則為零 */
    char notused1[34];  /* 34個保留位元組供將來使用 */
    uint16_t cport;      /* Sender TCP叢集匯流排埠 */
    uint16_t flags;      /* 發件人節點標誌 */
    unsigned char state; /* 來自發件人POV的叢集狀態 */
    unsigned char mflags[3]; /* 訊息標誌:CLUSTERMSG_FLAG [012] _... */
    union clusterMsgData data; /* 群集訊息資料 */
} clusterMsg;

相關文章