Dubbo系列之 (六)服務訂閱(3)

wsMrLin發表於2020-09-09

輔助連結

Dubbo系列之 (一)SPI擴充套件

Dubbo系列之 (二)Registry註冊中心-註冊(1)

Dubbo系列之 (三)Registry註冊中心-註冊(2)

Dubbo系列之 (四)服務訂閱(1)

Dubbo系列之 (五)服務訂閱(2)

Dubbo系列之 (六)服務訂閱(3)

RegistryDirectory

當RegistryDirectory#substribe()方法被RegistryProtocol#refer()方法呼叫時,本地服務消費端會與註冊中心互動,拉取最新的服務提供者,並與這些服務提供者建立TCP連線。

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

從上面的程式碼塊可以知道RegistryDirectory直接呼叫的註冊中心的substribe()方法。我們以ZookeeperRegistry為例,檢視其方法doSubscribe()。

  public void doSubscribe(final URL url, final NotifyListener listener) {       
    ......
            } else {
                // 正常服務訂閱
                List<URL> urls = new ArrayList<>();
                for (String path : toCategoriesPath(url)) {
                    ConcurrentMap<NotifyListener, ChildListener> listeners = zkListeners.computeIfAbsent(url, k -> new ConcurrentHashMap<>());
                    //監聽,當目錄變更時,呼叫notify方法
                    ChildListener zkListener = listeners.computeIfAbsent(listener, k -> (parentPath, currentChilds) -> ZookeeperRegistry.this.notify(url, k, toUrlsWithEmpty(url, parentPath, currentChilds)));
                    zkClient.create(path, false);
                    List<String> children = zkClient.addChildListener(path, zkListener);
                    if (children != null) {
                        urls.addAll(toUrlsWithEmpty(url, path, children));
                    }
                }
                //訂閱節點後,要拉取節點的最新資料
                notify(url, listener, urls);
            }
        } catch (Throwable e) {
            throw new RpcException("Failed to subscribe " + url + " to zookeeper " + getUrl() + ", cause: " + e.getMessage(), e);
        }
    }

從上面程式碼,可以知道就是監聽zookeeper上的providers,routers,configurations節點,並註冊其監聽器。當訂閱完這些節點後,需要重新拉取最新的提供者資料,即呼叫其notify()方法。

notify方法的作用

notify()方法最終會呼叫RegistryDirectory的notify()方法。該方法的主要完成如下內容:
1、得到zookeeper的configurations節點下的URLS,並轉化為configurators
2、得到zookeeper的routers節點下的URLS,並轉化為Routers
3、啟用3.x的AddressListener特性
4、得到zookeeper的providers節點下的URLS,與其服務提供者建立TCP連結,把URL轉化為 invoker

我們主要來看下第四點,其方法為refreshOverrideAndInvoker()。

 private void refreshOverrideAndInvoker(List<URL> urls) {
        // mock zookeeper://xxx?mock=return null
        overrideDirectoryUrl();
        refreshInvoker(urls);
    }

該方法主要是2個操作,1個是如果必要的話,重新覆蓋訂閱的URL,因為dubbo的服務呼叫URL的一些配置,比如路由,mock可以在monitor中心進行動態修改。所以需要重新覆蓋本地的URL一些引數。2、是通過refreshInvoker()與服務端建立TCP連結。

