当前位置: 首页 > news >正文

面试时Dubbo原理记不住?来看看《Dubbo原理浅析——从RPC本质看Dubbo》

RPC的本质是什么?通俗地讲RPC就是要解决远程服务间的调用问题,也就是管理服务配置并提供便捷可靠高效的服务间调用。

我们来看看dubbo的定义:dubbo是一个分布式的服务框架,致力于提供高性能和透明化的RPC远程服务调用方案,以及SOA服务治理方案。

通过定义,我们提出以下几个问题,并通过这几个问题来介绍DUBBO。

  1. DUBBO的实现思想(总体架构)什么?
  2. DUBBO是如何实现透明化使用的?
  3. DUBBO中服务配置与实际调用是怎么结合的(如何实现远程服务调用)?

总体架构

先附DUBBO官网的架构图,
在这里插入图片描述
简单解释下架构图,

DUBBO分为四个模块,分别为:注册中心(Registry)、提供者(Provider)、消费者(Consumer)和监控(Monitor)。

  • 注册中心(Registry):可以是zookeeper、redis、multicast、simple(官方推荐使用Zookeeper);
  • 提供者(Provider):服务启动时,Provider引用容器中的服务,并向Registry注册服务,同时暴露服务(Consumer是直接和Provider通讯实现服务调用的)。
  • 消费者(Consumer):服务启动时,Consumer向Registry订阅服务,如果没有订阅到自己想获得的服务,它会不断的尝试订阅。新的服务注册到注册中心以后,注册中心会将这些服务通过notify到消费者。Consumer直接调用Provider提供的服务。
  • 监控(Monitor):Consumer和Provider会通过异步的方式定时向Monitor发送消息,报告服务的状态。Monitor在整个架构中是可选的,Monitor功能需要单独配置,不配置或者配置后挂掉并不会影响服务的调用。

所以,DUBBO的实现思路是通过注册中心实现服务的动态注册与发现,Provider暴露服务,Consumer直接和Provider通讯实现服务间的调用的。

透明化使用

本节需要对Spring的Bean加载机制有一定的了解,如果大家感兴趣,后续我可以再详细介绍

用过DUBBO的,应该都知道,使用DUBBO时,只需要进行一些Spring的配置即能享受DUBBO远程调用的便利。显而易见,DUBBO就是利用的Spring的扩展性,通过全Spring配置方式,使得用户能够透明化接入应用,并且对应用没有任何API侵入。

Dubbo通过扩展Spring Schema完成自定义bean的IoC容器注入。

  • 自定义了XML Schema文件META-INF/dubbo.xsd描述自定义元素;
  • 自定义继承自org.springframework.beans.factory.xml.NamespaceHandlerSupport抽象类的DubboNamespaceHandler处理器类;
  • 自定义实现了org.springframework.beans.factory.xml.BeanDefinitionParser接口的DubboBeanDefinitionParser解析器类;

这样,DUBBO很好地将配置转化成bean,基于Spring容器的应用能够方便地使用。

public class DubboNamespaceHandler extends NamespaceHandlerSupport {

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

	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 DubboBeanDefinitionParser(AnnotationBean.class, true));
    }

}

如上,DubboNamespaceHandler实现NamespaceHandlerSupport接口,通过注册DubboBeanDefinitionParser解析器,将对应配置解析后转化成ApplicationConfig、ModuleConfig、RegistryConfig、MonitorConfig、ProviderConfig、ConsumerConfig、ProtocolConfig、AnnotationBean、ServiceBean和ReferenceBean。

我们着重看ServiceBean和ReferenceBean,这两个bean是服务的调用bean,业务上我们调用的服务就是通过这两个bean来交互的。

服务调用

在看ServiceBean和ReferenceBean前,我们先看一张图,
在这里插入图片描述

