dubbo服务暴露过程

405 阅读10分钟

参考资料:

说明:源码版本是2.6.5

总结

主线:

从业务代码角度看:封装URL -> 本地暴露 -> 远程暴露

从对象构建及转移角度看:具体实现类 -> Invoker -> Exporter

大概过程:

1.当spring容器刷新完成,会发布ContextRefreshEvent事件,ServiceBean执行doExport方法,开始服务暴露。

2.先获取注册中心的URL,把各种协议都注册到注册中心

3.根据各种参数,封装服务接口的URL对象

4.如果scope没有配置Remote,则进行本地暴露。把协议改成injvm,然后调用代理工厂生成Invoker,封装真正实现类的调用逻辑,最后通过具体协议,导出Exporter

5.如果scope没有配置Local,则进行远程暴露。也是通过代理工厂封装具体实现类,生成Invoker,接着导出Exporter。不过,这里的export分两种情况。

5.1 第一种情况:Registry类型的export;这是指将服务注册到注册中心,以zk为例,会创建一个zk连接,然后在指定目录下创建一个节点,节点信息包括了服务提供者的ip、端口、服务接口等信息

5.2 第二种情况:非Registry类型的export;比如,dubbo协议。这个过程是创建Netty服务端,打开并监听端口。

Dubbo的服务暴露过程

URL说明

在开始分析dubbo服务暴露过程前,先简单说下URL,因为dubbo就是把URL作为约定参数类型,即通过URL对象进行数据的传递。

URL是指统一资源定位符,标准格式如下:

protocol://username:password@host:port/path?key=value&key=value
  • protocol:是指dubbo中使用到的各种协议,比如 dubbo、http、registry
  • path:接口的全限定名
  • 参数:键值对

配置解析

我们使用xml文件配置dubbo时,是通过DubboNamespaceHandler向spring容器注册各种标签的解析器,用来读取标签内容,保存配置信息 。

<dubbo:service interface="cn.com.service.RegionService" ref="regionService" />
public class DubboNamespaceHandler extends NamespaceHandlerSupport {

    static {
        Version.checkDuplicate(DubboNamespaceHandler.class);
    }

    @Override
    public void init() {
        registerBeanDefinitionParser("application", new DubboBeanDefinitionParser(ApplicationConfig.class, true));
        registerBeanDefinitionParser("module", new DubboBeanDefinitionParser(ModuleConfig.class, true));
        registerBeanDefinitionParser("registry", new DubboBeanDefinitionParser(RegistryConfig.class, true));
        registerBeanDefinitionParser("monitor", new DubboBeanDefinitionParser(MonitorConfig.class, true));
        registerBeanDefinitionParser("provider", new DubboBeanDefinitionParser(ProviderConfig.class, true));
        registerBeanDefinitionParser("consumer", new DubboBeanDefinitionParser(ConsumerConfig.class, true));
        registerBeanDefinitionParser("protocol", new DubboBeanDefinitionParser(ProtocolConfig.class, true));
        registerBeanDefinitionParser("service", new DubboBeanDefinitionParser(ServiceBean.class, true));
        registerBeanDefinitionParser("reference", new DubboBeanDefinitionParser(ReferenceBean.class, false));
        registerBeanDefinitionParser("annotation", new AnnotationBeanDefinitionParser());
    }

}

从上面的代码可以看到,标签dubbo:service的解析器是ServiceBean

服务暴露过程

暴露时机

在spring容器刷新完成后,会发布ContextRefreshedEvent事件,ServiceBean执行onApplicationEvent方法,该方法调用父类ServiceConfig的export方法,开始暴露服务。

image-20201215211231158

doExportUrls

doExport方法很长,先经过各种配置的检查,填充属性。接着调用doExportUrls方法。该方法先调用loadRegistries获取所有注册中心的url,接着遍历各种协议,把每种协议都往注册中心注册。比如,同时支持 dubbo 协议和 hessian 协议,那么需要将这个服务用两种协议分别向注册中心暴露

private void doExportUrls() {
    List<URL> registryURLs = loadRegistries(true);
    for (ProtocolConfig protocolConfig : protocols) {
        doExportUrlsFor1Protocol(protocolConfig, registryURLs);
    }
}

loadRegistries

获取注册中心的url,如下所示

registry://127.0.0.1:2181/com.alibaba.dubbo.registry.RegistryService?application=dubbo-provider&application.version=1.0&dubbo=2.5.3&environment=product&organization=china&owner=cheng.xi&pid=2939&registry=zookeeper&timestamp=1488898049284

