Cluster分析(容錯機制)

1. FailoverClusterInvoker失敗自動切換

 在調用失敗時,會自動切換 Invoker 進行重試。默認確配置下,Dubbo 會使用這個類作爲缺省 Cluster Invoker。

  • doInvoke()
  1. 首先是獲取重試次數,然後根據重試次數進行循環調用,失敗後進行重試。
  2. 在 for 循環內,首先通過負載均衡組件選擇一個 Invoker(每次都得到最新可用的Invoker列表)。
  3. 通過這個 Invoker 的 invoke 方法進行遠程調用。如果失敗了,記錄下異常,並進行重試。重試時會再次調用父類的 list 方法列舉 Invoker。
public class FailoverClusterInvoker<T> extends AbstractClusterInvoker<T> {
 
    // 省略部分代碼
 
    @Override
    public Result doInvoke(Invocation invocation, final List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
        List<Invoker<T>> copyinvokers = invokers;
        checkInvokers(copyinvokers, invocation);
        // 獲取重試次數
        int len = getUrl().getMethodParameter(invocation.getMethodName(), Constants.RETRIES_KEY, Constants.DEFAULT_RETRIES) + 1;
        if (len <= 0) {
            len = 1;
        }
        RpcException le = null;
        List<Invoker<T>> invoked = new ArrayList<Invoker<T>>(copyinvokers.size());
        Set<String> providers = new HashSet<String>(len);
        // 循環調用,失敗重試
        for (int i = 0; i < len; i++) {
            if (i > 0) {
                checkWhetherDestroyed();
                // 在進行重試前重新列舉 Invoker,這樣做的好處是,如果某個服務掛了,
                // 通過調用 list 可得到最新可用的 Invoker 列表
                copyinvokers = list(invocation);
                // 對 copyinvokers 進行判空檢查
                checkInvokers(copyinvokers, invocation);
            }
 
            // 通過負載均衡選擇 Invoker
            Invoker<T> invoker = select(loadbalance, invocation, copyinvokers, invoked);
            // 添加到 invoker 到 invoked 列表中
            invoked.add(invoker);
            // 設置 invoked 到 RPC 上下文中
            RpcContext.getContext().setInvokers((List) invoked);
            try {
                // 調用目標 Invoker 的 invoke 方法
                Result result = invoker.invoke(invocation);
                return result;
            } catch (RpcException e) {
                if (e.isBiz()) {
                    throw e;
                }
                le = e;
            } catch (Throwable e) {
                le = new RpcException(e.getMessage(), e);
            } finally {
                providers.add(invoker.getUrl().getAddress());
            }
        }
         
        // 若重試失敗,則拋出異常
        throw new RpcException(..., "Failed to invoke the method ...");
    }
}
  • select()

select 方法的主要邏輯集中在了對粘滯連接特性的支持上。

  1. 首先是獲取 sticky 配置,
  2. 然後再檢測 invokers 列表中是否包含 stickyInvoker,如果不包含,則認爲該 stickyInvoker 不可用,此時將其置空。這裏的 invokers 列表可以看做是存活着的服務提供者列表,如果這個列表不包含 stickyInvoker,那自然而然的認爲 stickyInvoker 掛了,所以置空。
  3. 如果 stickyInvoker 存在於 invokers 列表中,此時要進行下一項檢測 — 檢測 selected 中是否包含 stickyInvoker。如果包含的話,說明 stickyInvoker 在此之前沒有成功提供服務(但其仍然處於存活狀態)。此時我們認爲這個服務不可靠,不應該在重試期間內再次被調用,因此這個時候不會返回該 stickyInvoker。如果 selected 不包含 stickyInvoker,此時還需要進行可用性檢測,比如檢測服務提供者網絡連通性等。
  4. 當可用性檢測通過,纔可返回 stickyInvoker,否則調用 doSelect 方法選擇 Invoker。如果 sticky 爲 true,此時會將 doSelect 方法選出的 Invoker 賦值給 stickyInvoker。
