一致性哈希算法(Java实现)

前言

一致性哈希算法在分布式系统的应用中是十分广泛的。常见的应用场景是分布式缓存。它主要解决了哈希取模算法在分布式系统中存在的动态伸缩等问题。

哈希取模算法的局限性

在分布式缓存集群中,当新增加缓存服务器或其中一台挂掉后,由路由算法发生改变,导致大量的缓存数据不能命中。从而造成数据库面临巨大压力而崩溃,可能导致整个系统崩溃。

一致性哈希算法原理

一致性哈希算法通过一个叫作一致性哈希环的数据结构实现。这个环的起点是0,终点是2^{32}-1,并且起点和终点相连接,故这个环的整数分布范围是[0, 2^{32}-1]

将服务器节点和key放到哈希环上

我们将服务器节点和key的hash值放置到哈希环上,如下图:

服务器节点分别是NODE0、NODE1、NODE2。key分别代表key1 ~ key8。

将key和服务器节点都放置到同一个哈希环后,在哈希环上顺时针查找距离这个 key 的 hash 值最近的机器,即是这个key所属的机器。

key1、key8在节点NODE0上;key2、key5在节点NODE1上;key3、key4、key6在节点NODE2上。

增加服务器(扩容)

由于业务需要,如缓存集群压力过大,我们需要增加一台服务器(NODE3)。经过hash函数计算,NODE3节点落在NODE1和NODE2之间。如下图:

对上述情况,只有NODE1和NODE2节点之间的key需要重新分配。key4没有变,还在NODE2节点。只是key3、key6重新分配新的节点NODE3上。我们发现,一致性哈希算法只需要很少部分key需要重新分配。而哈希取模方式则大部分缓存会失效。

减少服务器(缩容)

由于某个服务器出现故障导致下线,如NODE3下线,这时原本key3、key6存储在NODE3上,需要重新被分配至NODE2节点上,其它key不受此影响。

服务器节点分布不均匀

  1. 如果服务器节点不是均匀的分布在哈希环上,那么有可能造成服务节点负载压力不均衡。
  2. 当新增加一台服务器时,节点NODE3只是分担了节点NODE2的压力,造成服务器压力不均摊。显然这个结果不是我们期望的。

针对这个问题,我们可以通过引入虚拟节点来解决负载不均衡的问题。即将每台物理服务器虚拟为一组虚拟服务器,将虚拟服务器放置到哈希环上,如果要确定key所在的服务器,需先确定key所在的虚拟服务器,再由虚拟服务器确定物理服务器。

基于虚拟节点的一致性哈希

一台物理服务器,虚拟成若干个虚拟节点,随机分布在环上,压力近似均衡分摊。如有三台物理服务器,每台物理服务器虚拟出150个虚拟节点,随机分配在Hash环上的150个位置上。最后可使三台物理服务器近似均摊压力。当增加一台服务器时,先虚拟150个节点,然后散落在哈希环上。

一致性哈希算法实现

Java代码:

public class ConsistentHashing {

    private SortedMap<Integer, Node> hashCircle = new TreeMap<>();
    private int virtualNums; // 虚拟节点数

    public ConsistentHashing(Node[] nodes, int virtualNums) {
        this.virtualNums = virtualNums;
        // 初始化一致性hash环
        for (Node node : nodes) {
            // 创建虚拟节点
            add(node);
        }
    }

    /**
     * 添加服务器节点
     *
     * @param node the server
     */
    public void add(Node node) {
        for (int i = 0; i < virtualNums; i++) {
            hashCircle.put(hash(node.toString() + i), node);
        }
    }

    /**
     * 删除服务器节点
     *
     * @param node the server
     */
    public void remove(Node node) {
        for (int i = 0; i < virtualNums; i++) {
            hashCircle.remove(hash(node.toString() + i));
        }
    }

    /**
     * 获取服务器节点
     *
     * @param key the key
     * @return the server
     */
    public Node getNode(String key) {
        if (key == null || hashCircle.isEmpty())
            return null;
        int hash = hash(key);
        if (!hashCircle.containsKey(hash)) {
            // 未命中对应的节点
            SortedMap<Integer, Node> tailMap = hashCircle.tailMap(hash);
            hash = tailMap.isEmpty() ? hashCircle.firstKey() : tailMap.firstKey();
        }
        return hashCircle.get(hash);
    }

