dubbo源码分析5-负载均衡算法

本文详细分析了Dubbo的四种负载均衡算法:随机、轮询、最小活动数和一致性Hash。随机算法根据权重和进行随机选择;轮询算法考虑权重差异进行轮询;最小活动数算法选择活跃数最小的节点;一致性Hash算法解决节点失效后的流量分布问题,防止雪崩效应。每种算法的实现逻辑和关键点都有详细说明。

本文分享内容如下

  1. 随机算法分析
  2. 轮询算法分析
  3. 最小活动数算法分析
  4. 一致性hash算法分析

下面逐个进行分析

随机算法分析

实现类:RandomLoadBalance

protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {

int length = invokers.size(); // Number of invokers

int totalWeight = 0; // The sum of weights

boolean sameWeight = true; // Every invoker has the same weight?

for (int i = 0; i < length; i++) {//计算全部invoker 权重和

int weight = getWeight(invokers.get(i), invocation);

totalWeight += weight; // Sum

if (sameWeight && i > 0

&& weight != getWeight(invokers.get(i - 1), invocation)) {

sameWeight = false;

}

}

if (totalWeight > 0 && !sameWeight) {//如果权重和大于0且多个invoker 权重不同

// If (not every invoker has the same weight & at least one invoker's weight>0), select randomly based on totalWeight.

int offset = random.nextInt(totalWeight);//在权重和范围内计算出随机权重值

// Return a invoker based on the random value.

for (int i = 0; i < length; i++) {//按照invokers的顺序,看随机权重值落在那个invoker的权重值范围内,返回这个invoker

offset -= getWeight(invokers.get(i), invocation);

if (offset < 0) {

return invokers.get(i);

}

}

}

// If all invokers have the same weight value or totalWeight=0, return evenly.

//如果所有的invoker有相同的权重值或权重和等于0,在invokers中随机选中一个返回。

return invokers.get(random.nextInt(length));

}

处理逻辑:先计算全部invoker 权重和,如果权重和大于0且多个invoker 权重不同,在权重和范围内计算出随机权重值,按照invokers的顺序,看随机权重值落在那个invoker的权重值范围内,返回这个invoker,//如果所有的invoker有相同的权重值或权重和等于0,在invokers中随机选中一个返回。

小结: 权重结合随机的算法实现。

 

轮询算法分析

实现类:RoundRobinLoadBalance

最简单的轮询算法就是返回下个节点,到达尾部从头部开始,这里的实现 还需要处理权重

@Override

protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {

String key = invokers.get(0).getUrl().getServiceKey() + "." + invocation.getMethodName();

//key eg: tuling.dubbo.server.UserService.getUser 简称为 完整方法名

int length = invokers.size(); // Number of invokers

int maxWeight = 0; // The maximum weight

int minWeight = Integer.MAX_VALUE; // The minimum weight

final LinkedHashMap<Invoker<T>, IntegerWrapper> invokerToWeightMap = new LinkedHashMap<Invoker<T>, IntegerWrapper>();

int weightSum = 0;

for (int i = 0; i < length; i++) {

int weight = getWeight(invokers.get(i), invocation);

maxWeight = Math.max(maxWeight, weight); // Choose the maximum weight

minWeight = Math.min(minWeight, weight); // Choose the minimum weight

if (weight > 0) {

invokerToWeightMap.put(invokers.get(i), new IntegerWrapper(weight));//记录每个invoker对应的权重

weightSum += weight;//计算总权重

}

}

 

//获取上次完整方法名对应的执行次数记录。

AtomicPositiveInteger sequence = sequences.get(key);

if (sequence == null) {

sequences.putIfAbsent(key, new AtomicPositiveInteger());

sequence = sequences.get(key);

}

//增加一次执行次数。

int currentSequence = sequence.getAndIncrement();

if (maxWeight > 0 && minWeight < maxWeight) { 如果最大权重大于0且 最小权重小于最大权重,

int mod = currentSequence % weightSum;算出权重轮询值

for (int i = 0; i < maxWeight; i++) {//轮询 ,权重小的先被淘汰

for (Map.Entry<Invoker<T>, IntegerWrapper> each : invokerToWeightMap.entrySet()) {

final Invoker<T> k = each.getKey();

final IntegerWrapper v = each.getValue();

if (mod == 0 && v.getValue() > 0) {

return k;

}

if (v.getValue() > 0) {

v.decrement();

mod--;

}

}

}

}

// Round robin 简单轮询

return invokers.get(currentSequence % length);

}

