如果說單單只完成遠程調用的話,dubbo還算不上是一個合格的SOA服務架構,而它之所以那么碉堡,是因為它還提供了服務治理的功能,今天就讓我們來研究一下關於服務治理,dubbo都做了什么。
聽起來服務治理挺高大上的,但其實做的都是一些非常瑣碎的事兒,了解了dubbo的做法,你就會發覺其實一切並沒有想的那么復雜。遠程調用要解決的最本質的問題是通信,通信就好像人和人之間的互動,有效的溝通建立在雙方彼此了解的基礎上(我們團隊在溝通上就有死穴),同樣道理,服務提供方和消費方之間要相互了解對方的基本情況,才能做到更好的完成遠程調用。這里面就要提到dubbo的做法:URL。
前幾篇中大量提到dubbo的分層之間是依靠什么紐帶工作的:invoker,沒錯,比invoker更low的就是URL,這是dubbo帶給我的另一個非常重要的經驗。才疏學淺,並不知道dubbo是借鑒的哪里,但影響了全世界的WEB就是依賴URL機制建立了互聯網帝國的!
依賴URL機制,dubbo不僅打通了通信兩端,而且還靠URL機制完成了服務治理的任務。我們可以先看一下這些內容:
其實dubbo的路由和集群是在服務暴露,服務發現,服務引用中透明完成的,暴露給其他層的是同一個接口類型:Invoker。dubbo官方提供了一張巨清晰無比的圖:
這張圖是站在服務消費方的視角來看的(dubbo的服務治理都是針對服務消費方的),當業務邏輯中需要調用一個服務時,你真正調用的其實是dubbo創建的一個proxy,該proxy會把調用轉化成調用指定的invoker(cluster封裝過的)。而在這一系列的委托調用的過程里就完成了服務治理的邏輯,最終完成調用。
集群
當相同服務由多個提供方同時提供時,消費方就需要有個選擇的步驟,就好比你去電商平台買一本書,你自然會看一下哪兒買的最便宜。同樣,消費方也需要根據需求選擇到底使用哪個提供方的服務,而集群的主要作用就是從容錯的維度來幫我們選擇合適的服務提供方。
我們需要從Protocol接口的部分定義開始:
/*** 引用遠程服務:<br>* 1. 當用戶調用refer()所返回的Invoker對象的invoke()方法時,協議需相應執行同URL遠端export()傳入的Invoker對象的invoke()方法。<br>* 2. refer()返回的Invoker由協議實現,協議通常需要在此Invoker中發送遠程請求。<br>* 3. 當url中有設置check=false時,連接失敗不能拋出異常,並內部自動恢復。<br>** @param <T> 服務的類型* @param type 服務的類型* @param url 遠程服務的URL地址* @return invoker 服務的本地代理* @throws RpcException 當連接服務提供方失敗時拋出*/ @Adaptive<T> Invoker<T> refer(Class<T> type, URL url) throws RpcException;
注意這個方法的返回值,根據我們這一系列文章一直使用的場景(有注冊中心),看一下RegistryProtocol.doRefer方法的最后一行:
return cluster.join(directory);
之前的文章提到過這個directory,它在后面我們會再次提到,這里你只需要知道它不是我們需要的invoker類型,那么這個cluster對象又是什么呢?根據dubbo的SPI機制,我們知道,這里的cluster是動態創建的自適應擴展點:
package com.alibaba.dubbo.rpc.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"); //默認使用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); } }
我們再來看一下默認使用的FailoverCluster定義:
/** * 失敗轉移,當出現失敗,重試其它服務器,通常用於讀操作,但重試會帶來更長延遲。 * * <a href="http://en.wikipedia.org/wiki/Failover">Failover</a> * * @author william.liangf */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); } }
看到了嗎,這就是一開始圖上的所表明的,cluster把存有多個invoker的directory對象封裝成了單個的invoker。我們在來看一下FailoverClusterInvoker類的UML圖:
根據官方文檔的說明,dubbo提供了多種集群容錯方案供我們直接使用,至於各種集群容錯模式算法可以交給大家自己閱讀源碼來消化了,后面只會以FailoverClusterInvoker為基准來討論。
路由和配置
如果說集群幫我們以容錯的維度來完成選擇,那么路由和配置是在更細顆粒度的層面做的選擇,具體有多細,可以從官方文檔和dubbo-admin管理后台來了解,如下多圖:
總之很細吧,這么多配置參數最終都會交給誰來管理呢?
我們需要從Directory接口出發,你應該想到了該接口的一個實現類:
沒錯,就是這個RegistryDirectory,它在服務引用時被創建,用於充當url與多invoer的代理(或者叫目錄類更合適),從源碼可以看出,當服務引用時,對應該服務的目錄類實例會負責向注冊中心(zookeeper)訂閱該服務,第一次訂閱會同步拿到當前服務節點的詳細信息(也就是所有提供服務的提供方信息,包括:地址,配置,路由等),然后該目錄實例會根據這些信息來為后續的服務調用提供支撐。
根據描述我們可以鎖定代碼位置,RegistryDirectory.notify:
......// configurators 更新緩存的服務提供方動態配置規則if (configuratorUrls != null && configuratorUrls.size() >0 ){ this.configurators = toConfigurators(configuratorUrls); } // routers 更新緩存的路由配置規則if (routerUrls != null && routerUrls.size() >0 ){ List<Router> routers = toRouters(routerUrls); if(routers != null){ // null - do nothing setRouters(routers); } } ......
這些配置在什么時候發揮作用呢?往下看~
前面說到當調用invoker時,其實調用的是集群模塊封裝過的代理invoker,那么以我們的場景為例,最終會被調用的是FailoverClusterInvoker.invoke:
public Result invoke(final Invocation invocation) throws RpcException { checkWheatherDestoried(); LoadBalance loadbalance; //這里就是路由,配置等發揮作用地方,返回所有合法的invoker供集群做下一步的篩選 List<Invoker<T>> invokers = list(invocation); if (invokers != null && invokers.size() > 0) { 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); } RpcUtils.attachInvocationIdIfAsync(getUrl(), invocation); return doInvoke(invocation, invokers, loadbalance); }
再來看一下這個list方法的定義:
protected List<Invoker<T>> list(Invocation invocation) throws RpcException { List<Invoker<T>> invokers = directory.list(invocation); return invokers; }
很直接的把選擇合法invoker的工作交給了我們的目錄類實例,再來看一下directory是怎么list的:
public List<Invoker<T>> list(Invocation invocation) throws RpcException { if (destroyed){ throw new RpcException("Directory already destroyed .url: "+ getUrl()); } //根據請求服務的相關參數(方法名等)返回對應的invoker列表 List<Invoker<T>> invokers = doList(invocation); List<Router> localRouters = this.routers; // local referenceif (localRouters != null && localRouters.size() > 0) { for (Router router: localRouters){ try { //是否在每次調用時執行路由規則,否則只在提供者地址列表變更時預先執行並緩存結果,調用時直接從緩存中獲取路由結果。//如果用了參數路由,必須設為true,需要注意設置會影響調用的性能,可不填,缺省為flase。if (router.getUrl() == null || router.getUrl().getParameter(Constants.RUNTIME_KEY, true)) { invokers = router.route(invokers, getConsumerUrl(), invocation); } } catch (Throwable t) { logger.error("Failed to execute router: " + getUrl() + ", cause: " + t.getMessage(), t); } } } return invokers; }
到這里我們就已經把路由和配置的相關流程介紹完了,至於路由和配置的具體參數是如何發揮效果的,這個大家可以結合文檔提供的實例直接閱讀源碼即可。
負載均衡
到了負載均衡環節,維度就成了性能,這個詞你可以從gg里搜索大量的相關文獻,我就不在這里賣弄了。把焦點拉回到FailoverClusterInvoker.invoke方法:
......
if (invokers != null && invokers.size() > 0) { loadbalance = ExtensionLoader.getExtensionLoader(LoadBalance.class).getExtension(invokers.get(0).getUrl() .getMethodParameter(invocation.getMethodName(),Constants.LOADBALANCE_KEY, Constants.DEFAULT_LOADBALANCE)); } else { //todo 如果invokers為空,還有必要往下走么? loadbalance = ExtensionLoader.getExtensionLoader(LoadBalance.class).getExtension(Constants.DEFAULT_LOADBALANCE); } RpcUtils.attachInvocationIdIfAsync(getUrl(), invocation); return doInvoke(invocation, invokers, loadbalance); ......
可以看到,這里就創建了要使用的負載均衡算法,我們接下來看一下到底是怎么使用這個loadbalance對象的,一路跟蹤到AbstractClusterInvoker.doselect方法:
...... Invoker<T> invoker = loadbalance.select(invokers, getUrl(), invocation); ......
(其實本人並不喜歡這樣截取部分代碼展示,因為會讓讀者很窘迫,不過請相信我,這里這么做可以很好的排除干擾。)可見,最終是依靠負載均衡這最后一道關卡我們總算拿到了要調用的invoker。我們依然不去過多在意算法細節,到目前為止,負載均衡的流程也介紹完了。
其實dubbo服務治理相關的內容還有很多,官方文檔也提供了詳細的說明,希望大家都能成為dubbo大牛,bye~








