前言
在上一章節,我們曾提到這樣一個問題: 當呼叫服務失敗後,我們怎麼處理當前的請求?丟擲異常亦或是重試?
為了解決這個問題,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;
}
複製程式碼