处理逻辑:获取最大权重值,最小权重值,记录每个invoker对应的权重,计算总权重,然后获取上次完整方法名对应的执行次数记录,增加一次执行次数。如果权重有效且值不同,通过执行次数取模总权重值获取权重轮询值, 循环轮询, 减少权重轮询值同时减少invoker的权重值,权重小被淘汰,直到权重轮询值被耗尽,返回这个invoker。如果权重相同或所有的invoker权重值都为0,执行简单了轮询。

 

最小活动数算法分析

实现类:LeastActiveLoadBalance

最少活跃数的含义

官方解释:最少活跃调用数,相同活跃数的随机,活跃数指调用前后计数差,使慢的机器收到更少。

例如,每个服务维护一个活跃数计数器。当A机器开始处理请求,该计数器加1,此时A还未处理完成。若处理完毕则计数器减1。而B机器接受到请求后很快处理完毕。那么A,B的活跃数分别是1,0。当又产生了一个新的请求,则选择B机器去执行(B活跃数最小),这样使块的机器A收到少的请求。

protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {

int length = invokers.size(); // 总个数

int leastActive = -1; // 最小的活跃数

int leastCount = 0; // 相同最小活跃数的个数

int[] leastIndexs = new int[length]; // 相同最小活跃数的下标

int totalWeight = 0; // 总权重

int firstWeight = 0; // 第一个权重,用于于计算是否相同

boolean sameWeight = true; // 是否所有权重相同

for (int i = 0; i < length; i++) {

Invoker<T> invoker = invokers.get(i);

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) { // 发现更小的活跃数,重新开始

leastActive = active; // 记录最小活跃数

leastCount = 1; // 重新统计相同最小活跃数的个数

leastIndexs[0] = i; // 重新记录最小活跃数下标

totalWeight = weight; // 重新累计总权重

firstWeight = weight; // 记录第一个权重

sameWeight = true; // 还原权重相同标识

} else if (active == leastActive) { // 累计相同最小的活跃数

leastIndexs[leastCount ++] = i; // 累计相同最小活跃数下标

totalWeight += weight; // 累计总权重

// 判断所有权重是否一样

if (sameWeight && i > 0

&& weight != firstWeight) {

sameWeight = false;

}

}

}

// assert(leastCount > 0)

if (leastCount == 1) {

// 如果只有一个最小则直接返回

return invokers.get(leastIndexs[0]);

}

if (! sameWeight && totalWeight > 0) {

// 如果权重不相同且权重大于0则按总权重数随机

int offsetWeight = random.nextInt(totalWeight);

// 并确定随机值落在哪个片断上

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则均等随机

return invokers.get(leastIndexs[random.nextInt(leastCount)]);

}

}

处理逻辑:总的逻辑是选出最小活动数的invoker,如果有多个invoker相同,计算出他们的权重和,在权重和范围内计算出随机权重值,按照这些invokers的顺序,看随机权重值落在那个invoker的权重值范围内,返回这个invoker,如果权重相同或权重为0则均等随机从这些invokers选中一个。

 

一致性hash算法分析

先了解一下什么是一致性hash算法

假设一个图片服务,由于图片数据量比较大,如下图,将数据分布在4个节点 A,B,C,D上,这时希望其中一个节点挂掉,不会影响其他节点,一个简单的策略就是将A,B,C,D构成一个环形容灾集群,如果一个节点挂掉,则访问它相邻的下个节点。

这个方案看似美好,却有重大缺陷。如果A挂掉了 按照顺序 原来A的流量打到B上,这是 如果B扛不住,就会导致B,C,D相继挂掉,造成雪崩效应。那么怎么解决这个问题呢?

解决思路 让2个以上的节点分担 A的流量,如下图 设置 A的节点为3个虚拟节点 A1,A2,A3。假设A挂掉了 相当于 A1,A2,A3全部挂掉,A1的流程打到B上,A2的流程打到C上,A3的流程打到D上。 这样 A的流量由B,C,D分担,不容易发生雪崩效应。这个就是一致性hash算法要解决的问题。

要注意的是: 虚拟节点要与其他节点交错分布,否则解决不了雪崩效应,如下图的虚拟节点分布,还是会有雪崩效应。

