介绍
在实际生产环境中,为了保证服务稳定可靠的运行,我们需要将一个相同的服务部署多个实例。然而远程服务并不是每时每刻都能正常运行,当某个服务的调用出现异常,需要自动容错。
集群容错的流程如下图
Directory接口:服务目录接口,包含了每个服务的Invoker集合,并且这个集合是动态的。是后续集群容错,路由,负载均衡的基础
Cluster接口:集群容错的接口,当Consumer的调用某些Provider失败时,能将请求转发到那些正常的Provider节点上
Router接口:路由接口,按照用户指定的规则匹配出符合条件的Provider
LoadBalance接口:负载均衡接口,按照指定的负载均衡策略从Provider集合中选择一个来处理请求
Directory(服务目录)
Directory(服务目录)代表了多个Invoker(对于消费端来说,每个Invoker代表一个服务提供者)
RegistryDirectory:Invoker列表是根据注册中心的推送而进行变化的,实现了NotifyListener接口,当注册中心监听的路径发生变化的时候,会回调NotifyListener#notify方法,这样就能更新Invoker列表
StaticDirectory:当使用来多注册中心时,把所有注册中心的invoker列表汇集到一个invoker列表中
下面是服务引入过程中,订阅zookeeper服务,生成Invoker的过程。因为这个过程和本节相关性比较大,所以放到本节来分析
我们来分析一下Invoker的生成过程,顺便分析一下RegistryDirectory中的Invoker为什么能动态刷新
下图是ZookeeperRegistry#doSubscribe的实现,第一次订阅或者节点发生变化,都会执行ZookeeperRegistry#notify方法,这个方法会回调RegistryDirectory#notify方法,并更新缓存
RegistryDirectory#refreshOverrideAndInvoker(providerURLs)
方法就是根据providerURLs生成Invoker的过程
主要逻辑如下:
- 只有一个服务提供者,且协议为empty则会禁用改服务
- 根据providerURLs生成新的Invokers,如果某个providerURL的Invoker已经存在则不重新生成,否则生成Invoker
- 销毁旧的providerURLs生成的Invoker(被新的providerURL用了的Invoker不会销毁哈)
大概逻辑如下图
Cluster
在服务引入的过程中,Cluster会把多个Invoker合并,只暴露出一个Invoker让调用方使用
// RegistryProtocol#doRefer
Invoker invoker = cluster.join(directory);
Cluster包含了集群容错策略,最终使用的容错策略由Dubbo SPI来决定,默认是FailoverCluster
实现类 | 解释 |
---|---|
AvailableCluster | 找到一个可用的节点,直接发起调用 |
FailoverCluster | 失败重试(默认) |
FailfastCluster | 快速失败 |
FailsafeCluster | 安全失败 |
FailbackCluster | 失败自动恢复 |
ForkingCluster | 并行调用 |
BroadcastCluster | 广播调用 |
@SPI(FailoverCluster.NAME)
public interface Cluster {
@Adaptive
<T> Invoker<T> join(Directory<T> directory) throws RpcException;
}
public class FailoverCluster implements Cluster {
public final static String NAME = "failover";
@Override
public <T> Invoker<T> join(Directory<T> directory) throws RpcException {
return new FailoverClusterInvoker<T>(directory);
}
}
FailoverCluster#join方法只是简单的返回了一个FailoverClusterInvoker,其他集群容错的策略和这个一样,都是返回对应的Invoker
所有的集群容错的Invoker都实现了AbstractClusterInvoker接口
AbstractClusterInvoker接口主要抽象了如下两部分的逻辑
- 根据路由配置过滤出符合条件的Invoker
- 初始化负载均衡策略,对List<Invoker> invokers进行负载均衡
分析一下FailoverClusterInvoker的实现,其他Invoker的实现和这个类似,有兴趣的可以看看
public class FailoverClusterInvoker<T> extends AbstractClusterInvoker<T> {
@Override
@SuppressWarnings({
"unchecked", "rawtypes"})
public Result doInvoke(Invocation invocation, final List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
List<Invoker<T>> copyInvokers = invokers;
checkInvokers(copyInvokers, invocation);
String methodName = RpcUtils.getMethodName(invocation);
// 获取重试次数
int len = getUrl().getMethodParameter(methodName, Constants.RETRIES_KEY, Constants.DEFAULT_RETRIES) + 1;
if (len <= 0) {
len = 1;
}
// retry loop.
RpcException le = null; // last exception.
List<Invoker<T>> invoked = new ArrayList<Invoker<T>>(copyInvokers.size()); // invoked invokers.
Set<String> providers = new HashSet<String>(len);
// 循环调用,失败重试
for (int i = 0; i < len; i++) {
//Reselect before retry to avoid a change of candidate `invokers`.
//NOTE: if `invokers` changed, then `invoked` also lose accuracy.
if (i > 0) {
// 当前实例已经被销毁,则抛出异常
checkWhetherDestroyed();
// 重新获取服务提供者
copyInvokers = list(invocation);
// check again
// 重新检查一下
checkInvokers(copyInvokers, invocation);
}
// 通过负载均衡选择 Invoker,已经调用过的不会再选择
Invoker<T> invoker = select(loadbalance, invocation, copyInvokers, invoked);
invoked.add(invoker);
RpcContext.getContext().setInvokers((List) invoked);
try {
// 发起远程调用
Result result = invoker.invoke(invocation);
if (le != null && logger.isWarnEnabled()) {
logger.warn("Although retry the method " + methodName
+ " in the service " + getInterface().getName()
+ " was successful by the provider " + invoker.getUrl().getAddress()
+ ", but there have been failed providers " + providers
+ " (" + providers.size() + "/" + copyInvokers.size()
+ ") from the registry " + directory.getUrl().getAddress()
+ " on the consumer " + NetUtils.getLocalHost()
+ " using the dubbo version " + Version.getVersion() + ". Last error is: "
+ le.getMessage(), le);
}
return result;
} catch (RpcException e) {
// 业务类的异常直接跑出来
if (e.isBiz()) {
// biz exception.
throw e;
}
le = e;
} catch (Throwable e) {
le = new RpcException(e.getMessage(), e);
} finally {
providers.add(invoker.getUrl().getAddress());
}
}
throw new RpcException(le.getCode(), "Failed to invoke the method "
+ methodName + " in the service " + getInterface().getName()
+ ". Tried " + len + " times of the providers " + providers
+ " (" + providers.size() + "/" + copyInvokers.size()
+ ") from the registry " + directory.getUrl().getAddress()
+ " on the consumer " + NetUtils.getLocalHost() + " using the dubbo version "
+ Version.getVersion() + ". Last error is: "
+ le.getMessage(), le.getCause() != null ? le.getCause() : le);
}
}
执行调用的时候,doInvoke方法会执行,其中invokers(路由过后的Invokers),loadbalance(负载均衡策略)都是由AbstractClusterInvoker根据配置决定的,doInvoke调用成功则直接返回,否则遍历invokers列表继续遍历,直到超过重试次数。如果此时还没有调用成功,则会抛出RpcException
Router(路由)
从Directory中根据调用信息找到的Invoker并不能直接拿来调用,需要经过路由规则过滤后的Invoker才直接发起调用
配置如下路由规则,表示ip为 172.22.3.1的服务调用方,只会调用ip为172.22.3.2的服务
host = 172.22.3.1 => host = 172.22.3.2
路由规则可以看官方文档:
http://dubbo.apache.org/zh/docs/v2.7/user/examples/routing-rule/
路由分为如下三种
- 条件路由:使用Dubbo定义的语法规则去写路由规则
- 文件路由:框架从文件中读取路由规则
- 脚本路由:使用jdk自身的脚本解析引擎解析路由规则脚本
条件路由用的最多,简单介绍一下条件路由的实现
public class ConditionRouter extends AbstractRouter {
protected Map<String, MatchPair> whenCondition;
protected Map<String, MatchPair> thenCondition;
}
路由时主要和whenCondition和thenCondition打交道,在构造函数中会初始化这2个map
路由条件如下时,生成的2个map如下
host != 4.4.4.4 & host = 2.2.2.2,1.1.1.1,3.3.3.3 & method = sayHello => host = 1.2.3.4 & host != 4.4.4.4
下面就是执行路由逻辑的部分,matenWhen和matenThen的方法就是利用上面初始化好的whenCondition和thenCondition进行匹配的过程,不具体分析了,对实现感兴趣的话可以调试一下官方对这个类写的Test类(ctrl + shift + t 快捷键即可快速到达对应的Test类)
// ConditionRouter
public <T> List<Invoker<T>> route(List<Invoker<T>> invokers, URL url, Invocation invocation)
throws RpcException {
// 不生效
if (!enabled) {
return invokers;
}
if (CollectionUtils.isEmpty(invokers)) {
return invokers;
}
try {
// 没有whenRule匹配,返回所有
if (!matchWhen(url, invocation)) {
return invokers;
}
List<Invoker<T>> result = new ArrayList<Invoker<T>>();
// 没有thenRule,则表明服务消费者在黑名单中,返回空列表
if (thenCondition == null) {
logger.warn("The current consumer in the service blacklist. consumer: " + NetUtils.getLocalHost() + ", service: " + url.getServiceKey());
return result;
}
for (Invoker<T> invoker : invokers) {
// 匹配成功
if (matchThen(invoker.getUrl(), url)) {
result.add(invoker);
}
}
if (!result.isEmpty()) {
// result不为空,直接返回
return result;
} else if (force) {
// result为空 force=true 强制返回空列表
logger.warn("The route result is empty and force execute. consumer: " + NetUtils.getLocalHost() + ", service: " + url.getServiceKey() + ", router: " + url.getParameterAndDecoded(Constants.RULE_KEY));
return result;
}
} catch (Throwable t) {
logger.error("Failed to execute condition router rule: " + getUrl() + ", invokers: " + invokers + ", cause: " + t.getMessage(), t);
}
// result为空,force=false 返回所有Invoker列表
return invokers;
}
LoadBalance(负载均衡)
经过路由规则过滤后的Invoker,如果只有一个,就可以直接发起调用了。如果有多个,此时就涉及到负载均衡策略了,Dubbo提供了如下策略,如果不能满足你的需求,你可以自定义LoadBalance接口的实现
实现类 | 解释 |
---|---|
RandomLoadBalance | 随机策略(默认) |
RoundRobinLoadBalance | 轮询策略 |
LeastActiveLoadBalance | 最少活跃调用数 |
ConsistentHashLoadBalance | 一致性hash策略 |
AbstractLoadBalance主要提供了一个方法getWeight,根据服务的启动时间,给服务定一个权重。
主要思路如下
我们来分析一下RandomLoadBalance
思路如下:
RandomLoadBalance 是加权随机算法的具体实现,它的算法思想很简单。假设我们有一组服务器 servers = [A, B, C],他们对应的权重为 weights = [5, 3, 2],权重总和为10。现在把这些权重值平铺在一维坐标值上,[0, 5) 区间属于服务器 A,[5, 8) 区间属于服务器 B,[8, 10) 区间属于服务器 C。接下来通过随机数生成器生成一个范围在 [0, 10) 之间的随机数,然后计算这个随机数会落到哪个区间上。比如数字3会落到服务器 A 对应的区间上,此时返回服务器 A 即可。权重越大的机器,在坐标轴上对应的区间范围就越大,因此随机数生成器生成的数字就会有更大的概率落到此区间内。只要随机数生成器产生的随机数分布性很好,在经过多次选择后,每个服务器被选中的次数比例接近其权重比例。比如,经过一万次选择后,服务器 A 被选中的次数大约为5000次,服务器 B 被选中的次数约为3000次,服务器 C 被选中的次数约为2000次。
public class RandomLoadBalance extends AbstractLoadBalance {
public static final String NAME = "random";
@Override
protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
// Number of invokers
int length = invokers.size();
// Every invoker has the same weight?
boolean sameWeight = true;
// the weight of every invokers
int[] weights = new int[length];
// the first invoker's weight
// 获取第一个服务的权重
int firstWeight = getWeight(invokers.get(0), invocation);
weights[0] = firstWeight;
// The sum of weights
int totalWeight = firstWeight;
// 下面这个循环有两个作用
// 1. 计算总权重
// 2. 检测所有服务的权重是否相同
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;
}
}
// 下面的 if 分支主要用于获取随机数,并计算随机数落在哪个区间上
if (totalWeight > 0 && !sameWeight) {
// If (not every invoker has the same weight & at least one invoker's weight>0), select randomly based on totalWeight.
// 随机获取一个 [0, totalWeight) 区间内的数字
int offset = ThreadLocalRandom.current().nextInt(totalWeight);
// Return a invoker based on the random value.
// 循环让 offset 数减去服务提供者权重值,当 offset 小于0时,返回相应的 Invoker。
// 举例说明一下,我们有 servers = [A, B, C],weights = [5, 3, 2],offset = 7。
// 第一次循环,offset - 5 = 2 > 0,即 offset > 5,
// 表明其不会落在服务器 A 对应的区间上。
// 第二次循环,offset - 3 = -1 < 0,即 5 < offset < 8,
// 表明其会落在服务器 B 对应的区间上
for (int i = 0; i < length; i++) {
offset -= weights[i];
if (offset < 0) {
return invokers.get(i);
}
}
}
// 如果所有服务提供者权重值相同,此时直接随机返回一个即可
// If all invokers have the same weight value or totalWeight=0, return evenly.
return invokers.get(ThreadLocalRandom.current().nextInt(length));
}
}
其他的你可以看一下官网的分析,写的很清楚
http://dubbo.apache.org/zh/docs/v2.7/dev/source/loadbalance/
参考博客
服务目录
[0]http://dubbo.apache.org/zh/docs/v2.7/dev/source/directory/
负载均衡
[1]http://dubbo.apache.org/zh/docs/v2.7/dev/source/loadbalance/