圖解一致性雜湊演算法,看這一篇就夠了!

ITPUB社群發表於2022-11-28

接下來介紹一個非常重要、也非常實用的演算法:一致性雜湊演算法。透過介紹一致性雜湊演算法的原理並給出了一種實現和實際運用的案例,帶大家真正理解一致性雜湊演算法。


一、背景

在具體介紹一致性雜湊演算法之前,先問一個問題:為什麼需要一致性雜湊演算法?下面我們透過一個案例來回答這個問題。

假設有這麼一種場景:我們有三臺快取伺服器分別為:node0、node1、node2,有3000萬個快取資料需要儲存在這三臺伺服器組成的叢集中,希望可以將這些資料均勻的快取到三臺機器上,你會想到什麼方案呢?

我們可能首先想到的方案是:取模演算法hash(key)%N,即:對快取資料的key進行hash運算後取模,N是機器的數量;運算後的結果對映對應叢集中的節點。具體如下圖所示:

圖解一致性雜湊演算法,看這一篇就夠了!

如上圖所示,首先對key進行hash計算後的結果對3取模,得到的結果一定是0、1或者2;然後對映對應的伺服器node0、node1、node2,最後直接找對應的伺服器存取資料即可。


透過取模演算法將每個資料請求都均勻的分散到了三個不同的伺服器節點上,看起來很完美!但是,在分散式叢集系統的負載均衡實現上,這種模型在叢集擴容和收縮時卻有一定的侷限性:因為在生產環境中根據業務量的大小,調整伺服器數量是常有的事,而伺服器數量N發生變化後hash(key)%N計算的結果也會隨之變化!導致整個叢集的快取資料必須重新計算調整,進而導致大量快取在同一時間失效,造成快取的雪崩,最終導致整個快取系統的不可用,這是不能接受的。為了解決最佳化上述情況,一致性雜湊演算法應運而生。


二、一致性雜湊簡介

有些朋友一聽到演算法就頭大,其實大可不必,一致性雜湊演算法聽起來高大上,其實非常簡單。接下來開始介紹什麼是一致性雜湊演算法,它解決了什麼問題。

2.1 什麼是一致性雜湊?

一致性雜湊(Consistent Hash)演算法是1997年提出,是一種特殊的雜湊演算法,目的是解決分散式系統的資料分割槽問題:當分散式叢集移除或者新增一個伺服器時,必須儘可能小地改變已存在的服務請求與處理請求伺服器之間的對映關係。


2.2 一致性雜湊主要解決問題

我們知道,傳統的按伺服器節點數量取模在叢集擴容和收縮時存在一定的侷限性。而一致性雜湊演算法正好解決了簡單雜湊演算法在分散式叢集中存在的動態伸縮的問題。降低節點上下線的過程中帶來的資料遷移成本,同時節點數量的變化與分片原則對於應用系統來說是無感的,使上層應用更專注於領域內邏輯的編寫,使得整個系統架構能夠動態伸縮,更加靈活方便。


2.3 一致性雜湊的使用場景

一致性雜湊演算法是分散式系統中的重要演算法,使用場景也非常廣泛。主要是是負載均衡、快取資料分割槽等場景。

一致性雜湊應該是實現負載均衡的首選演算法,它的實現比較靈活,既可以在客戶端實現,也可以在中介軟體上實現,比如日常使用較多的快取中介軟體memcached 使用的路由演算法用的就是一致性雜湊演算法。

此外,其它的應用場景還有很多:

  • RPC框架Dubbo用來選擇服務提供者

  • 分散式關聯式資料庫分庫分表:資料與節點的對映關係

  • LVS負載均衡排程器


三、一致性雜湊的原理

2.1 演算法原理

前面介紹的取模演算法雖然使用簡單,但缺陷也很明顯,如果伺服器中儲存有服務請求對應的資料,那麼如果重新計算請求的雜湊值,會造成快取的雪崩的問題。這種情況在分散式系統中是非常糟糕的。一個設計良好的分散式系統應該具有良好的單調性,即伺服器的新增與移除不會造成大量的雜湊重定位,而一致性雜湊恰好可以解決這個問題 。