图例说明:
图中小方块 Protocol, Cluster, Proxy, Service, Container, Registry, Monitor
代表层或模块,蓝色的表示与业务有交互,绿色的表示只对 Dubbo 内部交互。 图中背景方块 Consumer, Provider,
Registry, Monitor 代表部署逻辑拓扑节点。 图中蓝色虚线为初始化时调用,红色虚线为运行时异步调用,红色实线为运行时同步调用。
图中只包含 RPC 的层,不包含 Remoting 的层,Remoting 整体都隐含在 Protocol 中。

图中我们可以看出,Consumer和Provider之间是通过Protocol交互的,不错,DUBBO可以自定义Protocol完成服务调用(Protocol是什么时候初始化,什么时候调用的,可以看后续对ReferenceBean和ServiceBean介绍)。

@SPI("dubbo")
public interface Protocol {

    /**
     * 获取缺省端口,当用户没有配置端口时使用。
     * 
     * @return 缺省端口
     */
    int getDefaultPort();

    /**
     * 暴露远程服务:<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;

    /**
     * 引用远程服务:<br>
     * 1. 当用户调用refer()所返回的Invoker对象的invoke()方法时,协议需相应执行同URL远端export()传入的Invoker对象的invoke()方法。<br>
     * 2. refer()返回的Invoker由协议实现,协议通常需要在此Invoker中发送远程请求。<br>
     * 3. 当url中有设置check=false时,连接失败不能抛出异常,并内部自动恢复。<br>
     * 
     * @param <T> 服务的类型
     * @param type 服务的类型
     * @param url 远程服务的URL地址
     * @return invoker 服务的本地代理
     * @throws RpcException 当连接服务提供方失败时抛出
     */
    @Adaptive
    <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException;

    /**
     * 释放协议:<br>
     * 1. 取消该协议所有已经暴露和引用的服务。<br>
     * 2. 释放协议所占用的所有资源,比如连接和端口。<br>
     * 3. 协议在释放后,依然能暴露和引用新的服务。<br>
     */
    void destroy();
}

Protocol的接口有export方法和refer方法,export方法负责暴露远程Invoker服务,而refer方法获取远程服务的Invoker实现。

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

public interface Invoker<T> extends Node {

    /**
     * get service interface.
     * 
     * @return service interface.
     */
    Class<T> getInterface();

    /**
     * invoke.
     * 
     * @param invocation
     * @return result
     * @throws RpcException
     */
    Result invoke(Invocation invocation) throws RpcException;

}

Invoker接口中的invoke方法需要一个Invocation类型的参数,很容易想象,Invocation是就是会话域,它持有调用过程中的变量,比如方法名,参数等。

Protocol、Invoker和Invocation是Dubbo中有3个很重要的概念,一切都是围绕这三个展开的。

现在我们继续来看ServiceBean和ReferenceBean。

配置与实际调用的关联

Consumer之ReferenceBean

在这里插入图片描述
ReferenceBean实现了FactoryBean接口,用于根据加载的配置(xml中dubbo:reference节点的配置或者@Reference注解的配置)创建bean实例。创建实例的过程就是创建引用接口的代理,ReferenceConfig中有个ref属性用于引用该接口的代理。

public class ReferenceBean<T> extends ReferenceConfig<T> implements FactoryBean, ApplicationContextAware, InitializingBean, DisposableBean {
    public Object getObject() throws Exception {
        return get();
    }
}

public class ReferenceConfig<T> extends AbstractReferenceConfig {

    private static final Protocol refprotocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();

    // 接口代理类引用
    private transient volatile T ref;
    private transient volatile Invoker<?> invoker;

    public synchronized T get() {
        if (destroyed){
            throw new IllegalStateException("Already destroyed!");
        }
        if (ref == null) {
            init();
        }
        return ref;
    }

    private void init() {
        ...
    // 创建引用的代理,这里的map是一些配置信息,如interface、methods、retried、application、dubbo(版本)等信息
        ref = createProxy(map);
        ...
    }

    private T createProxy(Map<String, String> map) {
        ...
        // Protocol映射的远程服务的Invoker,有对应Protocol有对应的Invoker实现
        invoker = refprotocol.refer(interfaceClass, urls.get(0));
        ... 
        // 根据invoker创建服务代理
        return (T) proxyFactory.getProxy(invoker);
    }
}

