接上篇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(); } } }