Dubbo原始碼分析(八)叢集容錯機制

清幽之地發表於2019-03-25

前言

在上一章節,我們曾提到這樣一個問題: 當呼叫服務失敗後,我們怎麼處理當前的請求?丟擲異常亦或是重試?

為了解決這個問題,Dubbo 定義了叢集介面 Cluster 以及 Cluster Invoker。叢集 Cluster 用途是將多個服務提供者合併為一個 Cluster Invoker,並將這個 Invoker 暴露給服務消費者。這樣一來,服務消費者只需通過這個 Invoker 進行遠端呼叫即可,至於具體呼叫哪個服務提供者,以及呼叫失敗後如何處理等問題,現在都交給叢集模組去處理。

一、合併

在服務引用的過程中,我們最終會將一個或多個服務提供者Invoker封裝成服務目錄物件,但最後還要將它合併轉換成Cluster Invoker物件。 Invoker invoker = cluster.join(directory);

這裡的cluster就是擴充套件點自適應類,在Dubbo中預設是Failover,所以上面程式碼會呼叫到:

public class FailoverCluster implements Cluster {

    public final static String NAME = "failover";
    public <T> Invoker<T> join(Directory<T> directory) throws RpcException {
        return new FailoverClusterInvoker<T>(directory);
    }
}
複製程式碼

上面的程式碼很簡單,所以最後的Invoker物件指向的是FailoverClusterInvoker例項。它也是一個Invoker,它繼承了抽象的AbstractClusterInvoker

我們看下AbstractClusterInvoker類中的invoke方法。

public abstract class AbstractClusterInvoker<T> implements Invoker<T> {

	
	public Result invoke(final Invocation invocation) throws RpcException {
      
        LoadBalance loadbalance = null;
		//呼叫服務目錄,獲取所有的服務提供者Invoker物件
		List<Invoker<T>> invokers = directory.list(invocation);
        if (invokers != null && !invokers.isEmpty()) {
			//載入負載均衡元件
            loadbalance = ExtensionLoader.getExtensionLoader(LoadBalance.class).
				getExtension(invokers.get(0).getUrl().
				getMethodParameter(invocation.getMethodName(), "loadbalance", "random"));
        }
        RpcUtils.attachInvocationIdIfAsync(getUrl(), invocation);
		//呼叫子類實現 ,不同的叢集容錯機制
        return doInvoke(invocation, invokers, loadbalance);
    }
}
複製程式碼

以上程式碼也很簡單,我們分為三個步驟來看

  • 呼叫服務目錄,獲取所有的服務提供者列表
  • 載入負載均衡元件
  • 呼叫子類實現,轉發請求

關於負載均衡我們後續再深入瞭解,這是隻知道它負責從多個Invoker中選取一個返回就行。

二、叢集容錯策略

Dubbo為我們提供了多種叢集容錯機制。主要如下:

  • Failover Cluster - 失敗自動切換

FailoverClusterInvoker在呼叫失敗時,會自動切換 Invoker 進行重試。預設配置下,Dubbo 會使用這個類作為預設 Cluster Invoker。

  • Failfast Cluster - 快速失敗

FailfastClusterInvoker 只會進行一次呼叫,失敗後立即丟擲異常。

  • Failsafe Cluster - 失敗安全

FailsafeClusterInvoker 當呼叫過程中出現異常時,僅會列印異常,而不會丟擲異常。

  • Failback Cluster - 失敗自動恢復

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

  • Forking Cluster - 並行呼叫多個服務提供者

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

  • BroadcastClusterInvoker - 廣播

BroadcastClusterInvoker 會逐個呼叫每個服務提供者,如果其中一臺報錯,在迴圈呼叫結束後,BroadcastClusterInvoker 會丟擲異常。該類通常用於通知所有提供者更新快取或日誌等本地資源資訊。

三、自動切換

FailoverClusterInvoker 在呼叫失敗時,會自動切換 Invoker 進行重試。我們重點看它的doInvoke方法。

public Result doInvoke(Invocation invocation, 
	    final List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {

	List<Invoker<T>> copyinvokers = invokers;
	//檢查invokers是否為空
	checkInvokers(copyinvokers, invocation);
	//獲取重試次數 這裡預設是3次
	int len = getUrl().getMethodParameter(invocation.getMethodName(), "retries",2) + 1;
	if (len <= 0) {
		len = 1;
	}
	//異常資訊物件
	RpcException le = null; // last exception.
	List<Invoker<T>> invoked = new ArrayList<Invoker<T>>(copyinvokers.size());
	Set<String> providers = new HashSet<String>(len);
	
	//迴圈呼叫 失敗重試len次
	for (int i = 0; i < len; i++) {
		if (i > 0) {
			checkWhetherDestroyed();
			//重新獲取服務提供者列表
			copyinvokers = list(invocation);
			//再次檢查
			checkInvokers(copyinvokers, invocation);
		}
		//通過loadbalance選取一個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("");
			}
			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("");
}
複製程式碼

我們可以看到,它的重點是invoker的呼叫是在一個迴圈方法中。只要不return,就會一直呼叫,重試 len 次。我們總結下它的過程:

  • 檢查invokers是否為空
  • 獲取重試次數,預設為3
  • 進入迴圈
  • 如果是重試,再次獲取服務提供者列表,並校驗
  • 選取Invoker,並呼叫
  • 無異常,返回結果,迴圈結束
  • 捕獲到異常,繼續迴圈呼叫直至重試最大次數

四、快速失敗

FailfastClusterInvoker就很簡單了,它只會進行一次呼叫,失敗後立即丟擲異常。

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()) {
			throw (RpcException) e;
		}
		throw new RpcException("....");
	}
}
複製程式碼