根据代码,我们可以发现ReferenceBean在get Bean的时候就是调用ReferenceConfig的get方法,而get方法最终调用的是createProxy方法,着重看这个方法,会先通过refprotocol的refer方法获得Invoker,refprotocol实际上就是通过ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension()创建的Protocol,而后再由Invoker创建该接口的代理。这样ReferenceBean中接口类代理的创建过程就很清晰了。
在这里插入图片描述

Provider之ServiceBean

ServiceBean实现了ApplicationListener接口,在容器初始化后调用Protocol的export方法暴露服务。

public class ServiceBean<T> extends ServiceConfig<T> implements InitializingBean, DisposableBean, ApplicationContextAware, ApplicationListener, BeanNameAware {
	public void onApplicationEvent(ApplicationEvent event) {
        if (ContextRefreshedEvent.class.getName().equals(event.getClass().getName())) {
        	if (isDelay() && ! isExported() && ! isUnexported()) {
                if (logger.isInfoEnabled()) {
                    logger.info("The service ready on spring started. service: " + getInterface());
                }
                export();
            }
        }
    }
}

public class ServiceConfig<T> extends AbstractServiceConfig {

    private static final Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();

    // 接口实现类引用
    private T                   ref;
    
    public synchronized void export() {
    	...
    	doExport();
    	...
    }

    protected synchronized void doExport() {
    	...
    	doExportUrls();
    	...
    }

    /**
     * 广播注册中心暴露发现服务地址
     **/
    private void doExportUrls() {
    	// 将registry配置转化成url,例如:registry://host:port/com.alibaba.dubbo.registry.RegistryService?application=xxx&pid=xxx&registry=zookeeper&timestamp=xxx
        List<URL> registryURLs = loadRegistries(true);
        // 这里的protocols是List<ProtocolConfig>,按指定支持的协议暴露服务
        for (ProtocolConfig protocolConfig : protocols) {
            doExportUrlsFor1Protocol(protocolConfig, registryURLs);
        }
    }
    
    private void doExportUrlsFor1Protocol(ProtocolConfig protocolConfig, List<URL> registryURLs) {
        ...
        // 将接口实体类引用转化成Invoker
        Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, registryURL.addParameterAndEncoded(Constants.EXPORT_KEY, url.toFullString()));

        Exporter<?> exporter = protocol.export(invoker);
        exporters.add(exporter);
        ...
    }
}

根据代码,我们可以发现在Spring在调用onApplicationEvent方法的时候,会调用export方法,而这个方法最终会调用doExportUrlsFor1Protocol方法,方法中很重要的一段逻辑是通过ProxyFactory根据引用的接口对象和拼装的URL生成Invoker,而后通过Protocol暴露出来。Provider暴露一个服务的详细过程如下:
在这里插入图片描述

现在我们就可以回答Protocol是如何和服务建立联系的?

ReferenceConfig和ServiceConfig都有一个属性Protocol,服务与Protocol间就是通过这个属性建立联系的。

细心地你会发现ReferenceConfig和ServiceConfig的Protocol属性是static final的,我们知道Dubbo是支持多协议的,那又是怎么实现的呢?

扩展点加载ExtensionLoader

ReferenceConfig和ServiceConfig的Protocol属性赋值是通过ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension()赋值的,深入代码,我们可以发现,它返回的是动态生成的实现Protocol接口的类,生成的类会对打了@Adaptive标注的方法重新生成方法,生成的方法调用逻辑是根据传入参数的协议号动态调用对应协议的方法。

public class ExtensionLoader<T> {
	public T getAdaptiveExtension() {
        Object instance = cachedAdaptiveInstance.get();
        if (instance == null) {
            ...
            instance = createAdaptiveExtension();
            cachedAdaptiveInstance.set(instance);
            ...
        }

        return (T) instance;
    }

