美文网首页我爱编程
一致性哈希算法(Consistent Hashing)

一致性哈希算法(Consistent Hashing)

作者: 邱simple | 来源:发表于2018-04-16 20:39 被阅读0次

    一致性哈希算法(Consistent Hashing )

    一、问题

    分布式架构中,无论是数据缓存还是持久化存储,都需要考虑负载均衡的问题,期望将对后台的访问或者数据存储能够尽可能地“平均”分发给每一个服务器节点。

    为满足上述需求,最容易想到的一个办法就是hash,假设有N个节点,根据hash(obj)%N的结果来判断应该访问哪一个节点。理论上,如果能保证hash函数的随机性,则能够实现负载均衡的需求。然而,在实际情况中,节点具有不可预测的运行故障,如果集群中的某个节点宕机,那么节点数量就从N变成了N-1,若使用hash(obj)%N的方法实现负载均衡,此时将有(N-1)/N的数据发生迁移(即有(N-1)/N的数据hash(obj)%N != hash(obj)%(N-1),这个结论的推导文末会说明)。这将是一次规模很大的数据迁移,几乎之前所有的结果都将重新计算。同时,若集群规模不足以满足需求,需要扩充服务器节点时(节点数量从N变到N+1),也将发生类似问题。此时,基本的散列取余的方法非常低效。

    上述问题其实是不满足hash函数的单调性,这是衡量hash函数好坏的一个指标。具体请见一致性hash(consistent hashing)

    二、算法原理

    2.1 算法简析

    一致性哈希算法在1997年由麻省理工学院的Karger等人提出,用以解决分布式Cache的负载均衡问题。这是个能够保证单调性的hash算法。对于有N个节点的集群,最常见的hash(obj)%N算法首先计算出数据对象的hashcode,然后映射到0~N-1的N点环形空间中。而Consistent Hashing则将对象映射到0~232-1的环形空间中,同时注意,这里的对象包括服务器节点对象和数据对象。如下图所示:

    consistent hashing的映射空间

    为什么选择232这个数呢?因为hash算法所得结果一般是一个unsigned int类型。

    因此,算法的步骤如下:

    1. 计算节点的hashcode,并将其映射在[0, 232-1]环形空间中(hash(node)%232)。
    2. 计算数据对象的hashcode,并映射到[0, 232-1]环形空间中(hash(obj)%232)。
    3. 在映射空间中,根据顺时针方向,数据对象找到离其最近的节点,即为该数据的存储(缓存)位置。

    下图形象地描述了算法的原理。


    consistent hashing 算法原理

    CacheA, CacheB, CacheC表示三个节点,object1, object2, object3, object4表示四个数据对象。根据图中的映射结果,我们可以看出存储的分布为:{CacheA: [object1]}, {CacheB: [object4]}, {CacheC: [object2, object3]}。

    那么回到一开始的问题,当节点增加或者减少时,Consistent Hashing算法是如何保证单调性的?

    2.2 移除节点

    假如此时CacheB节点宕机,从映射空间中移除。则根据顺时针查找的原理,object4将发现离它最近的节点变成了了CacheC,此时数据分布变成了:{CacheA: [object1]}, {CacheC: [object2, object3, object4]}。

    移除节点

    可以看出,只有object4发生了数据迁移,或者说只有CacheB存储的数据发生了迁移,而原本存储在其他节点上的数据依然维持原来的分布。

    2.3 增加节点

    当扩容时,增加一个节点CacheD,根据计算,映射在如下的位置上,则object2发现离他最近的节点变成了CacheD,则数据分布发生变化:{CacheA: [object1]}, {CacheB: [object4]}, {CacheC: [object3]}, {CacheD: [object2]}。

    增加节点

    此时,只有object2从CacheC迁移到CacheD上,而其他数据依然维持原来的分布关系。

    这就满足了hash算法的单调性
    内容通过hash算法分布到相应的节点中,若此时增加一个新节点,则旧节点中的内容要不就保持原来的分布关系,要不就分布到新增的节点中,而不会分布到其他的旧节点中。

    2.4 虚拟节点(Virtual nodes)

    为了衡量算法的稳定性,我们常常考虑一下极端场景。比如上面那张“移除节点”的图,当节点的数量比较少时,只有CacheA和CacheC,CacheA节点只存储一个数据,而CacheC却存储了3个数据,此时数据分布不均匀。

    虽然是极端情况,但却很具备现实意义,毕竟常见的集群规模与232比都是微不足道了,极有可能发生上述的数据分布不均匀的情况,或者说平衡性问题

    为此引入虚拟节点。

    虚拟节点

    如上图所示,CacheA2和CacheC2分别是CacheA1和CacheC1的虚拟节点,同样映射到了[0, 232-1]圆环空间。此时,逻辑上的数据分布为:{CacheA1: [object2]}, {CacheA2: [object1]}, {CacheC1: [object3]}, {CacheC2: [object4]}。

    要注意的是,虚拟节点之所以是虚拟的,是因为它们仅仅是逻辑上存在的,数据存储的物理位置是其对应的真实节点。所以,物理上的数据分布为:{CacheA1: [object1, object2]}, {CacheC1: [object3, object4]}。

    对比引入虚拟节点之前的数据分布:{CacheA1: [object1, object2, object4]}, {CacheC1: [object3]}。可以发现,虚拟节点的引入改善了consistent hashing算法的平衡性。

    如果要引入虚拟节点,则引入的数量应当如何选择呢?

    虚拟节点的引入数量

    上图横轴为虚拟节点的倍数,纵轴为真实节点的个数。蓝线表示为保证hash算法的平衡性,节点数量与虚拟节点副本倍数的关系。仅供参考。

    除了解决平衡性问题,节点的异构性也能通过引入虚拟节点加以解决。异构性通俗的讲就是不同服务器节点的存储、算力等性能是不一样的,一个完美的hash算法是连节点的异构性都能考虑进去的。根据每个节点的性能算出一个量化的权重,根据这个权重来计算这个节点引入虚拟节点的数量。性能差的,虚拟节点少点,分布的数据就少点,让他放轻松,别紧张。性能好的,就多整点,对着他干就对了。

    三、代码实现

    使用Java实现Consistent hashing。

    package cn.edu.njupt.qyz;
    
    import java.util.Collection;
    import java.util.SortedMap;
    import java.util.TreeMap;
    
    public class ConsistentHashing<T> {
    
        // hash函数
        private final HashFunction hashFunction;
        // 虚拟节点副本数
        private final int numberOfReplicas;
        // 映射圆环
        private final SortedMap<Integer, T> circle = new TreeMap<Integer, T>();
    
        // 构造方法
        public ConsistentHashing(HashFunction hashFunction, int numberOfReplicas, Collection<T> nodes) {
            this.hashFunction = hashFunction;
            this.numberOfReplicas = numberOfReplicas;
    
            for (T node : nodes) {
                add(node);
            }
        }
    
        // 将服务器节点映射到圆环上,同时加入对应的虚拟节点
        public void add(T node) {
            for (int i = 0; i < numberOfReplicas; i++) {
                circle.put(hashFunction.hash(node.toString() + i), node);
            }
        }
    
        // 移除映射,同时移除虚拟节点
        public void remove(T node) {
            for (int i = 0; i < numberOfReplicas; i++) {
                circle.remove(hashFunction.hash(node.toString() + i));
            }
        }
        // 根据传入的obj找到对应的节点
        public T get(Object key) {
            if (circle.isEmpty()) {
                return null;
            }
            int hash = hashFunction.hash(key);
            if (!circle.containsKey(hash)) {
                // 从比obj的hashcode更大的Map中找,即顺时针方向找node
                SortedMap<Integer, T> tailMap = circle.tailMap(hash);
                // 环形空间,找到顺时针方向第一个node,并将其hashcode赋值
                hash = tailMap.isEmpty() ? circle.firstKey() : tailMap.firstKey();
            }
            // 返回找到的节点
            return circle.get(hash);
        }
    }
    

    代码使用具有排序能力的的TreeMap实现环形映射结构,首先将节点加入到TreeMap中,然后定义一个get方法,根据传入的key对象来查找对应的节点。

    四、总结

    为了解决分布式架构的负载均衡问题,并保证hash算法的单调性,Consistent Hashing算法应运而生。同时,为了保证算法的平衡性,引入虚拟节点以达到令数据均匀分布在各个节点的目的。本文介绍了Consistent Hashing算法的基本原理,并进行了代码实现。

    五、参考资料

    1. Consistent Hashing算法
    2. 一致性hash算法:jump Consistent hash(零内存消耗,均匀,快速,简洁)
    3. 一致性hash(consistent hashing)

    推导

    对hash(obj)%N的结果呈[0, N-1]循环,hash(obj)%(N-1)的结果为[0, N-2]的循环。N和N-1的最小公倍数为N(N-1),则二者的结果每隔N(N-1)个数据发生一次重叠,而每次重叠有N-1个结果是相等的,且有(N-1)2个结果不相等,因此全部的数据中有(N-1)/N的不相等,将发生数据迁移。可以参考下表。

    hashCode N=2 N=3 N=4 N=5
    0 0 0 0 0
    1 1 1 1 1
    2 0 2 2 2
    3 1 0 3 3
    4 0 1 0 4
    5 1 2 1 0
    6 0 0 2 1
    7 1 1 3 2
    8 0 2 0 3
    9 1 0 1 4
    10 0 1 2 0
    11 1 2 3 1
    12 0 0 0 2
    13 1 1 1 3
    14 0 2 2 4

    相关文章

      网友评论

        本文标题:一致性哈希算法(Consistent Hashing)

        本文链接:https://www.haomeiwen.com/subject/xrkchftx.html