其實,一致性雜湊演算法本質上也是一種取模演算法。只不過前面介紹的取模演算法是按伺服器數量取模,而一致性雜湊演算法是對固定值2^32取模,這就使得一致性演算法具備良好的單調性:不管叢集中有多少個節點,只要key值固定,那所請求的伺服器節點也同樣是固定的。其演算法的工作原理如下:

  1. 一致性雜湊演算法將整個雜湊值空間對映成一個虛擬的圓環,整個雜湊空間的取值範圍為0~2^32-1;

  2. 計算各伺服器節點的雜湊值,並對映到雜湊環上;

  3. 將服務發來的資料請求使用雜湊演算法算出對應的雜湊值;

  4. 將計算的雜湊值對映到雜湊環上,同時沿圓環順時針方向查詢,遇到的第一臺伺服器就是所對應的處理請求伺服器。

  5. 當增加或者刪除一臺伺服器時,受影響的資料僅僅是新新增或刪除的伺服器到其環空間中前一臺的伺服器(也就是順著逆時針方向遇到的第一臺伺服器)之間的資料,其他都不會受到影響。


綜上所述,一致性雜湊演算法對於節點的增減都只需重定位環空間中的一小部分資料,具有較好的容錯性和可擴充套件性 。


2.2 深入剖析

說了那麼多,可能你還是雲裡霧裡的,那麼接下來我們詳細剖析一致性雜湊的實現原理。

2.2.1 雜湊環

首先,一致性雜湊演算法將整個雜湊值空間對映成一個虛擬的圓環。整個雜湊空間的取值範圍為0~2^32-1,按順時針方向開始從0~2^32-1排列,最後的節點2^32-1在0開始位置重合,形成一個虛擬的圓環。如下圖所示:

圖解一致性雜湊演算法,看這一篇就夠了!

2.2.2 伺服器對映到雜湊環

接下來,將伺服器節點對映到雜湊環上對應的位置。我們可以對伺服器IP地址進行雜湊計算,雜湊計算後的結果對2^32取模,結果一定是一個0到2^32-1之間的整數。最後將這個整數對映在雜湊環上,整數的值就代表了一個伺服器節點的在雜湊環上的位置。即:hash(伺服器ip)% 2^32。下面我們依次將node0、node1、node2三個快取伺服器對映到雜湊環上,如下圖所示:

圖解一致性雜湊演算法,看這一篇就夠了!


2.2.3 物件key對映到伺服器

當伺服器接收到資料請求時,首先需要計算請求Key的雜湊值;然後將計算的雜湊值對映到雜湊環上的具體位置;接下來,從這個位置沿著雜湊環順時針查詢,遇到的第一個節點就是key對應的節點;最後,將請求傳送到具體的伺服器節點執行資料操作。

假設我們有“key-01:張三”、“key-02:李四”、“key-03:王五”三條快取資料。經過雜湊演算法計算後,對映到雜湊環上的位置如下圖所示:

圖解一致性雜湊演算法,看這一篇就夠了!

如上圖所示,透過雜湊計算後,key-01順時針尋找將找到node0,key-02順時針尋找將找到node1,key-03順時針尋找將找到node2。最後,請求找到的伺服器節點執行具體的業務操作。


以上便是一致性雜湊演算法的工作原理。



四、伺服器擴容&縮容

前面介紹了一致性雜湊演算法的工作原理,那麼,一致性雜湊演算法如何避免伺服器動態伸縮的問題的呢?

4.1 伺服器縮容

伺服器縮容就是減少叢集中伺服器節點的數量或是叢集中某個節點故障。假設,叢集中的某個節點故障,原本對映到該節點的請求,會找到雜湊環中的下一個節點,資料也同樣被重新分配至下一個節點,其它節點的資料和請求不受任何影響。這樣就確保節點發生故障時,叢集能保持正常穩定。如下圖所示:

圖解一致性雜湊演算法,看這一篇就夠了!

如上圖所示:節點node2發生故障時,資料key-01和key-02不會受到影響,只有key-03的請求被重定位到node0。在一致性雜湊演算法中,如果某個節點當機不可用了,那麼受影響的資料僅僅是會定址到此節點和前一節點之間的資料。其他雜湊環上的資料不會受到影響。


4.2 伺服器擴容

伺服器擴容就是叢集中需要增加一個新的資料節點,假設,由於需要快取的資料量太大,必須對叢集進行擴容增加一個新的資料節點。此時,只需要計算新節點的雜湊值並將新的節點加入到雜湊環中,然後將雜湊環中從上一個節點到新節點的資料對映到新的資料節點即可。其他節點資料不受影響,具體如下圖所示:

圖解一致性雜湊演算法,看這一篇就夠了!

如上圖所示,加入新的node3節點後,key-01、key-02不受影響,只有key-03的定址被重定位到新節點node3,受影響的資料僅僅是會定址到新節點和前一節點之間的資料。