protected Invoker<T> select(LoadBalance loadbalance, Invocation invocation, List<Invoker<T>> invokers, List<Invoker<T>> selected) throws RpcException {
    if (invokers == null || invokers.isEmpty())
        return null;
    // 獲取調用方法名
    String methodName = invocation == null ? "" : invocation.getMethodName();
 
    // 獲取 sticky 配置,sticky 表示粘滯連接。所謂粘滯連接是指讓服務消費者儘可能的
    // 調用同一個服務提供者,除非該提供者掛了再進行切換
    boolean sticky = invokers.get(0).getUrl().getMethodParameter(methodName, Constants.CLUSTER_STICKY_KEY, Constants.DEFAULT_CLUSTER_STICKY);
    {
        // 檢測 invokers 列表是否包含 stickyInvoker,如果不包含,
        // 說明 stickyInvoker 代表的服務提供者掛了,此時需要將其置空
        if (stickyInvoker != null && !invokers.contains(stickyInvoker)) {
            stickyInvoker = null;
        }
         
        // 在 sticky 爲 true,且 stickyInvoker != null 的情況下。如果 selected 包含
        // stickyInvoker,表明 stickyInvoker 對應的服務提供者可能因網絡原因未能成功提供服務。
        // 但是該提供者並沒掛,此時 invokers 列表中仍存在該服務提供者對應的 Invoker。
        if (sticky && stickyInvoker != null && (selected == null || !selected.contains(stickyInvoker))) {
            // availablecheck 表示是否開啓了可用性檢查,如果開啓了,則調用 stickyInvoker 的
            // isAvailable 方法進行檢查,如果檢查通過,則直接返回 stickyInvoker。
            if (availablecheck && stickyInvoker.isAvailable()) {
                return stickyInvoker;
            }
        }
    }
     
    // 如果線程走到當前代碼處,說明前面的 stickyInvoker 爲空,或者不可用。
    // 此時繼續調用 doSelect 選擇 Invoker
    Invoker<T> invoker = doSelect(loadbalance, invocation, invokers, selected);
 
    // 如果 sticky 爲 true,則將負載均衡組件選出的 Invoker 賦值給 stickyInvoker
    if (sticky) {
        stickyInvoker = invoker;
    }
    return invoker;
}
  • doSelect ()

doSelect 主要做了兩件事:

  1. 通過負載均衡組件選擇 Invoker。
  2. 如果選出來的 Invoker 不穩定,或不可用,此時需要調用 reselect 方法進行重選。若 reselect 選出來的 Invoker 爲空,此時定位 invoker 在 invokers 列表中的位置 index,然後獲取 index + 1 處的 invoker,這也可以看做是重選邏輯的一部分。
private Invoker<T> doSelect(LoadBalance loadbalance, Invocation invocation, List<Invoker<T>> invokers, List<Invoker<T>> selected) throws RpcException {
    if (invokers == null || invokers.isEmpty())
        return null;
    if (invokers.size() == 1)
        return invokers.get(0);
    if (loadbalance == null) {
        // 如果 loadbalance 爲空,這裏通過 SPI 加載 Loadbalance,默認爲 RandomLoadBalance
        loadbalance = ExtensionLoader.getExtensionLoader(LoadBalance.class).getExtension(Constants.DEFAULT_LOADBALANCE);
    }
     
    // 通過負載均衡組件選擇 Invoker
    Invoker<T> invoker = loadbalance.select(invokers, getUrl(), invocation);
 
    // 如果 selected 包含負載均衡選擇出的 Invoker,或者該 Invoker 無法經過可用性檢查,此時進行重選
    if ((selected != null && selected.contains(invoker))
            || (!invoker.isAvailable() && getUrl() != null && availablecheck)) {
        try {
            // 進行重選
            Invoker<T> rinvoker = reselect(loadbalance, invocation, invokers, selected, availablecheck);
            if (rinvoker != null) {
                // 如果 rinvoker 不爲空,則將其賦值給 invoker
                invoker = rinvoker;
            } else {
                // rinvoker 爲空,定位 invoker 在 invokers 中的位置
                int index = invokers.indexOf(invoker);
                try {
                    // 獲取 index + 1 位置處的 Invoker,以下代碼等價於:
                    //     invoker = invokers.get((index + 1) % invokers.size());
                    invoker = index < invokers.size() - 1 ? invokers.get(index + 1) : invokers.get(0);
                } catch (Exception e) {
                    logger.warn("... may because invokers list dynamic change, ignore.");
                }
            }
        } catch (Throwable t) {
            logger.error("cluster reselect fail reason is : ...");
        }
    }
    return invoker;
}
  • reselect()

