关注可以查看更多粉丝专享blog~
Dubbo在使用负载均衡的时候并没有直接使用LoadBalance,而是使用的抽象父类AbstractClusterInvoker中定义的Invoker
粘滞连接用于有状态服务,尽可能让客户端总是向同一提供者发起调用,除非该提供者“挂了”,再连接另一台。
粘滞连接将自动开启延迟连接,以减少长连接数
Dubbo内置了4种负载均衡,也可以自行扩展,因为LoadBalance接口上有@SPI注解。
@SPI(RandomLoadBalance.NAME)
public interface LoadBalance {
@Adaptive("loadbalance")
<T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException;
}
从代码中可以看出默认的负载均衡实现就是RandomLoadBalance,即随机负载均衡。由于select方法上有@Activate(“loadbalance”)注解,因此我们可以在URL中可以通过loadalance=xxx来动态指定select时的负载均衡算法。
负载均衡算法名称 | 效果说明 |
---|---|
Random LoadBalance | 随机,按权重设置随机概率。在一个节点上碰撞的概率高,但调用量越大分布越均匀,而且按概率使用权重后也比较均匀,有利于动态调整提供者的权重 |
RoundRobin LoadBalance | 轮询,按公约后的权重设置比例。存在慢的提供者累积请求的问题。比如:某台机器很慢,但没“挂”,当请求到第二台时就卡在那里,久而久之,所有的请求都卡在那里 |
LeastActive LoadBalance | 最少活跃调用数,如果活跃数量相同则随机调用,活跃数指调用前后的计数差(调用前计数器+1,调用后计数器-1,如果一个服务提供者计数差很大,则说明该服务处理的比较慢,则减少请求量,防止服务请求阻塞,以及服务端崩溃。)。使慢的提供者收到更少的请求,因为越慢的提供者的调用前后计数差会越大 |
ConsistentHash LoadBalance | 一致性Hash,相同参数的请求总是发到同一提供者。当某台提供者“挂”掉的时候,原本发往该提供者的请求,基于虚拟节点,会平摊到其他提供者,不会引发剧烈变动。默认根据只对第一个参数hash,可以通过 配置进行修改。默认使用160个虚拟节点,可以通过 配置进行修改。 |
// 根据总权重计算一个随机的偏移量
int offset = ThreadLocalRandom.current().nextInt(totalWeight);
// 遍历所有Invoker,累减,得到被选中的Invoker
for (int i = 0; i < length; i++) {
// 累减的目的是如果轮询到某一个服务的时候累减完小于0,说明随机值落在此区间,则返回该服务提供者的Invoker
offset -= weights[i];
if (offset < 0) {
return invokers.get(i);
}
}
普通轮询的好处是每个节点获得的请求会很均匀,如果某些节点能力明显较弱,则这个节点会堆积比较多的请求。因此普通轮询还不能满足需求,还需要能根据节点权重进行干预。权重轮询又分为普通权重轮询和平滑权重轮询。普通权重轮询会造成某个节点会突然被频繁选中,这样容易让一个节点流量暴增。Dubbo的轮询负载均衡采用的是平滑加权轮询算法,负载均衡时挑选最大权重返回,并将最大权重数值进行调整(最大权重新值 = 最大权重当前值 - 总权重),然后开启下一轮,开启下一轮时,所有权重新值 = 当前值 + 自身基础权重值,听上去有点懵,但是列个表就清楚了。
请求次数 | 被选中前Invoker的current值 | 被选中后Invoker的current值 | 被选中的节点 |
---|---|---|---|
1 | {1,6,9} | {1,6,-7} | C |
2 | {2,12,2} | {2,-4,2} | B |
3 | {3,2,11} | {3,2,-5} | C |
4 | {4,8,4} | {4,-8,4} | B |
5 | {5,-2,13} | {5,-2,-3} | C |
6 | {6,4,6} | {-10,4,6} | A |
7 | {-9,10,15} | {-9,10,-1} | C |
8 | {-8,16,8} | {-8,0,8} | B |
9 | {-7,6,17} | {-7,6,1} | C |
10 | {-6,12,10} | {-6,-4,10} | B |
11 | {-5,2,19} | {-5,2,3} | C |
12 | {-4,8,12} | {-4,8,-4} | C |
13 | {-3,14,5} | {-3,-2,5} | B |
14 | {-2,4,14} | {-2,4,-2} | C |
15 | {-1,10,7} | {-1,-6,7} | B |
16 | {0,0,16} | {0,0,0} | C |
跟着表格从上到下捋一遍,自己算一下,思路就很清晰了。从这16次负载均衡来看,我们可以很清楚得知,A刚好被调用1次,B刚好被调用6次,C刚好被调用9次。符合权重轮询策略,因为他们的权重比例是1:6:9。此外,C并没有频繁地的一直调用,其中会穿插B和A的调用。
最少活跃调用数,如果活跃数量相同则随机调用,活跃数指调用前后的计数差(调用前计数器+1,调用后计数器-1,如果一个服务提供者计数差很大,则说明该服务处理的比较慢,则减少请求量,防止服务请求阻塞,以及服务端崩溃。)。使慢的提供者收到更少的请求,因为越慢的提供者的调用前后计数差会越大
public class LeastActiveLoadBalance extends AbstractLoadBalance {
public static final String NAME = "leastactive";
private final Random random = new Random();
@Override
protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
int length = invokers.size();
// 最小的活跃数
int leastActive = -1;
// 具有相同“最小活跃数”的服务者提供者(以下用 Invoker 代称)数量
int leastCount = 0;
// leastIndexs 用于记录具有相同“最小活跃数”的 Invoker 在 invokers 列表中的下标信息
int[] leastIndexs = new int[length];
int totalWeight = 0;
// 第一个最小活跃数的 Invoker 权重值,用于与其他具有相同最小活跃数的 Invoker 的权重进行对比,
// 以检测是否“所有具有相同最小活跃数的 Invoker 的权重”均相等
int firstWeight = 0;
boolean sameWeight = true;
// 遍历 invokers 列表
for (int i = 0; i < length; i++) {
Invoker<T> invoker = invokers.get(i);
// 获取 Invoker 对应的活跃数
int active = RpcStatus.getStatus(invoker.getUrl(), invocation.getMethodName()).getActive();
// 获取权重
int weight = invoker.getUrl().getMethodParameter(invocation.getMethodName(), Constants.WEIGHT_KEY, Constants.DEFAULT_WEIGHT);
// 发现更小的活跃数,重新开始
if (leastActive == -1 || active < leastActive) {
// 使用当前活跃数 active 更新最小活跃数 leastActive
leastActive = active;
// 更新 leastCount 为 1
leastCount = 1;
// 记录当前下标值到 leastIndexs 中
leastIndexs[0] = i;
totalWeight = weight;
firstWeight = weight;
sameWeight = true;
// 当前 Invoker 的活跃数 active 与最小活跃数 leastActive 相同
} else if (active == leastActive) {
// 在 leastIndexs 中记录下当前 Invoker 在 invokers 集合中的下标
leastIndexs[leastCount++] = i;
// 累加权重
totalWeight += weight;
// 检测当前 Invoker 的权重与 firstWeight 是否相等,
// 不相等则将 sameWeight 置为 false
if (sameWeight && i > 0
&& weight != firstWeight) {
sameWeight = false;
}
}
}
// 当只有一个 Invoker 具有最小活跃数,此时直接返回该 Invoker 即可
if (leastCount == 1) {
return invokers.get(leastIndexs[0]);
}
// 有多个 Invoker 具有相同的最小活跃数,但它们之间的权重不同
if (!sameWeight && totalWeight > 0) {
// 随机生成一个 [0, totalWeight) 之间的数字
int offsetWeight = random.nextInt(totalWeight);
// 循环让随机数减去具有最小活跃数的 Invoker 的权重值,
// 当 offset 小于等于0时,返回相应的 Invoker
for (int i = 0; i < leastCount; i++) {
int leastIndex = leastIndexs[i];
// 获取权重值,并让随机数减去权重值
offsetWeight -= getWeight(invokers.get(leastIndex), invocation);
if (offsetWeight <= 0)
return invokers.get(leastIndex);
}
}
// 如果权重相同或权重为0时,随机返回一个 Invoker
return invokers.get(leastIndexs[random.nextInt(leastCount)]);
}
}
Dubbo一致性Hash负载均衡采用的是一致性Hash算法
doSelect 方法主要做了一些前置工作,比如检测 invokers 列表是不是变动过,以及创建 ConsistentHashSelector。这些工作做完后,接下来开始调用 ConsistentHashSelector 的 select 方法执行负载均衡逻辑。
// ConsistentHashLoadBalance#doSelect
public class ConsistentHashLoadBalance extends AbstractLoadBalance {
private final ConcurrentMap<String, ConsistentHashSelector<?>> selectors =
new ConcurrentHashMap<String, ConsistentHashSelector<?>>();
@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;
// 获取 invokers 原始的 hashcode
int identityHashCode = System.identityHashCode(invokers);
ConsistentHashSelector<T> selector = (ConsistentHashSelector<T>) selectors.get(key);
// 如果 invokers 是一个新的 List 对象,意味着服务提供者数量发生了变化,可能新增也可能减少了。
// 此时 selector.identityHashCode != identityHashCode 条件成立
if (selector == null || selector.identityHashCode != identityHashCode) {
// 创建新的 ConsistentHashSelector
selectors.put(key, new ConsistentHashSelector<T>(invokers, methodName, identityHashCode));
selector = (ConsistentHashSelector<T>) selectors.get(key);
}
// 调用 ConsistentHashSelector 的 select 方法选择 Invoker
return selector.select(invocation);
}
}
在分析 select 方法之前,先看一下一致性 hash 选择器 ConsistentHashSelector 的初始化过程
private static final class ConsistentHashSelector<T> {
// 使用 TreeMap 存储 Invoker 虚拟节点
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();
// 获取虚拟节点数,默认为160
this.replicaNumber = url.getMethodParameter(methodName, "hash.nodes", 160);
// 获取参与 hash 计算的参数下标值,默认对第一个参数进行 hash 运算
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();
for (int i = 0; i < replicaNumber / 4; i++) {
// 对 address + i 进行 md5 运算,得到一个长度为16的字节数组
byte[] digest = md5(address + i);
// 对 digest 部分字节进行4次 hash 运算,得到四个不同的 long 型正整数
for (int h = 0; h < 4; h++) {
// h = 0 时,取 digest 中下标为 0 ~ 3 的4个字节进行位运算
// h = 1 时,取 digest 中下标为 4 ~ 7 的4个字节进行位运算
// h = 2, h = 3 时过程同上
long m = hash(digest, h);
// 将 hash 到 invoker 的映射关系存储到 virtualInvokers 中,
// virtualInvokers 需要提供高效的查询操作,因此选用 TreeMap 作为存储结构
virtualInvokers.put(m, invoker);
}
}
}
}
}
ConsistentHashSelector 的构造方法执行了一系列的初始化逻辑,比如从配置中获取虚拟节点数以及参与 hash 计算的参数下标,默认情况下只使用第一个参数进行 hash。需要特别说明的是,ConsistentHashLoadBalance 的负载均衡逻辑只受参数值影响,具有相同参数值的请求将会被分配给同一个服务提供者。ConsistentHashLoadBalance 不关心权重,因此使用时需要注意一下。
在获取虚拟节点数和参数下标配置后,接下来要做的事情是计算虚拟节点 hash 值,并将虚拟节点存储到 TreeMap 中。到此,ConsistentHashSelector 初始化工作就完成了。最后处理select逻辑。
select逻辑如下,选择的过程相对比较简单了。首先是对参数进行 md5 以及 hash 运算,得到一个 hash 值。然后再拿这个值到 TreeMap 中查找目标 Invoker 即可。
public Invoker<T> select(Invocation invocation) {
// 将参数转为 key
String key = toKey(invocation.getArguments());
// 对参数 key 进行 md5 运算
byte[] digest = md5(key);
// 取 digest 数组的前四个字节进行 hash 运算,再将 hash 值传给 selectForKey 方法,
// 寻找合适的 Invoker
return selectForKey(hash(digest, 0));
}
private Invoker<T> selectForKey(long hash) {
// 到 TreeMap 中查找第一个节点值大于或等于当前 hash 的 Invoker
Map.Entry<Long, Invoker<T>> entry = virtualInvokers.tailMap(hash, true).firstEntry();
// 如果 hash 大于 Invoker 在圆环上最大的位置,此时 entry = null,
// 需要将 TreeMap 的头节点赋值给 entry
if (entry == null) {
entry = virtualInvokers.firstEntry();
}
// 返回 Invoker
return entry.getValue();
}
相关文章:
Davids原理探究:Dubbo源码编译(2.7.8)
Davids原理探究:Dubbo SPI和Java SPI实现原理
Davids原理探究:Dubbo注册中心(ZooKeeper、Redis)实现原理
Davids原理探究:Dubbo配置解析原理
Davids原理探究:Dubbo服务暴露原理
Davids原理探究:Dubbo服务消费原理
Davids原理探究:Dubbo优雅停机原理解析
Davids原理探究:Dubbo调用流程图
Davids原理探究:Dubbo路由实现原理
Davids原理探究:Dubbo负载均衡实现原理
Davids原理探究:Dubbo过滤器原理