    private T createAdaptiveExtension() {
        try {
            return injectExtension((T) getAdaptiveExtensionClass().newInstance());
        } catch (Exception e) {
            throw new IllegalStateException("Can not create adaptive extenstion " + type + ", cause: " + e.getMessage(), e);
        }
    }

    private Class<?> getAdaptiveExtensionClass() {
        getExtensionClasses();
        if (cachedAdaptiveClass != null) {
            return cachedAdaptiveClass;
        }
        return cachedAdaptiveClass = createAdaptiveExtensionClass();
    }

    private Class<?> createAdaptiveExtensionClass() {
        String code = createAdaptiveExtensionClassCode();
        ClassLoader classLoader = findClassLoader();
        com.alibaba.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
        return compiler.compile(code, classLoader);
    }

    private String createAdaptiveExtensionClassCode() {
        StringBuilder codeBuidler = new StringBuilder();
        ...
        codeBuidler.append("package " + type.getPackage().getName() + ";");
        codeBuidler.append("
import " + ExtensionLoader.class.getName() + ";");
        codeBuidler.append("
public class " + type.getSimpleName() + "$Adpative" + " implements " + type.getCanonicalName() + " {");
        ...
        return codeBuidler.toString();
    }
}

附.通过ExtensionLoader动态生成的实现Protocol接口的类

package com.alibaba.dubbo.rpc;

import com.alibaba.dubbo.common.extension.ExtensionLoader;

public class Protocol$Adpative implements com.alibaba.dubbo.rpc.Protocol {

    public void destroy() {
        throw new UnsupportedOperationException(
                                                "method public abstract void com.alibaba.dubbo.rpc.Protocol.destroy() of interface com.alibaba.dubbo.rpc.Protocol is not adaptive method!");
    }

    public int getDefaultPort() {
        throw new UnsupportedOperationException(
                                                "method public abstract int com.alibaba.dubbo.rpc.Protocol.getDefaultPort() of interface com.alibaba.dubbo.rpc.Protocol is not adaptive method!");
    }

    public com.alibaba.dubbo.rpc.Exporter export(com.alibaba.dubbo.rpc.Invoker arg0)
                                                                                    throws com.alibaba.dubbo.rpc.Invoker {
        if (arg0 == null) throw new IllegalArgumentException("com.alibaba.dubbo.rpc.Invoker argument == null");
        if (arg0.getUrl() == null) throw new IllegalArgumentException(
                                                                      "com.alibaba.dubbo.rpc.Invoker argument getUrl() == null");
        com.alibaba.dubbo.common.URL url = arg0.getUrl();
        String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol());
        if (extName == null) throw new IllegalStateException(
                                                             "Fail to get extension(com.alibaba.dubbo.rpc.Protocol) name from url("
                                                                     + url.toString() + ") use keys([protocol])");
        com.alibaba.dubbo.rpc.Protocol extension = (com.alibaba.dubbo.rpc.Protocol) ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.rpc.Protocol.class).getExtension(extName);
        return extension.export(arg0);
    }

    public com.alibaba.dubbo.rpc.Invoker refer(java.lang.Class arg0, com.alibaba.dubbo.common.URL arg1)
                                                                                                       throws java.lang.Class {
        if (arg1 == null) throw new IllegalArgumentException("url == null");
        com.alibaba.dubbo.common.URL url = arg1;
        String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol());
        if (extName == null) throw new IllegalStateException(
                                                             "Fail to get extension(com.alibaba.dubbo.rpc.Protocol) name from url("
                                                                     + url.toString() + ") use keys([protocol])");
        com.alibaba.dubbo.rpc.Protocol extension = (com.alibaba.dubbo.rpc.Protocol) ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.rpc.Protocol.class).getExtension(extName);
        return extension.refer(arg0, arg1);
    }
}

小结

Dubbo通过扩展Spring Schema完成自定义bean的IoC容器注入。在Spring注入和初始化bean时,通过Protocol和ProxyFactory配合完成服务的暴露和服务代理的创建。这种全Spring的配置方式,使得用户能够透明化接入应用,并且对应用没有任何API侵入。

