前言
负载均衡是指在集群中,将多个数据请求分散在不同单元上进行执行,主要为了提高系统容错能力和加强系统对数据的处理能力。
在 Dubbo 中,一次服务的调用就是对所有实体域 Invoker 的一次筛选过滤,最终选定具体调用的 Invoker。首先在 Directory 中获取全部 Invoker 列表,通过路由筛选出符合规则的 Invoker,最后再经过负载均衡选出具体的 Invoker。所以 Dubbo 负载均衡机制是决定一次服务调用使用哪个提供者的服务。
整体结构
Dubbo 负载均衡的分析入口是 org.apache.dubbo.rpc.cluster.loadbalance.AbstractLoadBalance 抽象类,查看这个类继承关系。
这个被 RandomLoadBalance、LeastActiveLoadBalance、RoundRobinLoadBalance 及 ConsistentHashLoadBalance 类继承,这四个类是 Dubbo 中提供的四种负载均衡算法的实现。
名称 | 说明 |
---|---|
RandomLoadBalance | 随机算法,根据权重设置随机的概率 |
LeastActiveLoadBalance | 最少活跃数算法,指请求数和完成数之差,使执行效率高的服务接收更多请求 |
RoundRobinLoadBalance | 加权轮训算法,根据权重设置轮训比例 |
ConsistentHashLoadBalance | Hash 一致性算法,相同请求参数分配到相同提供者 |
以上则是 Dubbo 提供的四种负载均衡算法。
从上图中,看到 AbstractLoadBalance 实现了 LoadBalance 接口,同时是一个 SPI 接口,指定默认实现为 RandomLoadBalance 随机算法机制。
抽象类 AbstractLoadBalance 中,实现了负载均衡通用的逻辑,同时给子类声明了一个抽象方法供子类实现其负载均衡的逻辑。
1 | public abstract class AbstractLoadBalance implements LoadBalance { |
在 AbstractLoadBalance 中,getWeight 和 calculateWarmupWeight 方法是获取和计算当前 Invoker 的权重值。
getWeight 中获取当前权重值,通过 URL 获取当前 Invoker 设置的权重,如果当前服务提供者启动时间小于预热时间,则会重新计算权重值,对服务进行降权处理,保证服务能在启动初期不分发设置比例的全部流量,健康运行下去。
calculateWarmupWeight 是重新计算权重值的方法,计算公式为:服务运行时长 / (预热时长 / 设置的权重值)
,等价于(服务运行时长 / 预热时长) * 设置的权重值
,同时条件服务运行时长 < 预热时长
。由该公式可知,预热时长和设置的权重值不变,服务运行时间越长,计算出的值越接近 weight,但不会等于 weight。
在返回计算后的权重结果中,对小于1和大于设置的权重值进行了处理,当重新计算后的权重小于1时返回1;处于1和设置的权重值之间时,直接返回计算后的结果;当权重大于设置的权重值时(因为条件限制,不会出现该类情况),返回设置的权重值。所以得出结论:重新计算后的权重值为 1 ~ 设置的权重值,运行时间越长,计算出的权重值越接近设置的权重值。
配置方式
服务端
通过 XML 配置方式:
1 | <!-- 服务级别配置 --> |
通过 Properties 配置:
1 | dubbo.service.loadbalance=负载策略 |
通过注解方式:
1 |
客户端
通过 XML 配置方式:
1 | <!-- 服务级别配置 --> |
通过 Properties 配置:
1 | dubbo.reference.loadbalance=负载策略 |
通过注解配置方式:
1 |
实现方式也可通过 Dubbo-Admin 管理后台进行配置,如图:
随机算法
加权随机算法负载均衡策略(RandomLoadBalance)是 dubbo 负载均衡的默认实现方式,根据权重分配各个 Invoker 随机选中的比例。这里的意思是:将到达负载均衡流程的 Invoker 列表中的 权重进行求和,然后求出单个 Invoker 权重在总权重中的占比,随机数就在总权重值的范围内生成。
如图,假如当前有192.168.1.10
和192.168.1.11
两个负载均衡的服务,权重分别为 4、6 ,则它们的被选中的比例为 2/5、3/5。
当生成随机数为 6 时,就会选中192.168.1.11
的服务。
dubbo 中 RandomLoadBalance 的 doSelect 实现代码:
1 | public class RandomLoadBalance extends AbstractLoadBalance { |
以上就是加权随机策略的实现,这里比较主要关注计算生成的随机数对应的 Invoker。通过遍历权重数组,生成的数累减当前权重值,当 offset 为 0 时,就表示 offset 对应当前的 Invoker 服务。
以生成的随机数为 6 为例,遍历 Invokers 长度:
第一轮:offset = 6 - 4 = 2 不满足 offset < 0,继续遍历。
第二轮:offset = 2 - 6 = -4 满足 offset < 0,返回当前索引对应的 Invoker。因为 offset 返回负数,表示 offset 落在当前 Invoker 权重的区间里。
加权随机策略并非一定按照比例被选到,理论上调用次数越多,分布的比例越接近权重所占的比例。
最少活跃数算法
最小活跃数负载均衡策略(LeastActiveLoadBalance)是从最小活跃数的 Invoker 中进行选择。什么是活跃数呢?活跃数是一个 Invoker 正在处理的请求的数量,当 Invoker 开始处理请求时,会将活跃数加 1,完成请求处理后,将相应 Invoker 的活跃数减 1。找出最小活跃数后,最后根据权重进行选择最终的 Invoker。如果最后找出的最小活跃数相同,则随机从中选中一个 Invoker。
1 | public class LeastActiveLoadBalance extends AbstractLoadBalance { |
这段代码的整个逻辑就是,从 Invokers 列表中筛选出最小活跃数的 Invoker,然后类似加权随机算法策略方式选择最终的 Invoker 服务。
轮询算法
加权轮询负载均衡策略(RoundRobinLoadBalance)是基于权重来决定轮询的比例。普通轮询会将请求均匀的分布在每个节点,但不能很好调节不同性能服务器的请求处理,所以加权负载均衡来根据权重在轮询机制中分配相对应的请求比例给每台服务器。
1 | public class RoundRobinLoadBalance extends AbstractLoadBalance { |
上面选中 Invoker 逻辑为:每个 Invoker 都有一个 current 值,初始值为自身权重。在每个 Invoker 中current = current + weight
。遍历完 Invoker 后,current 最大的那个 Invoker 就是本次选中的 Invoker。选中 Invoker 后,将本次 current 值计算current = current - totalWeight
。
以上面192.168.1.10
和192.168.1.11
两个负载均衡的服务,权重分别为 4、6 。基于选中前current = current + weight
、选中后current = current - totalWeight
计算公式得出如下
请求次数 | 选中前 current | 选中后 current | 被选中服务 |
---|---|---|---|
1 | [4, 6] | [4, -4] | 192.168.1.11 |
2 | [8, 2] | [-2, 2] | 192.168.1.10 |
3 | [2, 8] | [2, -2] | 192.168.1.11 |
4 | [6, 4] | [-4, 4] | 192.168.1.10 |
5 | [0, 10] | [0, 0] | 192.168.1.11 |
一致性 Hash 算法
一致性 Hash 负载均衡策略(ConsistentHashLoadBalance)是让参数相同的请求分配到同一机器上。把每个服务节点分布在一个环上,请求也分布在环形中。以请求在环上的位置,顺时针寻找换上第一个服务节点。如图所示:
同时,为避免请求散列不均匀,dubbo 中会将每个 Invoker 再虚拟多个节点出来,使得请求调用更加均匀。
一致性 Hash 修改配置如下:
1 | <!-- dubbo 默认只对第一个参数进行 hash 标识,指定hash参数 --> |
一致性 Hash 实现如下:
1 | public class ConsistentHashLoadBalance extends AbstractLoadBalance { |
doSelect 中主要实现缓存检查和 Invokers 变动检查,一致性 hash 负载均衡的实现在这个内部类 ConsistentHashSelector 中实现。
1 | private static final class ConsistentHashSelector<T> { |
一致 hash 实现过程就是先创建好虚拟节点,虚拟节点保存在 TreeMap 中。TreeMap 的 key 为配置的参数先进行 md5 运算,然后将 md5 值进行 hash 运算。TreeMap 的 value 为被选中的 Invoker。
最后请求时,计算参数的 hash 值,去从 TreeMap 中获取 Invoker。
总结
Dubbo 负载均衡的实现,技巧上还是比较优雅,可以多多学习其编码思维。在研究其代码时,需要仔细研究其实现原理,否则比较难懂其思想。