Redis系列5:深入分析Cluster 集群模式

Redis系列1:深刻理解高性能Redis的本質
Redis系列2:數據持久化提高可用性
Redis系列3:高可用之主從架構
Redis系列4:高可用之Sentinel(哨兵模式)

1 背景

前面我們學習了Redis高可用的兩種架構模式:主從模式哨兵模式
解決了我們在Redis實例發生故障時,具備主從自動切換、故障轉移的能力,最終保證服務的高可用。
但是這些其實遠遠不夠,隨着我們業務規模的不斷擴展,用戶量膨脹,並發量持續提升。原有的主從架構,已經遠遠達不到我們的需求了,這時候會有一些問題出現,比如:

  • 單機的CPU、內存、連接數、計算力都是有極限的,不能無限制的承載流量的擴增。
  • 超額的請求、大規模的數據計算,導致必然的慢響應。
    這時候就需要適當的推進架構的演進,來滿足發展的需要。

2 Cluster 模式介紹

2.1 什麼是Cluster模式

Cluster 即 集群模式,類似MySQL,Redis 集群也是一種分佈式數據庫方案,集群通過分片(sharding)模式來對數據進行管理,並具備分片間數據複製、故障轉移和流量調度的能力。這種 分治模式很常見,我們在 微服務系列:拆分策略MySQL系列:分庫分表 中實踐過很多次了。
Redis集群的做法是 將數據劃分為 16384(2的14次方)個哈希槽(slots),如果你有多個實例節點,那麼每個實例節點將管理其中一部分的槽位,槽位的信息會存儲在各自所歸屬的節點中。以下圖為例,該集群有4個 Redis 節點,每個節點負責集群中的一部分數據,數據量可以不均勻。比如性能好的實例節點可以多分擔一些壓力。
image

一個Redis集群一共有16384個哈希槽,你可以有1 ~ n個節點來分配這些哈希槽,可以不均勻分配,每個節點可以處理0個 到至多 16384 個槽點。
當16384個哈希槽都有節點進行管理的時候,集群處於online 狀態。同樣的,如果有一個哈希槽沒有被管理到,那麼集群處於offline狀態。

上面圖中4個實例節點組成了一個集群,集群之間的信息通過 Gossip協議 進行交互,這樣就可以在某一節點記錄其他節點的哈希槽(slots)的分配情況。

2.2 為什麼需要Cluster模式

單機的吞吐無法承受持續擴增的流量的時候,最好的辦法是從橫向(scale out) 和 縱向(scale up)兩方面進行擴展,這個我們在 MySQL系列 和 微服務系列 的時候已經討論過了。

  • 縱向擴展(scale up):將單個實例的硬件資源做提升,比如 CPU核數量、內存容量、SSD容量。
  • 橫向擴展(scale out):橫向擴增 Redis 實例數,這樣每個節點只負責一部分數據就可以,分擔一下壓力,典型的分治思維。
    image
    那橫向擴展和縱向擴展各有什麼優缺點呢?
  • scale up 雖然操作起來比較簡易。但是沒法解決Redis一些瓶頸問題,比如持久化(如輪式RDB快照還是AOF指令),遇到大數據量的時候,照樣效率會很低,響應慢。另外,單台服務機硬件擴容也是有限制的,不可能無限操作。
  • scale out 更容易擴展,分片的模式可以解決很多問題,包括單一實例節點的硬件擴容限制、成本限制,還可以分攤壓力,精細化治理,精細化維護。但是同時也要面臨分佈式帶來的一些問題
    現實情況下,在面對千萬級甚至億級別的流量的時候,很多大廠都是在千百台的實例節點組成的集群上進行流量調度、服務治理的。所以,使用Cluster模式,是業內廣泛採用的模式。

3 Cluster 實現原理

3.1 集群的組群過程

集群是由一個個互相獨立的節點(readis node)組成的, 所以剛開始的時候,他們都是隔離,毫無聯繫的。我們需要通過一些操作,把他們聚集在一起,最終才能組成真正的可協調工作的集群。
各個節點的聯通是通過 CLUSTER MEET 命令完成的:CLUSTER MEET <ip> <port>
具體的做法是其中一個node向另外一個 node(指定 ip 和 port) 發送 CLUSTER MEET 命令,這樣就可以讓兩個節點進行握手(handshake操作) ,握手成功之後,node 節點就會將握手另一側的節點添加到當前節點所在的集群中。
這樣一步步的將需要聚集的節點都圈入同一個集群中,如下圖:
image

