一致性哈希原理及應用淺析

背景

現實場景

  1. 單個節點的容量達到上限,無法繼續單點增加內存,如何解決?
  2. 單個節點支撐的QPS達到上限,如何解決?

初步方案

   增加N個緩存節點,爲了保證緩存數據的均勻,一般情況會採用對key值hash,然後取模的方式,然後根據結果,確認數據落到哪臺節點上:如下:

hash(key)%N

很好,這個的確解決了上面的問題,實現了初步的分佈式存儲,數據均勻分散到了各個節點上,流量請求也均勻的分散到了各個節點。

方案問題

  1. 某臺服務器突然宕機。緩存服務器從N變爲N-1臺。 hash(key)%(N-1)
  2. 緩存容量達到上限或者請求處理達到上限,需要增加緩存服務器,假定增加1臺,則緩存服務器從N變爲N+1。hash(key)%(N+1)

       增加或者刪除緩存服務器的時候,意味着大部分的緩存都會失效。這個是比較致命的一點,緩存失效,如果業務爲緩存不命中,查詢DB的話,會導致一瞬間DB的壓力陡增。可能會導致整個服務不可用。

目標

   增刪機器時,希望大部分key依舊在原有的緩存服務器上保持不變。舉例來說:key1,key2,key3原先再Cache1機器上,現在增加一臺緩存服務器,希望key1,key2,key3依舊在Cache1機器上,而不是在Cache2機器上。

原理

基本概念

        一致性hash算法是希望在增刪節點的時候,讓儘可能多的數據不失效。

   判斷hash算法好壞的四個標準:

  • 平衡性:平衡性是指哈希的結果能夠儘可能分佈到所有的緩衝中去,這樣可以使得所有的緩衝空間都得到利用。很多哈希算法都能夠滿足這一條件。
  • 單調性:單調性是指如果已經有一些內容通過哈希分派到了相應的緩衝中,又有新的緩衝加入到系統中。哈希的結果應能夠保證原有已分配的內容可以被映射到原有的或者新的緩衝中去,而不會被映射到舊的緩衝集合中的其他緩衝區。
  • 分散性:在分佈式環境中,終端有可能看不到所有的緩衝,而是隻能看到其中的一部分。當終端希望通過哈希過程將內容映射到緩衝上時,由於不同終端所見的緩衝範圍有可能不同,從而導致哈希的結果不一致,最終的結果是相同的內容被不同的終端映射到不同的緩衝區中。這種情況顯然是應該避免的,因爲它導致相同內容被存儲到不同緩衝中去,降低了系統存儲的效率。分散性的定義就是上述情況發生的嚴重程度。好的哈希算法應能夠儘量避免不一致的情況發生,也就是儘量降低分散性。
  • 負載:負載問題實際上是從另一個角度看待分散性問題。既然不同的終端可能將相同的內容映射到不同的緩衝區中,那麼對於一個特定的緩衝區而言,也可能被不同的用戶映射爲不同 的內容。與分散性一樣,這種情況也是應當避免的,因此好的哈希算法應能夠儘量降低緩衝的負荷。

 

       使用常見的hash算法可以把一個key值哈希到一個具有2^32個桶的空間中。也可以理解成,將key值哈希到 [0, 2^32] 的一個數字空間中。 我們假設這個是個首尾連接的環形空間。

        

一致性Hash算法

  1. 構造hash環空間
  2. 把節點(服務器)映射到hash環
  3. 把數據對象映射到hash環
  4. 把數據對象映射到節點

 

   把數據用hash函數(如MD5,CRC32),映射到一個很大的空間裏,如圖所示。數據的存儲時,先得到一個hash值,對應到這個環中的每個位置,如k1對應到了圖中所示的位置,然後沿順時針找到一個機器節點B,將k1存儲到B這個節點中。

   如果B節點宕機了,則B上的數據就會落到C節點上,也就是說只有C節點受到影響,也就意味着解決了最開始的方案中可能的雪崩問題。如下圖所示:

上面的簡單的一致性hash的方案在某些情況下但依舊存在問題:一個節點宕機之後,數據需要落到距離他最近的節點上,會導致下個節點的壓力突然增大,可能導致雪崩,整個服務掛掉。

  1. 之前請求到B上的流量轉嫁到了C上,會導致C的流量增加,如果之前B上存在熱點數據,則可能導致C扛不住壓力掛掉。
  2. 之前存儲到B上的key值轉義到了C,會導致C的內容佔用量增加,可能存在瓶頸。

   當上面兩個壓力發生的時候,可能導致C節點也宕機了。那麼壓力便會傳遞到D上,又出現了類似滾雪球的情況,服務壓力出現了雪崩,導致整個服務不可用。

虛擬節點