那么虚拟节点是不是越多越好?肯定不是 ,如A,B,C,D 4个节点 A节点设置3个虚拟节点就可以了,多了没有什么实际作用。 每个节点的虚拟节点 等于 实际节点数据-1即可,注意要交错分布。一致性hash 负载策略通常用于 缓存服务,文件服务如 图片,音乐文件,文档。这些服务发生崩溃时通常不能通过扩展节点达到分担流量的效果,这些服务是有状态的服务。

 

 

 

dubbo一致性hash算法分析

主要配置

hash.arguments : 当进行调用时候根据调用方法的哪几个参数生成key,并根据key来通过一致性hash算法来选择调用结点。例如调用方法invoke(String s1,String s2); 若hash.arguments为0(默认值),则仅取invoke的参数1(s1), 若hash.arguments为(0,1),取invoke的 s1,s2 来生成hashCode。

hash.nodes: 为结点的副本数。

缺省只对第一个参数Hash,如果要修改,请配置

<dubbo:parameter key="hash.arguments" value="0,1" />

缺省用160份虚拟节点,如果要修改,请配置

<dubbo:parameter key="hash.nodes" value="320" />

实现类分析

实现类:ConsistentHashLoadBalance

protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {

// ServiceKey就是 接口名,如:tuling.dubbo.server.UserService

String key = invokers.get(0).getUrl().getServiceKey() + "." + invocation.getMethodName();

//key eg: tuling.dubbo.server.UserService.getUser

int identityHashCode = System.identityHashCode(invokers);

ConsistentHashSelector<T> selector = (ConsistentHashSelector<T>) selectors.get(key);

if (selector == null || selector.identityHashCode != identityHashCode) {

selectors.put(key, new ConsistentHashSelector<T>(invokers, invocation.getMethodName(), identityHashCode));

selector = (ConsistentHashSelector<T>) selectors.get(key);

}

return selector.select(invocation);//依赖ConsistentHashSelector

}

 

ConsistentHashSelector分析

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);//获取虚拟节点数 默认160。

String[] index = Constants.COMMA_SPLIT_PATTERN.split(url.getMethodParameter(methodName, "hash.arguments", "0"));//获取hash.arguments 并解析。

argumentIndex = new int[index.length];

for (int i = 0; i < index.length; i++) {

argumentIndex[i] = Integer.parseInt(index[i]);

}

//创建虚拟节点

//为每一个invoker生成replicaNumber 虚拟节点

for (Invoker<T> invoker : invokers) {

String address = invoker.getUrl().getAddress();

for (int i = 0; i < replicaNumber / 4; i++) {

byte[] digest = md5(address + i);//根据MD5算法为每4个节点生成一个信息摘要,长度为16字节 128位

for (int h = 0; h < 4; h++) {

long m = hash(digest, h); //将128位分为4部分, 抽取 其中 的 0~31 ,32~63,64~95,96~128 位,每32位转换成long类型作为虚拟节点的key. 这个key的高32位全部为0

virtualInvokers.put(m, invoker);

}

}

}

}

// number值传入的有 0,1,2,3 ,依次获取digest中的4个字节,拼接成long类型 (高32位全部为0)。

// 这是个截取4个字节拼装long值的方法,没看出有hash作用。

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,hash 是为了什么?

md5 获取 执行字符串的唯一标识 byte数组。

hash在这里的作用是取出32位组成一个long值,看不出其他作用。

hash的虚拟节点必须交错分布,否则 容易有雪崩效应,这里的算法是怎么处理的??

没有看出有交错配置的实现。有待以后研究

public Invoker<T> select(Invocation invocation) {

String key = toKey(invocation.getArguments());//根据方法参数 生成key

byte[] digest = md5(key);

return selectForKey(hash(digest, 0));//这里只取 0~31作为获取节点的hash值

}

private String toKey(Object[] args) {//拼接方法参数,作为key

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) {

Map.Entry<Long, Invoker<T>> entry = virtualInvokers.tailMap(hash, true).firstEntry();

if (entry == null) {

entry = virtualInvokers.firstEntry();

}

return entry.getValue();

}

selectForKey 依赖TreeMap的排序功能获取到了 key大于hash值的第一个节点。

知识点:

public SortedMap<K,V> tailMap(K fromKey)

