一致性哈希算法在分布缓存中的应用

1、简介node

    关于一致性哈希算法介绍有许多相似文章,须要把一些理论转为为本身的知识,因此有了这篇文章,本文部分实现也参照了原有的一些方法。算法

该算法在分布缓存的主机选择中很经常使用,详见 http://en.wikipedia.org/wiki/Consistent_hashing 。数据库

 

2、算法诞生原因缓存

    如今许多大型系统都离不开缓存(K/V)(因为高并发等因素照成的数据库压力(或磁盘IO等)超负荷,须要缓存缓解压力),为了得到良好的水平扩展性,并发

缓存主机互相不通讯(如Mencached),经过客户端计算Key而获得数据存放的主机节点,最简单的方式是取模,假如:负载均衡

----------------------------------------------------------------dom

如今有3台缓存主机,如今有一个key 为 cks 的数据须要存储:ide

key = "cks"函数

hash(key) = 10高并发

10 % 3 = 1  ---> 则表明选择第一台主机存储这个key和对应的value。

缺陷:

假若有一台主机宕机或增长一台主机(必须考虑的状况),取模的算法将致使大量的缓存失效(计算到其余没有缓存该数据的主机),数据库等忽然承受巨大负荷,很大可能致使DB服务不可用等。

----------------------------------------------------------------

 

3、一致性哈希算法原理

    该算法须要解决取模方法当增长主机或者宕机时带来的大量缓存抖动问题,要在生产环境中使用,算法需具有如下几个特色:

1. 平衡性 : 指缓存数据尽可能平衡分布到全部缓存主机上,有效利用每台主机的空间。

2. 

3. 负载均衡 : 每台缓存主机尽可能平衡分担压力,即Key的分配比例在这些主机中应趋于平衡。

 

假如咱们把键hash为int类型(32字节),取值范围为 -2^31 到 (2^31-1) , 咱们把这些值首尾相连造成一个圆环,以下图:

 

假设如今有3台缓存主机: C0一、C0二、C03 ,把它们放在环上(经过IP hash,后面实现会介绍),以下图:

 

假如如今有5个key须要缓存,它们分别为 A,B,C,D,E,假设它们通过hash后分布以下,顺时针找到它们最近的主机,并存储在上面:

 

假如当新加入节点C04的时候,数据分配以下:

 

能够发现,只有少许缓存被从新分配的新主机,减小抖动带来的压力。

但同时出现一个问题,数据分布并不尽可能均匀(当有大量缓存的时候能够看出来),这时候须要把真实的缓存节点虚拟为多个节点,分布在环上,

当顺时针找到虚拟节点的时候再映射到真实节点,则能够知道数据缓存在哪台主机。

 

4、算法实现(Java版本)

    算法的实现有许多,下面例子仅供参考,实际仍须要考虑其余多个问题:

假设有4主机:

192.168.70.1-5

public class Node {
    
    private String ip;
// 表明主机中存放的K/V
private ConcurrentMap<Object, Object> map = new ConcurrentHashMap<Object, Object>(); public Node(String ip) { this.ip = ip; } public String getIp() { return ip; } public ConcurrentMap<Object, Object> getMap() { return map; } @Override public String toString() { return ip; } }

下面是一个没有虚拟节点的状况实现方法:

public class ConsistentHash {
    
    // -2^31 - (2^31-1) 圆环, 用于存储节点
    private final SortedMap<Integer, Node> circle = new TreeMap<Integer, Node>();
    
    private IHash hashIf;
    
    public ConsistentHash(IHash hash) {
        this.hashIf = hash;
    }
    
    public void addNode(Node node) {
        circle.put(hashIf.hash(node.getIp()), node);
    }
    
    public void removeNode(Node node) {
        circle.remove(hashIf.hash(node.getIp()));
    }
    
    public Node getNode(Object key) {
        int hashCode = hashIf.hash(key);
        if (!circle.containsKey(hashCode)) {
            // 相似顺时针取得最近的存储节点
            SortedMap<Integer, Node> tailMap = circle.tailMap(hashCode);
            hashCode = tailMap.isEmpty()? circle.firstKey() : tailMap.firstKey();
        }
        return circle.get(hashCode);
    }
}

其中IHash 为散列方法接口,可实现不一样的散列方式,下面是一个基于MD5算法获得的int值(还有其余算法):

public interface IHash {
    
    int hash(Object key);
    
}
public class MD5HashImpl implements IHash {
    
    MessageDigest digest;
    
    public MD5HashImpl() throws NoSuchAlgorithmException {
        digest = MessageDigest.getInstance("MD5");
    }
    
    @Override
    public int hash(Object key) {
        if (key == null) return 0;
        
        int h = key.hashCode();
        byte[] bytes = new byte[4];  
        for(int i=3; i>-1; i--) {
            bytes[i] = (byte)( h>>(i*8) );
        } 
        
        byte[] hashBytes ;
        synchronized (digest) {
            hashBytes = digest.digest(bytes);
        }
        
        int result = 0;
        for (int i=0; i<4; i++) {
            int idx = i*4;
            result += (hashBytes[idx + 3]&0xFF << 24)
                    | (hashBytes[idx + 2]&0xFF << 16)
                    | (hashBytes[idx + 1]&0xFF << 8)
                    | (hashBytes[idx + 0]&0xFF);
        }
        return result;
    }
}

 

测试方法以下:

public class ConsistentHashTest {

