Dubbo——服務目錄

引言

前面幾篇文章分析了Dubbo的核心工作原理,本篇將對之前涉及到但卻未細講的服務目錄進行深入分析,在開始之前先結合前面的文章思考下什麼是服務目錄?它的作用是什麼?

正文

概念及作用

清楚Dubbo的調用過程就知道Dubbo在客戶端和服務端都會爲服務生成一個Invoker執行體,這個Invoker包含了所有的配置信息,也相當於是一個代理對象,所以這也就引發出幾個問題:

  • 怎麼管理Invoker?不可能讓用戶自己去管理,也不可能客戶端每次調用服務時都新創建Invoker。
  • 當服務存在集羣時,選擇使用哪一個Invoker?怎麼選擇?
  • 服務列表變化時,Invoker列表怎麼更新?

針對以上問題,Dubbo引入了服務目錄的概念,簡單的說就是Invoker的集合,由框架自身統一管理Invoker列表,並且提供訂閱服務功能,使得服務變更時,會自動更新Invoker列表;同時當存在集羣時,可以使得外部以統一的方法使用Invoker,即用戶不用關心怎麼選擇使用哪一個Invoker(在之前的源碼分析中我們看到過cluster.join就是實現該功能的API,將多個Invoker合成一個Invoker)。

繼承結構

瞭解了基本概念後,我們來看看服務目錄的繼承體系:
在這裏插入圖片描述
Directory繼承Node接口,該接口是Dubbo中節點的高度抽象,它提供了獲取url配置、判斷節點是否可用以及銷燬節點的接口,由各個子類實現,只要是和服務節點相關的實現都可以實現該接口。比如Invoker、Directory、Registry等。目錄的內置實現有StaticDirectory和RegistryDirectory兩個,第一個是靜態目錄服務,其中的Inovker列表是不會改變的;而RegistryDirectory實現了NotifyListener接口,表示會監聽註冊中心節點的變化,當節點信息改變時,RegistryDirectory中的Inovker列表會自動更新。

源碼分析

AbstractDirectory

從上面的繼承圖我們可以看到StaticDirectory和RegistryDirectory都繼承了AbstractDirectory,可以猜到多半又是模板方法模式的實現,確實也是這樣,AbstractDirectory提供了獲取Invoker的接口,而具體的實現則是子類自行實現的,我們來看看其源碼:

