Dubbo 的负载均衡策略:一致性哈希策略
Posted LieBrother
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Dubbo 的负载均衡策略:一致性哈希策略相关的知识,希望对你有一定的参考价值。
本文简单介绍 Dubbo 中的负载均衡策略:一致性哈希策略。
一致性哈希策略:指的是相同参数的请求总是发到同一个提供者实例。
简单描述一下一致性哈希,如下图所示。假设有 8 ,组成下面 A - H 的 8 个虚拟节点,A - H 哈希值分别是递增的,当有一个请求进来,通过计算参数的哈希值,比如该值刚好是 1 的位置,那么选中比 1 的值大的最小的那个实例,则结果该请求选中 C 虚拟节点;假如请求的参数的哈希值是在 2 的位置,那么找不到比 2 的值大的最小的实例,则选中所有实例中最小的一个,也就是 A。
Dubbo 在使用一致性哈希的时候,会用到如下配置:
配置对哪些参数做哈希计算,默认是只对第一个参数:
<dubbo:parameter key="hash.arguments" value="0,1" />
配置需要多少份虚拟节点,也就是一个提供服务的实例需要冗余多少份,默认是160份虚拟节点
<dubbo:parameter key="hash.nodes" value="320" />
Dubbo 在实现一致性哈希策略的时候使用了虚拟节点的概念,就是扩大实例个数,当然不是真的有那么多个实例,比如有 5 个实例,默认是 160 个备份,则真实的虚拟节点有 5 * 160 = 800 个。
为什么要采取虚拟节点?
假如不使用虚拟节点,只有 5 个节点,当移除掉 1 个节点的时候,会导致将改移除节点的请求分配到另外 4 个节点的时候不平衡,哈希计算并不保证平衡。当使用虚拟节点的时候,有更多个节点可分配,会更加均匀的分配到其他节点上。
优点:在一些需要将同一个请求参数对应到同个服务的场景下很适合;当对实例进行增加或者删除的时候,能避免引起很大的变动。
public class ConsistentHashLoadBalance extends AbstractLoadBalance {
public static final String NAME = "consistenthash";
// 记录所有
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) {
String methodName = RpcUtils.getMethodName(invocation);
String key = invokers.get(0).getUrl().getServiceKey() + "." + methodName;
// 计算哈希值 key,通过这个字段来识别出调用实例是否有变化,有变化则重新创建 ConsistentHashSelector
int identityHashCode = System.identityHashCode(invokers);
ConsistentHashSelector<T> selector = (ConsistentHashSelector<T>) selectors.get(key);
if (selector == null || selector.identityHashCode != identityHashCode) {
// 没有存在 selector 或者 invokers 实例有变化,重新创建
selectors.put(key, new ConsistentHashSelector<T>(invokers, methodName, identityHashCode));
selector = (ConsistentHashSelector<T>) selectors.get(key);
}
return selector.select(invocation);
}
private static final class ConsistentHashSelector<T> {
private final TreeMap<Long, Invoker<T>> virtualInvokers;
private final int replicaNumber;
private final int identityHashCode;
private final int[] argumentIndex;
ConsistentHashSelector(List<Invoker<T>> invokers, String methodName, int identityHashCode) {
// 虚拟节点
this.virtualInvokers = new TreeMap<Long, Invoker<T>>();
this.identityHashCode = identityHashCode;
URL url = invokers.get(0).getUrl();
// 虚拟节点数量
this.replicaNumber = url.getMethodParameter(methodName, "hash.nodes", 160);
// 需要哈希的参数,默认是第一个参数
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) {
String address = invoker.getUrl().getAddress();
// 每一个实例存 replicaNumber 份虚拟节点
for (int i = 0; i < replicaNumber / 4; i++) {
// 先将虚拟节点分为 (replicaNumber/4) 份,先做 MD5
byte[] digest = md5(address + i);
for (int h = 0; h < 4; h++) {
//再做哈希计算,得到 m,作为 Key
long m = hash(digest, h);
// 存到虚拟节点
virtualInvokers.put(m, invoker);
}
}
}
}
public Invoker<T> select(Invocation invocation) {
// 得出要进行哈希的字符串
String key = toKey(invocation.getArguments());
// 进行 MD5
byte[] digest = md5(key);
return selectForKey(hash(digest, 0));
}
// 根据 hash.arguments 配置的对多少个参数进行哈希,得出拼接后的字符串
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) {
// 找出大于或等于 hash 中最小的键值对
Map.Entry<Long, Invoker<T>> entry = virtualInvokers.ceilingEntry(hash);
if (entry == null) {
// 找出最小的键值对
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;
}
// MD5 加密
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();
}
}
}
参考文章:
https://web.archive.org/web/20120605030524/http://weblogs.java.net/blog/tomwhite/archive/2007/11/consistent_hash.html
https://blog.csdn.net/cywosp/article/details/23397179/
http://www.zsythink.net/archives/1182
做个有梦想的程序猿
以上是关于Dubbo 的负载均衡策略:一致性哈希策略的主要内容,如果未能解决你的问题,请参考以下文章
dubbo的负载均衡策略之RandomLoadBalance加权随机策略源码分析