doExportUrlsFor1Protocol

该方法大致做三件事情:

  1. 先拼接服务URL对象
  2. 本地暴露服务
  3. 远程暴露服务
拼接服务url

由于代码比较长就不贴出来了,就是指定服务的协议、服务接口及方法,最终拼接出服务URL对象,如下所示

URL url = new URL(name, host, port, (contextPath == null || contextPath.length() == 0 ? "" : contextPath + "/") + path, map);

image-20201215213737343

dubbo://10.0.0.83:20886/cn.com.bluemoon.asset.dubbo.IDubboCommonService?anyhost=true&application=bm-officeAuto-asset&complexity=0&dubbo=2.8.4.BM-SNAPSHOT&generic=false&interface=cn.com.bluemoon.asset.dubbo.IDubboCommonService&methods=assetPushSMS&pid=516&retries=0&sendmsg=false&side=provider&timeout=60000&timestamp=1608039139999
本地暴露

服务URL拼接完成后,接着就是暴露服务,主要分为本地暴露和远程暴露。两者过程基本都是,先使用代理工厂生成Invoker,封装实现类的调用逻辑。接着根据不同协议,导出对应的Exporter,提供给消费方使用。

image-20201215214131153

// 这段注释直接copy参考资料的。。。
private void exportLocal(URL url) {
	// 如果已经是injvm协议,不需要再执行
    if (!Constants.LOCAL_PROTOCOL.equalsIgnoreCase(url.getProtocol())) {
        
        //这时候转成本地暴露的url:injvm://127.0.0.1/dubbo.common.hello.service.HelloService?anyhost=true&
        //application=dubbo-provider&application.version=1.0&dubbo=2.5.3&environment=product&
        //interface=dubbo.common.hello.service.HelloService&methods=sayHello&
        //organization=china&owner=cheng.xi&pid=720&side=provider&timestamp=1489716708276
        URL local = URL.valueOf(url.toFullString())
                .setProtocol(Constants.LOCAL_PROTOCOL)
                .setHost(NetUtils.LOCALHOST)
                .setPort(0);
        
        //首先获得Invoker
        //然后导出成Exporter,并缓存
        //这里的proxyFactory实际是JavassistProxyFactory
        //有关详细的获得Invoke以及exporter会在下面的流程解析,在本地暴露这个流程就不再说明。
        Exporter<?> exporter = protocol.export(
                proxyFactory.getInvoker(ref, (Class) interfaceClass, local));
        
        exporters.add(exporter);
        logger.info("Export dubbo service " + interfaceClass.getName() +" to local registry");
    }
}

本地暴露时,协议被改成injvm,ip为127.0.0.1,端口是0;

远程暴露

image-20201215215901248

创建Invoker

在上面的代码中,我们都能看到先是创建Invoker,接着是根据协议导出Exporter。

Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, url);

目前,有两种方式获取Invoker,分别是JavassistProxyFactory#getInvokerJdkProxyFactory#getInvoker,默认使用JavassistProxyFactory

public class JavassistProxyFactory extends AbstractProxyFactory {

    @SuppressWarnings("unchecked")
    public <T> T getProxy(Invoker<T> invoker, Class<?>[] interfaces) {
        return (T) Proxy.getProxy(interfaces).newInstance(new InvokerInvocationHandler(invoker));
    }

    public <T> Invoker<T> getInvoker(T proxy, Class<T> type, URL url) {
        // TODO Wrapper类不能正确处理带$的类名
        // 如果类是$开头,使用接口类型获取
        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 {
                
                // wrapper.invokeMethod 会调用实现类的实现方法
                return wrapper.invokeMethod(proxy, methodName, parameterTypes, arguments);
            }
        };
    }

}
public class JdkProxyFactory extends AbstractProxyFactory {

    @SuppressWarnings("unchecked")
    public <T> T getProxy(Invoker<T> invoker, Class<?>[] interfaces) {
        return (T) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), 
                                          interfaces, new InvokerInvocationHandler(invoker));
    }

    public <T> Invoker<T> getInvoker(T proxy, Class<T> type, URL url) {
        return new AbstractProxyInvoker<T>(proxy, type, url) {
            @Override
            protected Object doInvoke(T proxy, String methodName, 
                                      Class<?>[] parameterTypes, 
                                      Object[] arguments) throws Throwable {
                Method method = proxy.getClass().getMethod(methodName, parameterTypes);
                return method.invoke(proxy, arguments);
            }
        };
    }

}

