Dubbo中集羣Cluster,負載均衡和容錯

Dubbo中的Cluster可以將多個服務提供方僞裝成一個提供方,具體也就是將Directory中的多個Invoker僞裝成一個Invoker,在僞裝的過程中包含了容錯的處理和負載均衡的處理。這篇文章介紹下集羣相關的東西,開始先對着文檔解釋下容錯模式,負載均衡等概念,然後解析下源碼的處理。

集羣的容錯模式

Failover Cluster

這是dubbo中默認的集羣容錯模式

  • 失敗自動切換,當出現失敗,重試其它服務器。
  • 通常用於讀操作,但重試會帶來更長延遲。
  • 可通過retries=”2”來設置重試次數(不含第一次)。

Failfast Cluster

  • 快速失敗,只發起一次調用,失敗立即報錯。
  • 通常用於非冪等性的寫操作,比如新增記錄。

Failsafe Cluster

  • 失敗安全,出現異常時,直接忽略。
  • 通常用於寫入審計日誌等操作。

Failback Cluster

  • 失敗自動恢復,後臺記錄失敗請求,定時重發。
  • 通常用於消息通知操作。

Forking Cluster

  • 並行調用多個服務器,只要一個成功即返回。
  • 通常用於實時性要求較高的讀操作,但需要浪費更多服務資源。
  • 可通過forks=”2”來設置最大並行數。

Broadcast Cluster

  • 廣播調用所有提供者,逐個調用,任意一臺報錯則報錯。(2.1.0開始支持)
  • 通常用於通知所有提供者更新緩存或日誌等本地資源信息。

負載均衡

dubbo默認的負載均衡策略是random,隨機調用。

Random LoadBalance

  • 隨機,按權重設置隨機概率。
  • 在一個截面上碰撞的概率高,但調用量越大分佈越均勻,而且按概率使用權重後也比較均勻,有利於動態調整提供者權重。

RoundRobin LoadBalance

  • 輪循,按公約後的權重設置輪循比率。
  • 存在慢的提供者累積請求問題,比如:第二臺機器很慢,但沒掛,當請求調到第二臺時就卡在那,久而久之,所有請求都卡在調到第二臺上。

LeastActive LoadBalance

  • 最少活躍調用數,相同活躍數的隨機,活躍數指調用前後計數差。
  • 使慢的提供者收到更少請求,因爲越慢的提供者的調用前後計數差會越大。

ConsistentHash LoadBalance

  • 一致性Hash,相同參數的請求總是發到同一提供者。
  • 當某一臺提供者掛時,原本發往該提供者的請求,基於虛擬節點,平攤到其它提供者,不會引起劇烈變動。
  • 缺省只對第一個參數Hash。
  • 缺省用160份虛擬節點。

集羣相關源碼解析

回想一下在服務消費者初始化的過程中,在引用遠程服務的那一步,也就是RegistryProtocol的refer方法中,調用了doRefer方法,doRefer方法中第一個參數就是cluster,我們就從這裏開始解析。RegistryProtocol的refer方法:

public <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException {
    url = url.setProtocol(url.getParameter(Constants.REGISTRY_KEY, Constants.DEFAULT_REGISTRY)).removeParameter(Constants.REGISTRY_KEY);
    //根據url獲取註冊中心實例
    //這一步連接註冊中心,並把消費者註冊到註冊中心
    Registry registry = registryFactory.getRegistry(url);
    //對註冊中心服務的處理
    if (RegistryService.class.equals(type)) {
        return proxyFactory.getInvoker((T) registry, type, url);
    }
    //以下是我們自己定義的業務的服務處理
    // group="a,b" or group="*"
    Map<String, String> qs = StringUtils.parseQueryString(url.getParameterAndDecoded(Constants.REFER_KEY));
    String group = qs.get(Constants.GROUP_KEY);
    //服務需要合併不同實現
    if (group != null && group.length() > 0 ) {
        if ( ( Constants.COMMA_SPLIT_PATTERN.split( group ) ).length > 1
                || "*".equals( group ) ) {
            return doRefer( getMergeableCluster(), registry, type, url );
        }
    }
    //這裏參數cluster是集羣的適配類,代碼在下面
    return doRefer(cluster, registry, type, url);
}