五、失敗安全

FailsafeClusterInvoker跟上面這個差異不大,它呼叫失敗後並不丟擲異常。而是列印異常資訊並返回一個空的結果物件。

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();
	}
}
複製程式碼

六、自動恢復

FailbackClusterInvoker 會在呼叫失敗後,也是列印異常資訊並返回一個空的結果物件,但是還沒結束,它還會偷偷開啟一個定時任務,再次去呼叫。

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);
		//新增失敗資訊
		addFailed(invocation, this);
		return new RpcResult();
	}
}
複製程式碼

我們可以看到,呼叫失敗後,除了列印異常資訊和返回空結果物件之外,還有一個方法addFailed 它就是開啟定時任務的地方。

1、開啟定時任務

首先,定義一個包含2個執行緒的執行緒池物件。

Executors.newScheduledThreadPool(2, new NamedThreadFactory("failback-cluster-timer", true));

然後,延遲5秒後,每隔5秒呼叫retryFailed方法,直到呼叫成功。

private void addFailed(Invocation invocation, AbstractClusterInvoker<?> router) {
	if (retryFuture == null) {
		synchronized (this) {
			if (retryFuture == null) {
				retryFuture = scheduledExecutorService.scheduleWithFixedDelay(new Runnable() {
					public void run() {
						try {
							//重試方法
							retryFailed();
						} catch (Throwable t) { 
							logger.error("Unexpected error occur at collect statistic", t);
						}
					}
				}, 5000, 5000, TimeUnit.MILLISECONDS);
			}
		}
	}
	//ConcurrentHashMap 新增失敗任務
	failed.put(invocation, router);
}
複製程式碼

最後,我們需要注意failed.put(invocation, router); 它將當前失敗的任務新增到failed,它是一個ConcurrentHashMap物件。

2、重試

重試的邏輯也不復雜,從failed物件中獲取失敗的記錄,呼叫即可。

void retryFailed() {
	
	//如果為空,說明已經沒有了失敗的任務
	if (failed.size() == 0) {
		return;
	}
	//遍歷failed,對失敗的呼叫進行重試
	Set<Entry<Invocation, AbstractClusterInvoker<?>>> failedSet = failed.entrySet();     
	for (Entry<Invocation, AbstractClusterInvoker<?>> entry : failedSet) {
		Invocation invocation = entry.getKey();
		Invoker<?> invoker = entry.getValue();
		try {
			// 再次進行呼叫
			invoker.invoke(invocation);
			// 呼叫成功後,從 failed 中移除 invoker
			failed.remove(invocation);
		} catch (Throwable e) {
			logger.error("......", e);
		}
	}
}
複製程式碼

如上程式碼,其中的重點是呼叫成功後,要將invocation移除。當再次呼叫到這個方法,開頭的條件判斷成立,就直接返回,不再繼續呼叫。

七、並行呼叫

ForkingClusterInvoker 會在執行時通過執行緒池建立多個執行緒,併發呼叫多個服務提供者。只要有一個服務提供者成功返回了結果,doInvoke 方法就會立即結束執行。

public Result doInvoke(final Invocation invocation, 
		List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
	
	final List<Invoker<T>> selected;
	//獲取最大並行數 預設為2
	final int forks = getUrl().getParameter("forks", 2);
	//超時時間
	final int timeout = getUrl().getParameter("timeout", 1000);
	if (forks <= 0 || forks >= invokers.size()) {
		selected = invokers;
	} else {
		selected = new ArrayList<Invoker<T>>();
		//選擇Invoker 並新增到selected
		for (int i = 0; i < forks; i++) {
			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<Object>();
	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);
		if (ret instanceof Throwable) {
			Throwable e = (Throwable) ret;
			throw new RpcException("....");
		}
		return (Result) ret;
	} catch (InterruptedException e) {
		throw new RpcException(e.getMessage(), e);
	}
}
複製程式碼

以上程式碼的重點就是阻塞佇列LinkedBlockingQueue。如果有結果放入,poll方法會立即返回,完成整個呼叫。我們再總結下整體流程:

  • 獲取最大並行數,預設為2;獲取超時時間
  • 選擇Invoker,並新增到selected
  • 通過newCachedThreadPool建立多個執行緒,呼叫服務。
  • 正常返回後,將結果offer到佇列。此時呼叫流程結束,返回正常資訊。
  • 呼叫服務異常後,判斷異常次數是否大於等於最大並行數,條件成立則將異常資訊offer到佇列,此時呼叫流程結束,返回異常資訊。

八、廣播

BroadcastClusterInvoker 會逐個呼叫每個服務提供者,如果其中一臺報錯,在迴圈呼叫結束後,BroadcastClusterInvoker 會丟擲異常。

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;
}
複製程式碼

相關文章