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

wsMrLin發表於2020-08-09

引導

dubbo的服務的註冊與發現,需要通過第三方註冊中心來協助完成,目前dubbo支援的註冊中心包括 zookeeper,consul,etcd3,eureka,nacas,redis,sofa。這些註冊中心的不同支援在之後的篇章進行分享。

基礎鋪墊

在鋪墊一些基礎內容之前,根據如果下幾個問題來進行回答,或許能更好的闡明dubbo的實現服務的註冊和發現的實現過程。
1、dubbo是在什麼時機與註冊中心建立連線。
2、dubbo服務註冊和匯出的時機在什麼時候。
3、dubbo服務的訂閱時機是在什麼時候。
4、dubbo服務的上下線是如何通知訂閱者的。
5、dubbo是如何把這些各種第三方註冊中心進行整合的。
為了回答上面的五個問題,我們一起去從dubbo的原始碼探尋答案,這些問題和服務的註冊有關,那麼首先我們需要的就是去dubbo-registry這個原始碼模組去查詢。

基礎資料結構

1、dubbo 還是通過SPI技術,根據引數URL來動態選擇不同的註冊中心。

@SPI("dubbo")
public interface RegistryFactory {
    @Adaptive({"protocol"})
    Registry getRegistry(URL url);
}

RegistryFactory 就是產生一個註冊中心的工程,它有個自適應的方法getRegistry,那麼我們知道dubbo會通過javassist動態產生一個RegistryFactory$Adaptive類,並且getRegistry方法的內部實現大致是如下:

public class RegistryFactory$Adaptive implements RegistryFactory {
    @Override
    public Registry getRegistry(URL url) {
        if (url == null) throw new IllegalArgumentException("url == null");
        String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol());
        if (extName == null)
            throw new IllegalStateException("Failed to get extension (org.apache.dubbo.registry.RegistryFactory) " +
                    "name from url (" + url.toString() + ") use keys([protocol])");
        RegistryFactory extension = ExtensionLoader.getExtensionLoader(RegistryFactory.class).getExtension(extName);
        return extension.getRegistry(url);
    }
}

它通過傳入的URL的protocol協議欄位排判斷是什麼型別註冊中心。例如,url的protocol的協議是zookeeper,那麼就會根據SPI的ExtensionLoader.getExtensionLoader(RegistryFactory.class).getExtension("zookeeper")得到一個產生ZooKeeper註冊中心的工廠,也就是ZookeeperRegistryFactory,而ZookeeperRegistryFactory這個類的getRegistry就是返回一個Zookeeper註冊中心。

2、Registry 介面是所有註冊中心的抽象。它的類關係圖如下:

可以看出其語義,一個註冊中心Registry是一個節點(extends Node),並且它具有註冊服務(extends RegistryService)的功能。
dubbo支援如下這些註冊中心zookeeper,consul,etcd3,eureka,nacas,redis,sofa,那麼就會產生相應如下的Registry:
ZookeeperRegistry,ConsulRegistry,EtcdRegistry,NacosRegistry,RedisRegistry,SofaRegistry。類圖如下:

所以我們知道,這些註冊中心都是繼承FailbackRegistry,這個FailbackRegistry其意思就是說,如果一個服務註冊到當前某個註冊中心註冊失敗後,可會在後臺產生一個daemon執行緒,定時的把註冊失敗服務重新註冊,並且有一定的重試限制。
在上面的類圖中我們並沒有發現有個名為EurekaRegistry這樣的類,因為實現了另一個介面ServiceDiscovery方式,類名為EurekaServiceDiscovery來進行服務發現。這些不同的註冊中心的實現方式,會在下一個章節去討論它。

3、RegistryProtocol

dubbo的協議是通過名為org.apache.dubbo.rpc.Protocol來進行抽象的,那麼註冊協議也是一樣的,是通過org.apache.dubbo.registry.integration.RegistryProtocol來表達的,繼承org.apache.dubbo.rpc.Protocol。RegistryPrtocol是擴充套件點Protocol的具體實現,在Dubbo系列之 (一)SPI擴充套件文章中提到,會一次呼叫其setter方法來注入其需要的屬性,RegistryPrtocol其中有個屬性就是RegistryFactory,那麼就要為它注入一個具體的RegistryFactory,那麼這個具體的RegistryFactory工廠是什麼型別,答案就是上面的RegistryFactory$Adaptive。為什麼?因為在Dubbo系列之 (一)SPI擴充套件中提到了注入的屬性物件會從SpringExtensionFactory和SpiExtensionFactory工廠中查詢,剛好RegistryFactory也是一個擴充套件點,所以會在SpiExtensionFactory找出,並且SpiExtensionFactory工廠的實現如下:

public class SpiExtensionFactory implements ExtensionFactory {

    @Override
    public <T> T getExtension(Class<T> type, String name) {
        if (type.isInterface() && type.isAnnotationPresent(SPI.class)) {
            ExtensionLoader<T> loader = ExtensionLoader.getExtensionLoader(type);
            if (!loader.getSupportedExtensions().isEmpty()) {
                return loader.getAdaptiveExtension();
            }
        }
        return null;
    }

}

所以知道是返回一個自適應的擴充套件點,即RegistryFactory$Adaptive。
Protocol協議具有匯出服務export()功能,和引用服務refer()功能。在RegistryProtocol中,在這個2個方法內就有對服務註冊到註冊中心的操作。

4、服務匯出

在服務匯出中,首先要有一個認知,這個認知會在後續章節中進行詳細的介紹,先開始知道有這麼一件事情即可,我們做dubbo服務暴露的時候,我們有2中方式,一種是通過註解的方式:
@DubboService,@Service(非spring的)。或者通過xml的方式<dubbo:service />。
不管採用哪一種方式,最終需要暴露的服務首先會包裝成一個ServiceBean的物件。這個ServiceBean 持有具體需要服務註冊的物件ref。ServiceBean的類圖如下:

服務匯出也是是一個繁瑣的過程,所以在後面的章節進行詳細的探討,本章我們只要知道其服務匯出引入與註冊中心互動。

5、dubbo啟動引導其服務匯出。

DubboBootstrap是一個dubbo框架啟動的幫助類,他有一個start()方法,在該方法的內部就會呼叫exportServices()用於匯出服務,和呼叫referServices()進行引用服務。那麼DubboBootstrap的start()方法是被誰呼叫?

一般使用dubbo框架的都會引入Spring框架,Spring框架有一個事件監聽機制,dubbo正是監聽Spring的上下文重新整理事件ContextRefreshedEvent,來啟動Dubbo服務的。這個服務監聽類就是DubboBootstrapApplicationListener。

DubboBootstrapApplicationListener是如何註冊到Spring中的呢?

1、如果是通過註解@DubboService,就是通過ServiceClassPostProcessor類,該類是實現了Spring的BeanDefinitionRegistryPostProcessor。所以通過registerBeans進行註冊。在@EnableDubbo註解上有一個@DubboComponentScan註解,該註解上的@export註解就會匯入DubboComponentScanRegistrar類,在該類中完成DubboBootstrapApplicationListener的註冊。
2、如果是通過<dubbo:service />的方式,我們知道Spring對於自定義的標籤,需要自已提供一個NamespaceHanlder的實現類來協助解析自定義標籤。而dubbo的NamespaceHanlder實現類為DubboNamespaceHandler。DubboNamespaceHandler該類就有該監聽器的注入。
並且classpath下的META-INF下新增spring.hanlders和spring.schemes。內容如下:
spring.shemes :
http://dubbo.apache.org/schema/dubbo/dubbo.xsd=META-INF/dubbo.xsd
說明dubbo的名稱空間檔案位置
spring.handler:
http://dubbo.apache.org/schema/dubbo=org.apache.dubbo.config.spring.schema.DubboNamespaceHandler
說明處理該名稱空間下的自定義標籤通過DubboNamespaceHandler.
3、具體的Spring 自定義標籤運用可以參考Netty自娛自樂之類Dubbo RPC 框架設計構想 【上篇】

特徵測試

在經過以上的基礎鋪墊之後,我們對Registry和RegistryProtocol協議進行測試。
本章主要主要的關注點在註冊上,把目光移到RegistryProtocol的registry方法上。

private void register(URL registryUrl, URL registeredProviderUrl) {
        Registry registry = registryFactory.getRegistry(registryUrl);
        registry.register(registeredProviderUrl);
}

其中,
registryUrl 為註冊的URL,例如:zookeeper://127.0.0.1:2181/org.apache.dubbo.registry.RegistryService?application=dubbo-demo-annotation-provider&dubbo=2.0.2&export=dubbo://192.168.0.105:20880/org.apache.dubbo.demo.DemoService?anyhost=true&application=dubbo-demo-annotation-provider&bind.ip=192.168.0.105&bind.port=20880&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=org.apache.dubbo.demo.DemoService&metadata-type=remote&methods=sayHello,sayHelloAsync&pid=9990&release=&side=provider&timestamp=1596943034484&pid=9990&registry_protocol=zookeeper&timestamp=1596943034477

registeredProviderUrl 為服務提供者需要被註冊的URL。例如:dubbo://192.168.0.105:20880/org.apache.dubbo.demo.DemoService?anyhost=true&application=dubbo-demo-annotation-provider&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=org.apache.dubbo.demo.DemoService&metadata-type=remote&methods=sayHello,sayHelloAsync&pid=9990&release=&side=provider&timestamp=1596943034484

