1. 程式人生 > >[dubbo 原始碼之 ]1. 服務提供方如何釋出服務

[dubbo 原始碼之 ]1. 服務提供方如何釋出服務

服務釋出

啟動流程

1.ServiceConfig#export

服務提供方在啟動部署時,dubbo會呼叫ServiceConfig#export來啟用服務釋出流程,如下所示:

  • Java API:
// 1. 建立ServiceConfig例項
            ServiceConfig<IGreetingService> serviceConfig = new ServiceConfig<>();
            // 2. 設定應用程式配置
            serviceConfig.setApplication(new ApplicationConfig("deep-in-dubbo-first-provider"));
            // 3. 設定註冊中心
            RegistryConfig registryConfig = new RegistryConfig("zookeeper://127.0.0.1:2181/");
            serviceConfig.setRegistry(registryConfig);
            // 4. 設定介面和實現類
            // 5. 設定服務分組和版本
            // dubbo中,服務介面+服務分組+服務版本 唯一的確定一個服務,同一個介面可以有不同版本,方便維護升級
            serviceConfig.setInterface(IGreetingService.class);
            serviceConfig.setRef(new GreetingServiceImpl());
            serviceConfig.setVersion("1.0.0");
            serviceConfig.setGroup("dubbo-sxzhongf-group");
            RpcContext.getContext().setAttachment("age","18");
    
            // 7. 匯出服務,啟動Netty監聽連結請求,並將服務註冊到註冊中心
            serviceConfig.export();
    
            // 8. 掛起執行緒,避免服務停止
            System.out.println("api provider service is started...");
            System.in.read();
  • XML
  <beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"
         xmlns="http://www.springframework.org/schema/beans"
         xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
         http://dubbo.apache.org/schema/dubbo http://dubbo.apache.org/schema/dubbo/dubbo.xsd">
  
      <!-- provider's application name, used for tracing dependency relationship -->
      <dubbo:application name="first-xml-provider"/>
      <!-- use multicast registry center to export service -->
      <dubbo:registry address="zookeeper://127.0.0.1:2181/"/>
      <!-- use dubbo protocol to export service on port 20880 -->
      <dubbo:protocol name="dubbo" port="20880"/>
      <!-- service implementation, as same as regular local bean -->
      <bean id="demoService" class="com.sxzhongf.deep.in.dubbo.provider.service.impl.GreetingServiceImpl"/>
      <!-- declare the service interface to be exported -->
      <dubbo:service interface="com.sxzhongf.deep.in.dubbo.api.service.IGreetingService"
                     ref="demoService" version="1.0.0" group="dubbo-sxzhongf-group">
          <dubbo:method name="sayHello" async="false" timeout="0" retries="3"></dubbo:method>
          <dubbo:method name="testGeneric" async="false" timeout="10000" retries="3"></dubbo:method>
      </dubbo:service>
  </beans>

