redis高可用模式比較及一致性hash

原文鏈接:https://cloud.tencent.com/developer/article/1500916

前面描述了redis主從複製和redis sentinel以及redis cluster的適用場景和各自的原理。本文主要描述下它們的功能和作用以及異同點。文中圖片大部分來自於網絡。

普通Master-Slave模式:

在這裏插入圖片描述
(圖片來源於網絡) 原理說明見上一篇文章

 

Sentinel

在這裏插入圖片描述
Sentinel爲Redis提供高可用。利用Sentinel,在無人干預的情況下,可用讓Redis服務抵禦一定程度的故障。主要發揮以下幾個方面的作用:

  • 監控(Monitoring),Sentinel可用持續不斷地檢查主從實例是否如期運行。用於監控master和slave節點。
  • 通知(Notification),當某個被監控的Redis實例出問題的時候,可以通過API接口向系統管理員和其他應用服務發通知。
  • 自動故障轉移(Automatic failover),當主出現故障時,Sentinel會自動啓動故障轉移流程,把其中一個從庫提升爲主庫,然後其他從庫重新認新主。集羣也會返回新的地址給客戶端。
  • 配置提供(Configuration provider),Sentinel可以作爲服務註冊中心,讓客戶端直接連接請求Sentinel去獲取主庫的地址,爲客戶端與redis服務建立橋樑。如果出現自動故障轉移,Sentinel也會提供新的主庫地址。

 

redis主從複製+Sentinel

在這裏插入圖片描述

  • 如果master有100G內存,slave有150G,集羣最大數據量還是100G,以master爲準。
  • 當master達到內存上限時,redis內部有緩存清理的機制,將舊的數據清除來保證內存中最大值不會超出master內存物理上限。
  • Redis Replication + Sentinel集羣架構如果想要存儲更多的數據,只能通過增加原來Master+Slave進行擴容,即垂直擴容。

 

redis cluster

在這裏插入圖片描述
Redis Cluster集羣架構,如果要存儲更多的數據,可以直接增加新的Master + Slave來進行水平擴容。

redis cluster數據分片

  • redis cluster 會對數據進行自動分片,將數據分配到每個Master 上(自動的負載均衡)。除了自動對數據分片,落到各個節點上,即使集羣部分節點失效或者連接不上,依然可以繼續處理命令。
  • redis cluster 所有節點直接都是相互連接的,它要求開放兩個端口,一個端口負責對外數據交換(port:6379),另外一個端口用來內部通信(port :6379 + 10000 = 16379),也就是集羣總線的通信(cluster bus)
  • cluster bus主要有以下兩個特點:
    • cluster bus 用來進行故障檢測,配置更新,故障轉移授權
    • cluster bus 使用一種二進制的協議,主要用於節點間進行高效的數據交換,佔用更少的網絡帶寬和處理時間

redis cluster數據分片相關算法

簡單hash算法

假設有三臺機,數據落在哪臺機的算法爲: node = hash(requestId) % 3 。這樣進入的請求會被分佈到三臺redis節點上,如下圖:
在這裏插入圖片描述
上面的方式看似能解決了我們將請求負載到多個節點的問題,實際上有一些問題:

  1. 緩存壓力增大,需要增加節點時,取模方式需要改成hash(requestId) % (3 + N),這時很多原本落在之前節點上的緩存都將錯位,大量緩存失效,會引起緩存雪崩。
  2. 當有一臺節點宕機時,也會導致可用節點數量變化,發生與上面相同的問題。
爲了解決上面的問題,我們引出了一致性hash算法

一致性hash算法

早在1997年就在論文《Consistent hashing and random trees》中提出,這個算法有一個環形hash空間的概念。
在這裏插入圖片描述
通常hash算法都是將value映射在一個32位的key值當中,那麼把數軸首尾相接就會形成一個圓形,取值範圍爲0 ~ 2^32-1,這個圓形就是環形hash空間。

現在假設有4個對象:object1-object4,將四個對象hash後映射到環形空間中:
在這裏插入圖片描述
首先通過hash函數計算出這四個對象的hash值key,這些對象的hash值肯定是會落在上述中的環形hash空間範圍上的,對象的hash對應到環形hash空間上的哪一個key值那麼 該對象就映射到那個位置上,這樣對象就映射到環形hash空間上了。

接下來把chche映射到hash空間(基本思想就是講對象和cache都映射到同一hash數值空間中,並且使用相同的hash算法,可以使用cache的ip地址或者其他因子),假設現在有三個cache:
在這裏插入圖片描述

hash(cache A) = key A;
... ...
hash(cache C) = key C;

