1. 程式人生 > >7.redis cluster理論詳解

7.redis cluster理論詳解

上一篇我們學習了redis sentinel,知道了它是redis高可用的一種實現方案。但是面對要求很高的場景,一臺master是一定不能解決問題的,redis 3.0給我們帶來了服務端叢集方案,解決了這個問題。

1. 資料分割槽

叢集,那麼就會涉及到資料是如何分片的。有兩種方式:順序分割槽和雜湊分割槽

image

兩者對比:

image

直接hash取模進行資料分片時,當節點增加,會有很多資料命中不了,需要重新對映。如果大多數資料在增加或者減少節點之後進行遷移的話,對於效能影響是很大的,因為資料遷移,那麼快取中現在是無法命中的,必須去資料庫取,是災難性的行為。

早期的做法就是這樣,在客戶端hash%節點個數進行資料分片。如果非要這樣,採取翻倍擴容會稍微好一點,遷移資料量會小一點。不過無論如何,這種方式在大資料量情況下是不可行的。

2. 一致性hash演算法

對於上面提到的直接hash取餘的方式,會導致大量資料的遷移。那麼有沒有一種方式,在增加或減少節點時,只有少部分資料遷移呢?

針對一致性hash演算法,在實戰專案–電商中已經詳細介紹了。不再贅述。

對於redis 3.0之前,客戶端可以用這種方式來實現資料分片。在redis 3.0之後,就不需要客戶端來實現分片演算法了,而是直接給我們提供了服務端叢集方案redis cluster.

3. 虛擬槽

redis cluster引入槽的概念,一定要與一致性hash的槽區分!這裡每一個槽對映一個數據集。

CRC16(key) & 16383

這裡計算結果傳送給redis cluster任意一個redis節點,這個redis節點發現他是屬於自己管轄範圍的,那就將它放進去;不屬於他的槽範圍的話,由於redis之間是相互通訊的,這個節點是知道其他redis節點的槽的資訊,那麼會告訴他去那個redis節點去看看。

那麼就實現了服務端對於槽、節點、資料的管理。

image

當master節點增加時,即擴容時,對於以上兩種方案,都會出現資料遷移,那麼只能作為快取場景使用。但是redis cluster,由於每個節點維護的槽的範圍是固定的,當有新加入的節點時,是不會干擾到其他節點的槽的,必須是以前的節點將使用槽的權利分配給你,並且將資料分配給你,這樣,新的節點才會真正擁有這些槽和資料。這種實現還處於半自動狀態,需要人工介入。—–++主要的思想是:槽到叢集節點的對映關係要改變,不變的是鍵到槽的對映關係。++

Redis叢集,要保證16384個槽對應的node都正常工作,++如果某個node發生故障,那它負責的slots也就失效,整個叢集將不能工++作。為了增加叢集的可訪問性,官方推薦的方案是將node配置成主從結構,即一個master主節點,掛n個slave從節點。這時,如果主節點失效,Redis Cluster會根據選舉演算法從slave節點中選擇一個上升為主節點,整個叢集繼續對外提供服務。

某個Master又怎麼知道某個槽自己是不是擁有呢?

Master節點維護著一個16384/8位元組的位序列,Master節點用bit來標識對於某個槽自己是否擁有。比如對於編號為1的槽,Master只要判斷序列的第二位(索引從0開始)是不是為1即可。

image

如上面的序列,表示當前Master擁有編號為1,134的槽。叢集同時還維護著槽到叢集節點的對映,是由長度為16384型別為節點的陣列實現的,槽編號為陣列的下標,陣列內容為叢集節點,這樣就可以很快地通過槽編號找到負責這個槽的節點。位序列這個結構很精巧,即不浪費儲存空間,操作起來又很便捷。

redis節點之間如何通訊的?

image

  • gossip協議:節點之間彼此不斷通訊交換資訊,一段時間後所有節點都會知道叢集完整的資訊。

  • 節點與節點之間通過二進位制協議進行通訊。

  • 客戶端和叢集節點之間通訊和通常一樣,通過文字協議進行。

  • 叢集節點不會代理查詢。

4. 叢集伸縮

這裡6385為新加入的節點,一開始是沒有槽的,所以進行slot的遷移。

image

叢集伸縮:槽和資料在節點之間的移動。

image

遷移資料的流程圖:

image

++遷移key可以用pipeline進行批量的遷移。++

對於擴容,原理已經很清晰了,至於具體操作,網上很多。至於縮容,也是先手動完成資料遷移,再關閉redis。

5. 客戶端路由

5.1 moved重定向

image

其中,槽直接命中的話,就直接返回槽編號:

image

槽不命中,返回帶提示資訊的異常,客戶端需要重新發送一條命令:

image

對於命令列的實驗,用redis-cli去連線叢集:

redis -c -p 7000:加上-c,表示使用叢集模式,幫助我們在第一次不命中的情況下自動跳轉到對應的節點上:

image

如果不加-c的話,會返回moved異常,不會自動跳轉:

image

5.2 ask重定向

在擴容縮容的時候,由於需要遍歷這個節點上的所有的key然後進行遷移,是比較慢的,對客戶端是一個挑戰。因為假設一個場景,客戶端訪問某個key,節點告訴客戶端這個key在源節點,當我們再去源節點訪問的時候,卻發現key已經遷移到目標節點。

image

5.3 moved重定向和ask重定向對比

  • 兩者都是客戶端單重定向
  • moved:槽已經確定轉移
  • ask:槽還在遷移中

問題:如果節點眾多,那麼讓客戶端隨機訪問節點,那麼直接命中的概率只有百分之一,還有就是發生ask異常時(即節點正在遷移時)客戶端如何還能高效運轉?

總結一句話就是redis cluster的客戶端的實現會更復雜。

6. smart客戶端

6.1 追求目標

追求效能,不會使用代理模式,而是直連對應節點。需要對moved異常和ask異常做相容。也就是說,需要有一個這個語言對應的客戶端來高效實現查詢等操作。

6.2 smart原理

  • 從叢集中選一個可執行節點,使用cluster slots初始化槽和節點對映
  • 將cluster cluster的結果對映到本地,為每個節點建立JedisPool
  • 準備執行命令

第一步中將slot與node節點的對應關係放在了map中,形成一個對映關係;key是通過CRC16演算法再取餘得到slot,所以key與slot的對映關係也是確定的。我們就可以直接傳送命令。只要後面叢集沒有發生資料遷移,那麼就會連線成功。但是如果在連線的時候出現了連接出錯,說明這個key已經遷移到其他的node上了。如果發現key不停地遷移,超過5次就報錯。

在發生move異常的時候,則需要重新整理快取,即一開始維護的map。
image

有一個情況比較全的圖:

image

java redis cluster客戶端:jedisCluster基本使用–虛擬碼

image

jedisCluster內部已經封裝好池的借還操作等。

先寫一個JedisClusterFactory:

import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.JedisCluster;
import redis.clients.jedis.JedisPoolConfig;

import java.io.IOException;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

public class JedisClusterFactory {
    private JedisCluster jedisCluster;
    private List<String> hostPortList;
    //超時時間
    private int timeout;

