“我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第2篇文章,点击查看活动详情”
前面文章分析了Dubbo客户端启动相关流程,其中提到了路由策略,当时由于篇幅问题,未展开详细讲解,本文将带大家一起深入分析下其实现。
一、温故知新
private <T> Invoker<T> doRefer(Cluster cluster, Registry registry, Class<T> type, URL url) {
... ...
// 1、建立路由规则
directory.buildRouterChain(subscribeUrl);
// 2、订阅服务提供方者地址
directory.subscribe(subscribeUrl.addParameter(CATEGORY_KEY,
PROVIDERS_CATEGORY + "," + CONFIGURATORS_CATEGORY + "," + ROUTERS_CATEGORY));
// 3、包装容错策略到invoker
Invoker invoker = cluster.join(directory);
return invoker;
}
public void buildRouterChain(URL url) {
this.setRouterChain(RouterChain.buildChain(url));
}
public static <T> RouterChain<T> buildChain(URL url) {
return new RouterChain<>(url);
}
前文分析RegistryProtocol-doRefer方法时,提到过,首先调用directory.buildRouterChain方法进行路由策略的建立,然后通过RouterChain.buildChain方法创建RouterChain对象,最终在进行远程方法调用的时候,会通过RouterChain.route方法过滤获取到可用的invoke list。那就让我们来看看RouterChain到底是什么东东吧。
二、RouterChain详解
其实故名思义,其本意为“路由链”,也就说要进行一系列的链式调用。最终过滤出来可用的的invokelist,供后面路由策略调用。前文也分析过,在进行RouterChain初始化的时候会进行四种路由策略的初始化,从初始化方法可以看到,默认四种routers实现,优先级分别是MockInvokersSelector>TagRouter>AppRouter>ServiceRouter
- MockInvokersSelector
- 该类与服务降级、Mock功能有关,其route方法判断invocation.need.mock参数的的值,默认情况未配置,因此不会执行任何操作。
- TagRouter
- tag路由是dubbo2.6.6新增的功能,可以实现流量隔离,访问指定的tag服务。consumer端可以通过RpcContext.getContext().setAttachment("dubbo.tag", "xxx")设置tag,即可访问到对应的服务。
- AppRouter & ServiceRouter
- 都继承了ListenableRouter类,区别在于调用ListenableRouter构造函数的时候,传参不一样,两个类的路由规则其实都是委托给ConditionRouter集合。
三、路由策略选择
拿到可用的invoke list后,就要进行负载均衡选择最终的invoke啦。集群容错策略以FailoverClusterInvoker为例,执行其doInvoke方法进行方法调用时,首先通过select获取到最终执行方法调用的invoke,这里的选择,就是经历负载均衡策略之后的结果。
public Result doInvoke(Invocation invocation, final List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
... ...
Invoker<T> invoker = select(loadbalance, invocation, copyInvokers, invoked);
... ...
查看select实现如下:
protected Invoker<T> select(LoadBalance loadbalance, Invocation invocation,
List<Invoker<T>> invokers, List<Invoker<T>> selected)
... ...
Invoker<T> invoker = doSelect(loadbalance, invocation, invokers, selected);
... ...
}
查看doSelect实现如下:
private Invoker<T> doSelect(LoadBalance loadbalance, Invocation invocation,
List<Invoker<T>> invokers, List<Invoker<T>> selected) throws RpcException {
... ...
Invoker<T> invoker = loadbalance.select(invokers, getUrl(), invocation);
... ...
}
内部其实是调用loadbalance.select方法进行实际的负载均衡选择。
四、LoadBalance分析
@SPI(RandomLoadBalance.NAME)
public interface LoadBalance {
@Adaptive("loadbalance")
<T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException;
}
可以看到LoadBalance本身也是一个SPI,默认实现为RandomLoadBalance,查看select实现,最终调用的是抽象方法doSelect。
@Override
public <T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) {
if (CollectionUtils.isEmpty(invokers)) {
return null;
}
if (invokers.size() == 1) {
return invokers.get(0);
}
return doSelect(invokers, url, invocation);
}
protected abstract <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation);
查看doSelect实现如下:
实际上Dubbo提供了四种负载均衡策略,分别是RandomLoadBalance-随机、ConsistentHashLoadBalance-一致性hash、LeastActiveLoadBalance-最小连接数、RoundRobinLoadBalance-轮询。
4.1 RandomLoadBalance-随机
@Override
protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
// 服务提供方总个数
int length = invokers.size();
// 权重是否一样
boolean sameWeight = true;
// 存放所有服务提供者设置的权重
int[] weights = new int[length];
// 第一个服务提供者的权重
int firstWeight = getWeight(invokers.get(0), invocation);
weights[0] = firstWeight;
// 所有权重之和
int totalWeight = firstWeight;
// 累加所有服务提供者设置的权重
for (int i = 1; i < length; i++) {
int weight = getWeight(invokers.get(i), invocation);
// save for later use
weights[i] = weight;
// Sum
totalWeight += weight;
if (sameWeight && weight != firstWeight) {
sameWeight = false;
}
}
// 如果所有提供者的权重不一样,并且至少有一个提供者的权重 >0,则基于总权重,随机选一个
if (totalWeight > 0 && !sameWeight) {
int offset = ThreadLocalRandom.current().nextInt(totalWeight);
for (int i = 0; i < length; i++) {
offset -= weights[i];
if (offset < 0) {
return invokers.get(i);
}
}
}
// 所有提供者的权重都一样,就基于服务提供方的个数轮训随机
return invokers.get(ThreadLocalRandom.current().nextInt(length));
}
4.2 ConsistentHashLoadBalance-一致性hash
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;
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, methodName, identityHashCode));
selector = (ConsistentHashSelector<T>) selectors.get(key);
}
return selector.select(invocation);
}
一致性hash算法实现主要依赖ConsistentHashSelector类,构造函数以及核心select方法如下
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 = 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]);
}
// 遍历invokers列表,计算每个阶段的hash值,保存到virtualInvokers
for (Invoker<T> invoker : invokers) {
String address = invoker.getUrl().getAddress();
for (int i = 0; i < replicaNumber / 4; i++) {
byte[] digest = md5(address + i);
for (int h = 0; h < 4; h++) {
long m = hash(digest, h);
virtualInvokers.put(m, invoker);
}
}
}
}
public Invoker<T> select(Invocation invocation) {
// 获取参数一致性hash算法的key
String key = toKey(invocation.getArguments());
// 根据key计算md5
byte[] digest = md5(key);
// 计算key对应的hash环上哪一个节点
return selectForKey(hash(digest, 0));
}
这里多说一句,其实Guava包里也提供了一个一致性hash算法实现,有兴趣的同学可以自行研究下。
4.3 LeastActiveLoadBalance-最小连接数
@Override
protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
// 服务提供者数量
int length = invokers.size();
// 最小调用次数
int leastActive = -1;
// 调用次数=最小调用次数的 服务提供者的数量
int leastCount = 0;
// 调用次数=最小调用次数的 服务提供这的下标
int[] leastIndexes = new int[length];
// 记录每个服务的权重
int[] weights = 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 afterWarmup = getWeight(invoker, invocation);
weights[i] = afterWarmup;
if (leastActive == -1 || active < leastActive) {
leastActive = active;
leastCount = 1;
leastIndexes[0] = i;
totalWeight = afterWarmup;
firstWeight = afterWarmup;
sameWeight = true;
} else if (active == leastActive) {
leastIndexes[leastCount++] = i;
totalWeight += afterWarmup;
if (sameWeight && i > 0
&& afterWarmup != firstWeight) {
sameWeight = false;
}
}
}
// 如果只有一个最小调用次数的的invoker,则直接返回
if (leastCount == 1) {
return invokers.get(leastIndexes[0]);
}
// 存在多个invoke的最小调用次数相同,并且多个权重不一样,则在最小调用次数的集合里随机选一个
if (!sameWeight && totalWeight > 0) {
int offsetWeight = ThreadLocalRandom.current().nextInt(totalWeight);
for (int i = 0; i < leastCount; i++) {
int leastIndex = leastIndexes[i];
offsetWeight -= weights[leastIndex];
if (offsetWeight < 0) {
return invokers.get(leastIndex);
}
}
}
// 存在多个最小invoker的最小调用次数相同,并且权重一样,则直接在最小的集合里随机一个
return invokers.get(leastIndexes[ThreadLocalRandom.current().nextInt(leastCount)]);
}
4.4 RoundRobinLoadBalance-轮询
@Override
protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
// 获取调用方法的key
String key = invokers.get(0).getUrl().getServiceKey() + "." + invocation.getMethodName();
// 获取该调用方法对应的每个服务提供者的WeightedRoundRobin对象
ConcurrentMap<String, WeightedRoundRobin> map = methodWeightMap.get(key);
if (map == null) {
methodWeightMap.putIfAbsent(key, new ConcurrentHashMap<String, WeightedRoundRobin>());
map = methodWeightMap.get(key);
}
// 遍历所有服务提供者,计算总权重最大的提供者
int totalWeight = 0;
long maxCurrent = Long.MIN_VALUE;
long now = System.currentTimeMillis();
Invoker<T> selectedInvoker = null;
WeightedRoundRobin selectedWRR = null;
for (Invoker<T> invoker : invokers) {
String identifyString = invoker.getUrl().toIdentityString();
WeightedRoundRobin weightedRoundRobin = map.get(identifyString);
int weight = getWeight(invoker, invocation);
if (weightedRoundRobin == null) {
weightedRoundRobin = new WeightedRoundRobin();
weightedRoundRobin.setWeight(weight);
map.putIfAbsent(identifyString, weightedRoundRobin);
}
// 权重发生了变化
if (weight != weightedRoundRobin.getWeight()) {
weightedRoundRobin.setWeight(weight);
}
long cur = weightedRoundRobin.increaseCurrent();
weightedRoundRobin.setLastUpdate(now);
if (cur > maxCurrent) {
maxCurrent = cur;
selectedInvoker = invoker;
selectedWRR = weightedRoundRobin;
}
totalWeight += weight;
}
// 更新map
if (!updateLock.get() && invokers.size() != map.size()) {
if (updateLock.compareAndSet(false, true)) {
try {
// 拷贝新值
ConcurrentMap<String, WeightedRoundRobin> newMap = new ConcurrentHashMap<>(map);
// 移除过期的
newMap.entrySet().removeIf(item -> now - item.getValue().getLastUpdate() > RECYCLE_PERIOD);
methodWeightMap.put(key, newMap);
} finally {
updateLock.set(false);
}
}
}
// 返回选择的select对象
if (selectedInvoker != null) {
selectedWRR.sel(totalWeight);
return selectedInvoker;
}
// 兜底,应该不会走到这
return invokers.get(0);
}
这里的轮训算法,选择的时候参考了机器权重,假如当前服务提供者有三台机器,权重都一样,那么就会按照A、B、C顺序的方式依次选择每台机器。引入了权重就相对复杂一点,以A、B、C对应的权重分别为1、2、3为例
- 第一次调用
| 机器 | (weight,current) | 计算(weight,current) | 选择机器 | 选择后的(weight,current) |
|---|---|---|---|---|
| A | (1,0) | (1,1) | 选择机器C | (1,1) |
| B | (2,0) | (2,2) | 选择机器C | (2,2) |
| C | (3,0) | (3,3) | 选择机器C | (3,-3) |
第一次调用服务时,WeightedRoundRobin里的weight,current如二列所示,执行完选择权重和最大的代码后,weight,current如第三列所示,此时机器C的权重最大,因此,第一次选择,会选择机器C用来发起远程服务调用。
- 第二次调用
| 机器 | (weight,current) | 计算(weight,current) | 选择机器 | 选择后的(weight,current) |
|---|---|---|---|---|
| A | (1,2) | (1,2) | 选择机器B | (1,2) |
| B | (2,2) | (2,4) | 选择机器B | (2,-2) |
| C | (3,-3) | (3,0) | 选择机器B | (3,0) |
第二次调用服务时,WeightedRoundRobin里的weight,current如二列所示,执行完选择权重和最大的代码后,weight,current如第三列所示,此时机器B的权重最大,因此,第二次选择,会选择机器B用来发起远程服务调用。
- 第三次调用
| 机器 | (weight,current) | 计算(weight,current) | 选择机器 | 选择后的(weight,current) |
|---|---|---|---|---|
| A | (1,2) | (1,3) | 选择机器A | (1,-3) |
| B | (2,-2) | (2,0) | 选择机器A | (2,0) |
| C | (3,0) | (3,3) | 选择机器A | (3,3) |
第三次调用服务时,WeightedRoundRobin里的weight,current如二列所示,执行完选择权重和最大的代码后,weight,current如第三列所示,此时机器A的权重最大,因此,第三次选择,会选择机器B用来发起远程服务调用。
四、小节
本文主要分析了Dubbo负载均衡实现,其中提到的几种负载均衡算法也是平时工作中经常用到的,能理解最好,实在不理解也先有个印象,等日后工作中用到时候,最起码知道去哪里参考(手动🐶)。