dubbo学习(九)集群容错策略

2020-11-06 08:12:26 浏览数 (1)

一、集群容错Cluster层的工作流程

(1)生成Invoker对象。不同的Cluster实现会生成不同类型的ClusterInvoker对象并返回。然后调用ClusterInvoker的invoker方法,开始正式进入集群容错流程中。

(2)获得可调用的服务列表。首先会做前置校验,检查远程服务是否已被销毁,然后通过directory.list方法获取所有可用的服务列表。然后使用Router接口处理已过滤的服务列表,再根据路由规则过滤一部分服务,最终返回路由过滤后的剩余服务列表。

(3)负载均衡。负载均衡的目的主要是从剩余的服务列表中选择最终的一个服务用于最终的调用。dubbo会根据用户的配置-即loadbalance配置,默认为随机。

(4)RPC调用。首先保持每次调用的Invoker到RPC上下文,并做RPC调用。然后处理调用结果,对于调用出现异常、成功、失败等情况,每种容错策略会有不同的处理方式。

相关代码:

代码语言:javascript复制
public <T> Invoker<T> join(Directory<T> directory) throws RpcException {
    //合并invoker列表    return new FailoverClusterInvoker<T>(directory);
}

这里使用默认的FailoverCluster策略来进行原理分析,集群容错策略默认为FailoverClusterInvoker。核心方法doInvoke是一个模板方法,真正调用的流程在AbstractClusterInvoker中。

代码语言:javascript复制
public Result invoke(final Invocation invocation) throws RpcException {
    //获取服务列表    List<Invoker<T>> invokers = list(invocation);
    //负责均衡选择服务    LoadBalance loadbalance = initLoadBalance(invokers, invocation);
    RpcUtils.attachInvocationIdIfAsync(getUrl(), invocation);
    //进入策略进行rpc调用    return doInvoke(invocation, invokers, loadbalance);
}

工作流程图如下:

二、集群容错机制

(1)Failover

当出现失败时,会重试其他服务器。可以设置重试次数。这是dubbo的默认容错机制,通常用在读操作或幂等的写操作上,但重试会增加接口的执行时间,并且当下游机器负载已经到达极限时,重试容易加重下游服务的负载。

服务提供方配置:<dubbo:service cluster="failover" retries="2" />

服务消费方配置:<dubbo:reference cluster="failover" retries="2" />

(2)Failfast

快速失败,当请求失败后,快速返回异常结果,不做任何重试。通常使用在非幂等接口的调用上。

服务提供方配置:<dubbo:service cluster="failfast" />

服务消费方配置:<dubbo:reference cluster="failfast" />

failover策略如果设置重试次数为0,则与该策略一样。

(3)Failsafe

当出现异常时,直接忽略异常。通常使用在不关心调用是否成功,并且不想抛异常影响外层的调用,如某些不重要的日志同步,即使出现异常也无所谓。

服务提供方配置:<dubbo:service cluster="failsafe" />

服务消费方配置:<dubbo:reference cluster="failsafe" />

(4)Failback

请求失败后,会自动记录在失败队列中,并由一个定时线程池定时重试,适用于一些异步或最终一致性的请求。

服务提供方配置:<dubbo:service cluster="failback" />

服务消费方配置:<dubbo:reference cluster="failback" />

(5)Forking

同时调用多个相同的服务,只要其中一个返回,则立即返回结果。用户可以配置forks="最大并行调用数"来确定最大并行调用的服务数量。通常使用在对接口实时性要求极高的调用上,但也会浪费更多的资源。

服务提供方配置:<dubbo:service cluster="forking" forks="4"/>

服务消费方配置:<dubbo:reference cluster="forking" forks="4"/>

(6)Broadcast

广播调用所有可用的服务,任意一个节点报错则报错。通常用于通知所有提供者更新缓存或日志等本地资源信息。

服务提供方配置:<dubbo:service cluster="broadcast" />

服务消费方配置:<dubbo:reference cluster="broadcast" />

(7)Mergeable

Mergeable可以自动把多个节点请求得到的结果进行合并。

服务消费方配置:<dubbo:reference group="*" merger="true" /> 或

<dubbo:reference group="aaa,bbb" merger="true" />等

(8)Mock

提供调用失败时,返回伪造的响应结果。或直接强制返回伪造的结果,不会发起远程调用。

服务消费方配置:<dubbo:reference mock="true" />

(9)Available

遍历所有服务列表,找到第一个可用的节点,直接请求并返回结果。如果没有可用的节点,则直接抛出异常。

服务提供方配置:<dubbo:service cluster="available" />

服务消费方配置:<dubbo:reference cluster="available" />