可以看到,其实两个实现方式大同小异,区别是JavassistProxyFactory会对具体实现类做了一个包装,最终都是调用具体实现类的目标方法。因此,Invoker作用是调用具体实现类的实现方法。

导出Exporter

Invoker导出为Exporter分为两种情况:第一种是Registry类型的Invoker,第二种是非Registry类型的Invoker。

方法描述:

/**
  * 暴露远程服务:<br>
  * 1. 协议在接收请求时,应记录请求来源方地址信息:RpcContext.getContext().setRemoteAddress();<br>
  * 2. export()必须是幂等的,也就是暴露同一个URL的Invoker两次,和暴露一次没有区别。<br>
  * 3. export()传入的Invoker由框架实现并传入,协议不需要关心。<br>
  * 
  * @param <T> 服务的类型
  * @param invoker 服务的执行体
  * @return exporter 暴露服务的引用,用于取消暴露
  * @throws RpcException 当暴露服务出错时抛出,比如端口已占用
  */
@Adaptive
<T> Exporter<T> export(Invoker<T> invoker) throws RpcException;

方法入口:

Exporter<?> exporter = protocol.export(invoker);
Registry类型的export

com.alibaba.dubbo.registry.integration.RegistryProtocol#export

public <T> Exporter<T> export(final Invoker<T> originInvoker) throws RpcException {
        // export invoker
    	// doLocalExport:这里是执行其他协议的暴露,比如dubbo
        final ExporterChangeableWrapper<T> exporter = doLocalExport(originInvoker);
    	
    	//registry provider
    	// 根据Invoker的url获取Registry实例对象
        final Registry registry = getRegistry(originInvoker);
    	// 获取要注册到注册中心的url,即服务接口url
    	final URL registedProviderUrl = getRegistedProviderUrl(originInvoker);
        // 调用远端注册中心的register方法进行服务注册
    	registry.register(registedProviderUrl);
    
        // 订阅override数据
        // FIXME 提供者订阅时,会影响同一JVM,即暴露服务,又引用同一服务的的场景,因为subscribed以服务名为缓存的key,导致订阅信息覆盖。
        final URL overrideSubscribeUrl = getSubscribedOverrideUrl(registedProviderUrl);
        final OverrideListener overrideSubscribeListener = new OverrideListener(overrideSubscribeUrl);
        overrideListeners.put(overrideSubscribeUrl, overrideSubscribeListener);
         //提供者向注册中心订阅所有注册服务的覆盖配置
    	//当注册中心有此服务的覆盖配置注册进来时,推送消息给提供者,重新暴露服务,这由管理页面完成。
    	registry.subscribe(overrideSubscribeUrl, overrideSubscribeListener);
        
    	//保证每次export都返回一个新的exporter实例
    	//返回暴露后的Exporter给上层ServiceConfig进行缓存,便于后期撤销暴露。
        return new Exporter<T>() {
            public Invoker<T> getInvoker() {
                return exporter.getInvoker();
            }
            public void unexport() {
            	try {
            		exporter.unexport();
            	} catch (Throwable t) {
                	logger.warn(t.getMessage(), t);
                }
                try {
                	registry.unregister(registedProviderUrl);
                } catch (Throwable t) {
                	logger.warn(t.getMessage(), t);
                }
                try {
                	overrideListeners.remove(overrideSubscribeUrl);
                	registry.unsubscribe(overrideSubscribeUrl, overrideSubscribeListener);
                } catch (Throwable t) {
                	logger.warn(t.getMessage(), t);
                }
            }
        };
    }

