Dubbo--服务注册与发布原理分析

Posted Hepburn Yang

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Dubbo--服务注册与发布原理分析相关的知识,希望对你有一定的参考价值。

文章目录


Dubbo 服务导出过程始于 Spring 容器发布刷新事件,Dubbo 在接收到事件后,会立即执行服务导出逻辑。
整个逻辑大致可分为三个部分:
第一部分是前置工作,主要用于检查配置参数,组装 URL。
第二部分是导出服务,包含导出服务到本地 (JVM),和导出服务到远程两个过程。
第三部分是向注册中心注册服务,用于服务发现;

实现的效果:

  1. Main()方法启动后:
  2. 开发了20880端口(默认端口)
  3. 发布了一个可远端调用的地址:dubbo:// 192.168.1.1:20880/interface?…… (发布到了注册中心)

------------------
这个过程都需要做哪些事情:

  • 解析配置文件–>基于spring标签进行的扩展
  • 暴露本地服务/远程服务
  • 启动netty --> 提供netty服务 暴露端口
  • 打开连接zk
  • 把地址注册到zk
  • 监听zk

1、解析配置

spring里面提供了很多扩展
dubbo-config-spring模块里面提供了配置文件的扩展
spring提供了两个可扩展的接口或类:

根据SPI扩展机制进入DubboNamespaceHandler解析配置文件,进入到serviceBean 和ServiceConfig中,开始进行导出服务的前置工作;通过onApplicationEvent 监听事件,检查服务是否发布,调用export导出服务,一下是对export的分析:

1.1 检查配置

  • 检查的interface属性是否合法
  • 检查providerConfig和ApplicationConfig等核心配置类对象是否为空,为空会从其他配置类获取对应的实例;
  • 检查并处理泛化服务和普通服务类
  • 检查本地存根配置
  • 对ApplicationConfig、RegisterConfig等配置类进行检测,为空则尝试创建,无法创建则抛异常;

1.2 多协议多注册中心导出服务

private void doExportUrls() 
    // 加载注册中心链接
    List<URL> registryURLs = loadRegistries(true);
    // 遍历 protocols,并在每个协议下导出服务
    for (ProtocolConfig protocolConfig : protocols) 
        doExportUrlsFor1Protocol(protocolConfig, registryURLs);
    

1.3 组装URL

走到这一步url大概是这个样子:_ registry://192.168.22.130:2181/org.apache.dubbo.registry.RegistryService/……_
这一步是根据配置信息组装url, 而且url驱动流程的执行,会再各个模块之间一直往下传,后续会不断修改URL头或协议地址,并根据url地址中的信息做相应的处理;
doExportUrlsFor1Protocol方法中做了url的组装处理,通过反射的方式获取到版本、时间戳、方法名以及各种配置对象的字段信息,然后放入map,源码太长了,这块只贴一下伪代码帮助理解:

private void doExportUrlsFor1Protocol(ProtocolConfig protocolConfig, List<URL> registryURLs) 
    // 获取 ArgumentConfig 列表
    for (遍历 ArgumentConfig 列表) 
        if (type 不为 null,也不为空串)     // 分支1
            1. 通过反射获取 interfaceClass 的方法列表
            for (遍历方法列表) 
                1. 比对方法名,查找目标方法
                2. 通过反射获取目标方法的参数类型数组 argtypes
                if (index != -1)     // 分支2
                    1. 从 argtypes 数组中获取下标 index 处的元素 argType
                    2. 检测 argType 的名称与 ArgumentConfig 中的 type 属性是否一致
                    3. 添加 ArgumentConfig 字段信息到 map 中,或抛出异常
                 else     // 分支3
                    1. 遍历参数类型数组 argtypes,查找 argument.type 类型的参数
                    2. 添加 ArgumentConfig 字段信息到 map 中
                
            
         else if (index != -1)     // 分支4
            1. 添加 ArgumentConfig 字段信息到 map 中
        
    

2. 导出Dubbo服务

准备工作做完了,接下来进行服务导出。服务导出分为导出到本地JVM和导出到远程。
代码根据 url 中的 scope 参数决定服务导出方式,分别如下:

  • scope = none,不导出服务
  • scope != remote,导出到本地
  • scope != local,导出到远程

服务发布的本质就是把export的每个服务转为一个对应的Invoker可执行体,然后把转换后的Invoker都放到一个exporterMap(key,invoker)集和中;