3.2 集群數據分片原理

現在的Redis集群分片的做法,主要是使用了官方提供的 Redis Cluster 方案。這種方案就是的核心就是集群的實例節點與哈希槽(slots)之間的劃分、映射與管理。下面我們來看看他具體的步驟。

3.2.1 哈希槽(slots)的劃分

這個前面已經說過了,我們會將整個Redis數據庫劃分為16384個哈希槽,你的Redis集群可能有n個實例節點,每個節點可以處理0個 到至多 16384 個槽點,這些節點把 16384個槽位瓜分完成。
而你實際存儲的Redis鍵值信息也必然歸屬於這 16384 個槽的其中一個。slots 與 Redis Key 的映射是通過以下兩個步驟完成的:

  • 使用 CRC16 算法計算鍵值對信息的Key,會得出一個 16 bit 的值。
  • 將 第1步中得到的 16 bit 的值對 16384 取模,得到的值會在 0 ~ 16383 之間,映射到對應到哈希槽中。
    當然,可能在一些特殊的情況下,你想把某些key固定到某個slot上面,也就是同一個實例節點上。這時候可以用hash tag能力,強制 key 所歸屬的槽位等於 tag 所在的槽位。
    其實現方式為在key中加個{},例如test_key{1}。使用hash tag後客戶端在計算key的crc16時,只計算{}中數據。如果沒使用hash tag,客戶端會對整個key進行crc16計算。下面演示下hash tag使用:
127.0.0.1:6380> cluster keyslot user:case{1}
(integer) 1024
127.0.0.1:6380> cluster keyslot user:favor
(integer) 1023
127.0.0.1:6380> cluster keyslot user:info{1}
(integer) 1024

如上,使用hash tag 後會對應到通一個hash slot:1024中。

3.2.2 哈希槽(slots)的映射

一種是初始化的時候均勻分配 ,使用 cluster create 創建,會將 16384 個slots 平均分配在我們的集群實例上,比如你有n個節點,那每個節點的槽位就是 16384 / n 個了 。
另一種是通過 CLUSTER MEET 命令將 node1、node2、ndoe3、node4 4個節點聯通成一個集群,剛聯通的時候因為還沒分配哈希槽,還是處於offline狀態。我們使用 cluster addslots 命令來指定。
指定的好處就是性能好的實例節點可以多分擔一些壓力。

可以通過 addslots 命令指定哈希槽範圍,比如下圖中,我們哈希槽是這麼分配的:實例 1 管理 0 ~ 7120 哈希槽,實例 2 管理 7121~9945 哈希槽,實例 3 管理 9946 ~ 13005 哈希槽,實例 4 管理 13006 ~ 16383 哈希槽。

redis-cli -h 192.168.0.1 –p 6379 cluster addslots 0,7120
redis-cli -h 192.168.0.2 –p 6379 cluster addslots 7121,9945
redis-cli -h 192.168.0.3 –p 6379 cluster addslots 9946,13005
redis-cli -h 192.168.0.4 –p 6379 cluster addslots 13006,16383

slots 和 Redis 實例之間的映射關係如下:
image

key testkey_1testkey_2 經過 CRC16 計算後再對slots的總個數 16384 取模,結果分別匹配到了 cache1 和 cache3 上。

3.3 數據複製過程和故障轉移

3.3.1 數據複製

Cluster 是具備Master 和 Slave模式,Redis 集群中的每個實例節點都負責一些槽位,比如上圖中的四個節點分管了不同的槽位區間。而每個Master至少需要一個Slave節點,Slave 節點是通過《Redis系列3:高可用之主從架構》方式同步主節點數據。 節點之間保持TCP通信,當Master發生了宕機, Redis Cluster自動會將對應的Slave節點選為Master,來繼續提供服務。與純主從模式不同的是,主從節點之間並沒有讀寫分離, Slave 只用作 Master 宕機的高可用備份,所以更合理來說應該是主備模式。
如果主節點沒有從節點,那麼一旦發生故障時,集群將完全處於不可用狀態。 但也允許配置 cluster-require-full-coverage參數,及時部分節點不可用,其他節點正常提供服務,這是為了避免全盤宕機。
主從切換之後,故障恢復的主節點,會轉化成新主節點的從節點。這種自愈模式對提高可用性非常有幫助。

3.3.2 故障檢測

