专栏首页LieBrotherDubbo 的负载均衡策略:一致性哈希策略

Dubbo 的负载均衡策略:一致性哈希策略

本文简单介绍 Dubbo 中的负载均衡策略:一致性哈希策略。

1

一致性哈希策略

一致性哈希策略:指的是相同参数的请求总是发到同一个提供者实例。

简单描述一下一致性哈希,如下图所示。假设有 8 ,组成下面 A - H 的 8 个虚拟节点,A - H 哈希值分别是递增的,当有一个请求进来,通过计算参数的哈希值,比如该值刚好是 1 的位置,那么选中比 1 的值大的最小的那个实例,则结果该请求选中 C 虚拟节点;假如请求的参数的哈希值是在 2 的位置,那么找不到比 2 的值大的最小的实例,则选中所有实例中最小的一个,也就是 A。

2

Dubbo 实现方式

Dubbo 在使用一致性哈希的时候,会用到如下配置:

  1. 配置对哪些参数做哈希计算,默认是只对第一个参数: <dubbo:parameter key="hash.arguments" value="0,1" />
  2. 配置需要多少份虚拟节点,也就是一个提供服务的实例需要冗余多少份,默认是160份虚拟节点 <dubbo:parameter key="hash.nodes" value="320" />

Dubbo 在实现一致性哈希策略的时候使用了虚拟节点的概念,就是扩大实例个数,当然不是真的有那么多个实例,比如有 5 个实例,默认是 160 个备份,则真实的虚拟节点有 5 * 160 = 800 个。

为什么要采取虚拟节点?

假如不使用虚拟节点,只有 5 个节点,当移除掉 1 个节点的时候,会导致将改移除节点的请求分配到另外 4 个节点的时候不平衡,哈希计算并不保证平衡。当使用虚拟节点的时候,有更多个节点可分配,会更加均匀的分配到其他节点上。

3

一致性哈希策略的优点

  • 优点:在一些需要将同一个请求参数对应到同个服务的场景下很适合;当对实例进行增加或者删除的时候,能避免引起很大的变动。

4

ConsistentHashLoadBalance 源码

public class ConsistentHashLoadBalance extends AbstractLoadBalance {
    public static final String NAME = "consistenthash";

    // 记录所有
    private final ConcurrentMap<String, ConsistentHashSelector<?>> selectors = new ConcurrentHashMap<String, ConsistentHashSelector<?>>();

    @SuppressWarnings("unchecked")
    @Override
    protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
        String methodName = RpcUtils.getMethodName(invocation);
        String key = invokers.get(0).getUrl().getServiceKey() + "." + methodName;
        // 计算哈希值 key,通过这个字段来识别出调用实例是否有变化,有变化则重新创建 ConsistentHashSelector
        int identityHashCode = System.identityHashCode(invokers);
        ConsistentHashSelector<T> selector = (ConsistentHashSelector<T>) selectors.get(key);
        if (selector == null || selector.identityHashCode != identityHashCode) {
            // 没有存在 selector 或者 invokers 实例有变化,重新创建
            selectors.put(key, new ConsistentHashSelector<T>(invokers, methodName, identityHashCode));
            selector = (ConsistentHashSelector<T>) selectors.get(key);
        }
        return selector.select(invocation);
    }

    private static final class ConsistentHashSelector<T> {

        private final TreeMap<Long, Invoker<T>> virtualInvokers;

        private final int replicaNumber;

        private final int identityHashCode;

        private final int[] argumentIndex;

        ConsistentHashSelector(List<Invoker<T>> invokers, String methodName, int identityHashCode) {
            // 虚拟节点
            this.virtualInvokers = new TreeMap<Long, Invoker<T>>();
            this.identityHashCode = identityHashCode;
            URL url = invokers.get(0).getUrl();
            // 虚拟节点数量
            this.replicaNumber = url.getMethodParameter(methodName, "hash.nodes", 160);
            // 需要哈希的参数,默认是第一个参数
            String[] index = Constants.COMMA_SPLIT_PATTERN.split(url.getMethodParameter(methodName, "hash.arguments", "0"));
            // 记录需要哈希的参数的下标
            argumentIndex = new int[index.length];
            for (int i = 0; i < index.length; i++) {
                argumentIndex[i] = Integer.parseInt(index[i]);
            }
            for (Invoker<T> invoker : invokers) {
                String address = invoker.getUrl().getAddress();
                // 每一个实例存 replicaNumber 份虚拟节点
                for (int i = 0; i < replicaNumber / 4; i++) {
                    // 先将虚拟节点分为 (replicaNumber/4) 份,先做 MD5
                    byte[] digest = md5(address + i);
                    for (int h = 0; h < 4; h++) {
                        //再做哈希计算,得到 m,作为 Key
                        long m = hash(digest, h);
                        // 存到虚拟节点
                        virtualInvokers.put(m, invoker);
                    }
                }
            }
        }

        public Invoker<T> select(Invocation invocation) {
            // 得出要进行哈希的字符串
            String key = toKey(invocation.getArguments());
            // 进行 MD5
            byte[] digest = md5(key);
            return selectForKey(hash(digest, 0));
        }

        // 根据 hash.arguments 配置的对多少个参数进行哈希,得出拼接后的字符串
        private String toKey(Object[] args) {
            StringBuilder buf = new StringBuilder();
            for (int i : argumentIndex) {
                if (i >= 0 && i < args.length) {
                    buf.append(args[i]);
                }
            }
            return buf.toString();
        }

        // 选择实例
        private Invoker<T> selectForKey(long hash) {
            // 找出大于或等于 hash 中最小的键值对
            Map.Entry<Long, Invoker<T>> entry = virtualInvokers.ceilingEntry(hash);
            if (entry == null) {
                // 找出最小的键值对
                entry = virtualInvokers.firstEntry();
            }
            return entry.getValue();
        }

        // 计算哈希值
        private long hash(byte[] digest, int number) {
            return (((long) (digest[3 + number * 4] & 0xFF) << 24)
                    | ((long) (digest[2 + number * 4] & 0xFF) << 16)
                    | ((long) (digest[1 + number * 4] & 0xFF) << 8)
                    | (digest[number * 4] & 0xFF))
                    & 0xFFFFFFFFL;
        }

        // MD5 加密
        private byte[] md5(String value) {
            MessageDigest md5;
            try {
                md5 = MessageDigest.getInstance("MD5");
            } catch (NoSuchAlgorithmException e) {
                throw new IllegalStateException(e.getMessage(), e);
            }
            md5.reset();
            byte[] bytes;
            try {
                bytes = value.getBytes("UTF-8");
            } catch (UnsupportedEncodingException e) {
                throw new IllegalStateException(e.getMessage(), e);
            }
            md5.update(bytes);
            return md5.digest();
        }

    }

}