    public static void main(String[] args) throws Exception {
        ConsistentHash cHash = new ConsistentHash(new MD5HashImpl());
        
        // Nodes
        List<Node> nodes = new ArrayList<Node>();
        for (int i=1; i<5; i++) {
            Node node = new Node("192.168.70." + i); // Fake
            nodes.add(node);
            cHash.addNode(node);
        }
        
        Map<String, Set<Integer>> counter = new HashMap<String, Set<Integer>>();
        for (Node n : nodes) {
            counter.put(n.getIp(), new HashSet<Integer>());
        }
        
        // 随机KEY测试分布状况
        Set<Integer> allKeys = new HashSet<Integer>();
     Random random = new Random(); int testTimes = 1000000; for (int i=0; i<testTimes; i++) { int randomInt = random.nextInt(); Node node = cHash.getNode(randomInt); Set<Integer> count = counter.get(node.getIp()); count.add(randomInt);
       allKeys.add(randomInt); }
for (Map.Entry<String, Set<Integer>> entry : counter.entrySet()) { System.out.println(entry.getKey() + "\t" + entry.getValue().size() + "\t" + (entry.getValue().size()*100/(float)allKeys.size()) + "%"); } } }

测试结果(每次运行的实际结果不一样):

IP Count Percent

--------------------------------------
192.168.70.1 216845    21.6845%
192.168.70.4 7207        0.7207%
192.168.70.2 749929    74.9929%
192.168.70.3 25891      2.5891%

-------------------------------------

这结果表示每一个节点分配并不均匀,须要把每一个节点虚拟为多个节点,ConsistentHash 算法更改以下:

public class ConsistentHash {
    
    // -2^31 - (2^31-1) 圆环, 用于存储节点
    private final SortedMap<Integer, Node> circle = new TreeMap<Integer, Node>();
    
    private IHash hashIf;
    private int virtualNum; // 把实际节点虚拟为多个节点
    
    public ConsistentHash(IHash hash, int virtualNum) {
        this.hashIf = hash;
        this.virtualNum = virtualNum;
    }
    
    public void addNode(Node node) {
        for (int i=0; i<virtualNum; i++) {
            circle.put(hashIf.hash(i + node.getIp()), node);
        }
    }
    
    public void removeNode(Node node) {
        for (int i=0; i<virtualNum; i++) {
            circle.remove(hashIf.hash(i + node.getIp()));
        }
    }
    
    public Node getNode(Object key) {
        int hashCode = hashIf.hash(key);
        if (!circle.containsKey(hashCode)) {
            // 相似顺时针取得最近的存储节点
            SortedMap<Integer, Node> tailMap = circle.tailMap(hashCode);
            hashCode = tailMap.isEmpty()? circle.firstKey() : tailMap.firstKey();
        }
        return circle.get(hashCode);
    }
}

只须要在测试函数里面修改:

  // 这里每一个节点虚拟为120个,根据实际状况考虑修改合理的值,虚拟数量少则致使部分不均匀,数量大则致使树的查找效率下降,二者须要权衡。

ConsistentHash cHash = new ConsistentHash(new MD5HashImpl(), 120);

--------------------------- 添加虚拟节点后的分布状况 ----------------------------------------

IP Count Percent
192.168.70.1 277916 27.7916%
192.168.70.4 251437 25.1437%
192.168.70.2 226645 22.6645%
192.168.70.3 243871 24.3871%

------------------------------------------------------------------------------------------------

 

下面再模拟宕机和新增主机状况下面的缓存失效率:

public class HitFailureTest {

    public static void main(String[] args) throws Exception {
        ConsistentHash cHash = new ConsistentHash(new MD5HashImpl(), 120);
        
        List<Node> nodes = new ArrayList<Node>();
        for (int i=1; i<5; i++) {
            Node node = new Node("192.168.70." + i); // Fake
            nodes.add(node);
            cHash.addNode(node);
        }
        
        Set<Integer> allKeys = new HashSet<Integer>();
        
        Random random = new Random();
        int testTimes = 1000000;
        for (int i=0; i<testTimes; i++) {
            int randomInt = random.nextInt();
            cHash.getNode(randomInt).getMap().put(randomInt, 0);
            allKeys.add(randomInt);
        }
        
        // 移除主机序号
        int removeIdx = 1;
        cHash.removeNode(nodes.get(removeIdx));
        
        int failureCount = 0;
        for (Integer key : allKeys) {
            if(!cHash.getNode(key).getMap().containsKey(key)) {
                failureCount ++;
            }
        }
        System.out.println("FailureCount \t Percent");
        System.out.println(failureCount + "\t" + (failureCount*100/(float)allKeys.size()) + "%");
    }

}

结果以下:

FailureCount Percent
231669 23.16975%

结果说明具备比较低的缓存失效率,当主机越多则失效率越低。

 

5、总结

一致性哈希算法能比较好地保证分布缓存的可用性与扩展性,目前大多缓存客户端都基于这方式实现(考虑的因素比上面多不少,如性能问题等),

上面实现方式在宕机或新增机器时候小部分缓存丢失,但有些状况下缓存不容许丢失,则须要作缓存备份,有两种方式:

1. 修改客户端,保证数据被缓存到两台不一样机器,任一一台宕机数据仍能找到。

2. 由缓存服务端实现备份,采用无固定主节点(当主节点失效时从新选举最老的机器做为主节点)模式,节点互备份。

相关文章
相关标签/搜索