reselect 方法總結下來其實只做了兩件事情:

  1. 查找可用的 Invoker,並將其添加到 reselectInvokers 集合中。
  2. 如果 reselectInvokers 不爲空,則通過負載均衡組件再次進行選擇。

其中第一件事情又可進行細分,一開始,reselect 從 invokers 列表中查找有效可用的 Invoker,若未能找到,此時再到 selected 列表中繼續查找。

​
private Invoker<T> reselect(LoadBalance loadbalance, Invocation invocation,
    List<Invoker<T>> invokers, List<Invoker<T>> selected, boolean availablecheck) throws RpcException {
 
    List<Invoker<T>> reselectInvokers = new ArrayList<Invoker<T>>(invokers.size() > 1 ? (invokers.size() - 1) : invokers.size());
 
    // 下面的 if-else 分支邏輯有些冗餘,pull request #2826 對這段代碼進行了簡化,可以參考一下
    // 根據 availablecheck 進行不同的處理
    if (availablecheck) {
        // 遍歷 invokers 列表
        for (Invoker<T> invoker : invokers) {
            // 檢測可用性
            if (invoker.isAvailable()) {
                // 如果 selected 列表不包含當前 invoker,則將其添加到 reselectInvokers 中
                if (selected == null || !selected.contains(invoker)) {
                    reselectInvokers.add(invoker);
                }
            }
        }
         
        // reselectInvokers 不爲空,此時通過負載均衡組件進行選擇
        if (!reselectInvokers.isEmpty()) {
            return loadbalance.select(reselectInvokers, getUrl(), invocation);
        }
 
    // 不檢查 Invoker 可用性
    } else {
        for (Invoker<T> invoker : invokers) {
            // 如果 selected 列表不包含當前 invoker,則將其添加到 reselectInvokers 中
            if (selected == null || !selected.contains(invoker)) {
                reselectInvokers.add(invoker);
            }
        }
        if (!reselectInvokers.isEmpty()) {
            // 通過負載均衡組件進行選擇
            return loadbalance.select(reselectInvokers, getUrl(), invocation);
        }
    }
 
    {
        // 若線程走到此處,說明 reselectInvokers 集合爲空,此時不會調用負載均衡組件進行篩選。
        // 這裏從 selected 列表中查找可用的 Invoker,並將其添加到 reselectInvokers 集合中
        if (selected != null) {
            for (Invoker<T> invoker : selected) {
                if ((invoker.isAvailable())
                        && !reselectInvokers.contains(invoker)) {
                    reselectInvokers.add(invoker);
                }
            }
        }
        if (!reselectInvokers.isEmpty()) {
            // 再次進行選擇,並返回選擇結果
            return loadbalance.select(reselectInvokers, getUrl(), invocation);
        }
    }
    return null;
}

​

2. FailbackClusterInvoker失敗自動恢復

在調用失敗後,返回一個空結果給服務提供者。並通過定時任務對失敗的調用進行重傳,適合執行消息通知等操作。

這個類主要由3個方法組成:

  1. doInvoker():該方法負責初次的遠程調用。若遠程調用失敗,則通過 addFailed 方法將調用信息存入到 failed 中,等待定時重試。
  2. addFailed():在開始階段會根據 retryFuture 爲空與否,來決定是否開啓定時任務。
  3. retryFailed():包含了失敗重試的邏輯,該方法會對 failed 進行遍歷,然後依次對 Invoker 進行調用。調用成功則將 Invoker 從 failed 中移除,調用失敗則忽略失敗原因。
public class FailbackClusterInvoker<T> extends AbstractClusterInvoker<T> {
 
    private static final long RETRY_FAILED_PERIOD = 5 * 1000;
 
    private final ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(2,
            new NamedInternalThreadFactory("failback-cluster-timer", true));
 
    private final ConcurrentMap<Invocation, AbstractClusterInvoker<?>> failed = new ConcurrentHashMap<Invocation, AbstractClusterInvoker<?>>();
    private volatile ScheduledFuture<?> retryFuture;
 
