13.dubbo源碼-集群容錯

集群容錯

在集群調(diào)用失敗時,Dubbo 提供了多種容錯方案,缺省為 failover ,即失敗重試。可通過接口com.alibaba.dubbo.rpc.cluster.Cluster的SPI注解可知:

/**
 * Cluster. (SPI, Singleton, ThreadSafe)
 * 
 * <a >Cluster</a>
 * <a >Fault-Tolerant</a>
 * 
 * @author william.liangf
 */
@SPI(FailoverCluster.NAME)
public interface Cluster {
    ...
}

接下來通過對dubbo源碼的分析,一一講解這些集群容錯模式的具體實現(xiàn);

集群調(diào)用關(guān)系

cluster.jpg

圖片來源: https://dubbo.gitbooks.io/dubbo-user-book/demos/fault-tolerent-strategy.html
由圖可知,通過Cluster的調(diào)用過程如下:

  1. 調(diào)用list()從Directory中取得可用Invoker集合;
  2. 根據(jù)路由規(guī)則過濾一些Invoker,得到可用Invoker集合;
  3. 根據(jù)負(fù)載均衡機(jī)制得到一個合適的Invoker,負(fù)載均衡機(jī)制參考
  4. 調(diào)用最終選出來的這個Invoker。

集群模式配置

按照以下示例在服務(wù)提供方和消費方配置集群模式

<dubbo:service cluster="failover" />

<dubbo:reference cluster="failsafe" />

集群模式概覽

dubbo支持的集群模式如下圖所示,由于dubbo通過SPI實現(xiàn)微內(nèi)核,集群模式也不例外,所以想擴(kuò)展自己對集群容錯的處理方式,非常簡單;


dubbo集群容錯總覽

接下來通過對源碼的閱讀,一一分析各個集群容錯模式的實現(xiàn);

Failover Cluster

dubbo默認(rèn)集群模式,失敗自動切換,當(dāng)出現(xiàn)失敗,重試其它服務(wù)器。通常用于讀操作,但重試會帶來更長延遲,且使集群的壓力更大。可通過 retries="2" 來設(shè)置重試次數(shù)(默認(rèn)為2,這個值是重試次數(shù),所以不包括第一次調(diào)用,而是第一次調(diào)用失敗后最大可重試次數(shù))。重試次數(shù)配置示例如下:

<dubbo:service retries="2" />

<dubbo:reference retries="2" />

<dubbo:reference><dubbo:method name="findFoo" retries="2" /></dubbo:reference>

核心實現(xiàn)源碼:

public Result doInvoke(Invocation invocation, final List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
    List<Invoker<T>> copyinvokers = invokers;
    // 檢查copyinvokers即可用Invoker集合是否為空,如果為空,那么拋出異常
    checkInvokers(copyinvokers, invocation);
    // 得到最大可調(diào)用次數(shù):最大可重試次數(shù)+1,默認(rèn)最大可重試次數(shù)Constants.DEFAULT_RETRIES=2
    int len = getUrl().getMethodParameter(invocation.getMethodName(), Constants.RETRIES_KEY, Constants.DEFAULT_RETRIES) + 1;
    // 如果用戶設(shè)置reties為負(fù)數(shù),那么也要調(diào)用至少1次
    if (len <= 0) {
        len = 1;
    }
    // 保存最后一次調(diào)用的異常
    RpcException le = null;
    // 保存已經(jīng)調(diào)用過的Invoker
    List<Invoker<T>> invoked = new ArrayList<Invoker<T>>(copyinvokers.size()); // invoked invokers.
    Set<String> providers = new HashSet<String>(len);
    // failover機(jī)制核心實現(xiàn):如果出現(xiàn)調(diào)用失敗,那么重試其他服務(wù)器
    for (int i = 0; i < len; i++) {
        //重試時,進(jìn)行重新選擇,避免重試時invoker列表已發(fā)生變化.
        //注意:如果列表發(fā)生了變化,那么invoked判斷會失效,因為invoker示例已經(jīng)改變
        if (i > 0) {
            checkWheatherDestoried();
            // 根據(jù)Invocation調(diào)用信息從Directory中獲取所有可用Invoker
            copyinvokers = list(invocation);
            //重新檢查一下
            checkInvokers(copyinvokers, invocation);
        }
        // 根據(jù)負(fù)載均衡機(jī)制從copyinvokers中選擇一個Invoker
        Invoker<T> invoker = select(loadbalance, invocation, copyinvokers, invoked);
        // 保存每次調(diào)用的Invoker
        invoked.add(invoker);
        RpcContext.getContext().setInvokers((List)invoked);
        try {
            // RPC調(diào)用得到Result
            Result result = invoker.invoke(invocation);
            // 重試過程中,將最后一次調(diào)用的異常信息以warn級別日志輸出
            if (le != null && logger.isWarnEnabled()) {
                logger.warn("Although retry the method " + invocation.getMethodName()
                        + " 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) {
            // 如果是業(yè)務(wù)性質(zhì)的異常,不再重試,直接拋出
            if (e.isBiz()) { // biz exception.
                throw e;
            }
            le = e;
        } catch (Throwable e) {
            // 其他性質(zhì)的異常統(tǒng)一封裝成RpcException
            le = new RpcException(e.getMessage(), e);
        } finally {
            providers.add(invoker.getUrl().getAddress());
        }
    }
    // 最大可調(diào)用次數(shù)用完還得到Result的話,拋出RpcException異常:重試了N次還是失敗,并輸出最后一次異常信息
    throw new RpcException(le != null ? le.getCode() : 0, "Failed to invoke the method "
            + invocation.getMethodName() + " 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);
}

Failfast Cluster

快速失敗,只發(fā)起一次調(diào)用,失敗立即報錯。通常用于非冪等性的寫操作,比如新增記錄。
核心實現(xiàn)源碼:

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);
    }
}

FailfastCluster實現(xiàn)比較簡單,根據(jù)負(fù)載均衡機(jī)制選擇一個Invoker后只調(diào)用1次,不管結(jié)果如何,不再進(jìn)行任何重試:如果調(diào)用正常就返回Result,否則返回<u>記錄了詳細(xì)異常信息的RpcException</u>;

Failsafe Cluster

失敗安全,出現(xiàn)異常時,直接忽略。通常用于寫入審計日志等操作。
核心實現(xiàn)源碼:

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
    }
}

FailsafeCluster實現(xiàn)比較簡單,根據(jù)負(fù)載均衡機(jī)制選擇一個Invoker后只調(diào)用1次,不管結(jié)果如何,不再進(jìn)行任何重試:如果調(diào)用正常就返回Result,否則返回<u>一個空的RpcResult</u>,這是和FailfastCluster的唯一區(qū)別,不會把任何異常信息返回給consumer;

Failback Cluster

失敗自動恢復(fù),后臺記錄失敗請求,定時重發(fā)。通常用于消息通知操作。
核心實現(xiàn)源碼:

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);
        // failback實現(xiàn)的核心,如果調(diào)用失敗,后臺記錄失敗請求,并定時重發(fā)
        addFailed(invocation, this);
        return new RpcResult(); // ignore
    }
}

定時重發(fā)核心實現(xiàn)源碼:

// 處理重試任務(wù)的線程池
private final ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(2, new NamedThreadFactory("failback-cluster-timer", true));

private void addFailed(Invocation invocation, AbstractClusterInvoker<?> router) {
    if (retryFuture == null) {
        // double-check保證線程安全
        synchronized (this) {
            if (retryFuture == null) {
                // 一個獨立的線程池處理,執(zhí)行周期是5s
                retryFuture = scheduledExecutorService.scheduleWithFixedDelay(new Runnable() {
                    public void run() {
                        // 收集統(tǒng)計信息
                        try {
                            // 重試失敗的請求,如果重試成功,把請求從remove掉;
                            retryFailed();
                        } catch (Throwable t) { // 防御性容錯
                            logger.error("Unexpected error occur at collect statistic", t);
                        }
                    }
                }, RETRY_FAILED_PERIOD, RETRY_FAILED_PERIOD, TimeUnit.MILLISECONDS);
            }
        }
    }
    failed.put(invocation, router);
}

Forking Cluster

