一致性哈希算法 理解及实际应用中 优化

 一 雪崩现象

       一致性哈希算法是分布式系统中常用的算法。比如,一个分布式的存储系统,要将数据存储到具体的节点上,如果采用普通的hash方法,将数据映射到具体的节点上,如key%N,key是数据的key,N是机器节点数,如果有一个机器加入或退出这个集群,则所有的数据映射都无效了,如果是持久化存储则要做数据迁移,如果是分布式缓存,则其他缓存就失效了。

    因此,引入了一致性哈希算法:


 

把数据用hash函数(如MD5),映射到一个很大的空间里,如图所示。数据的存储时,先得到一个hash值,对应到这个环中的每个位置,如k1对应到了图中所示的位置,然后沿顺时针找到一个机器节点B,将k1存储到B这个节点中。

如果B节点宕机了,则B上的数据就会落到C节点上,如下图所示:


 

这样,只会影响C节点,对其他的节点A,D的数据不会造成影响。然而,这又会造成一个“雪崩”的情况,即C节点由于承担了B节点的数据,所以C节点的负载会变高,C节点很容易也宕机,这样依次下去,这样造成整个集群都挂了。

       为此,引入了“虚拟节点”的概念:即把想象在这个环上有很多“虚拟节点”,数据的存储是沿着环的顺时针方向找一个虚拟节点,每个虚拟节点都会关联到一个真实节点,如下图所使用:


图中的A1、A2、B1、B2、C1、C2、D1、D2都是虚拟节点,机器A负载存储A1、A2的数据,机器B负载存储B1、B2的数据,机器C负载存储C1、C2的数据。由于这些虚拟节点数量很多,均匀分布,因此不会造成“雪崩”现象。

 二  关键理解

      一致性哈希,简单的说在移除 / 添加一个 cache 时,它能够尽可能小的改变已存在 key 映射关系,尽可能的满足单调性的要求。重点说一下最常见的实现方式。 


Java中采用md5散列的方式,计算hash值,这样基本上能保证key散列出啦的hash不会重复。 
Java代码  收藏代码
  1. private static long md5HashingAlg(String key) {  
  2.     MessageDigest md5 = MD5.get();  
  3.     md5.reset();  
  4.     md5.update(key.getBytes());  
  5.     byte[] bKey = md5.digest();  
  6.     long res = ((long) (bKey[3] & 0xFF) << 24) | ((long) (bKey[2] & 0xFF) << 16) | ((long) (bKey[1] & 0xFF) << 8)| (long) (bKey[0] & 0xFF);  
  7.     return res;  
  8. }  

在对server节点初始化的时候,为了避免节点过少数据分布不均匀,都会初始化一些虚拟节点。具体方法上面计算hash值的方式类同,一把采用根据权重虚拟出来一些key,具体不过多介绍。 
一致性哈希算法中,把哈希值想象成一个环状。有关一致性哈希算法,介绍里面说0~2^32-1的数据,不要误以为哈希环要保存2^32个数据。他只是说,哈希环存的key的哈希值范围是0~2^32-1,并不是key的哈希值要覆盖0~2^32-1所有数据。 

既要保存hash值,又要保存对应的节点地址,貌似最简单的就是map,在Java中没有什么map可以满足是个环状。那就找一个排序的,0开头,2^32-1做尾。查找时查到尾没有结果,再返回头找这样可以理解为是个环状了。 
在初始化的时候,把节点的hash和节点地址保存在TreeMap里,client查找时,根据key的hash值去treeMap得到自己应该查询的节点,往下查找比自己hash值大的,如果有则得到结果返回。如果没有,则回到treeMap的头,取第一个返回结果。 
如图中所示:根据key计算出hash,去treeMap查找比key哈希大的那部分,取出第一个值就是结果。如果没有别key哈希大的部分,则取treeMap的第一个值。 

代码的实现: 

Java代码  收藏代码
  1. private final Long findPointFor(Long hv) {  
  2.   
  3.         SortedMap<Long, String> tmap = this.consistentBuckets.tailMap(hv);  
  4.   
  5.         return (tmap.isEmpty()) ? this.consistentBuckets.firstKey() : tmap.firstKey();  

三    Jedis中一致性哈希的实现:

[java] view plaincopyprint?
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);  
            // for big-endian version, do this first:   
            // finish.position(8-buf.remaining());   
            finish.put(buf).rewind();  
            h ^= finish.getLong();  
            h *= m;  
        }  
  
        h ^= h >>> r;  
        h *= m;  
        h ^= h >>> r;  
  
        buf.order(byteOrder);  
        return h;  
    }  
  
} 

四  一致性哈希算法的优化----关于如何保正在环中增加新节点时,命中率不受影响

我们知道,在没有对数据做冗余存储的情况下,无论是一致性哈希还是求余数分布式算法,在新增或删除memcached节点时,命中率都会不同程度的降低。

本文旨在解决当新增redis节点时,如何保证命中率不变。

基本原理

   当新增一个redis节点时,将该新节点的下一个节点的且属于该新节点的数据迁移过来。

原理描述

如图1所示,假设当前哈希环上有nmemcached节点,记为M1~Mn,存储到这些节点上的数据的有效期都是一致的,记为Te

因此从图1可以看出,从M1Mk区间的数据均从Mk上存取。比如数据K1K2Kn

         
                  
    

                                                                       

当新增节点Mx时,如图2所示。

                  
                     
       
此时数据K1K2从新节点Mx读取不到的,但节点Mk存储了这些数据,我们需要做的就是将这些数据迁移到新节点Mx

 

具体做法是:


  将新加入的节点Mx标记为NNew)状态,表示该节点是新增的。在N状态下读取数据K1的步骤为:

1)Mx读取数据,如果读取得到,则返回,否则进行2);

2)Mk读取数据,如果读取不到,则返回,否则进行3);

3)将读取到的数据K1写入Mx

4)K1Mk删除;

   在New状态下,不断进行上面的4个步骤;


因为数据的有效期是Te,所以在经过Te时间后,Mk上的数据随之自动失效了,此时将Mx标记为OOld)状态,在O状态下,如果读取不到数据也立即返回,无需再次到它下一个节点尝试读取。



  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值