private void doExportUrlsFor1Protocol(ProtocolConfig protocolConfig, List<URL> registryURLs) 
    
    // 省略无关代码
    
    if (ExtensionLoader.getExtensionLoader(ConfiguratorFactory.class)
            .hasExtension(url.getProtocol())) 
        // 加载 ConfiguratorFactory,并生成 Configurator 实例,然后通过实例配置 url
        url = ExtensionLoader.getExtensionLoader(ConfiguratorFactory.class)
                .getExtension(url.getProtocol()).getConfigurator(url).configure(url);
    

    String scope = url.getParameter(Constants.SCOPE_KEY);
    // 如果 scope = none,则什么都不做
    if (!Constants.SCOPE_NONE.toString().equalsIgnoreCase(scope)) 
        // scope != remote,导出到本地
        if (!Constants.SCOPE_REMOTE.toString().equalsIgnoreCase(scope)) 
            exportLocal(url);
        

        // scope != local,导出到远程
        if (!Constants.SCOPE_LOCAL.toString().equalsIgnoreCase(scope)) 
            if (registryURLs != null && !registryURLs.isEmpty()) 
                for (URL registryURL : registryURLs) 
                    url = url.addParameterIfAbsent(Constants.DYNAMIC_KEY, registryURL.getParameter(Constants.DYNAMIC_KEY));
                    // 加载监视器链接
                    URL monitorUrl = loadMonitor(registryURL);
                    if (monitorUrl != null) 
                        // 将监视器链接作为参数添加到 url 中
                        url = url.addParameterAndEncoded(Constants.MONITOR_KEY, monitorUrl.toFullString());
                    

                    String proxy = url.getParameter(Constants.PROXY_KEY);
                    if (StringUtils.isNotEmpty(proxy)) 
                        registryURL = registryURL.addParameter(Constants.PROXY_KEY, proxy);
                    

                    // 为服务提供类(ref)生成 Invoker
                    Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, registryURL.addParameterAndEncoded(Constants.EXPORT_KEY, url.toFullString()));
                    // DelegateProviderMetaDataInvoker 用于持有 Invoker 和 ServiceConfig
                    DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);

                    // 导出服务,并生成 Exporter
                    Exporter<?> exporter = protocol.export(wrapperInvoker);
                    exporters.add(exporter);
                
                
            // 不存在注册中心,仅导出服务
             else 
                Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, url);
                DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);

                Exporter<?> exporter = protocol.export(wrapperInvoker);
                exporters.add(exporter);
            
        
    
    this.urls.add(url);

不管是导出到本地,还是远程。进行服务导出之前,均需要先创建 Invoker,这是一个很重要的步骤。因此下面先来分析 Invoker 的创建过程。

2.1 Invoker创建过程

Invoker是一个重要的模型,在服务的提供端和调用端都会出现invoker.

Invoker 是实体域,它是 Dubbo 的核心模型,其它模型都向它靠扰,或转换成它,它代表一个可执行体,可向它发起 invoke 调用,它有可能是一个本地的实现,也可能是一个远程的实现,也可能一个集群实现。

Invoker由ProxyFactory创建而来,Dubbo默认的ProxyFactory实现类是javassistProFactory。来看一下javassistProFactory类创建Invoker的过程

public <T> Invoker<T> getInvoker(T proxy, Class<T> type, URL url) 
	// 为目标类创建 Wrapper
    final Wrapper wrapper = Wrapper.getWrapper(proxy.getClass().getName().indexOf('$') < 0 ? proxy.getClass() : type);
    // 创建匿名 Invoker 类对象,并实现 doInvoke 方法。
    // 
    return new AbstractProxyInvoker<T>(proxy, type, url) 
        @Override
        protected Object doInvoke(T proxy, String methodName,
                                  Class<?>[] parameterTypes,
                                  Object[] arguments) throws Throwable 
			// 调用 Wrapper 的 invokeMethod 方法,invokeMethod 最终会调用目标方法
            return wrapper.invokeMethod(proxy, methodName, parameterTypes, arguments);
        
    ;

