Dubbo架構設計與原始碼解析(二) 服務註冊

2023-01-06 12:00:20

作者:黃金

一、Dubbo簡介

Dubbo是一款典型的高擴充套件、高效能、高可用的RPC微服務架構,用於解決微服務架構下的服務治理與通訊問題。其核心模組包含 【RPC通訊】【服務治理】 ,其中服務治理又分為服務註冊與發現、服務容錯、負載均衡、流量排程等。今天將重點介紹Dubbo的服務註冊與發現

二、SPI機制

在介紹服務註冊發現之前,先簡單介紹一下貫穿整個Dubbo原始碼,也是Dubbo實現自適應擴充套件的核心--SPI機制,下圖為Dubbo SPI實現的簡單類圖。

1、Dubbo SPI原理:通過讀取相應的組態檔找到具體實現類,然後通過以下兩種方式範例化物件:(1)通過自適應動態位元組碼編譯技術,生成相應的動態代理類,(2)利用反射機制實現範例化。相較於Java SPI,Dubbo SPI實現了內部的IoC和Aop

2、Dubbo SPI 優點:(1)高擴充套件:使用者可以根據實際業務需求擴充套件相應的實現模組,包含位元組碼編譯技術、rpc協定、通訊方式、註冊方式等,(2)解耦: 通過封裝SPI呼叫機制,架構上實現了上層應用與底層邏輯之間的解耦,為高擴充套件提供了支撐條件

3、Dubbo SPI 常用樣例(以getExtension和getAdaptiveExtension為例)

組態檔內容
test1=com.dubbo.demo.service.TestServiceimpl
test2=com.dubbo.demo.service.TestServiceImpl2

一、通過getExtension方法生成範例
    ExtensionLoader<TestService> extensionLoader = ExtensionLoader.getExtensionLoader(TestService.class);
    TestService t1 = extensionLoader.getExtension("test1");
    TestService t2 = extensionLoader.getExtension("test2");
   
二、通過getAdaptiveExtension生成範例(方法中需要@Adaptive註解,引數會對URL校驗)
    TestService testService = ExtensionLoader.getExtensionLoader(TestService.class).getAdaptiveExtension();
    URL url = new URL("test", "localhost", 8080, new String[]{"test.service", "test1"});
    testService.sayHello("bbb", url);

呼叫getAdaptiveExtension方法最終會生成相應的代理類,最終生成的代理類會根據URL引數裡面的protocol決定(以內部Protocol為例)

三、服務註冊

1、服務註冊流程

2、服務註冊類圖詳解

3、服務註冊步驟

(1)步驟一:初始化設定(類圖:抽象Config與初始化設定)

首先需要範例化ServiceConfig範例,宣告「註冊介面、介面範例、註冊中心設定」,其中「ServiceBean」是實現Spring與Dubbo整合的橋樑。然後會由DubboBootstrap呼叫initialize方法實現configManager和Environment的初始化,其中就包括將ServiceConfig中的設定轉換成內部封裝的協定(ApplicationModel、ProviderModel等)

private static void startWithExport() throws InterruptedException {
    //初始化設定
    ServiceConfig<DemoServiceImpl> service = new ServiceConfig<>();
    service.setInterface(DemoService.class);
    service.setRef(new DemoServiceImpl());
    service.setApplication(new ApplicationConfig("dubbo-demo-api-provider"));
    service.setRegistry(new RegistryConfig("zookeeper://127.0.0.1:2181"));
    //服務註冊入口
    service.export();
}
public synchronized void export() {
    if (bootstrap == null) {
        bootstrap = DubboBootstrap.getInstance();
        // compatible with api call.
        if (null != this.getRegistry()) {
            bootstrap.registries(this.getRegistries());
        }
        //初始化設定()
        bootstrap.initialize();
    }
    ......        
    if (shouldDelay()) {
        DELAY_EXPORT_EXECUTOR.schedule(this::doExport, getDelay(), TimeUnit.MILLISECONDS);
    } else {
        //服務註冊
        doExport();
    }

    exported();
 }