可以看到,Cache和Obejct都映射到這個環形hash空間中了,那麼接下來要考慮的就是如何將object映射到cache中。其實在這個環形hash空間進行一個順時針的計算即可, 例如key1順時針遇到的第一個cache是cacheA,所以就將key1映射到cacheA中,key2順時針遇到的第一個cache是cacheC,那麼就將key2映射到cacheC中,以此類推。如下圖:
在這裏插入圖片描述
現在移除一個cacheB節點、這時候key4將找不到cache,key4繼續使用一致性hash算法運算後算出最新的cacheC,以後存儲與讀取都將在cacheC上:
在這裏插入圖片描述
移除節點後的影響範圍在該節點逆時針計算到遇到的第一個cache節點之間的數據節點。這個範圍是比較小的。

現在看一下增加一個節點:
在這裏插入圖片描述
如上圖所示,在cacheB和cacheC之間增加了cacheD節點,那麼object2在順時針遇到的第一個cache是cacheD,此時就會將object2映射到cacheD中,增加cache節點所影響 的範圍也就是cacheD和cacheB之間的那一段,範圍也比較小。

上述情況僅適用於服務節點在哈希環上分佈均勻的情況,如果哈希環上服務器節點的 分佈位置不均勻,則會導致某個區間內的數據項的大量數據存放在一個服務器節點中。如下圖,A 緩存服務器就會接收大量請求,當該服務器崩潰掉之後,B 服務器,C 服務器會依次崩潰,這樣就會造成 服務器雪崩效應,整個緩存服務器集羣都會癱瘓。
在這裏插入圖片描述
這種時候就需要引入虛擬節點來解決問題了。

虛擬節點

一致性hash 算法 一定程度上解決了node宕機後的大部分數據失效問題,但是也會導致node 的熱點問題,降低性能,這個又該怎麼解決呢?可以通過增加虛擬節點的方式 讓 hash 點散落更均勻 ,不光能解決熱點問題,還可以達到自動的負載均衡效果。

例如我們擁有 A、B、C 三臺服務器,我們在哈希環上創建哈希服務器的時候,可以爲其創建 N 個虛擬節點,這些虛擬節點都是指向真實服務器的 IP,這樣我們在哈希環上的服務器節點分佈就會很均勻。
在這裏插入圖片描述
有了虛擬節點,就可以儘可能讓更多的Node有機會被請求,從而分擔熱點壓力,達到負載均衡的效果。這時環形hash空間上分佈就越來越均勻,移除或增加cache時所受到的影響就會越小。

一致性命中率計算公式:

(1 - n / (n + m)) * 100%
n = 現有的節點數量
m = 新增的節點數量

redis cluster的hash slot算法

Redis 集羣沒有使用一致性hash, 而是引入了 哈希槽的概念.

Redis 集羣有16384個哈希槽,每個key通過CRC16校驗後對16384取模來決定放置哪個槽.集羣的每個節點負責一部分hash槽,舉個例子,比如當前集羣有3個節點,那麼:

  • 節點 A 包含 0 到 5500號哈希槽.
  • 節點 B 包含5501 到 11000 號哈希槽.
  • 節點 C 包含11001 到 16384號哈希槽.

這種結構很容易添加或者刪除節點. 比如如果我想新添加個節點D, 我需要從節點 A, B, C中的部分槽到D上. 如果我想移除節點A,需要將A中的槽移到B和C節點上,然後將沒有任何槽的A節點從集羣中移除即可. 由於從一個節點將哈希槽移動到另一個節點並不會停止服務,所以無論添加刪除或者改變某個節點的哈希槽的數量都不會造成集羣不可用的狀態.

redis cluster擁有固定的16384個slot(槽) 這個槽是虛擬的,並不是真正存在。slot被分佈到各個master中,當某個key映射到某個master負責的槽時,就由 對應的master爲key提供服務。在redis cluster中只有master才擁有對slot的所有權,slave只負責使用slot,並沒有所有權。

那麼 redis Cluster 又是如何知道哪些槽是由哪些節點負責的呢?Master 又是如何知道哪個槽是自己的呢?

位序列結構(節約存儲空間)

每個Master節點都維護着一個位序列,爲16384 / 8 字節;Master 節點 通過 bit 來標識哪些槽自己是否擁有。比如對於編號爲1的槽,Master只要判斷序列的第二位(索引從0開始)是不是爲1即可。

集羣同時維護着槽與集羣節點的映射關係,由16384個長度的數組記錄,槽編號爲數組的下標,數組內容爲集羣節點,這樣就可以很快地通過槽編號找到負責這個槽的節點。

鍵空間分佈基本算法

下面看下redis cluster是通過什麼樣的方式進行分片存儲的

key 與 slot 的映射算法公式如下:

HASH_SLOT=CRC16(key) mod 16384
  • redis cluster 通過對每個key計算CRC16值,然後對16384取模,可以獲取key對應的hash slot,對於一批量數,如果想讓批量數據都在同一個slot,可以通過hash tag來實現。
  • redis cluster中每個master都會持有部分slot,比如有3個master,那麼可能每個master持有5000多個hash slot。
  • hash slot 讓 node 的增加和移除很簡單,增加一個master,就將其他master的hash slot移動部分過去,減少一個master,就將它的hash slot移動到其他master上去。
  • 移動 hash slot 的成本是非常低的。由於 16384 是固定的,當某個master 宕機時,不會影響其他機器的數據,因爲key 找得是hash slot ,而不是機器。

一致性hash的java實現

redis.clients.util.Hashing,Jedis中默認的hash值計算採取了MD5作爲輔助,似乎此算法已經成爲“標準”:

//少量優化性能
    public ThreadLocal<MessageDigest> md5Holder = new ThreadLocal<MessageDigest>();
    public static final Hashing MD5 = new Hashing() {
        public long hash(String key) {
            return hash(SafeEncoder.encode(key));
        }

        public long hash(byte[] key) {
            try {
                if (md5Holder.get() == null) {
                    md5Holder.set(MessageDigest.getInstance("MD5"));
                }
            } catch (NoSuchAlgorithmException e) {
                throw new IllegalStateException("++++ no md5 algorythm found");
            }
            MessageDigest md5 = md5Holder.get();

            md5.reset();
            md5.update(key);
            byte[] bKey = md5.digest();//獲得MD5字節序列
            //前四個字節作爲計算參數,最終獲得一個32位int值.
            //此種計算方式,能夠確保key的hash值更加“隨即”/“離散”
            //如果hash值過於密集,不利於一致性hash的實現(特別是有“虛擬節點”設計時)
            long res = ((long) (bKey[3] & 0xFF) << 24)
                    | ((long) (bKey[2] & 0xFF) << 16)
                    | ((long) (bKey[1] & 0xFF) << 8) | (long) (bKey[0] & 0xFF);
            return res;
        }
    };

node構建過程(redis.clients.util.Sharded)

//shards列表爲客戶端提供了所有redis-server配置信息,包括:ip,port,weight,name
//其中weight爲權重,將直接決定“虛擬節點”的“比例”(密度),權重越高,在存儲是被hash命中的概率越高
//--其上存儲的數據越多。
//其中name爲“節點名稱”,jedis使用name作爲“節點hash值”的一個計算參數。
//---
//一致性hash算法,要求每個“虛擬節點”必須具備“hash值”,每個實際的server可以有多個“虛擬節點”(API級別)
//其中虛擬節點的個數= “邏輯區間長度” * weight,每個server的“虛擬節點”將會以“hash”的方式分佈在全局區域中
//全局區域總長爲2^32.每個“虛擬節點”以hash值的方式映射在全局區域中。
// 環形:0-->vnode1(:1230)-->vnode2(:2800)-->vnode3(400000)---2^32-->0
//所有的“虛擬節點”將按照其”節點hash“順序排列(正序/反序均可),因此相鄰兩個“虛擬節點”之間必有hash值差,
//那麼此差值,即爲前一個(或者後一個,根據實現而定)“虛擬節點”所負載的數據hash值區間。
//比如hash值爲“2000”的數據將會被vnode1所接受。
//---
private void initialize(List<S> shards) {  
    nodes = new TreeMap<Long, S>();//虛擬節點,採取TreeMap存儲:排序,二叉樹

    for (int i = 0; i != shards.size(); ++i) {  
        final S shardInfo = shards.get(i);  
        if (shardInfo.getName() == null)  
                //當沒有設置“name”是,將“SHARD-NODE”作爲“虛擬節點”hash值計算的參數
                //"邏輯區間步長"爲160,爲什麼呢??
                //最終多個server的“虛擬節點”將會交錯佈局,不一定非常均勻。
            for (int n = 0; n < 160 * shardInfo.getWeight(); n++) {  
                nodes.put(this.algo.hash("SHARD-" + i + "-NODE-" + n), shardInfo);  
            }  
        else  
            for (int n = 0; n < 160 * shardInfo.getWeight(); n++) {  
                nodes.put(this.algo.hash(shardInfo.getName() + "*" + shardInfo.getWeight() + n), shardInfo);  
            }  
        resources.put(shardInfo, shardInfo.createResource());  
    }  
}

node選擇方式

public R getShard(String key) {  
    return resources.get(getShardInfo(key));  
}  
//here:
public S getShardInfo(byte[] key) {  
        //獲取>=key的“虛擬節點”的列表
    SortedMap<Long, S> tail = nodes.tailMap(algo.hash(key));  
        //如果不存在“虛擬節點”,則將返回首節點。
    if (tail.size() == 0) {  
        return nodes.get(nodes.firstKey());  
    }  
        //如果存在,則返回符合(>=key)條件的“虛擬節點”的第一個節點
    return tail.get(tail.firstKey());  
}

主要使用了TreeMap,細節見:https://www.iteye.com/blog/shift-alt-ctrl-1885959

參考

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章