一致性Hash算法與Java實現

1.簡介

1.1 普通hash算法

普通hash算法是通過key將數據映射到具體節點上,如key%N,key爲數據的hash值,N爲節點數量,如果有機器加入或者退出集羣,則key映射失效了,導致數據丟失。

1.2 一致性hash算法

相比普通hash算法,一致性hash就可以解決這種問題。一致性hash是分佈式系統常用的一種算法,常用於負載均衡。

2.原理分析

2.1 環形Hash空間

一致性hash算法,將value映射爲一個32的key(常用MD5函數),即key分配到一個0~2^32次方的空間裏。我們可以想象這些數字頭尾相連,形成一個閉合的環形。如下圖:

2.2 將數據映射到環形空間上

現在我們假設有4個對象,object1,object2,object3,object4,我們按照一致性Hash算法,將它們的key分散到環形空間上。

Hash(Object1) = key1

Hash(Object2) = key2

Hash(Object3) = key3

Hash(Object4) = key4

如下圖

2.2 將機器映射到環形空間上

在採用一致性哈希算法的分佈式集羣中將新的機器加入,其原理是通過使用與對象存儲一樣的Hash算法將機器也映射到環中(一般情況下對機器的hash計算是採用機器的IP或者機器唯一的別名作爲輸入值),然後以順時針的方向計算,將所有對象存儲到離自己最近的機器中。
假設現在有node1,node2,node3三臺機器,通過Hash算法得到對應的key值,映射到環中,其示意圖如下:

Hash(node1) = key1

Hash(node2) = key2

Hash(node3) = key3

如下圖

通過上圖可以看出對象與機器處於同一哈希空間中,這樣按順時針轉動object1存儲到了NODE1中,object3存儲到了NODE2中,object2、object4存儲到了NODE3中。在這樣的部署環境中,hash環是不會變更的,因此,通過算出對象的hash值就能快速的定位到對應的機器中,這樣就能找到對象真正的存儲位置了。

2.3 節點的加入和刪除

2.3.1 節點的加入

如果往集羣中添加一個新的節點NODE4,通過對應的哈希算法得到KEY4,並映射到環中,如下圖:

通過按順時針遷移的規則,那麼object2被遷移到了NODE4中,其它對象還保持這原有的存儲位置。通過對節點的添加和刪除的分析,一致性哈希算法在保持了單調性的同時,還是數據的遷移達到了最小,這樣的算法對分佈式集羣來說是非常合適的,避免了大量數據遷移,減小了服務器的的壓力。

2.3.2 節點的刪除

以上面的分佈爲例,如果NODE2出現故障被刪除了,那麼按照順時針遷移的方法,object3將會被遷移到NODE3中,這樣僅僅是object3的映射位置發生了變化,其它的對象沒有任何的改動。如下圖:

2.3.3 平衡性

根據上面的圖解分析,一致性哈希算法滿足了單調性和負載均衡的特性以及一般hash算法的分散性,但這還並不能當做其被廣泛應用的原由,因爲還缺少了平衡性。下面將分析一致性哈希算法是如何滿足平衡性的。hash算法是不保證平衡的,如上面只部署了NODE1和NODE3的情況(NODE2被刪除的圖),object1存儲到了NODE1中,而object2、object3、object4都存儲到了NODE3中,這樣就照成了非常不平衡的狀態。在一致性哈希算法中,爲了儘可能的滿足平衡性,其引入了虛擬節點。
“虛擬節點”( virtual node )是實際節點(機器)在 hash 空間的複製品( replica ),一實際個節點(機器)對應了若干個“虛擬節點”,這個對應個數也成爲“複製個數”,“虛擬節點”在 hash 空間中以hash值排列。
以上面只部署了NODE1和NODE3的情況(NODE2被刪除的圖)爲例,之前的對象在機器上的分佈很不均衡,現在我們以2個副本(複製個數)爲例,這樣整個hash環中就存在了4個虛擬節點,最後對象映射的關係圖如下:

根據上圖可知對象的映射關係:object1->NODE1-1,object2->NODE1-2,object3->NODE3-2,object4->NODE3-1。通過虛擬節點的引入,對象的分佈就比較均衡了。那麼在實際操作中,正真的對象查詢是如何工作的呢?對象從hash到虛擬節點到實際節點的轉換如下圖:

虛擬節點”的hash計算可以採用對應節點的IP地址加數字後綴的方式。例如假設NODE1的IP地址爲192.168.1.100。引入“虛擬節點”前,計算 cache A 的 hash 值:
Hash(“192.168.1.100”);
引入“虛擬節點”後,計算“虛擬節”點NODE1-1和NODE1-2的hash值:
Hash(“192.168.1.100#1”); // NODE1-1
Hash(“192.168.1.100#2”); // NODE1-2

3.Java代碼實現

import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.SortedMap;
import java.util.TreeMap;

/**
 * 
 */

/**
 * 一致性Hash算法-Java實現
 * 
 * @author tangthis
 *
 * <p>https://github.com/tangthis
 */
public class ConsistencyHash {
    private TreeMap<Long,Object> nodes = null;   
    //真實服務器節點信息   
    private List<Object> shards = new ArrayList();   
    //設置虛擬節點數目   
    private int VIRTUAL_NUM = 4;   

    /**  
     * 初始化一致環 
     */  
    public void init() {   
         shards.add("192.168.0.0-服務器0");   
         shards.add("192.168.0.1-服務器1");   
         shards.add("192.168.0.2-服務器2");   
         shards.add("192.168.0.3-服務器3");   
         shards.add("192.168.0.4-服務器4");   

        nodes = new TreeMap<Long,Object>();   
        for(int i=0; i<shards.size(); i++) {   
            Object shardInfo = shards.get(i);   
            for(int j=0; j<VIRTUAL_NUM; j++) {   
                nodes.put(hash(computeMd5("SHARD-" + i + "-NODE-" + j),j), shardInfo);   
            }   
        }   
    }   

    /**  
     * 根據key的hash值取得服務器節點信息  
     * @param hash  
     * @return  
     */  
    public Object getShardInfo(long hash) {   
        Long key = hash;   
        SortedMap<Long, Object> tailMap=nodes.tailMap(key);   
        if(tailMap.isEmpty()) {   
            key = nodes.firstKey();   
        } else {   
            key = tailMap.firstKey();   
        }   
        return nodes.get(key);   
    }   

    /**  
     * 打印圓環節點數據  
     */  
     public void printMap() {   
         System.out.println(nodes);   
     }   

    /**  
     * 根據2^32把節點分佈到圓環上面。  
     * @param digest  
     * @param nTime  
     * @return  
     */  
      public long hash(byte[] digest, int nTime) {   
        long rv = ((long) (digest[3+nTime*4] & 0xFF) << 24)   
                | ((long) (digest[2+nTime*4] & 0xFF) << 16)   
                | ((long) (digest[1+nTime*4] & 0xFF) << 8)   
                | (digest[0+nTime*4] & 0xFF);   

        return rv & 0xffffffffL; /* Truncate to 32-bits */  
      }   

    /**  
     * Get the md5 of the given key.  
     * 計算MD5值  
     */  
     public byte[] computeMd5(String k) {   
        MessageDigest md5;   
        try {   
            md5 = MessageDigest.getInstance("MD5");   
        } catch (NoSuchAlgorithmException e) {   
            throw new RuntimeException("MD5 not supported", e);   
        }   
        md5.reset();   
        byte[] keyBytes = null;   
        try {   
            keyBytes = k.getBytes("UTF-8");   
        } catch (UnsupportedEncodingException e) {   
            throw new RuntimeException("Unknown string :" + k, e);   
        }   

        md5.update(keyBytes);   
        return md5.digest();   
     }   

     public static void main(String[] args) {   
         Random ran = new Random();   
         ConsistencyHash hash = new ConsistencyHash();   
         hash.init();   
         hash.printMap();   
         //循環50次,是爲了取50個數來測試效果,當然也可以用其他任何的數據來測試   
         for(int i=0; i<50; i++) {   
             System.out.println(hash.getShardInfo(hash.hash(hash.computeMd5(String.valueOf(i)),ran.nextInt(hash.VIRTUAL_NUM))));   
         }   
   }   
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章