dubbo源碼愫讀(8) dubbo的集群、路由、負載均衡策略分析

dubbo是一個分布式服務框架,能避免單點故障和支持服務的橫向擴容。一個服務通常會部署多個實例,同時一個服務能注冊到多個注冊中心。如何從多個服務 Provider 組成的集群中挑選出一個進行調用,就涉及到一個負載均衡的策略。

1、dubbo負載均衡實現說明

dubbo服務調用流程圖:

dubbo服務調用流程圖.png

從以上調用流程圖可知,dubbo的負載均衡主要在客戶端實現,并通過封裝Cluster、Directory、LoadBalance相關接口實現。

1.1、Cluster、Directory、Router、LoadBalance關系

關系圖.png

各組件關系說明:

  • 這里的Invoker是Provider的一個可調用Service的抽象,Invoker封裝了Provider地址及Service接口信息。
  • Directory代表多個Invoker,可以把它看成List,但與List不同的是,它的值可能是動態變化的,比如注冊中心推送變更。
  • Cluster將Directory中的多個Invoker偽裝成一個Invoker,對上層透明,偽裝過程包含了容錯邏輯,調用失敗后,重試另一個。
  • Router負責從多個Invoker中按路由規則選出子集,比如讀寫分離,應用隔離等。
  • LoadBalance負責從多個Invoker中選出具體的一個用于本次調用,選的過程包含了負載均衡算法,調用失敗后,需要重選。

1.2、客戶端負載均衡源碼分析

1.2.1、ReferenceConfig中負載均衡的封裝

客戶端在進行代理處理時,在如下地方對負載均衡相關進行封裝:
包路徑:dubbo-config->dubbo-config-api
類名:ReferenceConfig
方法名:createProxy()

if (urls.size() == 1) {
    invoker = refprotocol.refer(interfaceClass, urls.get(0));
} else {
    List<Invoker<?>> invokers = new ArrayList<Invoker<?>>();
    URL registryURL = null;
    for (URL url : urls) {
        invokers.add(refprotocol.refer(interfaceClass, url));
        if (Constants.REGISTRY_PROTOCOL.equals(url.getProtocol())) {
            registryURL = url; // use last registry url
        }
    }
    if (registryURL != null) { // registry url is available
        // use RegistryAwareCluster only when register's cluster is available
        URL u = registryURL.addParameter(Constants.CLUSTER_KEY, RegistryAwareCluster.NAME);
        // The invoker wrap relation would be: RegistryAwareClusterInvoker(StaticDirectory) -> FailoverClusterInvoker(RegistryDirectory, will execute route) -> Invoker
        invoker = cluster.join(new StaticDirectory(u, invokers));
    } else { // not a registry url, must be direct invoke.
        invoker = cluster.join(new StaticDirectory(invokers));
    }
}

處理流程:

  • 若只有一個注冊中心,則直接調用RegistryProtocol.refer()進行服務引用處理,將其封裝成invoker,RegistryProtocol.refer()內部對負載均衡進行了封裝;
  • 若為多個注冊中心,先分別對各注冊中心進行服務引用處理,然后再應用Cluster.join()及StaticDirectory將多個注冊中心的invoker再封裝成一個invoker來達到對外透明;

1.2.2、RegistryProtocol中負載均衡的封裝

RegistryProtocol中對單個注冊中心進行了負載均衡的封裝:
包路徑:dubbo-registry->dubbo-registry-api
類名:RegistryProtocol
方法名:doRefer()

private <T> Invoker<T> doRefer(Cluster cluster, Registry registry, Class<T> type, URL url) {
    RegistryDirectory<T> directory = new RegistryDirectory<T>(type, url);
    directory.setRegistry(registry);
    directory.setProtocol(protocol);
    // all attributes of REFER_KEY
    Map<String, String> parameters = new HashMap<String, String>(directory.getUrl().getParameters());
    URL subscribeUrl = new URL(CONSUMER_PROTOCOL, parameters.remove(REGISTER_IP_KEY), 0, type.getName(), parameters);
    if (!ANY_VALUE.equals(url.getServiceInterface()) && url.getParameter(REGISTER_KEY, true)) {
        registry.register(getRegisteredConsumerUrl(subscribeUrl, url));
    }
    directory.buildRouterChain(subscribeUrl);
    directory.subscribe(subscribeUrl.addParameter(CATEGORY_KEY,
            PROVIDERS_CATEGORY + "," + CONFIGURATORS_CATEGORY + "," + ROUTERS_CATEGORY));

    Invoker invoker = cluster.join(directory);
    ProviderConsumerRegTable.registerConsumer(invoker, url, subscribeUrl, directory);
    return invoker;
}

主要處理流程:

  • 實例化RegistryDirectory類型的Directory,RegistryDirectory有如下功能,訂閱感興趣的服務提供者信息,當提供者或路由變更時,動態變更本地的提供者列表及路由過濾鏈;并根據Router、LoadBalance動態選擇調用的服務提供者;
  • 將消費端注冊到注冊中心;
  • 構建Directory中的路由過濾鏈;
  • 在Directory中對注冊中心中對應的提供者相關的配置、提供者、路由規則進行訂閱;
  • 用Cluster對Directory進行封裝;