public abstract class AbstractDirectory<T> implements Directory<T> {

    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; 
        if (localRouters != null && localRouters.size() > 0) {
            for (Router router: localRouters){
                try {
                	// 是否需要進行路由
                    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;
    }
    
    protected abstract List<Invoker<T>> doList(Invocation invocation) throws RpcException ;

	// 省略其它方法

}

這個方法邏輯很簡單,沒什麼好說的,其中路由配置可自行了解,本文不打算展開,下面主要看看子類是如何實現doList方法以及如何管理Inovker的。

RegistryDirectory

剛說了RegistryDirectory是動態的目錄服務,會監聽註冊中心的節點變化,並自動刷新Invoker列表,用戶可以通過它拿到實時的Invoker列表,下面就主要分析這三部分是如何實現的。

註冊監聽及自動更新Invoker列表

public void subscribe(URL url) {
    setConsumerUrl(url);
    registry.subscribe(url, this);
}

註冊監聽很簡單,客戶端創建Zookeeper連接時,會添加監聽器監聽節點變化,該監聽器最終會調用到RegistryDirectory的subscribe方法,使得目錄也可以監聽節點變化,當節點發生變化時,又會觸發NotifyListener的notify方法,RegistryDirectory就實現了該方法:

public synchronized void notify(List<URL> urls) {
	// 存放provider節點下的url
    List<URL> invokerUrls = new ArrayList<URL>();
    // 存放router節點下的url
    List<URL> routerUrls = new ArrayList<URL>();
    // 存放configurator節點下的url
    List<URL> configuratorUrls = new ArrayList<URL>();
    // 每次傳入的url都是節點下所有的url,並非增量
    for (URL url : urls) {
        String protocol = url.getProtocol();
        String category = url.getParameter(Constants.CATEGORY_KEY, Constants.DEFAULT_CATEGORY);
        // 根據url的協議和分類存放到對應的List中
        if (Constants.ROUTERS_CATEGORY.equals(category) 
                || Constants.ROUTE_PROTOCOL.equals(protocol)) {
            routerUrls.add(url);
        } else if (Constants.CONFIGURATORS_CATEGORY.equals(category) 
                || Constants.OVERRIDE_PROTOCOL.equals(protocol)) {
            configuratorUrls.add(url);
        } else if (Constants.PROVIDERS_CATEGORY.equals(category)) {
            invokerUrls.add(url);
        } else {
        	// 忽略不支持的url
            logger.warn("Unsupported category " + category + " in notified url: " + url + " from registry " + getUrl().getAddress() + " to consumer " + NetUtils.getLocalHost());
        }
    }
    // 緩存configurator url 
    if (configuratorUrls != null && configuratorUrls.size() >0 ){
        this.configurators = toConfigurators(configuratorUrls);
    }
    // 設置路由
    if (routerUrls != null && routerUrls.size() >0 ){
        List<Router> routers = toRouters(routerUrls);
        if(routers != null){ // null - do nothing
            setRouters(routers);
        }
    }
    List<Configurator> localConfigurators = this.configurators; // local reference
    // 合併override參數
    this.overrideDirectoryUrl = directoryUrl;
    if (localConfigurators != null && localConfigurators.size() > 0) {
        for (Configurator configurator : localConfigurators) {
            this.overrideDirectoryUrl = configurator.configure(overrideDirectoryUrl);
        }
    }
    // 根據provider節點下的url刷新invoker
    refreshInvoker(invokerUrls);
}

這個方法主要緩存各種類型的url以及配置路由,Invoker的刷新主要是在refreshInvoker方法中:

private void refreshInvoker(List<URL> invokerUrls){
    if (invokerUrls != null && invokerUrls.size() == 1 && invokerUrls.get(0) != null
            && Constants.EMPTY_PROTOCOL.equals(invokerUrls.get(0).getProtocol())) {
        // invokerUrls中只有一個url且協議爲empty,則禁用所有服務
        this.forbidden = true; // 禁止訪問
        this.methodInvokerMap = null; // 置空列表
        destroyAllInvokers(); // 關閉所有Invoker
    } else {
        this.forbidden = false; // 允許訪問
        Map<String, Invoker<T>> oldUrlInvokerMap = this.urlInvokerMap; // local reference
        if (invokerUrls.size() == 0 && this.cachedInvokerUrls != null){
        	// invokerUrls爲空但緩存中有url,則將緩存中的url添加到invokerUrls
            invokerUrls.addAll(this.cachedInvokerUrls);
        } else {
        	// 將invokerUrls中的所有url緩存起來,便於交叉對比
            this.cachedInvokerUrls = new HashSet<URL>();
            this.cachedInvokerUrls.addAll(invokerUrls);
        }
        if (invokerUrls.size() ==0 ){
        	return;
        }
        // 將URL列表轉成Invoker列表,key是url
        Map<String, Invoker<T>> newUrlInvokerMap = toInvokers(invokerUrls) ;
        // 換方法名映射Invoker列表
        Map<String, List<Invoker<T>>> newMethodInvokerMap = toMethodInvokers(newUrlInvokerMap); 
        // 轉換出錯,拋出異常
        if (newUrlInvokerMap == null || newUrlInvokerMap.size() == 0 ){
            logger.error(new IllegalStateException("urls to invokers error .invokerUrls.size :"+invokerUrls.size() + ", invoker.size :0. urls :"+invokerUrls.toString()));
            return ;
        }
        // 合併多個invoker
        this.methodInvokerMap = multiGroup ? toMergeMethodInvokerMap(newMethodInvokerMap) : newMethodInvokerMap;
        this.urlInvokerMap = newUrlInvokerMap;
        try{
        	// 銷燬無用的invoker
            destroyUnusedInvokers(oldUrlInvokerMap,newUrlInvokerMap); 
        }catch (Exception e) {
            logger.warn("destroyUnusedInvokers error. ", e);
        }
    }
}

該方法首先會判斷是否需要禁用服務,若不需要,則將url轉化爲Invoker,並將方法名映射到對應的Invoker,緊接着將多組Invoker合併後賦值給this.methodInvokerMap變量(該變量會在doList遍歷Invoker時用到),最後會銷燬掉緩存中已經無用的Invoker,避免調用到已怠機的服務。以上就是Invoker自動刷新的流程,其中各個依賴方法的細節感興趣的可自行分析,下面就一起來看看如何獲取Invoker列表。

獲取Invoker列表

public List<Invoker<T>> doList(Invocation invocation) {
	// 服務提供者關閉或禁止了服務拋出異常
    if (forbidden) {
        throw new RpcException(RpcException.FORBIDDEN_EXCEPTION, "Forbid consumer " +  NetUtils.getLocalHost() + " access service " + getInterface().getName() + " from registry " + getUrl().getAddress() + " use dubbo version " + Version.getVersion() + ", Please check registry access list (whitelist/blacklist).");
    }
    List<Invoker<T>> invokers = null;
    // 本地Invoker列表,在refreshInvoker中合併賦值
    Map<String, List<Invoker<T>>> localMethodInvokerMap = this.methodInvokerMap; 
    if (localMethodInvokerMap != null && localMethodInvokerMap.size() > 0) {
    	// 獲取方法名和參數列表
        String methodName = RpcUtils.getMethodName(invocation);
        Object[] args = RpcUtils.getArguments(invocation);
        // 第一個參數爲String或者Enum,不太清楚有什麼意義
        if(args != null && args.length > 0 && args[0] != null
                && (args[0] instanceof String || args[0].getClass().isEnum())) {
            invokers = localMethodInvokerMap.get(methodName + "." + args[0]); // 可根據第一個參數枚舉路由
        }
        // 根據方法名獲取Inovker列表
        if(invokers == null) {
            invokers = localMethodInvokerMap.get(methodName);
        }
        // 根據“*”獲取Invoker列表
        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 ? new ArrayList<Invoker<T>>(0) : invokers;
}

這個邏輯也非常清晰,就是根據方法名或“*”拿到對應的Invoker列表。以上就是動態服務目錄的實現原理,下面再來看看靜態服務目錄。

StaticDirectory

public class StaticDirectory<T> extends AbstractDirectory<T> {
    
    private final List<Invoker<T>> invokers;

    public StaticDirectory(URL url, List<Invoker<T>> invokers, List<Router> routers) {
        super(url == null && invokers != null && invokers.size() > 0 ? invokers.get(0).getUrl() : url, routers);
        if (invokers == null || invokers.size() == 0)
            throw new IllegalArgumentException("invokers == null");
        this.invokers = invokers;
    }
    
    public void destroy() {
        if(isDestroyed()) {
            return;
        }
        super.destroy();
        for (Invoker<T> invoker : invokers) {
            invoker.destroy();
        }
        invokers.clear();
    }
    
    @Override
    protected List<Invoker<T>> doList(Invocation invocation) throws RpcException {

        return invokers;
    }

}

相比較而言,靜態服務目錄就簡單多了,通過構造器創建,由於沒有提供更新的方法,所以一旦創建就不會改變,而讀取Inovker列表只需要將自身變量返回即可。

總結

服務目錄的原理我們搞清楚了,再結合前面幾篇文章,我們能夠掌握Dubbo的核心實現原理。現在我們再來看看Dubbo的架構圖:
在這裏插入圖片描述
拋開種種繁雜的功能,你會發現這個架構和RMI以及我們之前手寫實現的RPC架構沒太大區別,所以,大道至簡,掌握基礎才能更加快速地理解更復雜的框架應用。
至此,Dubbo系列暫時就寫到這了,但其本身做爲一個優秀的開源框架,發展這麼多年,不可能這幾篇文章就涵蓋完全了,其它的諸如序列化、路由、Monitor以及文中未做詳細分析的部分,讀者們可自行閱讀源碼分析。

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