    @Override
    protected Result doInvoke(Invocation invocation, List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
        try {
            checkInvokers(invokers, invocation);
            // 選擇 Invoker
            Invoker<T> invoker = select(loadbalance, invocation, invokers, null);
            // 進行調用
            return invoker.invoke(invocation);
        } catch (Throwable e) {
            // 如果調用過程中發生異常,此時僅打印錯誤日誌,不拋出異常
            logger.error("Failback to invoke method ...");
             
            // 記錄調用信息
            addFailed(invocation, this);
            // 返回一個空結果給服務消費者
            return new RpcResult();
        }
    }
 
    private void addFailed(Invocation invocation, AbstractClusterInvoker<?> router) {
        if (retryFuture == null) {
            synchronized (this) {
                if (retryFuture == null) {
                    // 創建定時任務,每隔5秒執行一次
                    retryFuture = scheduledExecutorService.scheduleWithFixedDelay(new Runnable() {
 
                        @Override
                        public void run() {
                            try {
                                // 對失敗的調用進行重試
                                retryFailed();
                            } catch (Throwable t) {
                                // 如果發生異常,僅打印異常日誌,不拋出
                                logger.error("Unexpected error occur at collect statistic", t);
                            }
                        }
                    }, RETRY_FAILED_PERIOD, RETRY_FAILED_PERIOD, TimeUnit.MILLISECONDS);
                }
            }
        }
         
        // 添加 invocation 和 invoker 到 failed 中
        failed.put(invocation, router);
    }
 
    void retryFailed() {
        if (failed.size() == 0) {
            return;
        }
         
        // 遍歷 failed,對失敗的調用進行重試
        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 中移除 invoker
                failed.remove(invocation);
            } catch (Throwable e) {
                // 僅打印異常,不拋出
                logger.error("Failed retry to invoke method ...");
            }
        }
    }
}

3. FailfastClusterInvoker快速失敗

只會進行一次調用,失敗後立即拋出異常。適用於冪等操作,比如新增記錄。

  1. doinvoke()
  2. 通過 select 方法選擇 Invoker,然後進行遠程調用。如果調用失敗,則立即拋出異常。
public class FailfastClusterInvoker<T> extends AbstractClusterInvoker<T> {
 
    @Override
    public Result doInvoke(Invocation invocation, List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
        checkInvokers(invokers, invocation);
        // 選擇 Invoker
        Invoker<T> invoker = select(loadbalance, invocation, invokers, null);
        try {
            // 調用 Invoker
            return invoker.invoke(invocation);
        } catch (Throwable e) {
            if (e instanceof RpcException && ((RpcException) e).isBiz()) {
                // 拋出異常
                throw (RpcException) e;
            }
            // 拋出異常
            throw new RpcException(..., "Failfast invoke providers ...");
        }
    }
}

4. FailsafeClusterInvoker失敗安全

是一種失敗安全的 Cluster Invoker。所謂的失敗安全是指,當調用過程中出現異常時,FailsafeClusterInvoker 僅會打印異常,而不會拋出異常。適用於寫入審計日誌等操作。

public class FailsafeClusterInvoker<T> extends AbstractClusterInvoker<T> {
 
    @Override
    public Result doInvoke(Invocation invocation, List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
        try {
            checkInvokers(invokers, invocation);
            // 選擇 Invoker
            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();
        }
    }
}

5. ForkingClusterInvoker並行調用多個提供者

在運行時通過線程池創建多個線程,併發調用多個服務提供者。只要有一個服務提供者成功返回了結果,doInvoke 方法就會立即結束運行。ForkingClusterInvoker 的應用場景是在一些對實時性要求比較高讀操作(注意是讀操作,並行寫操作可能不安全)下使用,但這將會耗費更多的資源。

  1. 從方法開始到分割線1之間的代碼主要是用於選出 forks 個 Invoker,爲接下來的併發調用提供輸入。
  2. 分割線1和分割線2之間的邏輯通過線程池併發調用多個 Invoker,並將結果存儲在阻塞隊列中。
  3. 分割線2到方法結尾之間的邏輯主要用於從阻塞隊列中獲取返回結果,並對返回結果類型進行判斷。如果爲異常類型,則直接拋出,否則返回。