圖中的A1、A2、B1、B2、C1、C2、D1、D2都是虛擬節點,機器A負載存儲A1、A2的數據,機器B負載存儲B1、B2的數據,機器C負載存儲C1、C2的數據。由於這些虛擬節點數量很多,均勻分佈,因此不會造成雪崩現象。

   虛擬節點的 hash 計算可以採用對應節點的 IP 地址加數字後綴的方式。例如假設 cache A 的 IP 地址爲202.168.14.241 。

   引入虛擬節點前,計算 cache A 的 hash 值:

Hash(“202.168.14.241”);

   引入虛擬節點後,計算虛擬節點cache A1 和 cache A2 的 hash 值:

Hash(“202.168.14.241#1”);  // cache A1

Hash(“202.168.14.241#2”);  // cache A2

   實際節點的N個虛擬節點儘量隨機分佈在整數增加cache,就能儘量保到新cache點的key來自於不同的cache從而保證負載均衡. 

hash(key) -> 虛擬節點 -> 真實節點

JAVA實現

public class Shard<S> { // S類封裝了機器節點的信息 ,如name、password、ip、port等

    private TreeMap<Long, S> nodes; // 虛擬節點
    private List<S> shards; // 真實機器節點
    private final int NODE_NUM = 100; // 每個機器節點關聯的虛擬節點個數

    public Shard(List<S> shards) {
        super();
        this.shards = shards;
        init();
    }

    private void init() { // 初始化一致性hash環
        nodes = new TreeMap<Long, S>();
        for (int i = 0; i != shards.size(); ++i) { // 每個真實機器節點都需要關聯虛擬節點
            final S shardInfo = shards.get(i);

            for (int n = 0; n < NODE_NUM; n++)
                // 一個真實機器節點關聯NODE_NUM個虛擬節點
                nodes.put(hash("SHARD-" + i + "-NODE-" + n), shardInfo);

        }
    }

    public S getShardInfo(String key) {
        SortedMap<Long, S> tail = nodes.tailMap(hash(key)); // 沿環的順時針找到一個虛擬節點
        if (tail.size() == 0) {
            return nodes.get(nodes.firstKey());
        }
        return tail.get(tail.firstKey()); // 返回該虛擬節點對應的真實機器節點的信息
    }

    /**
     *  MurMurHash算法,是非加密HASH算法,性能很高,
     *  比傳統的CRC32,MD5,SHA-1(這兩個算法都是加密HASH算法,複雜度本身就很高,帶來的性能上的損害也不可避免)
     *  等HASH算法要快很多,而且據說這個算法的碰撞率很低.
     *  http://murmurhash.googlepages.com/
     */
    private Long hash(String key) {

        ByteBuffer buf = ByteBuffer.wrap(key.getBytes());
        int seed = 0x1234ABCD;

        ByteOrder byteOrder = buf.order();
        buf.order(ByteOrder.LITTLE_ENDIAN);

        long m = 0xc6a4a7935bd1e995L;
        int r = 47;

        long h = seed ^ (buf.remaining() * m);

        long k;
        while (buf.remaining() >= 8) {
            k = buf.getLong();

            k *= m;
            k ^= k >>> r;
            k *= m;

            h ^= k;
            h *= m;
        }

        if (buf.remaining() > 0) {
            ByteBuffer finish = ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN);
            finish.put(buf).rewind();
            h ^= finish.getLong();
            h *= m;
        }

        h ^= h >>> r;
        h *= m;
        h ^= h >>> r;

        buf.order(byteOrder);
        return h;
    }

}

 

應用

  1. memcache、redis服務器等緩存服務器的負載均衡(分佈式cache);
  2. MySQL的分佈式集羣(分佈式DB);
  3. 大量session的共享存儲(分佈式文件,或session服務器等)。

   twemproxy 是 twitter 開源的一個輕量級的後端代理,兼容 redis/memcache 協議,可用以管理 redis/memcache 集羣。

   twemproxy 內部有實現一致性哈希算法,對於客戶端而言,twemproxy 相當於是緩存數據庫的入口,它無需知道後端的部署是怎樣的。twemproxy 會檢測與每個節點的連接是否健康,出現異常的節點會被剔除;待一段時間後,twemproxy 會再次嘗試連接被剔除的節點。

   通常,一個 Redis 節點池可以分由多個 twemproxy 管理,少數 twemproxy 負責寫,多數負責讀。twemproxy 可以實時獲取節點池內的所有 Redis 節點的狀態,但其對故障修復的支持還有待提高。解決的方法是可以藉助 redis sentinel 來實現自動的主從切換,當主機 down 掉後,sentinel 會自動將從機配置爲主機。而 twemproxy 可以定時向 redis sentinel 拉取信息,從而替換出現異常的節點。


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