在Dubbo的服务实际调用中都是围绕着Protocol、Invoker和Invocation三者,这也是DUBBO的核心领域模型:

  • Protocol 是服务域,它是 Invoker 暴露和引用的主功能入口,它负责 Invoker 的生命周期管理。
  • Invoker 是实体域,它是 Dubbo 的核心模型,其它模型都向它靠扰,或转换成它,它代表一个可执行体,可向它发起 invoke 调用,它有可能是一个本地的实现,也可能是一个远程的实现,也可能一个集群实现。
  • Invocation 是会话域,它持有调用过程中的变量,比如方法名,参数等。

参考

Dubbo官方文档

相关文章:

  • 开源治理:安全的关键
  • 什么是快应用?与原生APP相比优势在哪里
  • 卷积神经网络结构有哪些,卷积神经网络结构特点
  • 阿里内部首发面试终极指南V3.0,相对一线大厂面试知识点+面试题
  • vue路由原理
  • idea常用快捷键和插件
  • 04_feign介绍(OpenFeign)
  • (已更新)关于Visual Studio 2019安装时VS installer无法下载文件,进度条为0,显示网络有问题的解决办法
  • 长安链源码学习v2.2.1--ioc机制(九)
  • 面试必备:《Java 最常见 200+ 面试题全解析》
  • 抖音短视频运营规划内容孵化计划书模板
  • Leetcode 946.验证栈序列
  • CREO:利用CREO软件实现装配设计之四连杆机构设计案例应用(图文教程)之详细攻略
  • 基于数字孪生的智慧城市是如何发展的?
  • STL——list使用和模拟
  • create-react-app项目添加less配置
  • CSS居中完全指南——构建CSS居中决策树
  • Golang-长连接-状态推送
  • IP路由与转发
  • JavaWeb(学习笔记二)
  • PHP CLI应用的调试原理
  • React 快速上手 - 07 前端路由 react-router
  • spark本地环境的搭建到运行第一个spark程序
  • Vue2 SSR 的优化之旅
  • vue的全局变量和全局拦截请求器
  • 百度贴吧爬虫node+vue baidu_tieba_crawler
  • 从setTimeout-setInterval看JS线程
  • 紧急通知:《观止-微软》请在经管柜购买!
  • 聊聊redis的数据结构的应用
  • 微服务核心架构梳理
  • 项目管理碎碎念系列之一:干系人管理
  • 小试R空间处理新库sf
  • UI设计初学者应该如何入门?
  • 如何在招聘中考核.NET架构师
  • # include “ “ 和 # include < >两者的区别
  • # 达梦数据库知识点
  • ###项目技术发展史
  • #define、const、typedef的差别
  • #我与Java虚拟机的故事#连载03:面试过的百度,滴滴,快手都问了这些问题
  • (c语言)strcpy函数用法
  • (使用vite搭建vue3项目(vite + vue3 + vue router + pinia + element plus))
  • (四)Android布局类型(线性布局LinearLayout)
  • (转)IOS中获取各种文件的目录路径的方法
  • (最优化理论与方法)第二章最优化所需基础知识-第三节:重要凸集举例
  • .NET Core WebAPI中封装Swagger配置
  • .NET 程序如何获取图片的宽高(框架自带多种方法的不同性能)
  • .net程序集学习心得
  • .Net转前端开发-启航篇,如何定制博客园主题
  • @DateTimeFormat 和 @JsonFormat 注解详解
  • @FeignClient 调用另一个服务的test环境,实际上却调用了另一个环境testone的接口,这其中牵扯到k8s容器外容器内的问题,注册到eureka上的是容器外的旧版本...
  • @Resource和@Autowired的区别
  • @Transactional注解下,循环取序列的值,但得到的值都相同的问题
  • [8-27]正则表达式、扩展表达式以及相关实战
  • [AI]文心一言爆火的同时,ChatGPT带来了这么多的开源项目你了解吗
  • [BUG] Authentication Error