Dubbo一致性哈希算法在项目中的应用

下面的算法是对Dubbo源码中协调一致性Hash算法改进后在项目中做负载均衡使用:

注意:

1.Dubbo的一致性Hash算法实现逻辑: 

   对每个一个注册的服务名,创建一个选择器(ConsistentHashSelector),这个选择器中维护了一个hash环,这个Hash环里存储着所有这个RPC服务提供者的地址,所以逻辑总结就是一个RPC服务,对应一个Hash环,一个Hash环里保存了这个Rpc服务提供的地址。

2.Key的生成逻辑:

   当通过Key来选择对应的地址时,是以请求的方法加上参数生成Hash的,因为在Ketama哈希算法中,一串字符生成的Hash值是固定的。所以Dubbo的Key是以将方法的参数作为Key生成Hash的,我这里的逻辑是方法名+参数名作为Hash Key.这样保证每次请求时生成的Hash Key都不同,能够平衡的负载到各个服务器上。

/**
 * @author zyz
 */

public class ConsistentHashLoadBalance1 extends AbstractLoadBalance {

    // Key: rpcServiceName
    private final ConcurrentMap<String, ConsistentHashSelector> selectors = new ConcurrentHashMap<>();
    @Override
    protected String doSelect(List<String> serviceAddresses, RpcRequestMessage msg) {
        // 获取调用服务名
        String rpcServiceName = msg.getInterfaceName();
        // 生成调用列表hashCode
        int identityHashCode = System.identityHashCode(rpcServiceName);
        // 以调用rpcServiceName名为key,获取一致性hash选择器
        ConsistentHashSelector selector = selectors.get(rpcServiceName);
        // 若不存在则创建新的选择器
        if (selector == null || selector.getIdentityHashCode() != identityHashCode) {
            // 创建ConsistentHashSelector时会生成所有虚拟结点
            selectors.put(rpcServiceName, new ConsistentHashSelector(serviceAddresses,identityHashCode));
            // 获取选择器
            selector = selectors.get(rpcServiceName);
        }
        // 选择结点
        return selector.select(msg);
    }



    private static final class ConsistentHashSelector {

        private final TreeMap<Long, String> virtualInvokers; // 虚拟结点

        private final int replicaNumber = 160;   // 副本数

        private final int identityHashCode;// hashCode

//        private final int[]                     argumentIndex;   // 参数索引数组

        public ConsistentHashSelector(List<String> invokers, int identityHashCode) {
            // 创建TreeMap 来保存结点
            this.virtualInvokers = new TreeMap<>();
            // 生成调用结点HashCode
            this.identityHashCode = System.identityHashCode(invokers);

            // 创建虚拟结点
            // 对每个invoker生成replicaNumber个虚拟结点,并存放于TreeMap中
            for (String invoker : invokers) {

                for (int i = 0; i < replicaNumber / 4; i++) {
                    // 根据md5算法为每4个结点生成一个消息摘要,摘要长为16字节128位。 md5就是一个长16字节占128位的bit数组
                    //这里的意思就是每个节点扩展未160个虚拟节点,然后将虚拟节点分组 4 个一组,
                    //4个的原因是 md5共16字节 ,这一个组里的每个虚拟节点占用生成的md5数组中的4个字节
                    //正好4*4 所以分为4个一组
                    byte[] digest = md5(invoker + i);
                    // 随后将128位分为4部分,0-31,32-63,64-95,95-128,并生成4个32位数,存于long中,long的高32位都为0 long64位
                    // 并作为虚拟结点的key。
                    for (int h = 0; h < 4; h++) {
                        long m = hash(digest, h);
                        virtualInvokers.put(m, invoker);
                    }
                }
            }
        }

        public int getIdentityHashCode() {
            return identityHashCode;
        }

        // 选择结点
        public String select(RpcRequestMessage rpcRequestMessage) {
            // 根据调用参数来生成Key

            String key = toKey(rpcRequestMessage);
            // 根据这个参数生成消息摘要
            byte[] digest = md5(key);
            //调用hash(digest, 0),将消息摘要转换为hashCode,这里仅取0-31位来生成HashCode
            //调用sekectForKey方法选择结点。
            String invoker = sekectForKey(hash(digest, 0));
            return invoker;
        }