并行調(diào)用多個服務(wù)器,只要一個成功即返回。通常用于實時性要求較高的讀操作,但需要浪費更多服務(wù)資源??赏ㄟ^ forks="2" 來設(shè)置最大并行數(shù)。
核心實現(xiàn)源碼:

public Result doInvoke(final Invocation invocation, List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
    checkInvokers(invokers, invocation);
    final List<Invoker<T>> selected;
    // forks數(shù),默認(rèn)為2
    final int forks = getUrl().getParameter(Constants.FORKS_KEY, Constants.DEFAULT_FORKS);
    // 請求超時
    final int timeout = getUrl().getParameter(Constants.TIMEOUT_KEY, Constants.DEFAULT_TIMEOUT);
    // 如果設(shè)置的forks值為負(fù)數(shù),或者超過了可用Invoker數(shù),那么選擇所有可用Invoker,即invokers
    if (forks <= 0 || forks >= invokers.size()) {
        selected = invokers;
    } else {
        selected = new ArrayList<Invoker<T>>();
        // 只選擇forks值指定的Invoker數(shù)量
        for (int i = 0; i < forks; i++) {
            //在invoker列表(排除selected)后,如果沒有選夠,則存在重復(fù)循環(huán)問題.見select實現(xiàn).
            Invoker<T> invoker = select(loadbalance, invocation, invokers, selected);
            if(!selected.contains(invoker)){//防止重復(fù)添加invoker
                selected.add(invoker);
            }
        }
    }
    RpcContext.getContext().setInvokers((List)selected);
    final AtomicInteger count = new AtomicInteger();
    final BlockingQueue<Object> ref = new LinkedBlockingQueue<Object>();
    for (final Invoker<T> invoker : selected) {
        // ForkingCluster核心實現(xiàn),多線程并行調(diào)用
        executor.execute(new Runnable() {
            public void run() {
                try {
                    Result result = invoker.invoke(invocation);
                    // 把結(jié)果放到BlockingQueue中
                    ref.offer(result);
                } catch(Throwable e) {
                    int value = count.incrementAndGet();
                    if (value >= selected.size()) {
                        ref.offer(e);
                    }
                }
            }
        });
    }
    try {
        // 從BlockingQueue中取結(jié)果:即并行調(diào)用最先返回的結(jié)果
        Object ret = ref.poll(timeout, TimeUnit.MILLISECONDS);
        // 如果取得的是異常,那么將異常封裝成RpcException并拋給Consumer
        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) {
        // 如果timeout指定超時時間內(nèi)還沒有返回結(jié)果,那么將異常封裝成RpcException并拋給Consumer
        throw new RpcException("Failed to forking invoke provider " + selected + ", but no luck to perform the invocation. Last error is: " + e.getMessage(), e);
    }
}

Broadcast Cluster

廣播調(diào)用所有提供者,逐個調(diào)用,任意一臺報錯則報錯 。通常用于通知所有提供者更新緩存或日志等本地資源信息。
核心實現(xiàn)源碼:

public Result doInvoke(final Invocation invocation, List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
    checkInvokers(invokers, invocation);
    RpcContext.getContext().setInvokers((List)invokers);
    // 保存最后一個調(diào)用的異常
    RpcException exception = null;
    Result result = null;
    for (Invoker<T> invoker: invokers) {
        try {
            // 遍歷所有Invoker,每個Invoker都會被調(diào)用(不管某個Invoker是否拋出異常)
            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);
        }
    }
    // 如果調(diào)用過程有異常,那么拋出該異常
    if (exception != null) {
        throw exception;
    }
    return result;
}

Available Cluster

遍歷所有從Directory中l(wèi)ist出來的Invoker集合,調(diào)用第一個isAvailable()Invoker,只發(fā)起一次調(diào)用,失敗立即報錯。
isAvailable()判斷邏輯如下--Client處理連接狀態(tài),且不是READONLY:

@Override
public boolean isAvailable() {
    if (!super.isAvailable())
        return false;
    for (ExchangeClient client : clients){
        if (client.isConnected() && !client.hasAttribute(Constants.CHANNEL_ATTRIBUTE_READONLY_KEY)){
            //cannot write == not Available ?
            return true ;
        }
    }
    return false;
}

Mergeable Cluster

請戳鏈接14. dubbo源碼-集群容錯之MergeableCluster

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容