接上篇 https://cloud.tencent.com/developer/article/1109586 说
这篇说dubbo一致性hash负载均衡策略。要先大致了解下,什么是一直性hash算法。 一致性hash算法最早是用来解决,分布式缓存在有节点变动(新增后者删除)后,节点负载不均衡问题的。 而用一致性hash算法,就是为了达到,当集群中有节点加入或者节点删除时,尽量把负载的变化(加负,减负)均摊到每一个节点。 而没有接点变化时,一直性hash本身就是基本均衡(看hash函数)的负载策略。 基于dubbo一致性Hash,相同参数的请求总是发到同一提供者。 当某一台提供者挂时,原本发往该提供者的请求,基于虚拟节点,平摊到其它提供者,不会引起剧烈变动。 我们以3个服务提供者(invoker1,invoker2,invoker3),每个invoker虚拟3个节点(v1,v2,v3),把这9个节点映射到[0,23]的值域为例 看下图:
那么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();
}
}
}