(2)步驟二:組裝URL

根據初始化設定組轉註冊介面服務的URL。其中URL也是Dubbo內部通過@Adaptive註解實現SPI的核心,通過修改URL的頭部協定(如:register、dubbo、injvm等),在呼叫

private static final Protocol PROTOCOL = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();
PROTOCOL.export(wrapperInvoker)

該方法的時候,會根據不同的協定切換不通的實現類,實現了Dubbo技術架構與業務邏輯的解耦。

private void doExportUrls() {
    //組裝後的URL格式樣例
    //registry://127.0.0.1:2181/org.apache.dubbo.registry.RegistryService?application=dubbo-demo-api-provider&dubbo=2.0.2&pid=26212®istry=zookeeper×tamp=1663049763199
    List<URL> registryURLs = ConfigValidationUtils.loadRegistries(this, true);

    int protocolConfigNum = protocols.size();
    for (ProtocolConfig protocolConfig : protocols) {
        //組裝pathKey : org.apache.dubbo.demo.DemoService
        String pathKey = URL.buildKey(getContextPath(protocolConfig)
                .map(p -> p + "/" + path)
                .orElse(path), group, version);
        //儲存介面服務
        repository.registerService(pathKey, interfaceClass);
        //服務註冊
        doExportUrlsFor1Protocol(protocolConfig, registryURLs, protocolConfigNum);
    }
}

(3)步驟三:Invoker封裝(類圖:Ref -> Invoker)

通過內建的動態位元組碼編譯(預設javassist)生成Invoker代理類,然後通過反射機制生成Wrapper範例。其中Invoker是Dubbo的核心模型,Invoker是Dubbo中的實體域,也就是真實存在的。其他模型都向它靠攏或轉換成它

private void doExportUrlsFor1Protocol(ProtocolConfig protocolConfig, List<URL> registryURLs, int protocolConfigNum) {
    ......
    //組裝新的URL
    //dubbo://2.0.0.1:20880/org.apache.dubbo.demo.DemoService?anyhost=true&application=dubbo-demo-api-provider&bind.ip=2.0.0.1&bind.port=20880&default=true&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=org.apache.dubbo.demo.DemoService&methods=sayHello,sayHelloAsync&pid=46528&release=&service.name=ServiceBean:/org.apache.dubbo.demo.DemoService&side=provider×tamp=1663051456562
    URL url = new URL(name, host, port, getContextPath(protocolConfig).map(p -> p + "/" + path).orElse(path), map);
    ......
    //Invoker封裝
    Invoker<?> invoker = PROXY_FACTORY.getInvoker(ref, (Class) interfaceClass,
             registryURL.addParameterAndEncoded(EXPORT_KEY, url.toFullString()));
    //wrapper
    DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);

    //服務註冊(此時URL頭部協定變成了register,實際會呼叫RegistryProtocol)
    Exporter<?> exporter = PROTOCOL.export(wrapperInvoker);
    exporters.add(exporter);
}

# PROXY_FACTORY
public <T> Invoker<T> getInvoker(T proxy, Class<T> type, URL url) {
    // 動態代理類生成,反射生成範例
    final Wrapper wrapper = Wrapper.getWrapper(proxy.getClass().getName().indexOf('$') < 0 ? proxy.getClass() : type);
    return new AbstractProxyInvoker<T>(proxy, type, url) {
        @Override
        protected Object doInvoke(T proxy, String methodName,
                                  Class<?>[] parameterTypes,
                                  Object[] arguments) throws Throwable {
            return wrapper.invokeMethod(proxy, methodName, parameterTypes, arguments);
        }
    };
}

(4)步驟四:Exporter封裝(類圖:Invoker-> Exporter)

此時會依次呼叫RegistryProtocol 、DubboProtocol 將Invoker封裝成Exporter,並將封裝後的Exporter儲存到本地map中(類似於spring bean)。然後會呼叫底層通訊服務(預設netty)進行埠監聽,此時會通過責任鏈模式封裝Exchanger與Transporter,用於處理網路傳輸訊息的編碼/解碼。

