写在前面
为了提高系统的性能,优化用户体验,dubbo提供了负载均衡功能,即在一组服务中选择一个来进行调用,从而达到分流的效果。其对应的顶层接口是com.alibaba.dubbo.rpc.cluster.LoadBalance
。我们就从这个接口的分析开始吧!
1:LoadBalance
负载均衡策略的顶层接口,源码如下:
// com.alibaba.dubbo.rpc.cluster.LoadBalance
@SPI(RandomLoadBalance.NAME)
public interface LoadBalance {
// 从集群列表中选择一个invoker
@Adaptive("loadbalance")
<T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException;
}
@SPI(RandomLoadBalance.NAME)
默认使用名称为NAME = "random";
的负载均衡实现类,即随机选择的负载均衡策略。其实现类如下图:
可以看到一共有4个具体实现子类,即dubbo内置(画外音:你可以可以实现自己的负载均衡策略!!!)
了4中负载均衡策略。
2:AbstractLoadBalance
源码如下:
class FakeCls {
// com.alibaba.dubbo.rpc.cluster.loadbalance.AbstractLoadBalance.select
// 2022年2月28日16:27:41
@Override
public <T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) {
if (invokers == null || invokers.isEmpty())
return null;
// 如果是只有一个invoker,即只有一个服务提供者,则直接返回唯一的一个invoker
if (invokers.size() == 1)
return invokers.get(0);
// 2022年2月28日16:30:43
return doSelect(invokers, url, invocation);
}
}
2022年2月28日16:27:41
处使用的是模板方法设计模式 ,提供了公共实现,通过抽象模板方法的方式让子类实现特有逻辑。2022年2月28日16:30:43
处是抽象模板方法,每个具体子类根据自身具体场景提供实现,具体参考3:四种内置负载均衡策略
。另外,抽象类还针对权重的获取提供了实现,具体参考2.1:getWeight
。
2.1:getWeight
源码如下:
class FakeCls {
// com.alibaba.dubbo.rpc.cluster.loadbalance.AbstractLoadBalance.getWeight
protected int getWeight(Invoker<?> invoker, Invocation invocation) {
// 2022年3月1日10:39:08
int weight = invoker.getUrl().getMethodParameter(invocation.getMethodName(), Constants.WEIGHT_KEY, Constants.DEFAULT_WEIGHT);
// 如果是大于0
if (weight > 0) {
// 获取当前服务提供者的启动时间,为什么要有这个逻辑,因为JVM在刚刚启动的时候会有一个预热的过程,此时如果接收100%流量可能会出现吃力的情况,
// 所以这里需要结合服务提供者的预热时长,即已经启动的时间
long timestamp = invoker.getUrl().getParameter(Constants.REMOTE_TIMESTAMP_KEY, 0L);
if (timestamp > 0L) {
// 获取已经启动的毫秒数
int uptime = (int) (System.currentTimeMillis() - timestamp);
// 获取服务提供者service的预热时长(单位毫秒),如配置<dubbo:service ... warmup="100"/>,预热时长为100毫秒
// ,默认的预热时长”Constants.DEFAULT_WARMUP=600000“毫秒
int warmup = invoker.getUrl().getParameter(Constants.WARMUP_KEY, Constants.DEFAULT_WARMUP);
// 如果是启动时长小于需要的预热时长,则结合预热来重新计算权重(会降低权重值)
if (uptime > 0 && uptime < warmup) {
// 2022年3月1日11:27:22
weight = calculateWarmupWeight(uptime, warmup, weight);
}
}
}
// 返回[1,100]的权重值
return weight;
}
}
2022年3月1日10:39:08
处是获取当前服务提供者设置的权重 值,默认值是100,也可以通过如<dubbo:service ... weight="30"/>
配置自定义权重。2022年3月1日11:27:22
计算带有预热逻辑的权重值,具体参考2.2:calculateWarmupWeight
。
2.2:calculateWarmupWeight
源码如下:
class FakeCls {
// com.alibaba.dubbo.rpc.cluster.loadbalance.AbstractLoadBalance.calculateWarmupWeight
static int calculateWarmupWeight(int uptime, int warmup, int weight) {
// ((float) warmup / (float) weight) -> 计算1个权重值需要的预热时间
// ((float) uptime / ((float) warmup / (float) weight)) -> 计算当前启动时间包含了多少个“1个权重值需要的预热时间”
// 如需要的预热时间为36000 当前启动时间为18000 当前权重为20,则结算过程如下:
// ((float) 36000 / (float) 20) -> 计算1个权重值需要的预热时间为1800
// (float) 18000 / 1800 -> 10,即只能有一半权重
int ww = (int) ((float) uptime / ((float) warmup / (float) weight));
// 权重最小为1,(ww > weight ? weight : ww)考虑启动时长大于预热时长的情况
return ww < 1 ? 1 : (ww > weight ? weight : ww);
}
}
3:四种内置负载均衡策略
dubbo针对负载均衡策略,一种内置提供了4中具体策略,如下图红框中的类:
下面我们分别来看下。
3.1:RandomLoadBalance
基于权重
的随机选择策略,源码如下:
// com.alibaba.dubbo.rpc.cluster.loadbalance.RandomLoadBalance
public class RandomLoadBalance extends AbstractLoadBalance {
public static final String NAME = "random";
private final Random random = new Random();
@Override
protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
// 服务提供者invoker的总个数
int length = invokers.size();
// 所有服务提供者invoker的总权重
int totalWeight = 0;
// 所有的服务提供者invoker具有相同的权重?
boolean sameWeight = true;
// 遍历所有invoker
for (int i = 0; i < length; i++) {
// 2022年3月1日16:19:35
int weight = getWeight(invokers.get(i), invocation);
// 总权重
totalWeight += weight;
// 判断是否所有invoker权重都相同,每次都是与前一个比(此处定义一个记录上一个invoker权重的变量是否好些???)
if (sameWeight && i > 0
&& weight != getWeight(invokers.get(i - 1), invocation)) {
sameWeight = false;
}
}
// 不是所有invoker的权重值都相同情况,需要根据基于权重值来随机选择一个
if (totalWeight > 0 && !sameWeight) {
// 基于总权重计算一个偏移量
int offset = random.nextInt(totalWeight);
// 2022年3月1日16:47:37
for (int i = 0; i < length; i++) {
offset -= getWeight(invokers.get(i), invocation);
if (offset < 0) {
return invokers.get(i);
}
}
}
// 如果是所有invoker都有一样的权重,均匀的选择一个就可以了,当然直接使用上述选择逻辑也是可以了,但是这样做会节省CPU资源,因此改行代码
// 可以看做是一种优化
return invokers.get(random.nextInt(length));
}
}
2022年3月1日16:19:35
处是计算权重值,具体参考2.1:getWeight
。2022年3月1日16:47:37
处通过随机值来选择一个invoker,如下:
假设有3个invoker,invoker1->权重10,invoker2->权重20,invoker3->权重30,则总权重值=40,生成40以内的随机数,如下几种情况:
[0,9]:选中invoker1,因为[0,9]-10=[-10,-1] < 0
[10,29]:选中invoker2,因为[10,29]-10-20=[-20,-1] < 0
[30,59]:选中invoker3,因为[30,59]-10-20-30=[-30,-1] < 0
如下测试代码:
class FakeCls {
public static void main(String[] args) throws Exception {
// 模拟invoker集合
List<String> invokerList = new ArrayList<>();
invokerList.add("invoker_1");
invokerList.add("invoker_2");
invokerList.add("invoker_3");
// 模拟每个invoker的权重值
List<Integer> weightList = new ArrayList<>();
weightList.add(10);
weightList.add(20);
weightList.add(30);
testLoopRatio(100, invokerList, weightList);
testLoopRatio(10000, invokerList, weightList);
testLoopRatio(1000000, invokerList, weightList);
}
private static void testLoopRatio(int loopNum, List<String> invokerList, List<Integer> weightList) {
// 100次测试比例
// int loopNum = 100;
float invoker1Selected = 0;
float invoker2Selected = 0;
float invoker3Selected = 0;
for (int i = 0; i < loopNum; i++) {
String selectedInvoker = doSelect(invokerList, weightList);
if ("invoker_1".equals(selectedInvoker)) {
invoker1Selected++;
} else if ("invoker_2".equals(selectedInvoker)) {
invoker2Selected++;
} else if ("invoker_3".equals(selectedInvoker)) {
invoker3Selected++;
}
}
System.out.println(loopNum + "调用最终比例:1:" + (invoker2Selected / invoker1Selected) + ":" + ((invoker3Selected / invoker1Selected)));
}
private static Random random = new Random();
protected static String doSelect(List<String> invokers, List<Integer> weightList) {
// 服务提供者invoker的总个数
int length = invokers.size();
// 所有服务提供者invoker的总权重
int totalWeight = 0;
// 所有的服务提供者invoker具有相同的权重?
boolean sameWeight = true;
// 遍历所有invoker
for (int i = 0; i < length; i++) {
// 2022年3月1日16:19:35
// int weight = getWeight(invokers.get(i), invocation);
int weight = weightList.get(i);
// 总权重
totalWeight += weight;
// 判断是否所有invoker权重都相同,每次都是与前一个比(此处定义一个记录上一个invoker权重的变量是否好些???)
if (sameWeight && i > 0
&& weight != /*getWeight(invokers.get(i - 1), invocation)*/weightList.get(i - 1)) {
sameWeight = false;
}
}
// 不是所有invoker的权重值都相同情况,需要根据基于权重值来随机选择一个
if (totalWeight > 0 && !sameWeight) {
// 基于总权重计算一个偏移量
int offset = random.nextInt(totalWeight);
// 2022年3月1日16:47:37
for (int i = 0; i < length; i++) {
offset -= /*getWeight(invokers.get(i), invocation)*/weightList.get(i);
if (offset < 0) {
return invokers.get(i);
}
}
}
return invokers.get(random.nextInt(length));
}
}
当数据量越大时越接近权重的真实比例,如下是我本地的几次测试结果:
100调用最终比例:1:1.5625:3.6875
10000调用最终比例:1:2.0654087:3.2238994
1000000调用最终比例:1:1.9939269:2.9953523
100调用最终比例:1:1.8333334:2.7222223
10000调用最终比例:1:1.991716:2.925444
1000000调用最终比例:1:2.0033574:3.0026846
100调用最终比例:1:2.0526316:2.2105262
10000调用最终比例:1:2.0618114:3.0581396
1000000调用最终比例:1:2.004641:2.9994535
100调用最终比例:1:1.65:2.35
10000调用最终比例:1:2.0584257:3.091636
1000000调用最终比例:1:1.9908692:3.0004914
3.2:RoundRobinLoadBalance
当使用配置<dubbo:reference ... loadbalance="roundrobin"/>
时会使用该轮询负载均衡器。
源码如下:
public class RoundRobinLoadBalance extends AbstractLoadBalance {
public static final String NAME = "roundrobin";
private static int RECYCLE_PERIOD = 60000;
@Override
protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
// dongshi.daddy.service.cluster.ClusterService.sayHi 接口名.方法名
String key = invokers.get(0).getUrl().getServiceKey() + "." + invocation.getMethodName();
// 接口方法->一组invoker的轮询状态
ConcurrentMap<String, WeightedRoundRobin> map = methodWeightMap.get(key);
if (map == null) {
// 不存在才添加
methodWeightMap.putIfAbsent(key, new ConcurrentHashMap<String, WeightedRoundRobin>());
map = methodWeightMap.get(key);
}
// 总权重
int totalWeight = 0;
// 当前最大的轮询计数值,WeightedRoundRobin中的current
long maxCurrent = Long.MIN_VALUE;
long now = System.currentTimeMillis();
// 被选择的Invoker
Invoker<T> selectedInvoker = null;
// 被选择的invoker对应的WeightedRoundRobin
WeightedRoundRobin selectedWRR = null;
for (Invoker<T> invoker : invokers) {
// 获取当前invoker的标识字符串,如dubbo://192.168.64.1:20826/dongshi.daddy.service.cluster.ClusterService,不同invoker主要是ip,port不同
String identifyString = invoker.getUrl().toIdentityString();
// 2022年3月2日12:06:12
// 获取当前invoker的带权重的轮询状态对象
WeightedRoundRobin weightedRoundRobin = map.get(identifyString);
// 2022年3月2日13:05:45
int weight = getWeight(invoker, invocation);
// 权重最小为0,不同于random负载均衡的最下权重值1
if (weight < 0) {
weight = 0;
}
if (weightedRoundRobin == null) {
weightedRoundRobin = new WeightedRoundRobin();
// 设置权重
weightedRoundRobin.setWeight(weight);
// dubbo://192.168.64.1:20826/dongshi.daddy.service.cluster.ClusterService -> roundRobin
map.putIfAbsent(identifyString, weightedRoundRobin);
weightedRoundRobin = map.get(identifyString);
}
// 权重在运行过程中发生改变
if (weight != weightedRoundRobin.getWeight()) {
weightedRoundRobin.setWeight(weight);
}
// 获取当前的计数值,每次+weight
long cur = weightedRoundRobin.increaseCurrent();
// 设置当前为最新的更新时间
weightedRoundRobin.setLastUpdate(now);
// 当前的轮询计数值大于最大的轮询计数值
if (cur > maxCurrent) {
// 设置当前的轮询计数值到为最大的轮询计数值
maxCurrent = cur;
// 设置当前的invoker为选中的invoker
selectedInvoker = invoker;
// 设置选中WeightedRoundRobin,对应于选中的invoker
selectedWRR = weightedRoundRobin;
}
// 累加总权重
totalWeight += weight;
}
// 这里的代码不知道何时会执行,可以先忽略
if (!updateLock.get() && invokers.size() != map.size()) {
if (updateLock.compareAndSet(false, true)) {
try {
// copy -> modify -> update reference
ConcurrentMap<String, WeightedRoundRobin> newMap = new ConcurrentHashMap<String, WeightedRoundRobin>();
newMap.putAll(map);
Iterator<Entry<String, WeightedRoundRobin>> it = newMap.entrySet().iterator();
while (it.hasNext()) {
Entry<String, WeightedRoundRobin> item = it.next();
if (now - item.getValue().getLastUpdate() > RECYCLE_PERIOD) {
it.remove();
}
}
methodWeightMap.put(key, newMap);
} finally {
updateLock.set(false);
}
}
}
if (selectedInvoker != null) {
// 减去总权重值
selectedWRR.sel(totalWeight);
return selectedInvoker;
}
// 这里正常执行不到,可以看做是兜底代码,防止因为前面逻辑有bug导致没有选择任何的invoker,因此可以忽略!
return invokers.get(0);
}
}
2022年3月2日12:06:12
处是获取某个invoker对应的带权重的轮询对象,具体参考3.2.1:WeightedRoundRobin
。2022年3月2日13:05:45
处是获取当前invoker权重值,具体参考2.1:getWeight
。如下测试选择invoker的代码:
class FakeCls {
private static void testDubboRoundRobinLoadbalance() {
// 模拟invoker集合
List<String> invokerList = new ArrayList<>();
invokerList.add("invoker_1");
invokerList.add("invoker_2");
invokerList.add("invoker_3");
// 模拟每个invoker的权重值
List<Integer> weightList = new ArrayList<>();
weightList.add(10);
weightList.add(20);
weightList.add(30);
Foo f = new Foo();
for (int i = 0; i < 50; i++) {
System.out.println(f.doSelectWithRoundRobin(invokerList, weightList));
}
}
protected static class WeightedRoundRobin {
private int weight;
private AtomicLong current = new AtomicLong(0);
private long lastUpdate;
public int getWeight() {
return weight;
}
public void setWeight(int weight) {
this.weight = weight;
current.set(0);
}
public long increaseCurrent() {
return current.addAndGet(weight);
}
public void sel(int total) {
current.addAndGet(-1 * total);
}
public long getLastUpdate() {
return lastUpdate;
}
public void setLastUpdate(long lastUpdate) {
this.lastUpdate = lastUpdate;
}
}
private ConcurrentMap<String, ConcurrentMap<String, WeightedRoundRobin>> methodWeightMap = new ConcurrentHashMap<String, ConcurrentMap<String, WeightedRoundRobin>>();
// private AtomicBoolean updateLock = new AtomicBoolean();
protected String doSelectWithRoundRobin(List<String> invokerList, List<Integer> weightList) {
// dongshi.daddy.service.cluster.ClusterService.sayHi 接口名.方法名
// String key = invokers.get(0).getUrl().getServiceKey() + "." + invocation.getMethodName();
String key = "dongshi.daddy.service.cluster.ClusterService.sayHi";
// 接口方法->一组invoker的轮询状态
ConcurrentMap<String, WeightedRoundRobin> map = methodWeightMap.get(key);
if (map == null) {
// 不存在才添加
methodWeightMap.putIfAbsent(key, new ConcurrentHashMap<String, WeightedRoundRobin>());
map = methodWeightMap.get(key);
}
// 总权重
int totalWeight = 0;
// 当前最大的轮询计数值,WeightedRoundRobin中的current
long maxCurrent = Long.MIN_VALUE;
long now = System.currentTimeMillis();
// 被选择的Invoker
// Invoker<T> selectedInvoker = null;
String selectedInvoker = null;
// 被选择的invoker对应的WeightedRoundRobin
WeightedRoundRobin selectedWRR = null;
// for (Invoker<T> invoker : invokers) {
for (int i = 0; i < invokerList.size(); i++) {
// 获取当前invoker的标识字符串,如dubbo://192.168.64.1:20826/dongshi.daddy.service.cluster.ClusterService,不同invoker主要是ip,port不同
// String identifyString = invoker.getUrl().toIdentityString();
String invoker = invokerList.get(i);
// String identifyString = invoker.getUrl().toIdentityString();
String identifyString = invoker.toString();
// 2022年3月2日12:06:12
// 获取当前invoker的带权重的轮询状态对象
WeightedRoundRobin weightedRoundRobin = map.get(identifyString);
// 2022年3月2日13:05:45
// int weight = getWeight(invoker, invocation);
int weight = weightList.get(i);
// 权重最小为0,不同于random负载均衡的最下权重值1
if (weight < 0) {
weight = 0;
}
if (weightedRoundRobin == null) {
weightedRoundRobin = new WeightedRoundRobin();
// 设置权重
weightedRoundRobin.setWeight(weight);
// dubbo://192.168.64.1:20826/dongshi.daddy.service.cluster.ClusterService -> roundRobin
map.putIfAbsent(identifyString, weightedRoundRobin);
weightedRoundRobin = map.get(identifyString);
}
// 权重在运行过程中发生改变
if (weight != weightedRoundRobin.getWeight()) {
weightedRoundRobin.setWeight(weight);
}
// 获取当前的计数值,每次+weight
long cur = weightedRoundRobin.increaseCurrent();
// 设置当前为最新的更新时间
weightedRoundRobin.setLastUpdate(now);
// 当前的轮询计数值大于最大的轮询计数值
if (cur > maxCurrent) {
// 设置当前的轮询计数值到为最大的轮询计数值
maxCurrent = cur;
// 设置当前的invoker为选中的invoker
selectedInvoker = invoker;
// 设置选中WeightedRoundRobin,对应于选中的invoker
selectedWRR = weightedRoundRobin;
}
// 累加总权重
totalWeight += weight;
}
if (selectedInvoker != null) {
// 减去总权重值
selectedWRR.sel(totalWeight);
return selectedInvoker;
}
// 这里正常执行不到,可以看做是兜底代码,防止因为前面逻辑有bug导致没有选择任何的invoker,因此可以忽略!
return invokerList.get(0);
}
}
运行testDubboRoundRobinLoadbalance
测试,如下是我本地的运行结果:
invoker_3
invoker_2
invoker_1
invoker_3
invoker_2
invoker_3
...
3,2,1,3,2,3
一个循环,可以看到是和权重等比例出现的,和随机的区别就是,是有固定规律的,但是最终被选中的次数的比例是和权重值等比的。如果是将权重改成一致,则执行顺序是1,2,3,1,2,3...
。
3.2.1:WeightedRoundRobin
源码如下:
// com.alibaba.dubbo.rpc.cluster.loadbalance.RoundRobinLoadBalance.WeightedRoundRobin
protected static class WeightedRoundRobin {
// invoker对应的权重值
private int weight;
// 辅助轮询原子变量
private AtomicLong current = new AtomicLong(0);
private long lastUpdate;
public int getWeight() {
return weight;
}
public void setWeight(int weight) {
this.weight = weight;
current.set(0);
}
public long increaseCurrent() {
// 每次新增权重值
return current.addAndGet(weight);
}
public void sel(int total) {
current.addAndGet(-1 * total);
}
public long getLastUpdate() {
return lastUpdate;
}
public void setLastUpdate(long lastUpdate) {
this.lastUpdate = lastUpdate;
}
}
3.3:LeastActiveLoadBalance
在服务提供者端设置<dubbo:reference ... loadbalance="leastactive"/>
可以使用最小活跃负载均衡器
,源码如下:
// com.alibaba.dubbo.rpc.cluster.loadbalance.LeastActiveLoadBalance
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) {
// invoker的个数
int length = invokers.size();
// 所有invoker中的最小活跃值,比如三个invoker活跃值分别是1,1,6,则该值是1
int leastActive = -1;
// 拥有最小活跃值的个数,比如三个invoker活跃值分别是1,1,6,则该值是2
int leastCount = 0;
// 拥有相同最小活跃值的索引位数组
int[] leastIndexs = new int[length];
// 总权重(计算预热后的权重)
int totalWeight = 0;
int firstWeight = 0;
// 所有的invoker拥有相同的权重值?
boolean sameWeight = true;
for (int i = 0; i < length; i++) {
// 获取当前的invoker
Invoker<T> invoker = invokers.get(i);
// 2022年3月2日15:51:52
// 获取当前invoker的活跃值
int active = RpcStatus.getStatus(invoker.getUrl(), invocation.getMethodName()).getActive();
// 2022年3月2日15:55:55
int afterWarmup = getWeight(invoker, invocation);
// 还没有设置真实的活跃值,或者是当前invoker的活跃值小于当前最小的活跃值
if (leastActive == -1 || active < leastActive) {
// 记录最小的活跃值
leastActive = active;
// 重置拥有最小活跃值的invoker的个数为1,比如当前活跃值是2,8,2,1,1,则当执行到第一个1时lastCount=2,这里就会重置为1
leastCount = 1;
// 重置拥有最小活跃值的索引位置数组,比如当前活跃值是2,8,2,1,1,则当执行到第一个1时数组为[0,2],这里就会重置为[3]
leastIndexs[0] = i;
// 重置拥有相同最小活跃值的invoker的总权重值,比如当前活跃值对应权重值是2->20,8->10,2->25,1->50,1->40,则当执行到第一个1该值为20+25=45
// ,此处重置totalWeight=50
totalWeight = afterWarmup;
// 重置拥有最小活跃值的第一个权重值,比如当前活跃值对应权重值是2->20,8->10,2->25,1->50,1->40,则当执行到第一个1该值为20
// ,此处重置firstWeight=50
firstWeight = afterWarmup;
// 重置拥有相同活跃值的invoker集合是否都拥有相同的权重
sameWeight = true;
// 如果是当前的活跃值和当前最小的活跃值相等,则执行累加计算
} else if (active == leastActive) {
// 记录当前invoker的索引位置
leastIndexs[leastCount++] = i;
// 累加拥有最小活跃值的权重值
totalWeight += afterWarmup;
// 并非所有拥有最小活跃值的invoker权重都相等的情况
if (sameWeight && i > 0
&& afterWarmup != firstWeight) {
sameWeight = false;
}
}
}
// 拥有最小活跃值的invoker只有1个
if (leastCount == 1) {
// 直接返回唯一拥有最小活跃值的invoker
return invokers.get(leastIndexs[0]);
}
// 2022年3月2日16:17:35
if (!sameWeight && totalWeight > 0) {
int offsetWeight = random.nextInt(totalWeight) + 1;
for (int i = 0; i < leastCount; i++) {
int leastIndex = leastIndexs[i];
offsetWeight -= getWeight(invokers.get(leastIndex), invocation);
if (offsetWeight <= 0)
return invokers.get(leastIndex);
}
}
// 所有拥有最小活跃值的invoker都拥有相同的权重值,则随机选择一个
return invokers.get(leastIndexs[random.nextInt(leastCount)]);
}
}
2022年3月2日15:51:52
处RpcStatus是用来维护远程调用状态的对象,用来辅助QOS(个人猜测,非官方说法!!!)
,2022年3月2日15:55:55
处是获取权重值,具体参考2.1:getWeight
。2022年3月2日16:17:35
处是随机选择一个,可以参考3.1:RandomLoadBalance
。
3.4:ConsistentHashLoadBalance
基于一致性哈希 实现的负载均衡算法,源码如下:
class FakeCls {
// com.alibaba.dubbo.rpc.cluster.loadbalance.ConsistentHashLoadBalance.doSelect
@Override
protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
// 获取调用的服务类的方法名称
String methodName = RpcUtils.getMethodName(invocation);
// 服务类名称.方法名称 如:dongshi.daddy.service.cluster.ClusterService.sayHi
String key = invokers.get(0).getUrl().getServiceKey() + "." + methodName;
// 获取对象的哈希值
int identityHashCode = System.identityHashCode(invokers);
// private final ConcurrentMap<String, ConsistentHashSelector<?>> selectors = new ConcurrentHashMap<String, ConsistentHashSelector<?>>();
// 处获取对应的服务类方法的选择器,因为`服务类+方法`组合组成负载均衡的集群,所以这里以此为单位来操作
ConsistentHashSelector<T> selector = (ConsistentHashSelector<T>) selectors.get(key);
if (selector == null || selector.identityHashCode != identityHashCode) {
// 2022年3月8日17:58:30
// 创建ConsistentHashSelector实例
selectors.put(key, new ConsistentHashSelector<T>(invokers, methodName, identityHashCode));
// 获取一致性哈希选择器
selector = (ConsistentHashSelector<T>) selectors.get(key);
}
// 2022年3月8日17:55:45
return selector.select(invocation);
}
}
2022年3月8日17:58:30
处是创建一致性哈希选择器,具体参考3.4.1:ConsistentHashSelector
。2022年3月8日17:55:45
处是通过一致性哈希选择器选择一个执行器,具体参考3.4.3:select
。
3.4.1:ConsistentHashSelector
使用的是Ketama[一致性哈希算法}() 。
源码如下:
// com.alibaba.dubbo.rpc.cluster.loadbalance.ConsistentHashLoadBalance.ConsistentHashSelector
private static final class ConsistentHashSelector<T> {
// 虚拟invoker,即虚拟节点
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) {
// 初始化虚拟节点
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);
// 在选择目标invoker时使用方法参数中的哪个位置的参数来计算哈希值[0]
String[] index = Constants.COMMA_SPLIT_PATTERN.split(url.getMethodParameter(methodName, "hash.arguments", "0"));
// 赋值到argumentIndex,后续根据方法入参生成用于计算哈希值的key时,会用到,参考方法com.alibaba.dubbo.rpc.cluster.loadbalance.ConsistentHashLoadBalance.ConsistentHashSelector.toKey
argumentIndex = new int[index.length];
for (int i = 0; i < index.length; i++) {
argumentIndex[i] = Integer.parseInt(index[i]);
}
// 循环处理每一个invokers为其生成虚拟节点
for (Invoker<T> invoker : invokers) {
// 如192.168.64.1:20826
String address = invoker.getUrl().getAddress();
// 2022年3月10日10:45:08
for (int i = 0; i < replicaNumber / 4; i++) {
// 以地址+i作为虚拟节点的哈希值
byte[] digest = md5(address + i);
// 依次处理哈希结果的16byte数据添加到虚拟节点
for (int h = 0; h < 4; h++) {
// 2022年3月10日10:50:24
long m = hash(digest, h);
virtualInvokers.put(m, invoker);
}
}
}
}
}
2022年3月10日10:45:08
处每4个虚拟机节点作为一组这一组对应的是一个节点,为什么是4呢,因为后续计算哈希使用的是md5算法,而md5算法的结果是一个128bit即16byte的结果,这样将16byte平分为4份,在生成了对应数量的虚拟节点的同时,也充分利用了结果的每一个bit,增加了结果的随机性
。2022年3月10日10:50:24
计算哈希值,具体参考3.4.2:hash
。
3.4.2:hash
源码如下:
class FakeCls {
// com.alibaba.dubbo.rpc.cluster.loadbalance.ConsistentHashLoadBalance.ConsistentHashSelector.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;
}
}
digest可能如下图:
以上方法是取对应的4个byte,分别是[0,3],[4,7],[8,11],[12,15],左移运算<<
是将结果扩大对应的倍数,& 0xFF
是将负数转正数,如-82,结果是174,我们来简单看下这个计算过程:
在计算机中有符号数高位为1的代表负数,高位为0的代表正数,而负数是以补码形式存储的,而补码是绝对值二进制码取反然后加1的结果,因此-82在计算机中的存储形式如下:
取绝对值:82 -> 01010010
取反:01010010 -> 10101101
+1:10101101 -> 10101110
高位补1:10101110 -> 11111111 11111111 11111111 10101110
0xFF在计算机中的表示是:11111111,因为是正数,所以高位补0,结果为:00000000 00000000 00000000 11111111
-82 & 0xFF
11111111 11111111 11111111 10101110
& 00000000 00000000 00000000 11111111
= 00000000 00000000 00000000 10101110 = 174
最后的& 0xFFFFFFFFL
作用是转long。
3.4.3:select
源码如下:
class FakeCls {
// com.alibaba.dubbo.rpc.cluster.loadbalance.ConsistentHashLoadBalance.ConsistentHashSelector.select
public Invoker<T> select(Invocation invocation) {
// 使用调用方法的参数作为key,如sayHi("helloooo"),这里就是helloooo
String key = toKey(invocation.getArguments());
// 计算md5
byte[] digest = md5(key);
// 2022年3月10日13:31:38
// 计算哈希值,然后获取目标invoker
return selectForKey(hash(digest, 0));
}
}
2022年3月10日13:31:38
处源码如下:
class FakeCls {
// com.alibaba.dubbo.rpc.cluster.loadbalance.ConsistentHashLoadBalance.ConsistentHashSelector.selectForKey
private Invoker<T> selectForKey(long hash) {
// 获取大于等于hash的子字典,然后获取第一个,就是我们需要的
Map.Entry<Long, Invoker<T>> entry = virtualInvokers.tailMap(hash, true).firstEntry();
// 没有,则使用第一个,当没有落到哈希环内有这种情况???
if (entry == null) {
entry = virtualInvokers.firstEntry();
}
// 获取目标值
return entry.getValue();
}
}