接着看doRefer,真正去做服務引用的方法:

private <T> Invoker<T> doRefer(Cluster cluster, Registry registry, Class<T> type, URL url) {
    //Directory中是Invoker的集合,相當於一個List
    //也就是說這裏面存放了多個Invoker,那麼我們該調用哪一個呢?
    //該調用哪一個Invoker的工作就是Cluster來處理的
    RegistryDirectory<T> directory = new RegistryDirectory<T>(type, url);
    directory.setRegistry(registry);
    directory.setProtocol(protocol);
    URL subscribeUrl = new URL(Constants.CONSUMER_PROTOCOL, NetUtils.getLocalHost(), 0, type.getName(), directory.getUrl().getParameters());
    if (! Constants.ANY_VALUE.equals(url.getServiceInterface())
            && url.getParameter(Constants.REGISTER_KEY, true)) {
        
 //到註冊中心註冊服務       registry.register(subscribeUrl.addParameters(Constants.CATEGORY_KEY, Constants.CONSUMERS_CATEGORY,
                Constants.CHECK_KEY, String.valueOf(false)));
    }
    
 //訂閱服務,註冊中心會推送服務消息給消費者,消費者會再次進行服務的引用。   directory.subscribe(subscribeUrl.addParameter(Constants.CATEGORY_KEY, 
            Constants.PROVIDERS_CATEGORY 
            + "," + Constants.CONFIGURATORS_CATEGORY 
            + "," + Constants.ROUTERS_CATEGORY));
    //服務的引用和變更全部由Directory異步完成
    //Directory中可能存在多個Invoker
    //而Cluster會把多個Invoker僞裝成一個Invoker
    //這一步就是做這個事情的
    return cluster.join(directory);
}

集羣處理的入口

入口就是在doRefer的時候最後一步:cluster.join(directory);。

首先解釋下cluster,這個是根據dubbo的擴展機制生成的,在RegistryProtocol中有一個setCluster方法,根據擴展機制可以知道,這是注入Cluster的地方,代碼如下:

import com.alibaba.dubbo.common.extension.ExtensionLoader;
public class Cluster$Adpative implements com.alibaba.dubbo.rpc.cluster.Cluster {

  public com.alibaba.dubbo.rpc.Invoker join(com.alibaba.dubbo.rpc.cluster.Directory arg0) throws com.alibaba.dubbo.rpc.cluster.Directory {
    if (arg0 == null) throw new IllegalArgumentException("com.alibaba.dubbo.rpc.cluster.Directory argument == null");

    if (arg0.getUrl() == null) throw new IllegalArgumentException("com.alibaba.dubbo.rpc.cluster.Directory argument getUrl() == null");com.alibaba.dubbo.common.URL url = arg0.getUrl();

    String extName = url.getParameter("cluster", "failover");
    if(extName == null) throw new IllegalStateException("Fail to get extension(com.alibaba.dubbo.rpc.cluster.Cluster) name from url(" + url.toString() + ") use keys([cluster])");

    com.alibaba.dubbo.rpc.cluster.Cluster extension = (com.alibaba.dubbo.rpc.cluster.Cluster)ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.rpc.cluster.Cluster.class).getExtension(extName);

    return extension.join(arg0);
  }
}

可以看到,如果我們沒有配置集羣策略的話,默認是用failover模式,在Cluster接口的註解上@SPI(FailoverCluster.NAME)也可以看到默認是failover。

繼續執行cluster.join方法,會首先進入MockClusterWrapper的join方法:

public <T> Invoker<T> join(Directory<T> directory) throws RpcException {
    //先執行FailoverCluster的join方法處理
    //然後將Directory和返回的Invoker封裝成一個MockCluster
    return new MockClusterInvoker<T>(directory,
            this.cluster.join(directory));
}

看下Failover的join方法:

public <T> Invoker<T> join(Directory<T> directory) throws RpcException {
    //直接返回一個FailoverClusterInvoker的實例
    return new FailoverClusterInvoker<T>(directory);
}

到這裏就算把Invoker都封裝好了,返回的Invoker是一個MockClusterInvoker,MockClusterInvoker內部包含一個Directory和一個FailoverClusterInvoker。

Invoker都封裝好了之後,就是創建代理,然後使用代理調用我們的要調用的方法。