透過一致性雜湊演算法,叢集擴容或縮容時,只需要重新定位雜湊環空間內的一小部分資料。其他資料保持不變。當節點數越多的時候,使用雜湊演算法時,需要遷移的資料就越多,使用一致雜湊時,需要遷移的資料就越少。所以,一致雜湊演算法具有較好的容錯性和可擴充套件性。


五、資料傾斜與虛擬節點

5.1 什麼是資料傾斜?

前面說了一致性雜湊演算法的原理以及擴容縮容的問題。但是,由於雜湊計算的隨機性,導致一致性雜湊演算法存在一個致命問題:資料傾斜,,也就是說大多數訪問請求都會集中少量幾個節點的情況。特別是節點太少情況下,容易因為節點分佈不均勻造成資料訪問的冷熱不均。這就失去了叢集和負載均衡的意義。如下圖所示:

圖解一致性雜湊演算法,看這一篇就夠了!

如上圖所示,key-1、key-2、key-3可能被對映到同一個節點node0上。導致node0負載過大,而node1和node2卻很空閒的情況。這有可能導致個別伺服器資料和請求壓力過大和崩潰,進而引起叢集的崩潰。


5.2 如何解決資料傾斜?

為了解決資料傾斜的問題,一致性雜湊演算法引入了虛擬節點機制,即對每一個物理服務節點對映多個虛擬節點,將這些虛擬節點計算雜湊值並對映到雜湊環上,當請求找到某個虛擬節點後,將被重新對映到具體的物理節點。虛擬節點越多,雜湊環上的節點就越多,資料分佈就越均勻,從而避免了資料傾斜的問題。


說起來可能比較複雜,一句話概括起來就是:原有的節點、資料定位的雜湊演算法不變,只是多了一步虛擬節點到實際節點的對映。具體如下圖所示:

圖解一致性雜湊演算法,看這一篇就夠了!

如上圖所示,我們可以在伺服器ip或主機名的後面增加編號來實現,將全部的虛擬節點加入到雜湊環中,增加了節點後,資料在雜湊環上的分佈就相對均勻了。當有訪問請求定址到node0-1這個虛擬節點時,將被重新對映到物理節點node0。


六、一致性Hash演算法實現

前面介紹了一致性雜湊演算法的原理、動態伸縮以及資料傾斜的問題後,下面我們根據上面的講述,使用Java實現一個簡單的一致性雜湊演算法。

6.1 資料節點

首先定義一個節點類,實現資料節點的功能,具體程式碼如下:













































public class Node {    private static final int VIRTUAL_NODE_NO_PER_NODE = 200;    private final String ip;    private final List<Integer> virtualNodeHashes = new ArrayList<>(VIRTUAL_NODE_NO_PER_NODE);    private final Map<Object, Object> cacheMap = new HashMap<>();
   public Node(String ip) {        Objects.requireNonNull(ip);        this.ip = ip;        initVirtualNodes();    }

   private void initVirtualNodes() {        String virtualNodeKey;        for (int i = 1; i <= VIRTUAL_NODE_NO_PER_NODE; i++) {            virtualNodeKey = ip + "#" + i;            virtualNodeHashes.add(HashUtils.hashcode(virtualNodeKey));        }    }
   public void addCacheItem(Object key, Object value) {        cacheMap.put(key, value);    }

   public Object getCacheItem(Object key) {        return cacheMap.get(key);    }

   public void removeCacheItem(Object key) {        cacheMap.remove(key);    }

   public List<Integer> getVirtualNodeHashes() {        return virtualNodeHashes;    }
   public String getIp() {        return ip;    }}



6.2 實現一致性雜湊演算法

接下來實現核心功能:一致性雜湊演算法,主要使用java的TreeMap類,實現雜湊環和雜湊查詢的功能。具體程式碼如下所示:












































