/**
     *
     * 把提供者者的URL List 轉化為 Invoker Map結合,轉化規則如下:
     * Convert the invokerURL list to the Invoker Map. The rules of the conversion are as follows:
     * <ol>
     *
     * <li> If URL has been converted to invoker, it is no longer re-referenced and obtained directly from the cache,
     * and notice that any parameter changes in the URL will be re-referenced.</li>
     * 如果URL已經在快取中,則不用重新引用該服務提供者(即重新建立TCP連線),如果URL的引數變更需要重新引用。
     *
     *
     * <li>If the incoming invoker list is not empty, it means that it is the latest invoker list.</li>
     *  如果傳入的呼叫程式列表不是空的,這意味著它是最新的呼叫程式列表
     *

     * <li>If the list of incoming invokerUrl is empty, It means that the rule is only a override rule or a route
     * rule, which needs to be re-contrasted to decide whether to re-reference.</li>
     * </ol>
     * 如果傳入的invokerUrl列表為空,則意味著該規則只是一個覆蓋規則或路由規則,需要對其進行重新對比以決定是否重新引用
     *
     * @param invokerUrls this parameter can't be null
     */
    // TODO: 2017/8/31 FIXME The thread pool should be used to refresh the address, otherwise the task may be accumulated.
    private void refreshInvoker(List<URL> invokerUrls) {
        Assert.notNull(invokerUrls, "invokerUrls should not be null");


        //如果只有一個提供者,且為空協議,則禁止連結和銷燬invoker
        if (invokerUrls.size() == 1
                && invokerUrls.get(0) != null
                && EMPTY_PROTOCOL.equals(invokerUrls.get(0).getProtocol())) {

            this.forbidden = true; // Forbid to access
            this.invokers = Collections.emptyList();
            routerChain.setInvokers(this.invokers);
            destroyAllInvokers(); // Close all invokers
        } else {
            this.forbidden = false; // Allow to access
            Map<String, Invoker<T>> oldUrlInvokerMap = this.urlInvokerMap; // local reference
            if (invokerUrls == Collections.<URL>emptyList()) {
                invokerUrls = new ArrayList<>();
            }
            if (invokerUrls.isEmpty() && this.cachedInvokerUrls != null) {
                invokerUrls.addAll(this.cachedInvokerUrls);
            } else {
                this.cachedInvokerUrls = new HashSet<>();
                this.cachedInvokerUrls.addAll(invokerUrls);//Cached invoker urls, convenient for comparison
            }
            if (invokerUrls.isEmpty()) {
                return;
            }
            Map<String, Invoker<T>> newUrlInvokerMap = toInvokers(invokerUrls);// Translate url list to Invoker map

            /**
             * If the calculation is wrong, it is not processed.
             *
             * 1. The protocol configured by the client is inconsistent with the protocol of the server.
             *    eg: consumer protocol = dubbo, provider only has other protocol services(rest).
             * 2. The registration center is not robust and pushes illegal specification data.
             *
             */
            if (CollectionUtils.isEmptyMap(newUrlInvokerMap)) {
                logger.error(new IllegalStateException("urls to invokers error .invokerUrls.size :" + invokerUrls.size() + ", invoker.size :0. urls :" + invokerUrls
                        .toString()));
                return;
            }

            List<Invoker<T>> newInvokers = Collections.unmodifiableList(new ArrayList<>(newUrlInvokerMap.values()));
            // pre-route and build cache, notice that route cache should build on original Invoker list.
            // toMergeMethodInvokerMap() will wrap some invokers having different groups, those wrapped invokers not should be routed.
            routerChain.setInvokers(newInvokers);
            this.invokers = multiGroup ? toMergeInvokerList(newInvokers) : newInvokers;
            this.urlInvokerMap = newUrlInvokerMap;

            try {
                destroyUnusedInvokers(oldUrlInvokerMap, newUrlInvokerMap); // Close the unused Invoker
            } catch (Exception e) {
                logger.warn("destroyUnusedInvokers error. ", e);
            }
        }
    }
 private Map<String, Invoker<T>> toInvokers(List<URL> urls) {
        Map<String, Invoker<T>> newUrlInvokerMap = new HashMap<>();
        if (urls == null || urls.isEmpty()) {
            return newUrlInvokerMap;
        }
        Set<String> keys = new HashSet<>();
        //目前的協議
        String queryProtocols = this.queryMap.get(PROTOCOL_KEY);

        //服務提供方URLproviderUrl ,看這些提供方是否支援目前協議
        for (URL providerUrl : urls) {
            // If protocol is configured at the reference side, only the matching protocol is selected
            if (queryProtocols != null && queryProtocols.length() > 0) {
                boolean accept = false;
                String[] acceptProtocols = queryProtocols.split(",");
                for (String acceptProtocol : acceptProtocols) {
                    if (providerUrl.getProtocol().equals(acceptProtocol)) {
                        accept = true;
                        break;
                    }
                }
                if (!accept) {
                    continue;
                }
            }

            //空協議過濾
            if (EMPTY_PROTOCOL.equals(providerUrl.getProtocol())) {
                continue;
            }

            //沒有在Spi框架找不大的擴充套件點,過濾
            if (!ExtensionLoader.getExtensionLoader(Protocol.class).hasExtension(providerUrl.getProtocol())) {
                logger.error(new IllegalStateException("Unsupported protocol " + providerUrl.getProtocol() +
                        " in notified url: " + providerUrl + " from registry " + getUrl().getAddress() +
                        " to consumer " + NetUtils.getLocalHost() + ", supported protocol: " +
                        ExtensionLoader.getExtensionLoader(Protocol.class).getSupportedExtensions()));
                continue;
            }

            // 合併url,一般引數配置可能配置在消費端,提供端,需要進行合併。合併規則為:override(配置中心) > -D(啟動執行指定) >Consumer(消費端) > Provider(提供方)
            URL url = mergeUrl(providerUrl);


            String key = url.toFullString(); // The parameter urls are sorted
            if (keys.contains(key)) { // Repeated url
                continue;
            }
            keys.add(key);
            // Cache key is url that does not merge with consumer side parameters, regardless of how the consumer combines parameters, if the server url changes, then refer again
            /**
             *key:是沒有合併消費端端配置引數的Url(provider端),
             * 快取鍵是不與使用者端引數合併的url,無論使用者如何合併引數,如果伺服器url更改,則再次引用
             *
             *
             */
            Map<String, Invoker<T>> localUrlInvokerMap = this.urlInvokerMap; // local reference
            Invoker<T> invoker = localUrlInvokerMap == null ? null : localUrlInvokerMap.get(key);
            if (invoker == null) {// 看本地快取是否存在,如果存在// Not in the cache, refer again
                try {
                    boolean enabled = true;
                    if (url.hasParameter(DISABLED_KEY)) {//是否disable
                        enabled = !url.getParameter(DISABLED_KEY, false);
                    } else {
                        enabled = url.getParameter(ENABLED_KEY, true);
                    }
                    if (enabled) {
                        /**
                         * 把rpc invoker 、mergeUrl(override > -D >Consumer > Provider 引數內容),原providerUrl
                         * url: getProtocol()=dubbo
                         */
                        invoker = new InvokerDelegate<>(protocol.refer(serviceType, url), url, providerUrl);
                    }
                } catch (Throwable t) {
                    logger.error("Failed to refer invoker for interface:" + serviceType + ",url:(" + url + ")" + t.getMessage(), t);
                }
                if (invoker != null) { // Put new invoker in cache
                    newUrlInvokerMap.put(key, invoker);
                }
            } else {
                newUrlInvokerMap.put(key, invoker);
            }
        }
        keys.clear();
        return newUrlInvokerMap;
    }