重写后的doInvoke逻辑比较简单,仅仅是将调用请求转发给了Wrapper类的invokeMethod,Wrapper用于“包裹”目标类,Wrapper是一个抽象类,仅可通过getWrapper方法传入的Class对象进行解析,拿到类的信息,生成invokeMethod方法和其他方法代码,代码生成完毕之后,通过javassist生成Class对象,最后再通过反射创建Wrapper实例。

 public static Wrapper getWrapper(Class<?> c) 	
    while (ClassGenerator.isDynamicClass(c))
        c = c.getSuperclass();

    if (c == Object.class)
        return OBJECT_WRAPPER;

    // 从缓存中获取 Wrapper 实例
    Wrapper ret = WRAPPER_MAP.get(c);
    if (ret == null) 
        // 缓存未命中,创建 Wrapper
        ret = makeWrapper(c);
        // 写入缓存
        WRAPPER_MAP.put(c, ret);
    
    return ret;

2.2 导出服务到本地

private void exportLocal(URL url) 
    // 如果 URL 的协议头等于 injvm,说明已经导出到本地了,无需再次导出
    if (!Constants.LOCAL_PROTOCOL.equalsIgnoreCase(url.getProtocol())) 
        URL local = URL.valueOf(url.toFullString())
            .setProtocol(Constants.LOCAL_PROTOCOL)    // 设置协议头为 injvm
            .setHost(LOCALHOST)
            .setPort(0);
        ServiceClassHolder.getInstance().pushServiceClass(getServiceClass(ref));
        // 创建 Invoker,并导出服务,这里的 protocol 会在运行时调用 InjvmProtocol 的 export 方法
        Exporter<?> exporter = protocol.export(
            proxyFactory.getInvoker(ref, (Class) interfaceClass, local));
        exporters.add(exporter);
    

exportLocal 方法比较简单,首先根据 URL 协议头决定是否导出服务。若需导出,则创建一个新的 URL 并将协议头、主机名以及端口设置成新的值。然后创建 Invoker,并调用 InjvmProtocol 的 export 方法导出服务。下面我们来看一下 InjvmProtocol 的 export 方法都做了哪些事情。

public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException 
    // 创建 InjvmExporter
    return new InjvmExporter<T>(invoker, invoker.getUrl().getServiceKey(), exporterMap);

如上,InjvmProtocol 的 export 方法仅创建了一个 InjvmExporter,无其他逻辑。到此导出服务到本地就分析完了,接下来,我们继续分析导出服务到远程的过程。

2.3 导出服务到远程

与导出服务到本地相比,导出服务到远程包括了服务导出与服务注册两个过程;

2.3.1 服务导出

RegistryProtocol 的 export 方法

public <T> Exporter<T> export(final Invoker<T> originInvoker) throws RpcException 
    // 导出服务
    final ExporterChangeableWrapper<T> exporter = doLocalExport(originInvoker);

    // 获取注册中心 URL,以 zookeeper 注册中心为例,得到的示例 URL 如下:
    // zookeeper://127.0.0.1:2181/com.alibaba.dubbo.registry.RegistryService?application=demo-provider&dubbo=2.0.2&export=dubbo%3A%2F%2F172.17.48.52%3A20880%2Fcom.alibaba.dubbo.demo.DemoService%3Fanyhost%3Dtrue%26application%3Ddemo-provider
    URL registryUrl = getRegistryUrl(originInvoker);

    // 根据 URL 加载 Registry 实现类,比如 ZookeeperRegistry
    final Registry registry = getRegistry(originInvoker);
    
    // 获取已注册的服务提供者 URL,比如:
    // dubbo://172.17.48.52:20880/com.alibaba.dubbo.demo.DemoService?anyhost=true&application=demo-provider&dubbo=2.0.2&generic=false&interface=com.alibaba.dubbo.demo.DemoService&methods=sayHello
    final URL registeredProviderUrl = getRegisteredProviderUrl(originInvoker);

    // 获取 register 参数
    boolean register = registeredProviderUrl.getParameter("register", true);

    // 向服务提供者与消费者注册表中注册服务提供者
    ProviderConsumerRegTable.registerProvider(originInvoker, registryUrl, registeredProviderUrl);

    // 根据 register 的值决定是否注册服务
    if (register) 
        // 向注册中心注册服务
        register(registryUrl, registeredProviderUrl);
        ProviderConsumerRegTable.getProviderWrapper(originInvoker).setReg(true);
    

    // 获取订阅 URL,比如:
    // provider://172.17.48.52:20880/com.alibaba.dubbo.demo.DemoService?category=configurators&check=false&anyhost=true&application=demo-provider&dubbo=2.0.2&generic=false&interface=com.alibaba.dubbo.demo.DemoService&methods=sayHello
    final URL overrideSubscribeUrl = getSubscribedOverrideUrl(registeredProviderUrl);
    // 创建监听器
    final OverrideListener overrideSubscribeListener = new OverrideListener(overrideSubscribeUrl, originInvoker);
    overrideListeners.put(overrideSubscribeUrl, overrideSubscribeListener);
    // 向注册中心进行订阅 override 数据
    registry.subscribe(overrideSubscribeUrl, overrideSubscribeListener);
    // 创建并返回 DestroyableExporter
    return new DestroyableExporter<T>(exporter, originInvoker, overrideSubscribeUrl, registeredProviderUrl);