從上面的樣例可以知道,registeredProviderUrl就是registryUrl 中引數export中的值。

test1

@Test
    public  void testRegistry(){
        // 根據SPI 獲取RegistryFactory 自適應註冊工廠
        RegistryFactory registryFactory = ExtensionLoader.getExtensionLoader(RegistryFactory.class).getAdaptiveExtension();

        //通過url.getProtocol 和registryFactory得到 zookeeper註冊中心
        URL registryUrl=URL.valueOf("zookeeper://127.0.0.1:2181/org.apache.dubbo.registry.RegistryService?application=dubbo-demo-annotation-provider&dubbo=2.0.2&export=dubbo://192.168.0.105:20880/org.apache.dubbo.demo.DemoService?anyhost=true&application=dubbo-demo-annotation-provider&bind.ip=192.168.0.105&bind.port=20880&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=org.apache.dubbo.demo.DemoService&metadata-type=remote&methods=sayHello,sayHelloAsync&pid=9990&release=&side=provider&timestamp=1596943034484&pid=9990&registry_protocol=zookeeper&timestamp=1596943034477");
        Registry zookeeperRegistry = registryFactory.getRegistry(registryUrl);

        //根據zookeeperRegistry註冊中心註冊,需要的服務providerRegistryURL
        URL providerRegistryURL=URL.valueOf("dubbo://192.168.0.105:20880/org.apache.dubbo.demo.DemoService?anyhost=true&application=dubbo-demo-annotation-provider&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=org.apache.dubbo.demo.DemoService&metadata-type=remote&methods=sayHello,sayHelloAsync&pid=9990&release=&side=provider&timestamp=1596943034484");
        zookeeperRegistry.register(providerRegistryURL);
    }

原始碼跟蹤

registry方法定位到FailbackRegistry,主要作用當服務註冊失敗後,可以在後端執行緒重試。

  public void register(URL url) {
        // 判斷該註冊中心能接受的協議
        if (!acceptable(url)) {
            logger.info("URL " + url + " will not be registered to Registry. Registry " + url + " does not accept service of this protocol type.");
            return;
        }
        // 呼叫AbstractRegistry的register(),主要是吧註冊的URL放入registered集合中,說明該URL已經要被註冊
        super.register(url);
        removeFailedRegistered(url); // 當前URL需要被註冊,所以把它從註冊失敗列表裡移除,因為可能是重試註冊。
        removeFailedUnregistered(url); // 當前URL需要被註冊,所以把它從登出失敗列表裡移除,因為可能是重試註冊。
        try {
            //呼叫子類的具體doRegister,模板方法
            doRegister(url);
        } catch (Exception e) {
            Throwable t = e;

            // If the startup detection is opened, the Exception is thrown directly.
            // 檢視是否check欄位是否設定為true.
            boolean check = getUrl().getParameter(Constants.CHECK_KEY, true)
                    && url.getParameter(Constants.CHECK_KEY, true)
                    && !CONSUMER_PROTOCOL.equals(url.getProtocol());
            boolean skipFailback = t instanceof SkipFailbackWrapperException;
            //如果需要嚴格檢測的話,直接拋異常
            if (check || skipFailback) {
                if (skipFailback) {
                    t = t.getCause();
                }
                throw new IllegalStateException("Failed to register " + url + " to registry " + getUrl().getAddress() + ", cause: " + t.getMessage(), t);
            } else {
                logger.error("Failed to register " + url + ", waiting for retry, cause: " + t.getMessage(), t);
            }

            // 否則把註冊失敗的URL 新增到failedRegistered,註冊失敗列表
            addFailedRegistered(url);
        }
    }
   private void addFailedRegistered(URL url) {
        //獲取該註冊URL是否已經存在在註冊失敗列表裡,存在直接返回
        FailedRegisteredTask oldOne = failedRegistered.get(url);
        if (oldOne != null) {
            return;
        }
        // 否則建立一個失敗註冊重試任務FailedRegisteredTask,放入failedRegistered中。
        FailedRegisteredTask newTask = new FailedRegisteredTask(url, this);
        oldOne = failedRegistered.putIfAbsent(url, newTask);
        if (oldOne == null) {
            // 然後把該失敗註冊任務放入daemon執行緒retryTimer,定式重新註冊
            retryTimer.newTimeout(newTask, retryPeriod, TimeUnit.MILLISECONDS);
        }
    }

由於章節篇幅限時,具體的doRegistry方法在後面章節分享。在下一個章節詳細分析AbstractRegistry 的作用和FailbackRegistry的重試機制,並且詳細剖析ZookeeperRegistry。

相關文章