public class ForkingClusterInvoker<T> extends AbstractClusterInvoker<T> {
     
    private final ExecutorService executor = Executors.newCachedThreadPool(
            new NamedInternalThreadFactory("forking-cluster-timer", true));
 
    @Override
    public Result doInvoke(final Invocation invocation, List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
        try {
            checkInvokers(invokers, invocation);
            final List<Invoker<T>> selected;
            // 獲取 forks 配置
            final int forks = getUrl().getParameter(Constants.FORKS_KEY, Constants.DEFAULT_FORKS);
            // 獲取超時配置
            final int timeout = getUrl().getParameter(Constants.TIMEOUT_KEY, Constants.DEFAULT_TIMEOUT);
            // 如果 forks 配置不合理,則直接將 invokers 賦值給 selected
            if (forks <= 0 || forks >= invokers.size()) {
                selected = invokers;
            } else {
                selected = new ArrayList<Invoker<T>>();
                // 循環選出 forks 個 Invoker,並添加到 selected 中
                for (int i = 0; i < forks; i++) {
                    // 選擇 Invoker
                    Invoker<T> invoker = select(loadbalance, invocation, invokers, selected);
                    if (!selected.contains(invoker)) {
                        selected.add(invoker);
                    }
                }
            }
             
            // ----------------------✨ 分割線1 ✨---------------------- //
             
            RpcContext.getContext().setInvokers((List) selected);
            final AtomicInteger count = new AtomicInteger();
            final BlockingQueue<Object> ref = new LinkedBlockingQueue<Object>();
            // 遍歷 selected 列表
            for (final Invoker<T> invoker : selected) {
                // 爲每個 Invoker 創建一個執行線程
                executor.execute(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            // 進行遠程調用
                            Result result = invoker.invoke(invocation);
                            // 將結果存到阻塞隊列中
                            ref.offer(result);
                        } catch (Throwable e) {
                            int value = count.incrementAndGet();
                            // 僅在 value 大於等於 selected.size() 時,纔將異常對象
                            // 放入阻塞隊列中,請大家思考一下爲什麼要這樣做。
                            if (value >= selected.size()) {
                                // 將異常對象存入到阻塞隊列中
                                ref.offer(e);
                            }
                        }
                    }
                });
            }
             
            // ----------------------✨ 分割線2 ✨---------------------- //
             
            try {
                // 從阻塞隊列中取出遠程調用結果
                Object ret = ref.poll(timeout, TimeUnit.MILLISECONDS);
                 
                // 如果結果類型爲 Throwable,則拋出異常
                if (ret instanceof Throwable) {
                    Throwable e = (Throwable) ret;
                    throw new RpcException(..., "Failed to forking invoke provider ...");
                }
                 
                // 返回結果
                return (Result) ret;
            } catch (InterruptedException e) {
                throw new RpcException("Failed to forking invoke provider ...");
            }
        } finally {
            RpcContext.getContext().clearAttachments();
        }
    }
}

爲什麼要在value >= selected.size()的情況下,纔將異常對象添加到阻塞隊列中?這裏來解答一下。原因是這樣的,在並行調用多個服務提供者的情況下,只要有一個服務提供者能夠成功返回結果,而其他全部失敗。此時 ForkingClusterInvoker 仍應該返回成功的結果,而非拋出異常。在value >= selected.size()時將異常對象放入阻塞隊列中,可以保證異常對象不會出現在正常結果的前面,這樣可從阻塞隊列中優先取出正常的結果。

6. BroadcastClusterInvoker

會逐個調用每個服務提供者,如果其中一臺報錯,在循環調用結束後,BroadcastClusterInvoker 會拋出異常。該類通常用於通知所有提供者更新緩存或日誌等本地資源信息。

public class BroadcastClusterInvoker<T> extends AbstractClusterInvoker<T> {
 
    @Override
    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;
        // 遍歷 Invoker 列表,逐個調用
        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);
            }
        }
         
        // exception 不爲空,則拋出異常
        if (exception != null) {
            throw exception;
        }
        return result;
    }
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章