dubbo負載均衡代碼分析2(一致性hash策略)

接上篇http://www.javashuo.com/article/p-fjpmnwyl-db.htmljava

這篇說dubbo一致性hash負載均衡策略。要先大體瞭解下,什麼是一直性hash算法。
一致性hash算法最先是用來解決,分佈式緩存在有節點變更(新增後者刪除)後,節點負載不均衡問題的。
而用一致性hash算法,就是爲了達到,當集羣中有節點加入或者節點刪除時,儘可能把負載的變化(加負,減負)均攤到每個節點。
而沒有接點變化時,一直性hash自己就是基本均衡(看hash函數)的負載策略。
基於dubbo一致性Hash,相同參數的請求老是發到同一提供者。
當某一臺提供者掛時,本來發往該提供者的請求,基於虛擬節點,平攤到其它提供者,不會引發劇烈變更。
咱們以3個服務提供者(invoker1,invoker2,invoker3),每一個invoker虛擬3個節點(v1,v2,v3),把這9個節點映射到[0,23]的值域爲例
看下圖:node

那麼dubbo認爲
當一個接口方法參數(一個或者多個鏈接後)hash後獲得的hash(key1)值19,那麼它應該調用invoker2_v2節點,實際就是invoker2真實節點。
當一個接口方法參數(一個或者多個鏈接後)hash後獲得的hash(key2)值7,那麼它應該調用invoker3_v2節點,實際就是invoker3真實節點。算法

接下來看看具體代碼實現:緩存

/**
 * ConsistentHashLoadBalance
 *
 * @author william.liangf
 */
public class ConsistentHashLoadBalance extends AbstractLoadBalance {
    //接口名.方法名稱爲key hash選擇器爲value的map.每一個方法一個選擇器。
    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) {
        //接口名.方法名稱爲key
        String key = invokers.get(0).getUrl().getServiceKey() + "." + invocation.getMethodName();
        //取的invokers對象的hashcode,驗證對象變化
        int identityHashCode = System.identityHashCode(invokers);
        //根據key獲取hash選擇器
        ConsistentHashSelector<T> selector = (ConsistentHashSelector<T>) selectors.get(key);
        if (selector == null || selector.identityHashCode != identityHashCode) {
            //選擇器爲null或者,對象已變化,就建立新選擇器放入map中
            selectors.put(key, new ConsistentHashSelector<T>(invokers, invocation.getMethodName(), identityHashCode));
            selector = (ConsistentHashSelector<T>) selectors.get(key);
        }
        //經過選擇器的select方法,返回選中的invoker
        return selector.select(invocation);
    }

    private static final class ConsistentHashSelector<T> {
        //hash 環(值域)中,某些值(全部虛擬節點數)到虛擬節點的映射。
        private final TreeMap<Long, Invoker<T>> virtualInvokers;
        //每一個invoker 須要虛擬的節點數
        private final int replicaNumber;

        private final int identityHashCode;

        private final int[] argumentIndex;

        ConsistentHashSelector(List<Invoker<T>> invokers, String methodName, int identityHashCode) {
            //基於紅黑樹實現的有序map,有序很重要。
            this.virtualInvokers = new TreeMap<Long, Invoker<T>>();
            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" /> 默認只hash第一個,0位置參數
            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) {
                //獲取提供者host:port形式地址,以160個虛擬節點爲例
                String address = invoker.getUrl().getAddress();
                for (int i = 0; i < replicaNumber / 4; i++) {
                    byte[] digest = md5(address + i);//host:port(0,1,2,3.....39),40份
                    //每一個再分別,hash 4次,(這樣每一個機器就虛擬了160份)
                    for (int h = 0; h < 4; h++) {
                        long m = hash(digest, h);
                        //160份虛擬節點,每一份都映射同一個實際節點
                        virtualInvokers.put(m, invoker);
                    }
                }
            }
        }

        public Invoker<T> select(Invocation invocation) {
            String key = toKey(invocation.getArguments());
            //對拼接後的參數作MD5指紋摘要
            byte[] digest = md5(key);
           //摘要後,hash計算
            return selectForKey(hash(digest, 0));
        }

        /***
         * 把參數直接拼接。
         * @param args
         * @return
         */
        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();
        }
        //根據hash值,選擇invoker方法的核心方法
        private Invoker<T> selectForKey(long hash) {
            Invoker<T> invoker;
            Long key = hash;
	    //若是key不在map裏,也有可能key已在map裏,直接走下面流程
            if (!virtualInvokers.containsKey(key)) {
                //方法加參數hash 參數的key,沒在已近映射的ma中,
                //返回比key 大的map值
                SortedMap<Long, Invoker<T>> tailMap = virtualInvokers.tailMap(key);
                if (tailMap.isEmpty()) {//若是key最大,就取虛擬節點第一個(key值最小的節點)
                    key = virtualInvokers.firstKey();
                } else {
                    //取比key大的keys中,最小的一個節點,(離key最近的,大於key的節點)
                    key = tailMap.firstKey();
                }
            }
            //獲取key映射的實際invoker
            invoker = virtualInvokers.get(key);
            return invoker;
        }
//hash 算法,也很關鍵
        private long hash(byte[] digest, int number) {
            //number能夠是,0,1,2,3 long 類型64 bit位
            //最後0xFFFFFFFFL;保證4字節位表示數值。至關於Ingter型數值。因此hash環的值域是[0,Integer.max_value]
            //每次取digest4個字節(|操做),組成4字節的數值。
            //當number 爲 0,1,2,3時,分別對應digest第
            // 1,2,3,4;
            // 5,6,7,8;
            // 9,10,11,12;
            // 13,14,15,16;字節
            //4批
            return (
                    (
                     //digest的第4(number 爲 0時),8(number 爲 1),12(number 爲 2),16(number 爲 3)字節,&0xFF後,左移24位
                     (long) (digest[3 + number * 4] & 0xFF) << 24
                    )
                    |(
                     //digest的第3,7,11,15字節,&0xFF後,左移16位
                     (long) (digest[2 + number * 4] & 0xFF) << 16
                    )
                    |(
                     //digest的第2,6,10,14字節,&0xFF後,左移8位
                      (long) (digest[1 + number * 4] & 0xFF) << 8
                    )
                    |(
                    //digest的第1,5,9,13字節,&0xFF
                      digest[number * 4] & 0xFF
                    )
                    )
                    & 0xFFFFFFFFL;
        }
        //返回16字節總共128bit位的MD5指紋簽名byte[]。
        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();
        }

    }

}
相關文章
相關標籤/搜索