这里面涉及到的方法很多,具体解析可看参考资料。

  • getRegistry(originInvoker)

    • 根据dubbo spi机制获取到具体的Registry实例,如果是zookeeper协议,则返回ZookeeperRegistry,该对象保存着连接到zookeeper的zkClient

    • /**
          * 根据invoker的地址获取registry实例
          * @param originInvoker
          * @return
          */
         private Registry getRegistry(final Invoker<?> originInvoker){
             URL registryUrl = originInvoker.getUrl();
             if (Constants.REGISTRY_PROTOCOL.equals(registryUrl.getProtocol())) {
                 String protocol = registryUrl.getParameter(Constants.REGISTRY_KEY, Constants.DEFAULT_DIRECTORY);
                 registryUrl = registryUrl.setProtocol(protocol).removeParameter(Constants.REGISTRY_KEY);
             }
             return registryFactory.getRegistry(registryUrl);
         }
      
  • getRegistedProviderUrl(originInvoker)

    • 获取要注册到注册中心的url,即提供方的服务接口URL

    • final URL registedProviderUrl = getRegistedProviderUrl(originInvoker);
      //得到的URL是:
      //dubbo://192.168.1.100:20880/dubbo.common.hello.service.HelloService?
      //anyhost=true&application=dubbo-provider&application.version=1.0&dubbo=2.5.3&environment=product&
      //interface=dubbo.common.hello.service.HelloService&methods=sayHello&
      //organization=china&owner=cheng.xi&pid=9457&side=provider&timestamp=1489807681627
      
  • registry.register(registedProviderUrl)

    • 调用远端注册中心的register方法进行服务注册

    • 该方法会调用doRegistry,如果是zk注册中心,会默认创建一个临时节点

    • com.alibaba.dubbo.registry.support.FailbackRegistry#register

    • image-20201217155321638

    • com.alibaba.dubbo.registry.zookeeper.ZookeeperRegistry#doRegister

    • protected void doRegister(URL url) {
          try {
              //这里zkClient就是我们上面调用构造的时候生成的ZkClientZookeeperClient
              //开始注册,也就是在Zookeeper中创建节点
              //这里toUrlPath获取到的path为:
              ///dubbo/dubbo.common.hello.service.HelloService/providers/dubbo%3A%2F%2F192.168.1.100%3A20880%2F
              //dubbo.common.hello.service.HelloService%3Fanyhost%3Dtrue%26application%3Ddubbo-provider%26
              //application.version%3D1.0%26dubbo%3D2.5.3%26environment%3Dproduct%26interface%3D
              //dubbo.common.hello.service.HelloService%26methods%3DsayHello%26
              //organization%3Dchina%26owner%3Dcheng.xi%26pid%3D8920%26side%3Dprovider%26timestamp%3D1489828029449
              //默认创建的节点是临时节点
              zkClient.create(toUrlPath(url), url.getParameter(Constants.DYNAMIC_KEY, true));
          } catch (Throwable e) { }
      }
      
    • 注册成功后,zk上的节点信息

    • /dubbo
          dubbo.common.hello.service.HelloService
              providers
                  /dubbo/dubbo.common.hello.service.HelloService/providers/
                  dubbo%3A%2F%2F192.168.1.100%3A20880%2Fdubbo.common.hello.service.HelloService%3F
                  anyhost%3Dtrue%26application%3Ddubbo-provider%26
                  application.version%3D1.0%26dubbo%3D2.5.3%26environment%3Dproduct%26
                  interface%3Ddubbo.common.hello.service.HelloService%26methods%3DsayHello%26
                  organization%3Dchina%26owner%3Dcheng.xi%26pid%3D13239%26side%3D
                  provider%26timestamp%3D1489829293525
      
    • image-20201217155743907

非Registry类型的export