主要逻辑:

  1. 调用 doLocalExport 导出服务
  2. 向注册中心注册服务
  3. 向注册中心进行订阅 override 数据
  4. 创建并返回 DestroyableExporter

第一步:

1. doLocalExport

这里用了典型的双重检查锁机制来创建exporter;

private <T> ExporterChangeableWrapper<T> doLocalExport(final Invoker<T> originInvoker) 
    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 为委托类对象
                final Invoker<?> invokerDelegete = new InvokerDelegete<T>(originInvoker, getProviderUrl(originInvoker));
                // 核心代码在这里: 
                //调用 protocol 的 export 方法导出服务
                exporter = new ExporterChangeableWrapper<T>((Exporter<T>) protocol.export(invokerDelegete), originInvoker);
                
                // 写缓存
                bounds.put(key, exporter);
            
        
    
    return exporter;

重点在于protocol.export这部分,假定运行时协议为dubbo(默认),此处protocol变量在运行时候将会加载DubboProtocol,并调用DubboProtocol类的export方法,下面来分析DubboProtocol的export方法:

2.DubboProtocol的export

public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException 
    URL url = invoker.getUrl();

    // 获取服务标识,理解成服务坐标也行。由服务组名,服务名,服务版本号以及端口组成。比如:
    // demoGroup/com.alibaba.dubbo.demo.DemoService:1.0.1:20880
    String key = serviceKey(url);
    // 创建 DubboExporter
    DubboExporter<T> exporter = new DubboExporter<T>(invoker, key, exporterMap);
    // 将 <key, exporter> 键值对放入缓存中
    exporterMap.put(key, exporter);

    // 本地存根相关代码
    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);
        
    

    // 核心方法:--  启动服务器
    openServer(url);
    // 优化序列化
    optimizeSerialization(url);
    return exporter;

启动服务器

3. DubboExporter. openServer(url)

private void openServer(URL url) 
    // 获取 host:port,并将其作为服务器实例的 key,用于标识当前的服务器实例
    String key = url.getAddress();
    boolean isServer = url.getParameter(Constants.IS_SERVER_KEY, true);
    if (isServer) 
        // 访问缓存
        ExchangeServer server = serverMap.get(key);
        // 在同一台机器上(单网卡),同一个端口上仅允许启动一个服务器实例。
        // 若某个端口上已有服务器实例,此时则调用 reset 方法重置服务器的一些配置
        if (server == null) 
            // 核心方法:创建服务器实例
            serverMap.put(key, createServer(url));
         else 
            // 服务器已创建,则根据 url 中的配置重置服务器
            server.reset(url);
        
    



创建服务器实例

4. DubboExporter. createServer(URL url)

/*
createServer 包含三个核心的逻辑
1.检测是否存在 server 参数所代表的 Transporter 拓展,不存在则抛出异常
2.创建服务器实例
3.检测是否支持 client 参数所表示的 Transporter 拓展,不存在也是抛出异常
*/
private ExchangeServer createServer(URL url) 
    url = url.addParameterIfAbsent(Constants.CHANNEL_READONLYEVENT_SENT_KEY,
    // 添加心跳检测配置到 url 中
    url = url.addParameterIfAbsent(Constants.HEARTBEAT_KEY, String.valueOf(Constants.DEFAULT_HEARTBEAT));
	// 获取 server 参数,默认为 netty
    String str = url.getParameter(Constants.SERVER_KEY, Constants.DEFAULT_REMOTING_SERVER);

	以上是关于Dubbo--服务注册与发布原理分析的主要内容,如果未能解决你的问题,请参考以下文章

Dubbo的SPI机制与JDK机制的不同及原理分析

Nacos 服务注册与发现原理分析

Dubbo服务注册与动态发现机制的原理与实现细节

Dubbo 中 Zookeeper 注册中心原理分析

dubbo的本地存根(Stub)

Dubbo推刊