調用方法時集羣的處理

在進行具體方法調用的時候,代理中會invoker.invoke(),這裏Invoker就是我們上面封裝好的MockClusterInvoker,所以首先進入MockClusterInvoker的invoke方法:

    Result result = null;
    //我們沒配置mock,所以這裏爲false
    //Mock通常用於服務降級
    String value = directory.getUrl().getMethodParameter(invocation.getMethodName(), Constants.MOCK_KEY, Boolean.FALSE.toString()).trim(); 
    //沒有使用mock
    if (value.length() == 0 || value.equalsIgnoreCase("false")){
        //這裏的invoker是FailoverClusterInvoker
        result = this.invoker.invoke(invocation);
    } else if (value.startsWith("force")) {
        //mock=force:return+null
        //表示消費方對方法的調用都直接返回null,不發起遠程調用
        //可用於屏蔽不重要服務不可用的時候,對調用方的影響
        //force:direct mock
        result = doMockInvoke(invocation, null);
    } else {
        //mock=fail:return+null
        //表示消費方對該服務的方法調用失敗後,再返回null,不拋異常
        //可用於對不重要服務不穩定的時候,忽略對調用方的影響
        //fail-mock
        try {
            result = this.invoker.invoke(invocation);
        }catch (RpcException e) {
            if (e.isBiz()) {
                throw e;
            } else {
                result = doMockInvoke(invocation, e);
            }
        }
    }
    return result;
}

我們這裏麼有配置mock屬性。首先進入的是AbstractClusterInvoker的incoke方法:

public Result invoke(final Invocation invocation) throws RpcException {
    //檢查是否已經被銷燬
    checkWheatherDestoried();
    //可以看到這裏該處理負載均衡的問題了
    LoadBalance loadbalance;
    //根據invocation中的信息從Directory中獲取Invoker列表
    //這一步中會進行路由的處理
    List<Invoker<T>> invokers = list(invocation);
    if (invokers != null && invokers.size() > 0) {
        //使用擴展機制,加載LoadBalance的實現類,默認使用的是random
        //我們這裏得到的就是RandomLoadBalance
        loadbalance = ExtensionLoader.getExtensionLoader(LoadBalance.class).getExtension(invokers.get(0).getUrl()
                .getMethodParameter(invocation.getMethodName(),Constants.LOADBALANCE_KEY, Constants.DEFAULT_LOADBALANCE));
    } else {
        loadbalance = ExtensionLoader.getExtensionLoader(LoadBalance.class).getExtension(Constants.DEFAULT_LOADBALANCE);
    }
    //異步操作默認添加invocation id
    RpcUtils.attachInvocationIdIfAsync(getUrl(), invocation);
    //調用具體的實現類的doInvoke方法,這裏是FailoverClusterInvoker
    return doInvoke(invocation, invokers, loadbalance);
}

看下FailoverClusterInvoker的invoke方法:

public Result doInvoke(Invocation invocation, final List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
    //Invoker列表
    List<Invoker<T>> copyinvokers = invokers;
    //確認下Invoker列表不爲空
    checkInvokers(copyinvokers, invocation);
    //重試次數
    int len = getUrl().getMethodParameter(invocation.getMethodName(), Constants.RETRIES_KEY, Constants.DEFAULT_RETRIES) + 1;
    if (len <= 0) {
        len = 1;
    }
    // retry loop.
    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 (int i = 0; i < len; i++) {
        //重試時,進行重新選擇,避免重試時invoker列表已發生變化.
        //注意:如果列表發生了變化,那麼invoked判斷會失效,因爲invoker示例已經改變
        if (i > 0) {
            checkWheatherDestoried();
            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 {
            //使用選擇的結果Invoker進行調用,返回結果
            Result result = invoker.invoke(invocation);
            return result;
        } catch (RpcException e) {。。。} finally {
            providers.add(invoker.getUrl().getAddress());
        }
    }
    throw new RpcException(。。。);
}

先看下使用loadbalance選擇invoker的select方法:

protected Invoker<T> select(LoadBalance loadbalance, Invocation invocation, List<Invoker<T>> invokers, List<Invoker<T>> selected) throws RpcException {
    if (invokers == null || invokers.size() == 0)
        return null;
    String methodName = invocation == null ? "" : invocation.getMethodName();

    //sticky,滯連接用於有狀態服務,儘可能讓客戶端總是向同一提供者發起調用,除非該提供者掛了,再連另一臺。
    boolean sticky = invokers.get(0).getUrl().getMethodParameter(methodName,Constants.CLUSTER_STICKY_KEY, Constants.DEFAULT_CLUSTER_STICKY) ;
    {
        //ignore overloaded method
        if ( stickyInvoker != null && !invokers.contains(stickyInvoker) ){
            stickyInvoker = null;
        }
        //ignore cucurrent problem
        if (sticky && stickyInvoker != null && (selected == null || !selected.contains(stickyInvoker))){
            if (availablecheck && stickyInvoker.isAvailable()){
                return stickyInvoker;
            }
        }
    }
    Invoker<T> invoker = doselect(loadbalance, invocation, invokers, selected);

    if (sticky){
        stickyInvoker = invoker;
    }
    return invoker;
}

doselect方法:

private Invoker<T> doselect(LoadBalance loadbalance, Invocation invocation, List<Invoker<T>> invokers, List<Invoker<T>> selected) throws RpcException {
    if (invokers == null || invokers.size() == 0)
        return null;
    //只有一個invoker,直接返回,不需要處理
    if (invokers.size() == 1)
        return invokers.get(0);
    // 如果只有兩個invoker,退化成輪循
    if (invokers.size() == 2 && selected != null && selected.size() > 0) {
        return selected.get(0) == invokers.get(0) ? invokers.get(1) : invokers.get(0);
    }
    //使用loadBalance進行選擇
    Invoker<T> invoker = loadbalance.select(invokers, getUrl(), invocation);

    //如果 selected中包含(優先判斷) 或者 不可用&&availablecheck=true 則重試.
    if( (selected != null && selected.contains(invoker))
            ||(!invoker.isAvailable() && getUrl()!=null && availablecheck)){
        try{
            //重新選擇
            Invoker<T> rinvoker = reselect(loadbalance, invocation, invokers, selected, availablecheck);
            if(rinvoker != null){
                invoker =  rinvoker;
            }else{
                //看下第一次選的位置,如果不是最後,選+1位置.
                int index = invokers.indexOf(invoker);
                try{
                    //最後在避免碰撞
                    invoker = index <invokers.size()-1?invokers.get(index+1) :invoker;
                }catch (Exception e) {。。。 }
            }
        }catch (Throwable t){。。。}
    }
    return invoker;
}

接着看使用loadBalance進行選擇,首先進入AbstractLoadBalance的select方法:

 public <T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) {
    if (invokers == null || invokers.size() == 0)
        return null;
    if (invokers.size() == 1)
        return invokers.get(0);
    //  進行選擇,具體的子類實現,我們這裏是RandomLoadBalance
    return doSelect(invokers, url, invocation);
}

接着去RandomLoadBalance中查看:

protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
    int length = invokers.size(); // 總個數
    int totalWeight = 0; // 總權重
    boolean sameWeight = true; // 權重是否都一樣
    for (int i = 0; i < length; i++) {
        int weight = getWeight(invokers.get(i), invocation);
        totalWeight += weight; // 累計總權重
        if (sameWeight && i > 0
                && weight != getWeight(invokers.get(i - 1), invocation)) {
            sameWeight = false; // 計算所有權重是否一樣
        }
    }
    if (totalWeight > 0 && ! sameWeight) {
        // 如果權重不相同且權重大於0則按總權重數隨機
        int offset = random.nextInt(totalWeight);
        // 並確定隨機值落在哪個片斷上
        for (int i = 0; i < length; i++) {
            offset -= getWeight(invokers.get(i), invocation);
            if (offset < 0) {
                return invokers.get(i);
            }
        }
    }
    // 如果權重相同或權重爲0則均等隨機
    return invokers.get(random.nextInt(length));
}

上面根據權重之類的來進行選擇一個Invoker返回。接下來reselect的方法不在說明,是先從非selected的列表中選擇,沒有在從selected列表中選擇。
選擇好了Invoker之後,就回去FailoverClusterInvoker的doInvoke方法,接着就是根據選中的Invoker調用invoke方法進行返回結果,接着就是到具體的Invoker進行調用的過程了。

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