        private String toKey(RpcRequestMessage msg) {
            StringBuilder buf = new StringBuilder();
            // 由于hash.arguments没有进行配置,因为只取方法的第1个参数作为key
            buf.append(msg.getMethodName());
            Object[] parameters = msg.getParameters();
            for (Object o : parameters) {
                buf.append(o);
            }
            return buf.toString();
        }

        //根据hashCode选择结点
        private String sekectForKey(long hash) {
            String invoker;
            Long key = hash;
            // 若HashCode直接与某个虚拟结点的key一样,则直接返回该结点
            if (!virtualInvokers.containsKey(key)) {
                // 若不一致,找到一个最小上届的key所对应的结点。
                SortedMap<Long, String> tailMap = virtualInvokers.tailMap(key);
                // 若存在则返回,例如hashCode落在图中[1]的位置
                // 若不存在,例如hashCode落在[2]的位置,那么选择treeMap中第一个结点
                // 使用TreeMap的firstKey方法,来选择最小上界。
                if (tailMap.isEmpty()) {
                    key = virtualInvokers.firstKey();
                } else {

                    key = tailMap.firstKey();
                }
            }
            invoker = virtualInvokers.get(key);
            return invoker;
        }

    // Ketama 算法
        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[0 + number * 4] & 0xFF))
                    & 0xFFFFFFFFL;
        }

        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 = null;
            try {
                bytes = value.getBytes("UTF-8");
            } catch (UnsupportedEncodingException e) {
                throw new IllegalStateException(e.getMessage(), e);
            }
            md5.update(bytes);
            return md5.digest();
        }

    }
}

下面是原版的一致性Hash算法:

(引用自一致性hash算法及java实现_青鱼入云的博客-CSDN博客_一致性hash算法实现

 private static String[] servers = {"192.168.0.0:111", "192.168.0.1:111",
            "192.168.0.2:111", "192.168.0.3:111", "192.168.0.4:111"};

    private static List<String> realNodes=new LinkedList<>();
    private static SortedMap<Integer,String> sortedMap=new TreeMap<>();
    private static final int NUM_HOST=5;

    static {
        for (int i=0;i<servers.length;i++)
        {
            realNodes.add(servers[i]);
        }
        for (String str:realNodes)
        {
            for (int i=1;i<=NUM_HOST;i++)
            {
                String nodeName=str+i;
                int hash=getHash(nodeName);
                sortedMap.put(hash,nodeName);
                System.out.println("虚拟节点hash:" + hash + "【" + nodeName + "】放入");
            }
        }
    }
    private static String getServer(String key)
    {
        int hash=getHash(key);
        String host;
        SortedMap<Integer,String> subMap=sortedMap.tailMap(hash);
        Integer index;
        if (subMap.isEmpty())
        {
             index=sortedMap.firstKey();
             host=sortedMap.get(index);
        }else {
             index=subMap.firstKey();
             host=subMap.get(index);
        }
        return host;
    }

    private static int getHash(String str) {

        final int p = 16777619;
        int hash = (int) 2166136261L;
        for (int i = 0; i < str.length(); i++) {
            hash = (hash ^ str.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 void main(String[] args) {
        String[] keys = {"太阳", "月亮", "星星","JLU","HENY"};
        for(int i=0; i<keys.length; i++)
            System.out.println("[" + keys[i] + "]的hash值为" + getHash(keys[i])
                    + ", 被路由到结点[" + getServer(keys[i]) + "]");
    }

比较好的文章:

1>介绍一致性Hash算法极为详细 :白话解析:一致性哈希算法 consistent hashing-朱双印博客https://www.zsythink.net/archives/1182

2>源码讲解

Dubbo负载均衡:一致性Hash的实现分析_guolong1983811的专栏-CSDN博客http://blog.csdn.net/Revivedsun/article/details/71022871LoadBalance负责从多个Invoker中选出具体的一个用于本次调用,以分摊压力。Dubbo中LoadBalance结构如下图。com.alibaba.dubbo.rpc.cluster.LoadBalance 接口提供了 Invoker selechttps://blog.csdn.net/guolong1983811/article/details/78715470

おすすめ

転載: blog.csdn.net/qq_39552268/article/details/120541616