一個節點認為某個節點宕機不能說明這個節點真的掛起了,無法提供服務了。只有佔據多數的實例節點都認為某個節點掛起了,這時候cluster才進行下線和主從切換的工作。
Redis 集群的節點採用 Gossip 協議來廣播信息,每個節點都會定期向其他節點發送ping命令,如果接受ping消息的節點在指定時間內沒有回復pong,則會認為該節點失聯了(PFail),則發送ping的節點就把接受ping的節點標記為主觀下線。
如果集群半數以上的主節點都將主節點 xxx 標記為主觀下線,則節點 xxx 將被標記為客觀下線,然後向整個集群廣播,讓其它節點也知道該節點已經下線,並立即對下線的節點進行主從切換。

3.3.3 主從故障轉移

當一個從節點發現自己正在複製的主節點進入了已下線,則開始對下線主節點進行故障轉移,故障轉移的步驟如下:

  • 如果只有一個slave節點,則從節點會執行SLAVEOF no one命令,成為新的主節點。

  • 如果是多個slave節點,則採用選舉模式進行,競選出新的Master

    • 集群中設立一個自增計數器,初始值為 0 ,每次執行故障轉移選舉,計數就會+1。
    • 檢測到主節點下線的從節點向集群所有master廣播一條CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST消息,所有收到消息、並具備投票權的主節點都向這個從節點投票。
    • 如果收到消息、並具備投票權的主節點未投票給其他從節點(只能投一票哦,所以投過了不行),則返回一條CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息,表示支持。
    • 參與選舉的從節點都會接收CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息,如果收集到的選票 大於等於 (n/2) + 1 支持,n代表所有具備選舉權的master,那麼這個從節點就被選舉為新主節點。
    • 如果這一輪從節點都沒能爭取到足夠多的票數,則發起再一輪選舉(自增計數器+1),直至選出新的master。
  • 新的主節點會撤銷所有對已下線主節點的slots指派,並將這些slots全部指派給自己。

  • 新的主節點向集群廣播一條PONG消息,這條PONG消息可以讓集群中的其他節點立即知道這個節點已經由從節點變成了主節點,並且這個主節點已經接管了原本由已下線節點負責處理的槽。

  • 新的主節點開始接收和自己負責處理的槽有關的命令請求,故障轉移完成。

跟哨兵類似,兩者都是基於 Raft 算法來實現的,流程如圖所示:
image

3.4 client 訪問 數據集群的過程

3.4.1 定位數據所在節點

我們前面說過了,Redis 中的每個實例節點會將自己負責的哈希槽信息 通過 Gossip 協議廣播給集群中其他的實例,實現了slots分配信息的擴散。這樣的話,每個實例都知道整個集群的哈希槽分配情況以及映射信息。
所以客戶端想要快捷的連接到服務端,並對某個redis數據進行快捷訪問,一般是經過以下步驟:

  • 客戶端連接任一實例,獲取到slots與實例節點的映射關係,並將該映射關係的信息緩存在本地。
  • 將需要訪問的redis信息的key,經過CRC16計算後,再對16384 取模得到對應的 Slot 索引。
  • 通過slot的位置進一步定位到具體所在的實例,再將請求發送到對應的實例上。
    下圖展示了 Redis 客戶端如何定位數據所在節點:
    image

4 總結

  • 哨兵模式已經實現了故障自動轉移的能力,但業務規模的不斷擴展,用戶量膨脹,並發量持續提升,會出現了 Redis 響應慢的情況。
  • 使用 Redis Cluster 集群,主要解決了大數據量存儲導致的各種慢問題,同時也便於橫向拓展。在面對千萬級甚至億級別的流量的時候,很多大廠的做法是在千百台的實例節點組成的集群上進行流量調度、服務治理的。
  • 整個Redis數據庫劃分為16384個哈希槽,Redis集群可能有n個實例節點,每個節點可以處理0個 到至多 16384 個槽點,這些節點把 16384個槽位瓜分完成。
  • Cluster 是具備Master 和 Slave模式,Redis 集群中的每個實例節點都負責一些槽位,節點之間保持TCP通信,當Master發生了宕機, Redis Cluster自動會將對應的Slave節點選為Master,來繼續提供服務。
  • 客戶端能夠快捷的連接到服務端,主要是將slots與實例節點的映射關係存儲在本地,當需要訪問的時候,對key進行CRC16計算後,再對16384 取模得到對應的 Slot 索引,再定位到相應的實例上。實現高效的連接。