1.2.3、RegistryDirectory中負載均衡處理的封裝

RegistryDirectory主要封裝了訂閱、信息變更通知處理、獲取服務提供者信息等;
包路徑:dubbo-registry->dubbo-registry-api
類名:RegistryDirectory

(1)、訂閱感興趣的信息

源碼如下:

public void subscribe(URL url) {
    setConsumerUrl(url);
    consumerConfigurationListener.addNotifyListener(this);
    serviceConfigurationListener = new  ReferenceConfigurationListener(this, url);
    registry.subscribe(url, this);
}

本處主要對消費端的訂閱進行了處理,消費端向注冊中心訂閱三個信息:配置信息、服務提供者、路由信息;當這三個信息有任何變更,本地就會接到通知,并進行處理;

(2)、配置信息、服務提供者、路由信息變更通知處理

源碼如下:

public synchronized void notify(List<URL> urls) {
    List<URL> categoryUrls = urls.stream()
            .filter(this::isValidCategory)
            .filter(this::isNotCompatibleFor26x)
            .collect(Collectors.toList());

    /**
     * TODO Try to refactor the processing of these three type of urls using Collectors.groupBy()?
     */
    this.configurators = Configurator.toConfigurators(classifyUrls(categoryUrls, UrlUtils::isConfigurator))
            .orElse(configurators);

    toRouters(classifyUrls(categoryUrls, UrlUtils::isRoute)).ifPresent(this::addRouters);

    // providers
    refreshOverrideAndInvoker(classifyUrls(categoryUrls, UrlUtils::isProvider));
}

當有信息變更時,本方法就會被調用,會根據變更的配置或路由信息或服務提供者進行相應處理;若路由信息變更,則重新構建路由過濾鏈;若服務提供者變更,則重構刷新本地緩存的服務提供者列表;

(3)、獲取服務提供者列表

源碼如下:

public List<Invoker<T>> doList(Invocation invocation) {
    if (forbidden) {
        // 1. No service provider 2. Service providers are disabled
        throw new RpcException(RpcException.FORBIDDEN_EXCEPTION, "No provider available from registry " +
                getUrl().getAddress() + " for service " + getConsumerUrl().getServiceKey() + " on consumer " +
                NetUtils.getLocalHost() + " use dubbo version " + Version.getVersion() +
                ", please check status of providers(disabled, not registered or in blacklist).");
    }

    if (multiGroup) {
        return this.invokers == null ? Collections.emptyList() : this.invokers;
    }

    List<Invoker<T>> invokers = null;
    try {
        // Get invokers from cache, only runtime routers will be executed.
        invokers = routerChain.route(getConsumerUrl(), invocation);
    } catch (Throwable t) {
        logger.error("Failed to execute router: " + getUrl() + ", cause: " + t.getMessage(), t);
    }


    // FIXME Is there any need of failing back to Constants.ANY_VALUE or the first available method invokers when invokers is null?
    /*Map<String, List<Invoker<T>>> localMethodInvokerMap = this.methodInvokerMap; // local reference
    if (localMethodInvokerMap != null && localMethodInvokerMap.size() > 0) {
        String methodName = RpcUtils.getMethodName(invocation);
        invokers = localMethodInvokerMap.get(methodName);
        if (invokers == null) {
            invokers = localMethodInvokerMap.get(Constants.ANY_VALUE);
        }
        if (invokers == null) {
            Iterator<List<Invoker<T>>> iterator = localMethodInvokerMap.values().iterator();
            if (iterator.hasNext()) {
                invokers = iterator.next();
            }
        }
    }*/
    return invokers == null ? Collections.emptyList() : invokers;
}

當負載均衡獲取可用的服務提供者列表時會調用此方法,此方法主要根據注冊中心提供的服務提供者列表,并利用路由規則對提供者列表進行過濾。

1.2.4、FailoverCluster中負載均衡處理的封裝

包路徑:dubbo-cluster

dubbo中默認的Cluster實現為FailoverCluster,其主要是通過join()方法將Directory進行封裝的,而實際的處理是通過FailoverClusterInvoker實現的,客戶端調用服務時就是通過此invoker.invoke()進行實際調用處理的;

源碼如下:

public <T> Invoker<T> join(Directory<T> directory) throws RpcException {
    return new FailoverClusterInvoker<T>(directory);
}

FailoverClusterInvoker.invoke()實現:

public Result invoke(final Invocation invocation) throws RpcException {
    checkWhetherDestroyed();

    // binding attachments into invocation.
    Map<String, String> contextAttachments = RpcContext.getContext().getAttachments();
    if (contextAttachments != null && contextAttachments.size() != 0) {
        ((RpcInvocation) invocation).addAttachments(contextAttachments);
    }

    List<Invoker<T>> invokers = list(invocation);
    LoadBalance loadbalance = initLoadBalance(invokers, invocation);
    RpcUtils.attachInvocationIdIfAsync(getUrl(), invocation);
    return doInvoke(invocation, invokers, loadbalance);
}