public class ConsistentHash {    private final TreeMap<Integer, Node> hashRing = new TreeMap<>();
   public List<Node> nodeList = new ArrayList<>();
   /**     * 增加節點     * 每增加一個節點,就會在閉環上增加給定虛擬節點     * 例如虛擬節點數是2,則每呼叫此方法一次,增加兩個虛擬節點,這兩個節點指向同一Node     * @param ip     */    public void addNode(String ip) {        Objects.requireNonNull(ip);        Node node = new Node(ip);        nodeList.add(node);        for (Integer virtualNodeHash : node.getVirtualNodeHashes()) {            hashRing.put(virtualNodeHash, node);            System.out.println("虛擬節點[" + node + "] hash:" + virtualNodeHash + ",被新增");        }    }
   /**     * 移除節點     * @param node     */    public void removeNode(Node node){        nodeList.remove(node);    }
   /**     * 獲取快取資料     * 先找到對應的虛擬節點,然後對映到物理節點     * @param key     * @return     */    public Object get(Object key) {        Node node = findMatchNode(key);        System.out.println("獲取到節點:" + node.getIp());        return node.getCacheItem(key);    }
   /**     * 新增快取     * 先找到hash環上的節點,然後在對應的節點上新增資料快取     * @param key     * @param value     */    public void put(Object key, Object value) {        Node node = findMatchNode(key);
       node.addCacheItem(key, value);    }
   /**     * 刪除快取資料     */    public void evict(Object key) {        findMatchNode(key).removeCacheItem(key);    }

   /**     *  獲得一個最近的順時針節點     * @param key 為給定鍵取Hash,取得順時針方向上最近的一個虛擬節點對應的實際節點     *      * @return 節點物件     * @return     */    private Node findMatchNode(Object key) {        Map.Entry<Integer, Node> entry = hashRing.ceilingEntry(HashUtils.hashcode(key));        if (entry == null) {            entry = hashRing.firstEntry();        }        return entry.getValue();    }}

如上所示,透過TreeMap的ceilingEntry() 方法,實現順時針查詢下一個的伺服器節點的功能。


6.3 雜湊計算方法

雜湊計算方法比較常見,網上也有很多計算hash 值的函式。示例程式碼如下:




























public class HashUtils {
   /**     * FNV1_32_HASH     *     * @param obj     *         object     * @return hashcode     */    public static int hashcode(Object obj) {        final int p = 16777619;        int hash = (int) 2166136261L;        String str = obj.toString();        for (int i = 0; i < str.length(); i++)            hash = (hash ^ str.charAt(i)) * p;        hash += hash << 13;        hash ^= hash >> 7;        hash += hash << 3;        hash ^= hash >> 17;        hash += hash << 5;
       if (hash < 0)            hash = Math.abs(hash);        //System.out.println("hash computer:" + hash);        return hash;    }}



6.4 驗證測試

一致性雜湊演算法實現後,接下來新增一個測試類,驗證此演算法時候正常。示例程式碼如下:




















































public class ConsistentHashTest {    public static final int NODE_SIZE = 10;    public static final int STRING_COUNT = 100 * 100;    private static ConsistentHash consistentHash = new ConsistentHash();    private static List<String> sList = new ArrayList<>();
   public static void main(String[] args) {        // 增加節點        for (int i = 0; i < NODE_SIZE; i++) {            String ip = new StringBuilder("10.2.1.").append(i)                    .toString();            consistentHash.addNode(ip);        }
       // 生成需要快取的資料;        for (int i = 0; i < STRING_COUNT; i++) {            sList.add(RandomStringUtils.randomAlphanumeric(10));        }
       // 將資料放入到快取中。        for (String s : sList) {            consistentHash.put(s, s);        }
       for(int i = 0 ; i < 10 ; i ++) {            int index = RandomUtils.nextInt(0, STRING_COUNT);            String key = sList.get(index);            String cache = (String) consistentHash.get(key);            System.out.println("Random:"+index+",key:" + key + ",consistentHash get value:" + cache +",value is:" + key.equals(cache));        }
       // 輸出節點及資料分佈情況        for (Node node : consistentHash.nodeList){            System.out.println(node);        }
       // 新增一個資料節點        consistentHash.addNode("10.2.1.110");        for(int i = 0 ; i < 10 ; i ++) {            int index = RandomUtils.nextInt(0, STRING_COUNT);            String key = sList.get(index);            String cache = (String) consistentHash.get(key);            System.out.println("Random:"+index+",key:" + key + ",consistentHash get value:" + cache +",value is:" + key.equals(cache));        }
       // 輸出節點及資料分佈情況        for (Node node : consistentHash.nodeList){            System.out.println(node);        }    }}

執行此測試,輸出結果如下所示:

圖解一致性雜湊演算法,看這一篇就夠了!


最後

以上,我們就把一致性雜湊演算法的實現原理,應用場景、解決了哪些問題都介紹完了,並用java簡單實現了一個一致性雜湊演算法。相信看完之後,大家對一致性雜湊演算法應該不會那麼陌生害怕了吧。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70024420/viewspace-2925492/,如需轉載,請註明出處,否則將追究法律責任。

相關文章