理解一致性哈希算法


场景分析

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


余数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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,012评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,589评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,819评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,652评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,954评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,381评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,687评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,404评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,082评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,355评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,880评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,249评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,864评论 3 232
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,007评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,760评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,394评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,281评论 2 259