截取Key大于等于fromKey的所有元素

public NavigableMap<K,V> tailMap(K fromKey, boolean inclusive)

当inclusive为true时,截取Key大于等于fromKey的所有元素,否则截取Key大于fromKey的所有元素

总结:dubbo的一致性hash算法 依赖选择器ConsistentHashSelector, 它创建的时候初始化了虚拟节点,放入TreeMap, 选择节点的时候 根据参数生成key,依赖TreeMap排序功能 找出节点。

参考资料:

treemap函数常用方法 讲解:https://www.cnblogs.com/xiaostudy/p/9511910.html

一致性哈希负载均衡算法的探讨:https://lexburner.github.io/consistent-hash-lb/

深入探讨hash算法。

Dubbo负载均衡:一致性Hash的实现分析:https://blog.csdn.net/revivedsun/article/details/71022871

 

### Dubbo源码分析 #### 负载均衡器选择 Invoker 的详细逻辑 在 Dubbo 框架中,负载均衡器的核心作用是在多个可用的服务提供者之间分配请求流量。具体来说,在负载均衡器选择 `Invoker` 的过程中,Dubbo 首先会获取当前服务的所有可用提供者列表,并通过指定的负载均衡策略来决定最终调用哪个具体的 `Invoker` 实例[^1]。 以下是负载均衡器的主要工作流程: - **获取候选 Provider 列表**:从注册中心拉取所有在线的服务提供方地址。 - **过滤不可用实例**:移除那些因网络异常或其他原因而无法正常工作的服务实例。 - **执行负载均衡算法**:根据配置的负载均衡策略(如随机、轮询或一致性哈希),计算并返回目标 `Invoker`。 #### 配置管理机制解析 Dubbo 支持多种方式加载配置信息,其中一种常见的方式是通过 ZooKeeper 作为配置中心。为了确保系统的高可靠性和灵活性,Dubbo 提供了一套完善的配置优先级规则: 1. **本地配置覆盖远程配置**:如果同一参数既定义在本地也存在远端,则以本地为准[^2]。 2. **默认值补充缺失字段**:当某些必要选项未被显式声明时,默认设置会被自动填充进去。 这种设计不仅简化了运维操作难度,还增强了应对突发状况的能力——即使连接不上外部存储库也能维持基本功能运转。 #### 请求处理链路剖析 当客户端发起一次 RPC 调用后,该请求经过一系列处理器层层传递直至抵达实际业务方法之前经历了复杂的转换过程。以下列举几个关键环节及其职责所在: - **解码阶段 (DecodeHandler)** :负责将字节流还原成 Java 对象形式以便后续步骤能够理解其含义; - **头部交换协议适配层(HeaderExchangeHandler)** :主要完成消息头部分的数据组装拆分任务; - **核心协议实现类(DubboProtocol.requestHandler)** :承担着最为繁重的工作量,包括但不限于序列化反序列化控制以及线程池调度安排等等[^3]. 以上各组件紧密协作共同构建起了完整的通信桥梁结构图谱如下所示: ```mermaid sequenceDiagram participant Client as 客户端 participant NettyServer as Netty服务器 participant DecodeHandler as 解码处理器 participant HeaderExchangeHandler as 头部交换处理器 participant RequestHandler as 请求处理器 Client->>NettyServer: 发送数据包 activate NettyServer Note over NettyServer: 接收到原始二进制数据\n触发回调函数 NettyServer->>DecodeHandler: 执行decode()方法 activate DecodeHandler DecodeHandler-->>NettyServer: 返回Java对象表示的消息体 deactivate DecodeHandler NettyServer->>HeaderExchangeHandler: 继续向下游转发已解析好的Request实体 activate HeaderExchangeHandler HeaderExchangeHandler->>RequestHandler: 委托给requestHandler进一步加工 activate RequestHandler ... 更深层次的具体事务逻辑 ... end ``` #### 动态上下线通知机制探讨 对于动态调整集群规模场景下的优雅停机需求而言,Dubbo 设计了一个简单有效的解决方案即主动注销机制。每当某个 Provider 准备退出运行环境前都会提前告知 Registry 自己即将消失的事实;随后 Consumer 端订阅到这一变更事件之后便会及时更新内部缓存状态从而避免继续尝试访问已经失效的目标资源[^4]。 --- ### 相关问题
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值