    /**
     * FNV1_32_HASH算法
     *
     * @param key the key
     * @return
     */
    private int hash(String key) {
        final int p = 16777619;
        int hash = (int) 2166136261L;
        for (int i = 0; i < key.length(); i++) {
            hash = (hash ^ key.charAt(i)) * p;
        }
        hash += hash << 13;
        hash ^= hash >> 7;
        hash += hash << 3;
        hash ^= hash >> 17;
        hash += hash << 5;
        // 如果算出来的值为负数则取其绝对值
        if (hash < 0) {
            hash = Math.abs(hash);
        }
        return hash;
    }

    /**
     * 集群节点的机器地址
     */
    public static class Node {
        private String ipAddr;
        private int port;
        private String name;

        public Node(String ipAddr, int port, String name) {
            this.ipAddr = ipAddr;
            this.port = port;
            this.name = name;
        }

        @Override
        public String toString() {
            return name + ":<" + ipAddr + ":" + port + ">";
        }
    }
}

评估服务器节点的负载均衡性

我们通过方差计算服务器节点的均衡性,代码如下:

public class ConsistentHashingTest {

    public static void main(String[] args) {
        ConsistentHashing.Node[] nodes = new ConsistentHashing.Node[4];
        Map<ConsistentHashing.Node, List<String>> map = new HashMap<>();

        // make nodes 4台服务器节点
        for (int i = 0; i < nodes.length; i++) {
            nodes[i] = new ConsistentHashing.Node("10.1.32.2" + i, 8070, "myNode" + i);
        }

        ConsistentHashing ch = new ConsistentHashing(nodes, 160);

        // make keys 100万个key
        String[] keys = new String[1_000_000];
        for (int i = 0; i < keys.length; i++) {
            keys[i] = "key" + (i + 17) + "ss" + (i * 19);
        }

        // make results
        for (String key : keys) {
            ConsistentHashing.Node n = ch.getNode(key);
            List<String> list = map.computeIfAbsent(n, k -> new ArrayList<>());
            list.add(key);
        }

        // 统计标准差,评估服务器节点的负载均衡性
        int[] loads = new int[nodes.length];
        int x = 0;
        for (Iterator<ConsistentHashing.Node> i = map.keySet().iterator(); i.hasNext(); ) {
            ConsistentHashing.Node key = i.next();
            List<String> list = map.get(key);
            loads[x++] = list.size();
        }
        int min = Integer.MAX_VALUE;
        int max = 0;
        for (int load : loads) {
            min = Math.min(min, load);
            max = Math.max(max, load);
        }
        System.out.println("最小值: " + min + "; 最大值: " + max);
        System.out.println("方差:" + variance(loads));
    }

    public static double variance(int[] data) {
        double variance = 0;
        double expect = (double) sum(data) / data.length;
        for (double datum : data) {
            variance += (Math.pow(datum - expect, 2));
        }
        variance /= data.length;
        return Math.sqrt(variance);
    }

    private static int sum(int[] data) {
        int sum = 0;
        for (int i = 0; i < data.length; i++) {
            sum += data[i];
        }
        return sum;
    }
}

测试结果:

# 虚拟节点是 120
最小值: 243919; 最大值: 253236
方差:3692.7378054771234

# 虚拟节点是 130
最小值: 240190; 最大值: 257384
方差:7432.346466628153

# 虚拟节点是 150
最小值: 233002; 最大值: 260369
方差:10227.744937179456

# 虚拟节点是 160
最小值: 241154; 最大值: 253743
方差:5150.429156876153

# 虚拟节点是 170
最小值: 235938; 最大值: 260044
方差:9244.906895150432

# 虚拟节点是 200
最小值: 233187; 最大值: 263222
方差:11395.83342717855

通过测试,每台物理服务的虚拟节点在120到200之间,均衡性更好。

总结

一致性hash算法解决了分布式环境下机器增加或者减少时,简单的取模运算无法获取较高命中率的问题。通过虚拟节点的使用,一致性hash算法可以均匀分担机器的负载,使得这一算法更具现实的意义。正因如此,一致性hash算法被广泛应用于分布式系统中。

参考资源

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

推荐阅读更多精彩内容