    public void init(){
        //這裡可以設定相關引數
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();

        //從配置檔案中讀取ip:port的引數放進Set中
        Set<HostAndPort> nodeSet = new HashSet<HostAndPort>();
        for(String hostPort : hostPortList){
            String[] arr = hostPort.split(":");
            if(arr.length != 2){
                continue;
            }
            nodeSet.add(new HostAndPort(arr[0],Integer.parseInt(arr[1])));
        }

        try {
            jedisCluster = new JedisCluster(nodeSet,timeout,jedisPoolConfig);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


    public void destory(){
        if(jedisCluster != null){
            try {
                jedisCluster.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public JedisCluster getJedisCluster() {
        return jedisCluster;
    }

    //spring注入hostPortList和timeout
    public void setHostPortList(List<String> hostPortList) {
        this.hostPortList = hostPortList;
    }

    public void setTimeout(int timeout) {
        this.timeout = timeout;
    }
}

hostPortList 放入spring bean中,spring自動完成注入。

image

6.3 多節點命令實現

有的時候我們想操作所有節點的資料。如何實現呢?

image

6.4 批量操作

++mget,mset必須在一個槽++。這個條件比較苛刻,一般是不能保證的,那麼如何實現批量的操作呢?

Redis Cluster的行為和Redis 的單節點不同,甚至和一個Sentinel 監控的主從模式也不一樣。主要原因是叢集自動分片,將一個key 對映到16384個槽中的一個,這些槽分佈在多個節點上。因此操作多個key 的命令必須保證所有的key 都對映到同一個槽上,避免跨槽執行錯誤。更進一步說,今後一個單獨的叢集節點,只服務於一組專用的keys,請求一個命令到一個Server,只能得到該Server 上擁有keys 的對應結果。一個非常簡單的例子是執行KEYS命令,當釋出該命令到叢集環境中的某個節點是,只能得到該節點上擁有的keys,而不是叢集中所有的keys。所以要得到叢集中所有的keys,必須從叢集的所有主節點上獲取所有的keys。

對於分散在redis叢集中不同節點的資料,我們如何比較高效地批量獲取資料呢????

  1. 序列mget–原始方案,整一個for迴圈

image

  1. 序列IO

對key進行RCR16和取餘操作得到slot,將slots按照節點進行分批傳送:

image

  1. 並行IO

image

  1. hash_tag

不做任何改變的話,hash之後就比較均勻地散在每個節點上:

image

那麼我們能不能像使用單機redis一樣,一次IO將所有的key取出來呢?hash-tag提供了這樣的功能,如果將上述的key改為如下,也就是用大括號括起來相同的內容,那麼這些key就會到指定的一個節點上。

image

在mget的時候只需要在一臺機器上去即可。

image

  1. 對比

方案三比較複雜,一般不用;方案四可能會出現資料傾斜,也不用。方案一在key小的時候可以用;方案二相對來說有一點優勢;

image

為什麼說是一點優勢呢?pipeline批量處理不應該比單挑處理好很多嗎?

7. 故障轉移

7.1 故障發現

  • 通過ping/pong訊息實現故障發現:不需要sentinel

  • 分為主觀下線和客觀下線

主觀下線:

image

客觀下線:

image

pfail訊息就是主觀下線的資訊,,維護在一個連結串列中,連結串列中包含了所有其他節點對其他節點所有的主觀資訊,是有時間週期的,為了防止很早以前的主觀下線資訊還殘留在這裡。對這個連結串列進行分析,符合條件就嘗試客觀下線。

image

7.2 故障恢復

從節點接收到他的主節點客觀下線的通知,則進行故障恢復的操作。

  • 資格檢查

選取出符合條件的從節點:當從節點和故障主節點的斷線時間太長,會被取消資格。

  • 準備選舉時間

就是為了保證偏移量大的從節點優先被選舉投票

image

  • 選舉投票

image

  • 替換主節點

image

這些所有步驟加起來,差不多十幾秒左右。最後如果故障節點又恢復功能了,就稱為新的Master的slave節點。

8. 常見問題

8.1 叢集完整性

cluster-require-full-coverage預設為yes

- 要求所有節點都在服務,叢集中16384個槽全部可用:保證叢集完整性
- 節點故障或者正在故障轉移:(error)CLUSTERDOWN the cluster is down

++但是大多數業務都無法容忍。需要將cluster-require-full-coverage設定為no++

8.2 頻寬消耗

image

  • 訊息傳送頻率:節點發現與其他節點最後通訊時間超過cluster-node-timeout/2時會直接傳送Ping訊息
  • 訊息資料量:slots槽陣列(2k空間)和整個叢集1、10的狀態資料(10個節點狀態資料約10k)
  • 節點部署的機器規模:進去分佈的機器越多且每臺機器劃分的節點數越均勻,則叢集內整體的可用頻寬越高。
  • 優化:避免“大”叢集,:避免多業務使用一個叢集,大業務可用多叢集;cluster-node-timeout時間設定要注意是頻寬和故障轉移速度的均衡;儘量均勻分配到多機器上:保證高可用和頻寬。

8.3 PubSub廣播

image

  • 問題:publish在叢集中每個節點廣播:加重頻寬。
  • 解決:單獨“走”一套redis sentinel。

8.4 資料傾斜

  • 節點和槽分配不均勻

    ./redis-trib.rb info ip:port檢視節點、槽、鍵值分佈

    慎用rebalance命令

  • 不同槽位對應鍵數量差異較大

    CRC16正常情況下比較均勻

    可能存在hash_tag

    cluster countKeysinslot {slot}獲取槽對應鍵值個數

  • 包含bigkey

    例如大字串、幾百萬的元素的hash、set等

    在從節點上執行:redis-cli –bigkeys來檢視bigkey情況

    優化:優化資料結構

  • 記憶體相關配置不一致

    因為某種情況下,某個節點對hash或者Set這種資料結構進行了單獨的優化,而其他節點都沒有配置,會出現配置不一致的情況。

8.5 請求傾斜

  • 熱點key:重要的key或者bigkey
  • 優化:避免bigkey;熱鍵不使用hash_tag;當一致性不高時,可以用本地快取+MQ

8.6 讀寫分離

  • 只讀連線:叢集模式的從節點不接受任何讀寫請求

重定向到負責槽的主節點(對從節點進行讀,都是重定向到主節點再返回資訊)

readonly命令可以讀:連線級別命令(每次重新連線都要寫一次)

image

上圖可以看出,redis cluster 預設slave 也是不能讀的,如果要讀取,需要執行 readonly,就可以了。


  • 讀寫分離:更加複雜(成本很高,儘量不要使用)
同樣的問題:複製延遲、讀取過期資料、從節點故障

修改客戶端

8.7 資料遷移

分為離線遷移和線上遷移(唯品會redis-migrate-tool和豌豆莢redis-port)。

官方的方式:只能從單機遷移到叢集、不支援線上遷移、不支援斷點續傳、單執行緒遷移影響速度

./redis-trib.rb import –from 源ip:port –copy 目標ip:port

加入在遷移時再往源redis插入幾條資料,這幾條資料會丟失(丟失一部分)

8.8 叢集vs單機

叢集也有一定的限制:

image

分散式redis不一定是好的:

image

9. 簡單總結

image

image