# RegistryProtocol : export
public <T> Exporter<T> export(final Invoker<T> originInvoker) throws RpcException {
    ......
    //此時URL頭部協定已變成dubbo
    //dubbo://2.0.0.1:20880/org.apache.dubbo.demo.DemoService?anyhost=true&application=dubbo-demo-api-provider&bind.ip=2.0.0.1&bind.port=20880&default=true&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=org.apache.dubbo.demo.DemoService&methods=sayHello,sayHelloAsync&pid=56036&release=&service.name=ServiceBean:/org.apache.dubbo.demo.DemoService&side=provider×tamp=1663052353098
    providerUrl = overrideUrlWithConfig(providerUrl, overrideSubscribeListener);
    // export invoker
    final ExporterChangeableWrapper<T> exporter = doLocalExport(originInvoker, providerUrl);

    // 此時Registry範例預設是ZookeeperRegistry
    final Registry registry = getRegistry(originInvoker);

    final URL registeredProviderUrl = getUrlToRegistry(providerUrl, registryUrl);
    
    // decide if we need to delay publish
    boolean register = providerUrl.getParameter(REGISTER_KEY, true);
    if (register) {
        //底層呼叫ZK,建立node節點
        registry.register(registeredProviderUrl);
    }
    ....
}

# RegistryProtocol : doLocalExport
private <T> ExporterChangeableWrapper<T> doLocalExport(final Invoker<T> originInvoker, URL providerUrl) {
    String key = getCacheKey(originInvoker);

    return (ExporterChangeableWrapper<T>) bounds.computeIfAbsent(key, s -> {
        Invoker<?> invokerDelegate = new InvokerDelegate<>(originInvoker, providerUrl);
        //此時會呼叫DubboProtocol進行exporter封裝
        return new ExporterChangeableWrapper<>((Exporter<T>) protocol.export(invokerDelegate), originInvoker);
    });
}
# DubboProtocol : export
public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException { 
    ......
    // export service.
    String key = serviceKey(url);
    //exporter封裝
    DubboExporter<T> exporter = new DubboExporter<T>(invoker, key, exporterMap);
    exporterMap.put(key, exporter);
    ......
    //開啟服務監聽
    openServer(url);
    optimizeSerialization(url);
    
    return exporter;
}

(5)步驟五:註冊服務節點

封裝Exporter並開啟伺服器埠監聽後,會呼叫註冊中心(預設Zookeeper)註冊服務節點資訊

# RegistryProtocol : export
public <T> Exporter<T> export(final Invoker<T> originInvoker) throws RpcException {
    ......
    //此時URL頭部協定已變成dubbo
    //dubbo://2.0.0.1:20880/org.apache.dubbo.demo.DemoService?anyhost=true&application=dubbo-demo-api-provider&bind.ip=2.0.0.1&bind.port=20880&default=true&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=org.apache.dubbo.demo.DemoService&methods=sayHello,sayHelloAsync&pid=56036&release=&service.name=ServiceBean:/org.apache.dubbo.demo.DemoService&side=provider×tamp=1663052353098
    providerUrl = overrideUrlWithConfig(providerUrl, overrideSubscribeListener);
    // export invoker
    final ExporterChangeableWrapper<T> exporter = doLocalExport(originInvoker, providerUrl);

    // 此時Registry範例預設是ZookeeperRegistry
    final Registry registry = getRegistry(originInvoker);

    final URL registeredProviderUrl = getUrlToRegistry(providerUrl, registryUrl);
    
    // decide if we need to delay publish
    boolean register = providerUrl.getParameter(REGISTER_KEY, true);
    if (register) {
        //底層呼叫ZK,建立node節點
        registry.register(registeredProviderUrl);
    }
    ....
}

四、總結

至此,Dubbo服務註冊的整體流程已大致結束,文中如有不當或者錯誤觀點,歡迎大家評論區指出。感興趣的同學,可以關注後續「Dubbo架構設計與原始碼解析」系列的文章。