接上篇http://www.javashuo.com/article/p-fjpmnwyl-db.html說java
這篇說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(); } } }