主要步骤是打开本地端口,并监听。

  • doLocalExport(invoker)

    • private <T> ExporterChangeableWrapper<T>  doLocalExport(final Invoker<T> originInvoker){
          //原始的invoker中的url:
          //registry://127.0.0.1:2181/com.alibaba.dubbo.registry.RegistryService?
          //application=dubbo-provider&application.version=1.0&dubbo=2.5.3
          //&environment=product&export=dubbo%3A%2F%2F10.42.0.1%3A20880%2F
          //dubbo.common.hello.service.HelloService%3Fanyhost%3Dtrue%26application%3Ddubbo-provider%26
          //application.version%3D1.0%26dubbo%3D2.5.3%26environment%3Dproduct%26
          //interface%3Ddubbo.common.hello.service.HelloService%26methods%3DsayHello%26
          //organization%3Dchina%26owner%3Dcheng.xi%26pid%3D7876%26side%3Dprovider%26timestamp%3D1489057305001&
          //organization=china&owner=cheng.xi&pid=7876&registry=zookeeper&timestamp=1489057304900
          
          //从原始的invoker中得到的key: (读取原始url中的export参数值)
          //dubbo://10.42.0.1:20880/dubbo.common.hello.service.HelloService?anyhost=true&application=dubbo-provider&
          //application.version=1.0&dubbo=2.5.3&environment=product&interface=dubbo.common.hello.service.HelloService&
          //methods=sayHello&organization=china&owner=cheng.xi&pid=7876&side=provider&timestamp=1489057305001
          String key = getCacheKey(originInvoker);
          ExporterChangeableWrapper<T> exporter = (ExporterChangeableWrapper<T>) bounds.get(key);
          if (exporter == null) {
              synchronized (bounds) {
                  exporter = (ExporterChangeableWrapper<T>) bounds.get(key);
                  if (exporter == null) {
                      //得到一个Invoker代理,里面包含原来的Invoker
                      final Invoker<?> invokerDelegete = new InvokerDelegete<T>(originInvoker, getProviderUrl(originInvoker));
                      //此处protocol还是最上面生成的代码,调用代码中的export方法,会根据协议名选择调用具体的实现类
                      //这里我们需要调用DubboProtocol的export方法
                      //这里的使用具体协议进行导出的invoker是个代理invoker
                      //导出完之后,返回一个新的ExporterChangeableWrapper实例
                      exporter = new ExporterChangeableWrapper<T>((Exporter<T>)protocol.export(invokerDelegete), originInvoker);
                      bounds.put(key, exporter);
                  }
              }
          }
          return (ExporterChangeableWrapper<T>) exporter;
      }
      
  • DubboProtocol的export方法

    • 生成的exporter会被放到缓存中,key是由服务名、端口、ip组成。客户端发送请求,根据key找到Exporter,然后找到invoker进行调用。

    • public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
          //dubbo://10.42.0.1:20880/dubbo.common.hello.service.HelloService?
          //anyhost=true&application=dubbo-provider&
          //application.version=1.0&dubbo=2.5.3&environment=product&
          //interface=dubbo.common.hello.service.HelloService&
          //methods=sayHello&organization=china&owner=cheng.xi&
          //pid=7876&side=provider&timestamp=1489057305001
          URL url = invoker.getUrl();
      
          // export service.
          //key由serviceName,port,version,group组成
          //当nio客户端发起远程调用时,nio服务端通过此key来决定调用哪个Exporter,也就是执行的Invoker。
          //dubbo.common.hello.service.HelloService:20880
          String key = serviceKey(url);
          
          //将Invoker转换成Exporter
          //直接new一个新实例
          //没做啥处理,就是做一些赋值操作
          //这里的exporter就包含了invoker
          DubboExporter<T> exporter = new DubboExporter<T>(invoker, key, exporterMap);
          
          //缓存要暴露的服务,key是上面生成的
          exporterMap.put(key, exporter);
      
          //export an stub service for dispaching event
          //是否支持本地存根
          //远程服务后,客户端通常只剩下接口,而实现全在服务器端,
          //但提供方有些时候想在客户端也执行部分逻辑,比如:做ThreadLocal缓存,
          //提前验证参数,调用失败后伪造容错数据等等,此时就需要在API中带上Stub,
          //客户端生成Proxy实,会把Proxy通过构造函数传给Stub,
          //然后把Stub暴露组给用户,Stub可以决定要不要去调Proxy。
          Boolean isStubSupportEvent = url.getParameter(Constants.STUB_EVENT_KEY,Constants.DEFAULT_STUB_EVENT);
          Boolean isCallbackservice = url.getParameter(Constants.IS_CALLBACK_SERVICE, false);
          if (isStubSupportEvent && !isCallbackservice){
              String stubServiceMethods = url.getParameter(Constants.STUB_EVENT_METHODS_KEY);
              if (stubServiceMethods == null || stubServiceMethods.length() == 0 ){
              } else {
                  stubServiceMethodsMap.put(url.getServiceKey(), stubServiceMethods);
              }
          }
          //根据URL绑定IP与端口,建立NIO框架的Server
          openServer(url);
      
          return exporter;
      }
      
  • openServer(url)

    • 创建NIO服务器,并监听指定端口。(这里涉及到代码也很多,具体看参考资料)

    • private void openServer(URL url) {
          // find server.
          //key是IP:PORT
          //192.168.110.197:20880
          String key = url.getAddress();
          //client 也可以暴露一个只有server可以调用的服务。
          boolean isServer = url.getParameter(Constants.IS_SERVER_KEY,true);
          if (isServer) {
              ExchangeServer server = serverMap.get(key);
              //同一JVM中,同协议的服务,共享同一个Server,
              //第一个暴露服务的时候创建server,
              //以后相同协议的服务都使用同一个server
              if (server == null) {
                  serverMap.put(key, createServer(url));
              } else {
                  //同协议的服务后来暴露服务的则使用第一次创建的同一Server
                  //server支持reset,配合override功能使用
                  //accept、idleTimeout、threads、heartbeat参数的变化会引起Server的属性发生变化
                  //这时需要重新设置Server
                  server.reset(url);
              }
          }
      }
      

至此,dubbo的服务暴露过程基本过了一遍,里面内容很多很复杂,只能慢慢消化。