Dubbo架构进阶
Dubbo架构主要包含四个角色:消费者、提供者、注册中心和监控系统,如下图所示。
具体的交互流程是:消费者(Consumer)通过注册中心获取提供者(Provider)节点后,通过Dubbo的客户端SDK与Provider建立连接,并发起调用。Provider通过Dubbo的服务端SDK接收Consumer的请求,处理后再把结果返回给Consumer。
对于采用Dubbo进行RPC调用的解决方案,消费者和提供者都需要引入Dubbo的SDK来完成远程调用。因为Dubbo本身是采用Java实现的,所以要求服务消费者和服务提供者也都必须采用Java实现。不过开源社区已经开始使用对核心扩展点进行TCK(Technology CompatibilityKit)提升框架的兼容性。它为用户增加一种扩展实现,只需通过TCK,即可确保与框架的其他部分兼容运行,可以有效提高健壮性,也方便第三方接入。下面是Dubbo的官方详细架构。
- 左边部分是服务消费者使用的接口,右边部分是服务提供者使用的接口,位于中轴线上的为双方都用到的接口。
- 从下至上分为十层,各层均为单向依赖,右边的黑色箭头代表层之间的依赖关系,每一层都可以剥离上层被复用,其中Service 和 Config 层 为 API , 其 他 各 层 均 为 SPI ( ServiceProvider Interface)。
- 浅色小块为扩展接口,深色小块为实现类,图中只显示用于关联各层的实现类。
- 深色虚线为初始化过程,即启动时组装链,红色实线为方法调用过程,即运行时调时链,紫色三角箭头为继承(读者可到官网查看彩色图片),可以把子类看作父类的同一个节点,线上的文字为调用的方法。Dubbo服务调用过程Dubbo服务调用过程比较复杂,包含众多步骤,比如发送请求、编解码、服务降级、过滤器链处理、序列化、线程派发及响应请求等。
下面我们重点分析请求的发送与接收、编解码、线程派发及响应的发送与接收等过程。
Dubbo的服务调用过程
如下图所示。
首先服务消费者通过代理对象Proxy发起远程调用,接着通过网络客户端Client将编码后的请求发送给服务提供者的网络层,也就是Server。Server在收到请求后,首先要做的事情是对数据包进行解码。然后将解码后的请求发送至分发器Dispatcher,再由分发器将请求派发到指定的线程池上,最后由线程池调用具体的服务。这就是一个远程调用请求的发送与接收过程。
服务消费者发送请求
Dubbo支持同步和异步两种调用方式,其中异步调用还可细分为“有返回值”的异步调用和“无返回值”的异步调用。所谓“无返回值”的异步调用是指服务消费者只管调用,但不关心调用结果,此时Dubbo会直接返回一个空的RpcResult。若要使用异步特性,需要服务消费者手动进行配置。默认情况下,Dubbo使用同步调用方式。服务调用的线程栈快照如下图所示。
服务提供者接收请求
默认情况下,Dubbo使用Netty作为底层的通信框架。Netty首先会通过解码器对数据进行解码,并将解码后的数据传递给下一个处理器的指定方法。
解 码 器 将 数 据 包 解 析 成 Request 对 象 后 , NettyHandler 的messageReceived方法紧接着会收到这个对象,并将这个对象继续向下传 递 。 其 间 该 对 象 会 被 依 次 传 递 给 NettyServer 、MultiMessageHandler、HeartbeatHandler以及AllChannelHandler处理。最后由AllChannelHandler将该对象封装到Runnable实现类对象中,并将Runnable放入线程池中执行后续的调用逻辑,调用栈如下图所示。
Dispatcher就是线程派发器。需要说明的是,Dispatcher真实的职 责 是 创 建 具 有 线 程 派 发 能 力 的 ChannelHandler , 比 如AllChannelHandler、MessageOnlyChannelHandler和ExecutionChannelHandler等,其本身并不具备线程派发能力。Dubbo的5种不同的线程派发策略如下表所示。
默认配置下,Dubbo使用all派发策略,即将所有的消息都派发到线 程 池 。 请 求 对 象 会 被 封 装 在 ChannelEventRunnable 中 ,ChannelEventRunnable将会是服务调用过程的新起点。所以接下来我们看一下以ChannelEventRunnable为起点的服务提供者的线程调用栈,如下图所示。
向用户线程传递调用结果
响应数据解码完成后,Dubbo会将响应对象派发到线程池。要注意的是,线程池中的线程并非用户的调用线程,所以要想办法将响应对象从线程池传递到用户线程上。
用户线程在发送完请求后,调用DefaultFuture的get方法等待响应对象的到来。当响应对象到来后,用户线程会被唤醒,并通过调用编号获取属于自己的响应对象。
Dubbo设计原理
Dubbo在架构上通过SPI机制(SPI的全称为Service ProviderInterface,SPI机制是一种服务发现机制)的设计,使得整体架构具备了极高的可扩展性。
下面是Dubbo的核心设计原理:
- 采 用 Microkernel+Plugin 模 式 , Microkernel 负 责 组 装Plugin,Dubbo自身的功能也是通过扩展点实现的,也就是Dubbo的所有功能点都可被用户自定义扩展所替换。
- 采用URL作为配置信息的统一格式,所有扩展点都通过传递URL携带配置信息。
SPI机制的本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类,这样它可以在运行时动态为接口替换实现类。正因为此特性,我们可以通过SPI机制为程序提供拓展功能,这样可以在运行时动态为接口替换实现类。Dubbo就是通过SPI机制加载所有组件的,不过Dubbo并未使用Java原生的SPI机制,而是对其进行了增强,使其能够更好地满足需求。
Dubbo SPI示例
首先,我们定义一个接口,名称为Hello:
Dubbo SPI的相关逻辑被封装在了ExtensionLoader类中,通过ExtensionLoader类我们可以加载指定的实现类。Dubbo SPI所需的配置文件需放置在META-INF/dubbo路径下,配置内容如下:
与Java SPI实现类配置不同,Dubbo SPI通过键值对的方式进行配置,我们可以按需加载指定的实现类。另外,在测试Dubbo SPI时,需要在Robot接口上标注@SPI注解。
上述代码的输出结果如下:
SPI机制
下面我们结合源码来理解Dubbo的SPI机制和整体架构特性,需要明确几个核心概念,如下图所示。
- ExtensionLoader
ExtensionLoader作为整个SPI机制的核心起着无可替代的作用,扩展点并不会强制所有用户都使用Dubbo提供的某些架构。例如Dubbo提供了ZooKeeper注册中心,但是如果我们更倾向于其他的注册中心,我们可以替换掉Dubbo提供的注册中心。我们称这种可被替换的技术实现点为扩展点,类似的扩展点有很多,例如Protocol、Filter、Loadbalance等。
鉴 于 ExtensionLoader 的 用 法 比 较 多 , 下 面 我 们 以ExtensionLoader 类 作 为 入 口 进 行 讲 解 。 首 先 , 我 们 通 过ExtensionLoader的getExtensionLoader方法获取一个单例实例,然后通过ExtensionLoader的getExtension方法获取拓展类对象。其中,getExtensionLoader 方 法 用 于 从 缓 存 中 获 取 与 拓 展 类 对 应 的ExtensionLoader实例,若缓存未命中,则创建一个新的实例。下面我们以ExtensionLoader的getExtension方法作为入口,代码如下:
上面代码的逻辑比较简单,首先检查缓存,缓存未命中则创建拓展对象。下面我们来看一下创建实例化对象的代码实现:
createExtension模块中包含了如下步骤:
(1)通过getExtensionClasses获取所有的拓展类。
(2)通过反射创建拓展对象。
(3)向拓展对象中注入依赖。
(4)将拓展对象包裹在相应的Wrapper对象中。
我们在通过名称获取拓展类之前,需要根据配置文件解析出拓展项名称到拓展类的映射关系表(Map<名称,拓展类>),之后再根据拓展项名称从映射关系表中取出相应的拓展类即可。相关过程的代码如下:
这里也是先检查缓存,若缓存未命中则通过synchronized加锁,加锁后再次检查缓存,并判空。此时如果classes仍空,则通过loadExtensionClasses加载拓展类。
下面分析loadExtensionClasses方法的逻辑:
loadExtensionClasses方法总共做了两件事情,一是对SPI注解进行解析,二是调用loadDirectory方法加载指定目录中的配置文件。
SPI注解解析过程比较简单,loadDirectory方法先通过类加载器获 取 所 有 资 源 链 接 , 然 后 通 过 loadResource 方 法 加 载 资 源 。
loadResource方法用于读取和解析配置文件,并通过反射加载类,最后调用loadClass方法进行其他操作。Dubbo会从以下三个路径读取并加载扩展点配置文件:
- Wrapper
在实例化扩展点的代码中可以看到,在加载某个接口的扩展类时,如果某个实现中有一个拷贝类构造函数,那么该接口实现就是该接口的包装类,此时Dubbo会在真正的实现类上层包装上Wrapper。即这个时候从ExtensionLoader中返回的实际扩展类是被Wrapper包装的接口实现类。在上文代码的createExtension(String name)实例化扩展点中(代码1#)可以看到相关代码实现:将反射创建的instance实例作为参数传给Wrapper的构造方法,并通过反射创建Wrapper实例 , 而 后 在 Wapper 实 例 中 注 入 依 赖 , 最 后 将 Wapper 实 例 赋 值 给instance实例。
- Setter
Dubbo IoC通过setter方法注入依赖。Dubbo首先会通过反射获取实例的所有方法,然后遍历方法列表,检测方法名是否具有setter方法特征。若有这个特征则通过ObjectFactory获取依赖对象,最后通过反射调用setter方法将依赖设置到目标对象中。整个过程对应的注入扩展点代码如下:
扩展点实现类的成员如果为其他扩展点类型,ExtensionLoader会自动注入依赖的扩展点。ExtensionLoader通过扫描扩展点实现类的所有set方法来判定其成员。
- @SPI
在SPI代码实例中,Dubbo只有接口类使用了@SPI注解才会去加载扩展点实现,Dubbo本身重新实现了一套SPI机制,支持AOP与依赖注入,并且可以利用缓存提升加载实现类的性能,也支持实现类的灵活获取。下面是@SPI的定义:
在上文的loadExtensionClasses中(代码2#)中,我们可以看到getExtensionLoader会对传入的接口进行校验,其中就会检验接口是否被@SPI注解,通过获取并缓存接口的@SPI注解上的默认实现类cacheDefaultExtensionName,再调用loadDirectory方法记载指定目录中的配置文件。源码实现如下:
- @Adaptive
在 Dubbo 中 , 很 多 扩 展 都 是 通 过 SPI 机 制 进 行 加 载 的 , 比 如Protocol、Cluster、LoadBalance等。然而有些扩展并不想在框架启动阶段被加载,而是希望在扩展方法被调用时根据运行时参数进行加载。
在对自适应扩展生成过程进行深入分析之前,我们来看一下与自适应扩展息息相关的一个注解,即@Adaptive注解,该注解的定义如下:
@Adaptive可注解在类或方法上。当@Adaptive注解在类上时,Dubbo不会为该类生成代理类。当@Adaptive注解在方法(接口方法)上时,Dubbo则会为该方法生成代理逻辑。@Adaptive注解在类上的情况 很 少 , 在 Dubbo 中 仅 有 两 个 类 被 @Adaptive 注 解 了 , 分 别 是AdaptiveCompiler和AdaptiveExtensionFactory。
getAdaptiveExtension方法是获取自适应扩展的入口方法,相关代码如下:
getAdaptiveExtension方法首先会检查缓存,如果缓存未命中,则 调 用 方 法 创 建 自 适 应 扩 展 。 下 面 我 们 看 一 下createAdaptiveExtension方法的代码:
createAdaptiveExtension方法的代码包含了三个逻辑,分别如下:
○ 调 用 getAdaptiveExtensionClass 方 法 获 取 自 适 应 扩 展Class对象。
○ 通过反射进行实例化。
○ 调用injectExtension方法向扩展实例中注入依赖。
- @Activate
@Activate注解表示一个扩展是否被激活,可以放在类定义和方法上,Dubbo将它用在SPI扩展类定义上,表示这个扩展实现的激活条件和时机。下面是代码示例:
上 述 示 例 表 示 只 有 当 group 参 数 作 为 提 供 者 时 才 会 使RpcServerInterceptor拦截逻辑生效,这个注解的作用和Spring Boot中的@Condition注解类似。