檢視export原始碼可知,總共有三種服務匯出選項:
java public synchronized void export() { //1. 是否匯出 if (!shouldExport()) { return; } ... //2.延遲匯出 if (shouldDelay()) { DELAY_EXPORT_EXECUTOR.schedule(this::doExport, getDelay(), TimeUnit.MILLISECONDS); } else { //3.立刻匯出 doExport(); } }

2.ServiceConfig#doExport

此方法主要是根據設定的屬性進行合法性檢查,主要包含是否已被匯出,doExportUrls();

3.doExportUrls
4.ConfigValidationUtils#loadRegistries

此方法用來載入所有的服務註冊中心物件,在dubbo中,一個service可以被註冊到多個註冊中心。

通過doExportUrlsFor1Protocol(protocolConfig, registryURLs);

5.doExportUrlsFor1Protocol

在此方法中會將所有的引數封裝成org.apache.dubbo.common.URL物件,然後執行具體的服務匯出。

具體過程分為:

  • 1.解析MethodConfig配置(單獨的方法呼叫引數設定)

  • 2.泛型呼叫型別設定

  • 3.拼接URL引數

  • 4.匯出具體服務

    匯出又分為四種範圍(scope):

    • SCOPE_NONE = "none",如果設定為none,表示該服務不匯出。

    • SCOPE_LOCAL = "local" ,如果設定為local,表示該服務匯出到本地(injvm--偽協議,實現類為:org.apache.dubbo.rpc.protocol.injvm.InjvmProtocol

      • SCOPE_REMOTE = "remote",如果設定為remote,表示該服務匯出到遠端。
    • 如果有註冊中心,釋出到註冊中心

    • 如果沒有註冊中心,則表示服務是直連方式

    • dubbo-2.7.0開始,新增加了WritableMetadataService 來儲存dubbo 服務的元資料,元資料可以儲存在遠端配置中心和本地,預設是儲存在本地,通過設定:METADATA_KEY = "metadata"

      • DEFAULT_METADATA_STORAGE_TYPE = "local"
      • REMOTE_METADATA_STORAGE_TYPE = "remote"
        java /** * @since 2.7.0 * ServiceData Store */ WritableMetadataService metadataService = WritableMetadataService.getExtension(url.getParameter(METADATA_KEY, DEFAULT_METADATA_STORAGE_TYPE)); if (metadataService != null) { metadataService.publishServiceDefinition(url); }

      • 不設定,匯出到本地和遠端

    • 最終執行匯出的程式碼如下

      // 擴充套件適配類
      private static final Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();
      
      /**
       * A {@link ProxyFactory} implementation that will generate a exported service proxy,the JavassistProxyFactory is its
       * default implementation
       */
      // 擴充套件適配類
      private static final ProxyFactory PROXY_FACTORY = ExtensionLoader.getExtensionLoader(ProxyFactory.class).getAdaptiveExtension();
      ...
      
      Invoker<?> invoker = PROXY_FACTORY.getInvoker(ref, (Class) interfaceClass, registryURL.addParameterAndEncoded(EXPORT_KEY, url.toFullString()));
      DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);
      
      Exporter<?> exporter = protocol.export(wrapperInvoker);
      exporters.add(exporter);

      由於protocolPROXY_FACTORY都是擴充套件適配類,跟蹤程式碼我們可以發現:

      • 執行PROXY_FACTORY.getInvoker的時候實際上首先執行擴充套件介面ProxyFactory的適配類ProxyFactory$AdaptivegetInvoker方法,根據URL中引數proxy的設定型別選擇具體的代理工廠,預設使用的是javassist,,因此又呼叫了org.apache.dubbo.rpc.proxy.javassist.JavassistProxyFactory#getInvoker來獲取代理實現類,程式碼如下:

        /**
         * JavaassistRpcProxyFactory
         */
        public class JavassistProxyFactory extends AbstractProxyFactory {
            ...
            @Override
            public <T> Invoker<T> getInvoker(T proxy, Class<T> type, URL url) {
                // TODO Wrapper cannot handle this scenario correctly: the classname contains '$'
                // 這裡使用javassist動態代理生成serviceImpl實現類的包裝類`Wraaper...`
                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);
                    }
                };
            }
            ...
        }

        上面程式碼有2個目的:

        1. inal Wrapper wrapper = Wrapper.getWrapper(...);用來生成具體serviceImpl的包裝類,減少反射的效能損耗;
        2. return new AbstractProxyInvoker<T>... 返回了一個抽象的代理invoker,並且重寫了doInvoker方法,重寫之後使用包裝類中的invokeMethod來呼叫方法。

        經過上述2步,服務提供方就將具體的實現類轉換為Invoker代理。

      • 然後,當執行protocol.export(),實際上也是呼叫了Protocol$Adaptive#export()方法,同時也分為兩種情況

        • 如果為遠端暴露,則執行RegistryProtocol#export
        • 如果為本地暴露,則只需InjvmProtocol#export

        由於dubbo的增強SPI特性支援,injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));,則在呼叫之前會一層一層呼叫,ProtocolFilterWrapper->ProtocolListenerWrapper->QosProtocolWrapper,最後會呼叫export方法,此方法會將Invoker轉換為Exporter物件,在org.apache.dubbo.registry.integration.RegistryProtocol#export方法中,org.apache.dubbo.registry.integration.RegistryProtocol#doLocalExport方法啟NettyServer來監聽服務,org.apache.dubbo.registry.integration.RegistryProtocol#register將當前的服務註冊到註冊中心。

        • doLocalExport 是如何啟動NettyServer呢?

              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);
                      return new ExporterChangeableWrapper<>((Exporter<T>) protocol.export(invokerDelegate), originInvoker);
                  });
              }

          此時URL中的protocol型別為預設的dubbo,因此會執行DubboProtocol#export進行轉換,如下:

          @Override
              public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
                  URL url = invoker.getUrl();
          
                  // export service.
                  String key = serviceKey(url);
                  // invoker->exporter
                  DubboExporter<T> exporter = new DubboExporter<T>(invoker, key, exporterMap);
                  exporterMap.put(key, exporter);
          
                  //export an stub service for dispatching event
                  Boolean isStubSupportEvent = url.getParameter(STUB_EVENT_KEY, DEFAULT_STUB_EVENT);
                  Boolean isCallbackservice = url.getParameter(IS_CALLBACK_SERVICE, false);
                  if (isStubSupportEvent && !isCallbackservice) {
                      String stubServiceMethods = url.getParameter(STUB_EVENT_METHODS_KEY);
                      if (stubServiceMethods == null || stubServiceMethods.length() == 0) {
                          if (logger.isWarnEnabled()) {
                              logger.warn(new IllegalStateException("consumer [" + url.getParameter(INTERFACE_KEY) +
                                      "], has set stubproxy support event ,but no stub methods founded."));
                          }
          
                      } else {
                          stubServiceMethodsMap.put(url.getServiceKey(), stubServiceMethods);
                      }
                  }
                  //建立server
                  openServer(url);
                  //序列化提示
                  optimizeSerialization(url);
          
                  return exporter;
              }

          可以看到程式碼執行到openServer,因為key=getAddress()=ip+port,因此,同一臺機器只會開啟一個NettyServer.

              private void openServer(URL url) {
                  // find server.
                  String key = url.getAddress();
                  //client can export a service which's only for server to invoke
                  boolean isServer = url.getParameter(IS_SERVER_KEY, true);
                  if (isServer) {
                      ProtocolServer server = serverMap.get(key);
                      if (server == null) {
                          synchronized (this) {
                              server = serverMap.get(key);
                              if (server == null) {
                                  serverMap.put(key, createServer(url));
                              }
                          }
                      } else {
                          // server supports reset, use together with override
                          server.reset(url);
                      }
                  }
              }

          對於org.apache.dubbo.remoting.Transporter 的適配類選擇有三種:MinaTransporterNettyTransporterGrizzlyTransporter,關於JavaNIO:Apache Mina、JBoss Netty、Sun Grizzly 框架對比:傳送門

        • NettyServer啟動之後,回到org.apache.dubbo.registry.integration.RegistryProtocol#export方法,繼續執行將服務註冊到註冊中心,我們以Zookeeper為例:

          • 1.首先查詢所有註冊中心

            final Registry registry = getRegistry(originInvoker);
            ...
            protected Registry getRegistry(final Invoker<?> originInvoker) {
                URL registryUrl = getRegistryUrl(originInvoker);
                return registryFactory.getRegistry(registryUrl);
            }

            因為RegistryFactory是一個SPI擴充套件介面,程式碼中設定的為zookeeper,因此這裡呼叫的是ZookeeperRegistryFactory,繼承自:org.apache.dubbo.registry.support.AbstractRegistryFactory#getRegistry(org.apache.dubbo.common.URL),在此方法中呼叫了createRegistry,但是ZookeeperRegistryFactory重寫了createRegistry,因此具體呼叫的是ZookeeperRegistryFactory#createRegistry,該方法返回了一個new ZookeeperRegistry(url, zookeeperTransporter)例項物件。

          • 2.開始註冊,RegistryProtocol#register方法執行註冊動作,首先獲取到我們在上一步找到的註冊中心ZookeeperRegistry,ZookeeperRegistry 執行父類org.apache.dubbo.registry.support.FailbackRegistry#register,在該方法中會呼叫抽象方法:doRegister,ZookeeperRegistry 重寫了改方法,則執行ZookeeperRegistry#doRegister ,如下:

            @Override
            public void doRegister(URL url) {
                try {
                    zkClient.create(toUrlPath(url), url.getParameter(DYNAMIC_KEY, true));
                } catch (Throwable e) {
                    throw new RpcException("Failed to register " + url + " to zookeeper " + getUrl() + ", cause: " + e.getMessage(), e);
                }
            }
          • 3.toUrlPath方法會把org.apache.dubbo.common.URL轉換格式後儲存到zookeeper,如下:

            dubbo://172.16.44.21:20880/com.sxzhongf.deep.in.dubbo.api.service.IGreetingService?anyhost=true&application=deep-in-dubbo-first-provider&default=true&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&group=dubbo-sxzhongf-group&interface=com.sxzhongf.deep.in.dubbo.api.service.IGreetingService&methods=sayHello,testGeneric&pid=8480&release=2.7.5&revision=1.0.0&side=provider&timestamp=1582872610313&version=1.0.0
            
            -----------------------轉換------------------------
            
            /dubbo/com.sxzhongf.deep.in.dubbo.api.service.IGreetingService/providers/dubbo%3A%2F%2F172.16.44.21%3A20880%2Fcom.sxzhongf.deep.in.dubbo.api.service.IGreetingService%3Fanyhost%3Dtrue%26application%3Ddeep-in-dubbo-first-provider%26default%3Dtrue%26deprecated%3Dfalse%26dubbo%3D2.0.2%26dynamic%3Dtrue%26generic%3Dfalse%26group%3Ddubbo-sxzhongf-group%26interface%3Dcom.sxzhongf.deep.in.dubbo.api.service.IGreetingService%26methods%3DsayHello%2CtestGeneric%26pid%3D8480%26release%3D2.7.5%26revision%3D1.0.0%26side%3Dprovider%26timestamp%3D1582872610313%26version%3D1.0.0

            轉換之後的格式其實就是我們在zookeeper中看到的一樣了,不過有幾個目錄:

            • dubbo
            • com.sxzhongf.deep.in.dubbo.api.service.IGreetingService
            • providers
            [zk: localhost:2181(CONNECTED) 2] ls  /dubbo/com.sxzhongf.deep.in.dubbo.api.service.IGreetingService/providers
            [dubbo%3A%2F%2F172.16.44.21%3A20880%2Fcom.sxzhongf.deep.in.dubbo.api.service.IGreetingService%3Fanyhost%3Dtrue%26application%3Ddeep-in-dubbo-first-provider%26default%3Dtrue%26deprecated%3Dfalse%26dubbo%3D2.0.2%26dynamic%3Dtrue%26generic%3Dfalse%26group%3Ddubbo-sxzhongf-group%26interface%3Dcom.sxzhongf.deep.in.dubbo.api.service.IGreetingService%26methods%3DsayHello%2CtestGeneric%26pid%3D15716%26release%3D2.7.5%26revision%3D1.0.0%26side%3Dprovider%26timestamp%3D1582872850187%26version%3D1.0.0]

至此,服務消費端就可以從註冊中心獲取服務提供service進行呼叫了,下節我們繼續來分析,消費端是如何從註冊中心拉取service進行處理的