最新版本dubbo源碼之LoadBalance(二)- ConsistentHashLoadBalance

寫在開頭

      本文是接上篇文章以後的第二篇,主要是一致性hash算法的實現,下面會經過詳細註解來描述流程。有不清楚的能夠評論留言。java

開始正文

      首先咱們來看下ConsistentHashLoadBalance的核心結構,先有個整體的概念,而後順着方法調用流程逐步往下講。node

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

    //一個方法對應一個一致性hash選擇器
    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) {
        // 精確到方法,最爲緩存一致性hash選擇器的key
        String methodName = RpcUtils.getMethodName(invocation);
        String key = invokers.get(0).getUrl().getServiceKey() + "." + methodName;
        int identityHashCode = System.identityHashCode(invokers);  //基於invoker集合,根據對象內存地址來定義hash值
        //得到ConsistentHashSelector,爲空則建立再緩存(要麼第一次,要麼invokers集合發生了變化,都須要從新建立ConsistentHashSelector)
        ConsistentHashSelector<T> selector = (ConsistentHashSelector<T>) selectors.get(key);
        if (selector == null || selector.identityHashCode != identityHashCode) {
            //初始化並緩存一致性hash選擇器
            selectors.put(key, new ConsistentHashSelector<T>(invokers, methodName, identityHashCode));
            selector = (ConsistentHashSelector<T>) selectors.get(key);
        }
        return selector.select(invocation);  //選擇一個invoker
    }
    //接下來要講下一致性hash選擇器了
    private static final class ConsistentHashSelector<T> {
        //.....
    }   
}複製代碼

      上面的doSelect方法主要的做用就是建立和初始化了一致性hash選擇器。下面咱們看下這個選擇器是如何實現負載均衡的。算法

private static final class ConsistentHashSelector<T> {
    //treemap底層是棵紅黑樹。
    //紅黑樹不瞭解的,能夠看下個人博客 https://juejin.im/post/5bef5de46fb9a049c15ecbd9
    private final TreeMap<Long, Invoker<T>> virtualInvokers;

    private final int replicaNumber;  //每一個invoker對應的虛擬節點數

    private final int identityHashCode;  //定義hash值

    private final int[] argumentIndex;  //參數位置數組(缺省是第一個參數)

    ConsistentHashSelector(List<Invoker<T>> invokers, String methodName, int identityHashCode) {
        this.virtualInvokers = new TreeMap<Long, Invoker<T>>();  // 虛擬節點與 Invoker 的映射關係
        this.identityHashCode = identityHashCode;
        URL url = invokers.get(0).getUrl();
        //缺省用 160 份虛擬節點,若是要修改,請配置 <dubbo:parameter key="hash.nodes" value="320" />
        this.replicaNumber = url.getMethodParameter(methodName, "hash.nodes", 160);
        //缺省只對第一個參數 Hash,若是要修改,請配置 <dubbo:parameter key="hash.arguments" value="0,1" />
        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]);
        }
        //初始化virtualInvokers,這樣能夠最終每一個invoker都有replicaNumber個虛擬節點
        for (Invoker<T> invoker : invokers) {
            String address = invoker.getUrl().getAddress();
            //每4個虛擬節點爲一組,4個節點一共32位。之因此是32位是由於一致性哈希環取值範圍爲0~2^32
            for (int i = 0; i < replicaNumber / 4; i++) {
                // 地址加上後綴數字,計算md5值
                byte[] digest = md5(address + i);
                // Md5是一個16字節長度的數組,將16字節的數組每四個字節一組,分別對應一個虛擬結點
                for (int h = 0; h < 4; h++) {
                    //計算hash值,做爲key,存儲節點
                    long m = hash(digest, h);
                    virtualInvokers.put(m, invoker);
                }
            }
        }
    }

    public Invoker<T> select(Invocation invocation) {
        String key = toKey(invocation.getArguments());  //參數轉換爲string類型
        byte[] digest = md5(key);  // key md5計算,返回16個字節的byte數組
        // 一、hash(digest, 0),取前四個字節,計算hash值
        // 二、查找臨近節點,獲取invoker
        return selectForKey(hash(digest, 0));
    }

    //這個方法太簡單
    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) {
        //返回大於或等於給定鍵,若是不存在這樣的鍵的鍵 - 值映射,則返回null相關聯(treemap底層是棵紅黑樹,紅黑樹不瞭解的,能夠看下個人博客 https://juejin.im/post/5bef5de46fb9a049c15ecbd9 )
        Map.Entry<Long, Invoker<T>> entry = virtualInvokers.ceilingEntry(hash);
        if (entry == null) {
            //若是不存在,那麼可能這個hash值比虛擬節點的最大值還大,那麼取第一個。這樣就造成了一個環
            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;
    }

    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();
    }
}複製代碼

小結

      其實ConsistentHashLoadBalance就是實現了一致性hash算法,以前瞭解這種算法的同窗可能很輕鬆就能看懂。dubbo的實現其實也很簡單,步驟總結以下:數組

一、生成虛擬節點:每一個invoker生成replicaNumber個hash值,即產生replicaNumber個虛擬節點。緩存

二、請求者生成hash值:請求方法進來,會按配置的值,取指定的參數,拼接字符串,生成md5值,再取數組的前4個值生成hash值。bash

三、取臨近節點:根據步驟2生成的hash值,取treemap中獲取大於等於該hash值的節點。若是節點不存在,則取第一個節點(造成環)。app

四、返回invoker      負載均衡

相關文章
相關標籤/搜索