上面的註釋已經非常清楚,不在詳細講解。最後所以提供者的URL,會被轉化為InvokerDelegate。該類代表一個Invoker物件的委派類,裡面包括真實的Invoker和相應的提供者的URL。並把這些InvokerDelegate放入到newUrlInvokerMap成員變數上。

Protocol.refer()的博大精深

來看下如下這條語句:
invoker = new InvokerDelegate<>(protocol.refer(serviceType, url), url, providerUrl);
又是通過protocol.refer(serviceType, url)獲取一個Invoker,此時URL的getProtocol()==dubbo,所以會呼叫DubboProtocol#refer()方法。而DubboProtocol的refer()還是AbstractProtocol#refer()。

   public <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException {
        /**
         * 非同步轉同步Invoker
         */
        return new AsyncToSyncInvoker<>(protocolBindingRefer(type, url));
    }

即返回一個非同步轉同步的AsyncToSyncInvoker。DubboProtocol實現模板方法protocolBindingRefer()。

   public <T> Invoker<T> protocolBindingRefer(Class<T> serviceType, URL url) throws RpcException {
        // 優化序列化內容,目前沒什麼內容
        optimizeSerialization(url);

        // create rpc invoker.
        /**
         *
         * 建立RPC DubboInvoker
         */
        DubboInvoker<T> invoker = new DubboInvoker<T>(serviceType, url, getClients(url), invokers);
        invokers.add(invoker);

        return invoker;
    }

該方法主要建立了DubboInvoker物件,並放入invokers中,通過getClients()方法得到具體的TCP連線客戶端ExchangeClient。

總結

從上面的分析可以知道,服務訂閱的過程,服務拉取的方式是通過通知這種方式來獲取,並且知道了Invoker的具體一個實現DubboInvoker和TCP連線客戶端ExchangeClient進行關聯的,在下一章我們將剖析ExchangeClient是如何實現的。

相關文章