主要處理流程:

  • 獲取rpc上下文信息,將這些信息添加到調用附加參數信息中;
  • 調用list(),實際調用的是Directory.list()獲取可用的服務提供者列表,此列表是通過Directory中的路由器過濾后的列表;
  • 調用initLoadBalance(),獲取配置的LoadBalance,若為配置,則使用默認的LoadBalance實現RandomLoadBalance;
  • 調用doInvode()進行實際的服務提供者選取及服務調用等;

FailoverClusterInvoker.doInvoke()實現:

public Result doInvoke(Invocation invocation, final List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
    List<Invoker<T>> copyInvokers = invokers;
    checkInvokers(copyInvokers, invocation);
    String methodName = RpcUtils.getMethodName(invocation);
    int len = getUrl().getMethodParameter(methodName, 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++) {
        //Reselect before retry to avoid a change of candidate `invokers`.
        //NOTE: if `invokers` changed, then `invoked` also lose accuracy.
        if (i > 0) {
            checkWhetherDestroyed();
            copyInvokers = list(invocation);
            // check again
            checkInvokers(copyInvokers, invocation);
        }
        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("Although retry the method " + methodName
                        + " 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) {
            if (e.isBiz()) { // biz exception.
                throw e;
            }
            le = e;
        } catch (Throwable e) {
            le = new RpcException(e.getMessage(), e);
        } finally {
            providers.add(invoker.getUrl().getAddress());
        }
    }
    throw new RpcException(le.getCode(), "Failed to invoke the method "
            + methodName + " 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.getMessage(), le.getCause() != null ? le.getCause() : le);
}

主要處理流程為:調用select()選取服務提供者并調用;select()中主要調用LoadBalance.selct()進行選擇;

2、Directory實現分析

Directory代表多個Invoker,可以把它看成List,但與List不同的是,它的值可能是動態變化的,比如注冊中心推送變更。Cluster將Directory中的多個Invoker偽裝成一個Invoker,對上層透明,偽裝過程包含了容錯邏輯,調用失敗后,重試另一個。

Directory接口類繼承圖:

Directory類繼承圖.png

RegistryDirectory:

RegistryDirectory實現了NotifyListener接口,因此他本身也是一個監聽器,可以在服務變更時接受通知,消費方要調用遠程服務,會向注冊中心訂閱這個服務的所有的服務提供方,訂閱的時候會調用notify方法,進行invoker實例的重新生成,也就是服務的重新引用。在服務提供方有變動時,也會調用notify方法,有關notify方法在Dubbo中訂閱和通知解析那篇文章中已經解釋,不做重復。subscribe方法也不做重復解釋。

StaticDirectory:

靜態目錄服務,當有多個注冊中心時會使用此實現。

3、Cluster實現分析

Dubbo中的Cluster可以將多個服務提供方偽裝成一個提供方,具體也就是將Directory中的多個Invoker偽裝成一個Invoker,在偽裝的過程中包含了容錯的處理,負載均衡的處理和路由的處理。

Cluster主要實現的類繼承圖:

Cluster類繼承圖.png

AbstractClusterInvoker主要實現類繼承圖:

AbstractClusterInvoker類繼承圖.png

集群的容錯模式:

failover(默認):

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

failfast:

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

failsafe:

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

failback:

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

forking:

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

broadcast:

  • 廣播調用所有提供者,逐個調用,任意一臺報錯則報錯。
  • 通常用于通知所有提供者更新緩存或日志等本地資源信息。

4、LoadBalance實現分析

LoadBalance類繼承圖.png

random:

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

roundrobin:

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

leastactive:

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

consistenthash:

  • 一致性Hash,相同參數的請求總是發到同一提供者。
  • 當某一臺提供者掛掉時,原本發往該提供者的請求,基于虛擬節點,平攤到其他提供者,不會引起劇烈變動。
  • 算法參見:https://en.wikipedia.org/wiki/Consistent_hashiing
  • 缺省只對第一個參考Hash,如果要修改,請配置<dubbo:parameter key="hash.arguments" value="0,1" />
  • 缺省用160份虛擬節點,如果要修改,請配置<dubbo:parameter key="hash.nodes" value="320" />

5、Router實現分析

dubbo的路由干的事,就是一個請求過來,dubbo依據配置的路由規則,計算出哪些提供者可以提供這次的請求服務。所以,它的優先級是在集群容錯策略和負載均衡策略之前的。即先有路由規則遴選出符合條件的服務提供者然后,再在這些服務提供者之中應用負載均衡,集群容錯策略。

Router接口繼承圖:

Router類繼承圖.png

ScriptRouter:
腳本路由規則 支持 JDK 腳本引擎的所有腳本,比如:javascript, jruby, groovy 等,通過 type=javascript 參數設置腳本類型,缺省為 javascript。

ConditionRouter:
條件路由主要就是根據dubbo管理控制臺配置的路由規則來過濾相關的invoker,當我們對路由規則點擊啟用的時候,就會觸發RegistryDirectory類的notify方法,其會重構本地路由調用鏈,而當從Directory中獲取服務提供者的list時,會利用此路由規則將提供者列表進行過濾;

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容