• 理解一致性哈希算法


    场景分析

    在分布式缓存的伸缩性设计中,最主要的目标就是在新加入缓存服务器后,应该使整个服务器集群中已经缓存的数据尽可能还被访问到。对于服务器集群管理,路由算法至关重要,它决定着客户端究竟该访问集群中的哪台服务器。


    余数Hash

    简单的路由算法可以使用余数Hash

    用服务器数目除缓存数据key的Hash值,余数为服务器列表下标编号。

    该思路的简单代码实现如下:

    class RemainderHash {
    
        private List<String> serverNodes;
        private int serverNodeSize;
    
        public RemainderHash(List<String> serverNodes) {
            this.serverNodes = serverNodes;
            this.serverNodeSize = serverNodes.size();
        }
    
        public String getServerNode(String key) {
            return serverNodes.get(hash(key));
        }
    
        public int hash(String key) {
            return Math.abs(key.hashCode() % serverNodeSize);
        }
    }
    

    该算法可以保证缓存数据在整个服务器集群的均衡分布。
    但是假如服务器由3台扩容至4台,扩容前后的缓存重复命中率(扩容前后,同一缓存数据仍在同一服务器上)只有24.99%,而假如服务器由100台扩容至101台,扩容前后的缓存重复命中率只有0.71%。均衡性和重复命中率的测试代码在这

    所以这种余数Hash算法不能满足实际生产需要。


    一致性Hash算法的思想

    要解决上述问题,目前比较流行的算法是一致性Hash算法,该算法通过一致性Hash环的数据结构实现key到缓存服务器的映射。

    该算法的具体过程为:
    ①先构造一个长度为232的整数环(称作一致性Hash环),
    ②根据节点名称的Hash值(其分布范围为[0, 232-1])将缓存服务器节点放置在这个Hash环上;(要保证n个服务器节点均衡地放置在环上)
    ③根据缓存数据的key的Hash值(其分布范围也为[0, 232-1])在Hash环上顺时针查找距离这个key的Hash值最近的缓存服务器节点,即为该数据应该放置的的服务器。

    一致性Hash示意图.png

    图中,以Node开头的圆形表示服务器,以key开头的方形表示缓存数据,带箭头的虚线表示缓存数据应该放置在哪台服务器。可以看出,在新增一台服务器后,受影响的缓存数据只是分布在Node2和Node4之间的一小段(顺时针)数据。

    但是,上面描述的算法存在一个问题:新加入的结点Node4只影响了Node3,原来的Node1和Node2不受影响,即扩容后,每台服务器的负载不同,没有达到缓存数据均衡分布在各台服务器上的效果。

    上述负载不均衡问题的解决方案是使用虚拟层:将每台物理服务器虚拟为一组虚拟缓存服务器,将虚拟缓存服务器的Hash值放置在Hash环上,缓存数据通过key的Hash值首先找到虚拟服务器节点,再找到这个虚拟服务器节点对应的物理服务器。
    使用虚拟节点后,缓存数据在服务器节点的分布示意图如下:(图片来源:The Simple Magic of Consistent Hashing

    使用虚拟节点的一致性Hash算法示意图.png

    这样,在新加入一个物理服务器节点时,是将一组虚拟节点(假设数量为n)加入环中,这些虚拟节点会影响n个虚拟节点,受影响的n个虚拟节点又对应不同的物理服务器,最终,新加入一台服务器将会分摊原集群中所有服务器的一部分负载,从而达到负载均衡的目的。(每个物理节点对应的虚拟节点越多,各物理节点之间的负载越均衡,但太多会影响性能。在实践中,经验值为150)。


    一致性Hash算法的实现

    public class ConsistentHash<T> {
    
        /**
         * 哈希函数
         */
        private final HashFunction hashFunction;
        /**
         * 每台物理服务器节点虚拟出虚拟节点个数
         */
        private final int numberOfReplicas;
        /**
         * Hash环
         * <p>
         * 该Map中仅存放虚拟服务器节点,并不存放实际的缓存数据!!!
         * 该Map中key为虚拟服务器节点对应在环中的位置
         * <p>
         * 使用TreeMap的原因:
         * TreeMap的内部使用红黑树(平衡查找树),
         * 因而在确定了一个缓存数据key的hash值在该环中的位置后,
         * 可以很快查找到该缓存数据应该放置的物理服务器
         */
        private final SortedMap<Integer, T> circle = new TreeMap<Integer, T>();
    
        /**
         * 构造器
         *
         * @param hashFunction     哈希函数
         * @param numberOfReplicas 每台物理服务器节点虚拟出虚拟节点个数
         * @param nodes            物理服务器节点
         */
        public ConsistentHash(HashFunction hashFunction, int numberOfReplicas, Collection<T> nodes) {
    
            this.hashFunction = hashFunction;
            this.numberOfReplicas = numberOfReplicas;
            //将物理服务器放在环上
            for (T node : nodes) {
                add(node);
            }
        }
    
        /**
         * 添加物理服务器节点
         * <p>
         * 实现:将该物理服务器节点放在环中的numberOfReplicas个位置处,
         * (添加一个物理服务器节点,就要添加numberOfReplicas个虚拟节点)
         *
         * @param node 要添加的物理服务器节点
         */
        public void add(T node) {
            for (int i = 0; i < numberOfReplicas; i++) {
                circle.put(hashFunction.hash(node.toString() + i),
                        node);
            }
        }
    
        /**
         * 移除物理服务器节点
         * <p>
         * 实现:因为该物理服务器节点放在了环中的多个位置,所以在删除时都要删除
         * (移除一个物理服务器节点,就要将该服务器节点对应的numberOfReplicas个虚拟节点移除)
         *
         * @param node 要移除的物理服务器节点
         */
        public void remove(T node) {
            for (int i = 0; i < numberOfReplicas; i++) {
                circle.remove(hashFunction.hash(node.toString() + i));
            }
        }
    
        /**
         * 根据缓存数据的key获取物理服务器节点
         *
         * @param key 缓存数据的key
         * @return 该缓存数据所在的物理服务器节点
         */
        public T get(Object key) {
            if (circle.isEmpty()) {
                return null;
            }
            int hash = hashFunction.hash(key);
            //如果缓存数据的key的哈希值没有落在虚拟服务器节点上,
            //则
            if (!circle.containsKey(hash)) {
                //获取map中key值大于该缓存数据的key的哈希值的子map
                SortedMap<Integer, T> tailMap = circle.tailMap(hash);
                //如果该子map不为空,则返回该子map的第一个元素(因为该map是排序好的,第一个即是最小的元素)
                //如果该子map为空,说明该缓存数据的key的哈希值超出了哈希环中最后的那个虚拟服务器节点对应的位置(顺时针),
                //                则将该缓存数据放在哈希环中第一个虚拟服务器节点中(顺时针)
                hash = tailMap.isEmpty() ? circle.firstKey() : tailMap.firstKey();
            }
            return circle.get(hash);
        }
    
    }
    
    
    class HashFunction {
    
        int hash(Object key) {
            return key.hashCode();
        }
    }
    

    上述代码中,Hash算法有待改进,因为Hash算法至关重要,大部分Object的hashCode()方法不能很好地得到分布均衡的哈希值,比如,通常这些哈希值都是一定范围内的比较小的整数。

    目前流行的 Hash 算法包括 MD5、SHA-1 和 SHA-2,但由于MD5碰撞案例较多,因此,MD5在数年前就已经不被推荐作为应用中的散列算法方案,取代它的是安全散列算法

    //TODO 哈希算法归类

    那么如何使用一致性Hash呢?通常,只会在别人的依赖库里遇到一致性Hash算法,而不是自己编写。比如,大部分的支持分布式的缓存组件的客户端都支持一致性哈希。值得注意的是,仅仅客户端需要实现一致性哈希算法,缓存服务器除了缓存数据,什么都不用做。


    参考

    ①《大型网站技术架构--核心原理与案例分析》 :(文字描述部分)
    Consistent Hashing :(一致性算法的代码)
    Hash算法总结
    The Simple Magic of Consistent Hashing



    作者:maxwellyue
    链接:https://www.jianshu.com/p/05ad6637e66b
    来源:简书
    简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。
  • 相关阅读:
    PHP递归方法实现前序、中序、后序遍历二叉树
    php循环方法实现先序、中序、后序遍历二叉树
    Mac charles 抓取https请求,安装证书后还是显示unknown
    PHP工厂模式
    PHP策略模式2
    PHP单例模式
    PHP 面试知识点整理归纳
    十大迷你iPhone天气应用
    来自极客标签10款最新设计素材-系列十三
    帮助快速生成页面固定显示元素的jQuery插件
  • 原文地址:https://www.cnblogs.com/xiaoshen666/p/11258597.html
Copyright © 2020-2023  润新知