三、集群容错策略实现

· FailoverClusterInvoker

代码语言:javascript复制
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;
    }
    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循环进行重试    for (int i = 0; i < len; i  ) {
        //校验,如果有过一次失败
        if (i > 0) {            //校验节点是否被销毁
            checkWhetherDestroyed();            copyinvokers = list(invocation);
            //校验invoker列表是否为空
            checkInvokers(copyinvokers, invocation);
        }        //负载均衡,选择需要调用的节点
        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 != null ? le.getCode() : 0, "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 != null ? le.getMessage() : ""), le != null && le.getCause() != null ? le.getCause() : le);
}

Failover策略主要做了这些步骤:

(1)校验。校验传入的invoker列表是否为空,为空则抛出异常。

(2)获取重试次数。

(3)初始化一些集合和对象。主要用于保存调用过程中出现的异常、记录调用的节点(这 个会在负载均衡中使用,在某些配置下,尽量不要一直调用同一个服务)。

(4)根据重试次数进行遍历。成功直接return,如果失败则循环重试调用。

(5)负载均衡。调用select方法做负责均衡,得到要调用的节点。

(6)远程调用。调用invoke方法进行远程调用,成功则返回,失败则打印日志信息。

(7)抛出异常。重试次数达到上限仍失败,则抛出异常。

· FailfastClusterInvoker

代码语言:javascript复制
public Result doInvoke(Invocation invocation, List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
    //校验服务列表是否为空    checkInvokers(invokers, invocation);
    //负责均衡    Invoker<T> invoker = select(loadbalance, invocation, invokers, null);
    try {        //远程调用
        return invoker.invoke(invocation);
    } 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);
    }
}

(1)校验。校验传入的invoker列表是否为空,为空则抛出异常。

(2)负载均衡。调用select方法做负责均衡,得到要调用的节点。

(3)远程调用。调用invoke方法进行远程调用,失败则抛出异常。

· FailsafeClusterInvoker

代码语言:javascript复制
public Result doInvoke(Invocation invocation, List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
    try {        //校验服务列表是否为空
        checkInvokers(invokers, invocation);
        //负载均衡        Invoker<T> invoker = select(loadbalance, invocation, invokers, null);
        //远程调用
        return invoker.invoke(invocation);
    } catch (Throwable e) {
        logger.error("Failsafe ignore exception: "   e.getMessage(), e);
        return new RpcResult(); // ignore
    }
}

该策略与FailfastClusterInvoker总体类似,区别在于FailsafeClusterInvoker如果出现异常会对异常进行捕获,不抛出去,忽略异常。

· FailbackClusterInvoker

代码语言:javascript复制
protected Result doInvoke(Invocation invocation, List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
    try {        //校验服务列表是否为空        checkInvokers(invokers, invocation);
        //负载均衡        Invoker<T> invoker = select(loadbalance, invocation, invokers, null);
        //远程调用        return invoker.invoke(invocation);
    } catch (Throwable e) {
        logger.error("Failback to invoke method "   invocation.getMethodName()   ", wait for retry in background. Ignored exception: "
                  e.getMessage()   ", ", e);        //加入到失败队列中,队列实际是一个chm        addFailed(invocation, this);
        return new RpcResult();
    }
}

该策略与FailsafeClusterInvoker总体类似,区别在于FailbackClusterInvoker如果出现异常会将dubbo执行请求加入到concurrentHashMap队列中,由定时任务5秒重试一次。

重试代码:

代码语言:javascript复制
void retryFailed() {
    if (failed.size() == 0) {
        return;
    }    //遍历chm获得所有的失败请求
    for (Map.Entry<Invocation, AbstractClusterInvoker<?>> entry : new HashMap<Invocation, AbstractClusterInvoker<?>>(
            failed).entrySet()) {
        Invocation invocation = entry.getKey();
        Invoker<?> invoker = entry.getValue();
        try {            //重试
            invoker.invoke(invocation);
            //成功则从队列中移除            failed.remove(invocation);
        } catch (Throwable e) {
            logger.error("Failed retry to invoke method "   invocation.getMethodName()   ", waiting again.", e);
        }
    }
}

· AvailableClusterInvoker

代码语言:javascript复制
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);
}

AvailableClusterInvoker策略会对传入的服务列表进行遍历,直到找到第一个可用的服务进行远程调用。如果没有可用服务,则会抛出异常。

以上总结了常用的集群容错策略,其余的策略不进行详细阐述。以上的路由策略,在一般情况下采用failover(读操作)或failfast(写操作)规则就可以满足日常开发需求。

下一篇会分析集群容错中的路由和负载均衡的原理。

0 人点赞