一、总览
- Failover——失败自动切换
当出现失败,重试其它服务器,通常用于读操作(推荐使用)。 重试会带来更长延迟。 - Failfast—— 快速失败
只发起一次调用,失败立即报错,通常用于非幂等性的写操作。 如果有机器正在重启,可能会出现调用失败 。 - Failsafe——失败安全
出现异常时,直接忽略,通常用于写入审计日志等操作。 调用信息丢失 可用于生产环境 Monitor。 - Failback——失败自动恢复
后台记录失败请求,定时重发。通常用于消息通知操作,不可靠,重启丢失。可用于生产环境 Registry。 - Forking——并行调用多个服务器
只要一个成功即返回,通常用于实时性要求较高的读操作。 需要浪费更多服务资源 。 - Broadcast——广播调用
所有提供逐个调用,任意一台报错则报错。通常用于更新提供方本地状态 速度慢,任意一台报错则报错 。
现在的服务一般都要求高可用性,在dubbo集群中提供了多种集群容错策略。本节将重点分析集群容错机制 ( AbstractClusterInvoker)。
在集群调用失败时,Dubbo 提供了多种容错方案,缺省为 failover 重试。
各节点关系:
- 这里的
Invoker
是Provider
的一个可调用Service
的抽象,Invoker
封装了Provider
地址及Service
接口信息 Directory
代表多个Invoker
,可以把它看成List<Invoker>
,但与List
不同的是,它的值可能是动态变化的,比如注册中心推送变更Cluster
将Directory
中的多个Invoker
伪装成一个Invoker
,对上层透明,伪装过程包含了容错逻辑,调用失败后,重试另一个Router
负责从多个Invoker
中按路由规则选出子集,比如读写分离,应用隔离等LoadBalance
负责从多个Invoker
中选出具体的一个用于本次调用,选的过程包含了负载均衡算法,调用失败后,需要重选
1.1 AbstractClusterInvoker
AbstractClusterInvoker 就是将上述机制融合在一起,整个集群容错中,上述组件扮演的角色见下图所示,本文将重点分析 AbstractClusterInvoker 是如何融合这些组件的。
1.1.1 AbstractClusterInvoker#invoke
public Result invoke(final Invocation invocation) throws RpcException {
checkWhetherDestroyed();
// binding attachments into invocation.
Map<String, String> contextAttachments = RpcContext.getContext().getAttachments();
if (contextAttachments != null && contextAttachments.size() != 0) {
((RpcInvocation) invocation).addAttachments(contextAttachments);
}
List<Invoker<T>> invokers = list(invocation); //@1
LoadBalance loadbalance = initLoadBalance(invokers, invocation); //@2
RpcUtils.attachInvocationIdIfAsync(getUrl(), invocation);
return doInvoke(invocation, invokers, loadbalance); //@3
}
代码@1:根据调用上下文,获取服务提供者列表,服务提供者从Directory。
protected List<Invoker<T>> list(Invocation invocation) throws RpcException {
return directory.list(invocation);
}
最终会调用RegistryDirecotry的list方法,该方法的服务提供者是当该消费者订阅的服务的服务提供者列表发送变化后,会在注册中心产生事件,然后通知消费者更新服务提供者列表(本地缓存)。需要注意的是RegistryDirecotry在返回Invoker之前,已经使用Router进行了一次筛选,具体实现在RegistryDirectory#notify方法时。
代码@2:根据SPI机制,获取负载均衡算法的实现类,根据< dubbo:consumer loadbalance=""/>、< dubbo:reference loadbalance=""/>等标签的配置值,默认为random,加权随机算法。
protected LoadBalance initLoadBalance(List<Invoker<T>> invokers, Invocation invocation) {
if (CollectionUtils.isNotEmpty(invokers)) {
return ExtensionLoader.getExtensionLoader(LoadBalance.class).getExtension(invokers.get(0).getUrl()
.getMethodParameter(RpcUtils.getMethodName(invocation), LOADBALANCE_KEY, DEFAULT_LOADBALANCE));
} else {
return ExtensionLoader.(LoadBalance.class).getExtension(DEFAULT_LOADBALANCE);
}
}
代码@3:根据调用上下文,服务提供者列表,负载均衡算法选择一服务提供者,具体代码由AbstractClusterInvoker的各个子类实现。
Dubbo目前支持的集群容错策略在中/dubbo-cluster/src/main/resources/METAINF/dubbo/internal/com.alibaba.dubbo.rpc.cluster.Cluster定义,具体内容如下:
mock=com.alibaba.dubbo.rpc.cluster.support.wrapper.MockClusterWrapper failover=com.alibaba.dubbo.rpc.cluster.support.FailoverCluster failfast=com.alibaba.dubbo.rpc.cluster.support.FailfastCluster failsafe=com.alibaba.dubbo.rpc.cluster.support.FailsafeCluster failback=com.alibaba.dubbo.rpc.cluster.support.FailbackCluster forking=com.alibaba.dubbo.rpc.cluster.support.ForkingCluster available=com.alibaba.dubbo.rpc.cluster.support.AvailableCluster mergeable=com.alibaba.dubbo.rpc.cluster.support.MergeableCluster broadcast=com.alibaba.dubbo.rpc.cluster.support.BroadcastCluster
上述各种集群策略,对应的执行器为Cluser+Invoker,例如FailoverCluster对应的Invoker为:FailoverClusterInvoker。
在讲解各种集群容错策略之前,我们首先关注一下AbstractClusterInvoker具体从服务提供者中按照不同的负载均衡算法选取服务提供者的算法。
1.1.2 源码分析AbstractClusterInvoker#select
protected Invoker<T> select(LoadBalance loadbalance, Invocation invocation,
List<Invoker<T>> invokers, List<Invoker<T>> selected) throws RpcException { //@1
if (CollectionUtils.isEmpty(invokers)) {
return null;
}
String methodName = invocation == null ? StringUtils.EMPTY : invocation.getMethodName();
boolean sticky = invokers.get(0).getUrl()
.getMethodParameter(methodName, CLUSTER_STICKY_KEY, DEFAULT_CLUSTER_STICKY); //@2
//ignore overloaded method
if (stickyInvoker != null && !invokers.contains(stickyInvoker)) {
stickyInvoker = null;
}
//ignore concurrency problem
if (sticky && stickyInvoker != null && (selected == null || !selected.contains(stickyInvoker))) {
if (availablecheck && stickyInvoker.isAvailable()) {
return stickyInvoker;
}
}
Invoker<T> invoker = doSelect(loadbalance, invocation, invokers, selected); //@3
if (sticky) {
stickyInvoker = invoker;
}
return invoker;
}
代码@1:参数说明
- LoadBalance loadbalance:负载均衡算法。
- Invocation invocation:服务调用上下文环境。
- List< Invoker< T>> invokers:待选的服务提供者列表。
- List< Invoker< T>> selected:本次集群测试,已选择的服务提供者。
代码@2:sticky机制(粘性),如果开启了粘性机制的话。通过< dubbo:method sticky="true"/>,默认不开启。如果开启,上一次该服务调用的是哪个服务提供者,只要调用过程中不发生错误,后续都会选择该服务提供者进行调用。
代码@3:执行doSelect选择。
1.1.2.1
private Invoker<T> doSelect(LoadBalance loadbalance, Invocation invocation,
List<Invoker<T>> invokers, List<Invoker<T>> selected) throws RpcException {
if (CollectionUtils.isEmpty(invokers)) {
return null;
}
if (invokers.size() == 1) { //@1
return invokers.get(0);
}
Invoker<T> invoker = loadbalance.select(invokers, getUrl(), invocation); //@2
//If the `invoker` is in the `selected` or invoker is unavailable && availablecheck is true, reselect.
if ((selected != null && selected.contains(invoker))
|| (!invoker.isAvailable() && getUrl() != null && availablecheck)) {
try {
Invoker<T> rInvoker = reselect(loadbalance, invocation, invokers, selected, availablecheck); // @3
if (rInvoker != null) {
invoker = rInvoker;
} else {
//Check the index of current selected invoker, if it's not the last one, choose the one at index+1.
int index = invokers.indexOf(invoker);
try {
//Avoid collision
invoker = invokers.get((index + 1) % invokers.size());
} catch (Exception e) {
logger.warn(e.getMessage() + " may because invokers list dynamic change, ignore.", e);
}
}
} catch (Throwable t) {
logger.error("cluster reselect fail reason is :" + t.getMessage() + " if can not solve, you can set cluster.availablecheck=false in url", t);
}
}
return invoker;
}
=代码@1:如果可选Invoker只有一个的话,直接返回该Invoker。
代码@2:调用loadBalance负载均衡算法,选择一个服务提供者。
代码@3:如果选择的Invoker已被选择,则重新选择,这里有一个疑问,为什么不在选之前,先过滤掉已被选的Invoker。
从服务提供者列表中选择一个服务提供者算法就介绍到这里,接下来将一一分析Dubbo提供的集群容错方式。
二、集群容错策略
2.1内置容错策略
Dubbo主要内置了如下几种策略:
- Failover(失败自动切换)
- Failsafe(失败安全)
- Failfast(快速失败)
- Failback(失败自动恢复)
- Forking(并行调用)
- Broadcast(广播调用)
下表对各种策略做一个简单对比,
策略名称 | 优点 | 缺点 | 主要应用场景 |
---|---|---|---|
Failover | 对调用者屏蔽调用失败的信息 | 增加RT,额外资源开销,资源浪费 | 对调用rt不敏感的场景 |
Failfast | 业务快速感知失败状态进行自主决策 | 产生较多报错的信息 | 非幂等性操作,需要快速感知失败的场景 |
Failsafe | 即使失败了也不会影响核心流程 | 对于失败的信息不敏感,需要额外的监控 | 旁路系统,失败不影响核心流程正确性的场景 |
Failback | 失败自动异步重试 | 重试任务可能堆积 | 对于实时性要求不高,且不需要返回值的一些异步操作 |
Forking | 并行发起多个调用,降低失败概率 | 消耗额外的机器资源,需要确保操作幂等性 | 资源充足,且对于失败的容忍度较低,实时性要求高的场景 |
Broadcast | 支持对所有的服务提供者进行操作 | 资源消耗很大 | 通知所有提供者更新缓存或日志等本地资源信息 |
2.1.1 FailoverClusterInvoker(FailoverCluster,dubbo默认策略)
策略:失败自动重试
其配置方法,容错策略既可以在服务提供方配置,也可以服务调用方进行配置。而重试次数的配置则更为灵活,既可以在服务级别进行配置,也可以在方法级别进行配置。具体优先顺序为:
服务调用方方法级配置 > 服务调用方服务级配置 > 服务提供方方法级配置 > 服务提供方服务级配置
xml配置:
<dubbo:service interface="org.apache.dubbo.demo.DemoService" ref="demoService" cluster="failover" retries="2" />
默认为2,代表重试2次,最多执行3次 。
FailoverClusterInvoker#doInvoke
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, RETRIES_KEY, DEFAULT_RETRIES) + 1; //@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); //@2
for (int i = 0; i < len; i++) { //@3
//Reselect before retry to avoid a change of candidate `invokers`.
//NOTE: if `invokers` changed, then `invoked` also lose accuracy.
if (i > 0) { //@4
checkWhetherDestroyed();
copyInvokers = list(invocation);
// check again
checkInvokers(copyInvokers, invocation);
}
Invoker<T> invoker = select(loadbalance, invocation, copyInvokers, invoked); //@5
invoked.add(invoker);
RpcContext.getContext().setInvokers((List) invoked);
try {
Result result = invoker.invoke(invocation); //@6
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. //@A
throw e;
}
le = e;
} catch (Throwable e) {
le = new RpcException(e.getMessage(), e);
} finally {
providers.add(invoker.getUrl().getAddress()); //@7
}
}
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);
}
代码@1:首先校验服务提供者列表,如果为空,则抛出RpcException,提示没有可用的服务提供者。
代码@2:构建Set< Stirng> providers,主要用来已调用服务提供者的地址,如果本次调用失败,将在日志信息中打印已调用的服务提供者信息。
代码@3,循环执行次数,等于retries + 1 次。
代码@4:如果i>0,表示服务调用,在重试,此时需要重新调用Directory#list方法,获取最小的服务提供者列表。
代码@5:根据负载均衡算法,选择Invoker,后续详细分析。
代码@6:根据负载算法,路由算法从服务提供者列表选一个服务提供者,发起RPC调用。
代码@7:将本次服务提供者的地址添加到providers集合中,如果多次重试后,无法完成正常的调用,将在错误日志中包含这些信息。
代码@A:判断是否业务异常(code是否为3),是直接抛异常;不是继续for循环调用。
2.1.2 源码分析AvailableClusterInvoker
策略:选择集群第一个可用的服务提供者。 缺点:相当于服务的主备,但同时只有一个服务提供者承载流量,并没有使用集群的负载均衡机制。 AvailableClusterInvoker#doInvoke
public Result doInvoke(Invocation invocation, List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
for (Invoker<T> invoker : invokers) {
if (invoker.isAvailable()) {
return invoker.invoke(invocation);
}
}
throw new RpcException("No provider available in " + invokers);
}
遍历服务提供者列表,选择第一个可用服务提供者,然后执行RPC服务调用,如果调用失败,则失败。
2.1.3 源码分析BroadcastClusterInvoker
策略:广播调用,将调用所有服务提供者,一个服务调用者失败,并不会熔断,并且一个服务提供者调用失败,整个调用认为失败。 场景:刷新缓存。
BroadcastClusterInvoker#doInvoke
public Result doInvoke(final Invocation invocation, List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
checkInvokers(invokers, invocation);
RpcContext.getContext().setInvokers((List) invokers);
RpcException exception = null;
Result result = null;
for (Invoker<T> invoker : invokers) {
try {
result = invoker.invoke(invocation);
} catch (RpcException e) {
exception = e;
logger.warn(e.getMessage(), e);
} catch (Throwable e) {
exception = new RpcException(e.getMessage(), e);
logger.warn(e.getMessage(), e);
}
}
if (exception != null) {
throw exception;
}
return result;
}
代码@1:检测服务提供者列表,如果为空,则抛出没有服务提供的异常。
代码@2:遍历服务提供者列表,依次调用服务提供者的invoker,每个服务调用用try catch语句包裹,当服务调用发生异常时,记录异常信息,但并不立即返回,广播模式,每个服务提供者调用是异步还是同步,取决服务调用的配置,默认是同步调用。
代码@3:只要其中一个服务调用发送一次,将抛出异常 信息,异常信息被封装为RpcException。
2.1.4 源码分析FailbackClusterInvoker
策略:调用失败后,返回成功,但会在后台定时重试,重试次数(反复) 场景:通常用于消息通知,但消费者重启后,重试任务丢失。
FailbackClusterInvoker#doInvoke
protected Result doInvoke(Invocation invocation, List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
Invoker<T> invoker = null;
try {
checkInvokers(invokers, invocation); //@1
invoker = select(loadbalance, invocation, invokers, null); //@2
return invoker.invoke(invocation); //@3
} catch (Throwable e) {
logger.error("Failback to invoke method " + invocation.getMethodName() + ", wait for retry in background. Ignored exception: "
+ e.getMessage() + ", ", e);
addFailed(loadbalance, invocation, invokers, invoker); //@4
return AsyncRpcResult.newDefaultAsyncResult(null, null, invocation); // ignore
}
}
代码@1:校验服务提供者列表,如果为空,则抛出没有服务提供者错误。
代码@2:根据负载均衡机制,选择一个服务提供者。
代码@3:发起远程服务调用,如果出现异常,调用addFailed方法,添加重试任务,然后返回给调用方成功。
接下来看一下addFailed方法。
FailbackClusterInvoker#addFailed
private void addFailed(LoadBalance loadbalance, Invocation invocation, List<Invoker<T>> invokers, Invoker<T> lastInvoker) { //@1
if (failTimer == null) { //@2
synchronized (this) {
if (failTimer == null) {
failTimer = new HashedWheelTimer(
new NamedThreadFactory("failback-cluster-timer", true),
1,
TimeUnit.SECONDS, 32, failbackTasks);
}
}
}
RetryTimerTask retryTimerTask = new RetryTimerTask(loadbalance, invocation, invokers, lastInvoker, retries, RETRY_FAILED_PERIOD); //@3
try {
failTimer.newTimeout(retryTimerTask, RETRY_FAILED_PERIOD, TimeUnit.SECONDS); //@4
} catch (Throwable e) {
logger.error("Failback background works error,invocation->" + invocation + ", exception: " + e.getMessage());
}
}
代码@1:LoadBalance loadBalance :负载均衡策略;
Invocation invocation:调用上下文;
List<Invoker<T>> invokers:提供者列表;
Invoker<T> lastInvoker:上次选中的invoker。
代码@2:如果failTimer为空,则加锁创建一个定时调度任务,任务以每隔3s的频率调用。
代码@3:创建重试定时任务,retries就是配置的重试次数。
代码@4:延迟启动任务。
2.1.5 源码分析FailfastClusterInvoker
策略:快速失败,服务调用失败后立马抛出异常,不进行重试。 场景:是否修改类服务(未实行幂等的服务调用)
FailfastClusterInvoker#doInvoker
public Result doInvoke(Invocation invocation, List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
checkInvokers(invokers, invocation); //@1
Invoker<T> invoker = select(loadbalance, invocation, invokers, null); //@2
try {
return invoker.invoke(invocation); //@3
} catch (Throwable e) {
if (e instanceof RpcException && ((RpcException) e).isBiz()) { // biz exception.
throw (RpcException) e;
}
throw new RpcException(e instanceof RpcException ? ((RpcException) e).getCode() : 0,
"Failfast invoke providers " + invoker.getUrl() + " " + loadbalance.getClass().getSimpleName()
+ " select from all providers " + invokers + " for service " + getInterface().getName()
+ " method " + invocation.getMethodName() + " on consumer " + NetUtils.getLocalHost()
+ " use dubbo version " + Version.getVersion()
+ ", but no luck to perform the invocation. Last error is: " + e.getMessage(),
e.getCause() != null ? e.getCause() : e); //@4
}
}
代码@1:检查服务提供者,如果服务提供者列表为空,抛出没有服务提供者错误。
代码@2:根据负载算法选择一个服务提供者。
代码@3:发起RPC服务调用。
代码@4:如果服务调用异常,抛出异常,打印服务消费者,服务提供者信息。
2.1.6 源码分析FailsafeClusterInvoker
策略:服务调用失败后,只打印错误日志,然后返回服务调用成功。 场景:调用审计,日志类服务接口。
FailsafeClusterInvoker#doInvoker
@Override
public Result doInvoke(Invocation invocation, List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
try {
checkInvokers(invokers, invocation); //@1
Invoker<T> invoker = select(loadbalance, invocation, invokers, null); //@2
return invoker.invoke(invocation); //@3
} catch (Throwable e) {
logger.error("Failsafe ignore exception: " + e.getMessage(), e);
return AsyncRpcResult.newDefaultAsyncResult(null, null, invocation); // ignore //@4
}
}
代码@1:检查服务提供者,如果服务提供者列表为空,抛出没有服务提供者错误。
代码@2:根据负载算法选择一个服务提供者。
代码@3:发起RPC服务调用。
代码@4:如果出现异常,记录错误堆栈信息,并返回成功。
2.1.7 源码分析ForkingClusterInvoker
策略:并行调用多个服务提供者,当一个服务提供者返回成功,则返回成功。 场景:实时性要求比较高的场景,但浪费服务器资源,通常可以通过forks参数设置并发调用度。
ForkingClusterInvoker#doInvoker
public Result doInvoke(final Invocation invocation, List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
try {
checkInvokers(invokers, invocation); //@1
final List<Invoker<T>> selected;
final int forks = getUrl().getParameter(FORKS_KEY, DEFAULT_FORKS); //@2
final int timeout = getUrl().getParameter(TIMEOUT_KEY, DEFAULT_TIMEOUT);
if (forks <= 0 || forks >= invokers.size()) {
selected = invokers;
} else {
selected = new ArrayList<>();
for (int i = 0; i < forks; i++) { //@3
Invoker<T> invoker = select(loadbalance, invocation, invokers, selected);
if (!selected.contains(invoker)) {
//Avoid add the same invoker several times.
selected.add(invoker);
}
}
}
RpcContext.getContext().setInvokers((List) selected);
final AtomicInteger count = new AtomicInteger();
final BlockingQueue<Object> ref = new LinkedBlockingQueue<>();
for (final Invoker<T> invoker : selected) {
executor.execute(new Runnable() {
@Override
public void run() {
try {
Result result = invoker.invoke(invocation);
ref.offer(result);
} catch (Throwable e) {
int value = count.incrementAndGet();
if (value >= selected.size()) {
ref.offer(e);
}
}
}
});
}
try {
Object ret = ref.poll(timeout, TimeUnit.MILLISECONDS); //@4
if (ret instanceof Throwable) {
Throwable e = (Throwable) ret;
throw new RpcException(e instanceof RpcException ? ((RpcException) e).getCode() : 0, "Failed to forking invoke provider " + selected + ", but no luck to perform the invocation. Last error is: " + e.getMessage(), e.getCause() != null ? e.getCause() : e);
}
return (Result) ret;
} catch (InterruptedException e) {
throw new RpcException("Failed to forking invoke provider " + selected + ", but no luck to perform the invocation. Last error is: " + e.getMessage(), e);
}
} finally {
// clear attachments which is binding to current thread.
RpcContext.getContext().clearAttachments();
}
}
代码@1:检查服务提供者,如果服务提供者列表为空,抛出没有服务提供者错误。
代码@2:获取forks属性,貌似只能通过在< dubbo:reference />用< dubbo:parameter key="forks" value=""/>来设置forks,其默认值为2,如果forks值大于服务提供者的数量或者<0,则将调用所有服务提供者,如果forks值小于服务提供者的数量,则使用负载均衡算法,选择forks个服务提供者。
代码@3:依次异步向服务提供者发起RPC调用,并将结果添加到BlockingQueue< Object> ref,如果服务调用发送错误,并且发生错误的个数大于等于本次调用的个数,则将错误信息放入BlockingQueue< Object> ref,否则,将错误数增加1。
代码@4:Object ret = ref.poll(timeout, TimeUnit.MILLISECONDS),从该队列中获取结果,如果队列未空,则会阻塞等待,直到超时,当有一个调用成功后,将返回,忽略其他调用结果。