参考文章:

https://web.archive.org/web/20120605030524/http://weblogs.java.net/blog/tomwhite/archive/2007/11/consistent_hash.html

https://blog.csdn.net/cywosp/article/details/23397179/

http://www.zsythink.net/archives/1182

做个有梦想的程序猿

本文分享自微信公众号 - LieBrother(gh_b6002a4cbc1f),作者:JamesCSH

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2018-08-20

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 接口隔离原则

    1. Clients should not be forced to depend upon interfaces that they don't use.(客...

    LieBrother
  • Linux 中不用再 ↑ ↓ 了

    Linux 下,如果要执行一条或多条之前输过的指令,要怎么处理?很多人会想到使用上下箭头去翻查历史输入的命令。这当然是可以了,除了这种方法,本文再介绍另外 5 ...

    LieBrother
  • 结构型模式:适配器模式

    姓名 :适配器模式 英文名 :Adapter Pattern 价值观 :老媒人,牵线搭桥 个人介绍 : Convert the interface of a c...

    LieBrother
  • 10分钟了解一致性hash算法

    当我们的数据表超过500万条或更多时,我们就会考虑到采用分库分表;当我们的系统使用了一台缓存服务器还是不能满足的时候,我们会使用多台缓存服务器,那我们如何去访问...

    Edison.Ma
  • 区块链101:Ethereum如何扩展

    和其他公共区块链一样,ethereum打算尽可能多地支持用户。 问题是,今天,我们还不知道这个平台的极限。 由于每个块的计算都有硬编码的限制,ethereum区...

    首席架构师智库
  • 基于consul的Redis高可用方案

    这几天在研究如何做Redis的高可用容灾方案,查询了资料和咨询DBA同行,了解到Redis可以基于consul和sentinel实现读写分离以及HA高可用方案。...

    用户1278550
  • 2012年10月9号阿里巴巴笔试(c++)

    http://blog.csdn.net/liuzhanchen1987/article/details/8058177#comments

    bear_fish
  • P1004 方格取数

    题目描述 设有N*N的方格图(N<=9),我们将其中的某些方格中填入正整数,而其他的方格中则放 人数字0。如下图所示(见样例): A 0 0 0 0 ...

    attack
  • 从Storyboard到DIY实现一个漫画生成器-01

    用户只需拍摄一段视频并将其加载到 Storyboard 中即可将视频转换为单页漫画的布局。该应用会自动选择有趣的帧,并将其应用于6种视觉样式中的一种。生成的漫画...

    mixlab
  • 一个「学渣」从零开始的Web前端自学之路

    从 13 年专科毕业开始,一路跌跌撞撞走了很多弯路,做过餐厅服务员,进过工厂干过流水线,做过客服,干过电话销售可以说经历相当的“丰富”。

    六小登登

扫码关注云+社区

领取腾讯云代金券