高性能可扩展分布式RPC框架Dubbo-内核原理揭秘

一、前言

整体来说,一个公司业务系统的演进流程基本都是从单体应用到多体应用。在单体应用时,不同业务模块相互调用直接在本地 JVM 进程内就可以完成;而变为多个应用时,相互之间进行通信的方式就不能简单的进行本地调用了,因为不同业务模块部署到了不同的 JVM 进程里面,更常见的是部署到了不同的机器,这时候一个高效、稳定的 RPC 远程调用框架就变得非常重要。

Dubbo作为阿里巴巴开发的一个开源的高性能的RPC调用框架,其致力于提供高性能和透明化的 RPC 远程调用服务解决方案。作为阿里巴巴 SOA 服务化治理方案的核心框架,目前它已进入 Apache 孵化器顶级项目,前景可谓无限光明。

二、Dubbo-基础篇

2.1 Dubbo系统组成概述

使用Dubbo框架搭建的系统架构如下:640?wx_fmt=png

如上图是 Dubbo 的架构图,其中:

  • 服务提供方在启动时候会注册自己提供的服务到服务注册中心。

  • 服务消费方在启动时候会去服务注册中心订阅自己需要的服务的地址列表,然后服务注册中心异步把消费方需要的服务接口的提供者的地址列表返回给服务消费方,服务消费方根据路由规则和设置的负载均衡算法选择一个服务提供者 IP 进行调用。

  • 监控平台主要用来统计服务的调用次数和调用耗时,服务消费者和提供者,在内存中累计调用次数和调用耗时,并定时每分钟发送一次统计数据到监控中心,监控中心则使用数据绘制图表来显示,监控平台不是分布式系统必须的,但是这些数据有助于系统运维和调优。服务提供者和消费者可以直接配置监控平台的地址,也可以通过服务注册中心来获取。

  • 服务注册中心则负责服务注册与发现,常见的服务注册中心有zookeeper、etcd。

2.2 Dubbo基础

本节主要简单的讲解Dubbo如何使用,以及本书中的demo实例,建议读者先阅读基础篇在进入后面的章节,因为后面章节基本是基于本章的demo进行讲解的。

demo中 Consumer 模块为服务消费者相关,本书中所有与消费端有关的demo都在该模块中,包含普通调用、各种异步调用、泛化调用、基于扩展接口实现的自定义负载均衡策略、集群容错策略等等。

其中 Provider 模块为服务提供者相关,本书中所有与服务提供端有关的demo都在该模块中,包含服务接口的实现类、服务提供方的同步处理请求、各种异步处理请求的实现等等。

其中 SDK 模块是一个二方包,用来存放服务接口,这是为了代码复用,在服务提供者和消费者(泛化调用除外)的模块里面都需要引入这个二方包。

三、Dubbo-高级篇

3.1 Dubbo分层架构

本节我们从整体上来看看 Dubbo 的分层架构设计,架构分层是一个比较经典的模式,比如网络中的7层协议,每层执行固定的功能,上层依赖下层提供的功能,下层对上层的提供功能,下层的改变对上层不可见,并且每层都是一个可被替换的组件。

如下图是 Dubbo 官方提供的Dubbo的整体架构图:

640?wx_fmt=png

Dubbo 官方提供的该架构图很复杂,一开始我们没必要深入细节,下面我们简单讲解下其中的主要模块:

  • 其中 Service 和 Config 层为 API接口层,是为了方便的让Dubbo使用方发布服务和引用服务;对于服务提供方来说需要实现服务接口,然后使用 ServiceConfig API 来发布该服务;对于服务消费方来说需要使用ReferenceConfig 对服务接口进行代理。Dubbo服务发布与引用方可以直接初始化配置类,也可以通过 Spring 配置自动生成配置类。

  • 其它各层均为 SPI层,SPI 意味着下面各层都是组件化可以被替换的,这也是 Dubbo 设计的比较好的一点。Dubbo 增强了 JDK 中提供的标准 SPI 功能,在 Dubbo 中除了 Service 和 Config 层外,其它各层都是通过实现扩展点接口来提供服务的;Dubbo 增强的 SPI 增加了对扩展点 IoC 和 AOP 的支持,一个扩展点可以直接 setter 注入其它扩展点;并且不会一次性实例化扩展点的所有实现类,这避免了当扩展点实现类初始化很耗时,但当前还没用上它的功能时仍进行加载实例化,浪费资源的情况;增强的 SPI 是在具体用某一个实现类的时候才对具体实现类进行实例化。后续会具体讲解 Dubbo 增强的 SPI 的实现原理。

  • Proxy 服务代理层:该层主要是对服务消费端使用的接口进行代理,把本地调用透明的转换为远程调用;另外对服务提供方的服务实现类进行代理,把服务实现类转换为 Wrapper 类,这是为了减少反射的调用,后面会具体讲解到。Proxy层的SPI扩展接口为 ProxyFactory,Dubbo 提供的实现主要有 JavassistProxyFactory(默认使用)和 JdkProxyFactory,用户可以实现ProxyFactory SPI接口,自定义代理服务层的实现。

  • Registry 服务注册中心层:服务提供者启动时候会把服务注册到服务注册中心,消费者启动时候会去服务注册中心获取服务提供者的地址列表,Registry层主要功能是封装服务地址的注册与发现逻辑,扩展接口 Registry 对应的扩展实现为 ZookeeperRegistry、RedisRegistry、MulticastRegistry、DubboRegistry等。扩展接口 RegistryFactory 对应的扩展接口实现为 DubboRegistryFactory、DubboRegistryFactory、RedisRegistryFactory、ZookeeperRegistryFactory。另外该层扩展接口Directory实现类有RegistryDirectory、StaticDirectory用来透明的把invoker列表转换为一个invoker;用户可以实现该层的一系列扩展接口,自定义该层的服务实现。

  • Cluster 路由层:封装多个服务提供者的路由规则、负载均衡、集群容错的实现,并桥接服务注册中心;扩展接口 Cluster 对应的实现类有 FailoverCluster(失败重试)、FailbackCluster(失败自动恢复)、FailfastCluster(快速失败)、FailsafeCluster(失败安全)、ForkingCluster(并行调用)等;负载均衡扩展接口 LoadBalance 对应的实现类为 RandomLoadBalance(随机)、RoundRobinLoadBalance(轮询)、LeastActiveLoadBalance(最小活跃数)、ConsistentHashLoadBalance(一致性hash)等。用户可以实现该层的一系列扩展接口,自定义集群容错和负载均衡策略。

  • Monitor 监控层:用来统计RPC 调用次数和调用耗时时间,扩展接口为 MonitorFactory,对应的实现类为 DubboMonitorFactroy。用户可以实现该层的MonitorFactory扩展接口,实现自定义监控统计策略。

  • Protocol 远程调用层:封装 RPC 调用逻辑,扩展接口为 Protocol, 对应实现有 RegistryProtocol、DubboProtocol、InjvmProtocol 等。

  • Exchange 信息交换层:封装请求响应模式,同步转异步,扩展接口 Exchanger,对应扩展实现有 HeaderExchanger 等。

  • Transport 网络传输层:抽象 mina 和 netty 为统一接口。扩展接口为 Channel,对应实现有 NettyChannel(默认)、MinaChannel 等;扩展接口Transporter对应的实现类有GrizzlyTransporter、MinaTransporter、NettyTransporter(默认实现);扩展接口Codec2对应实现类有DubboCodec、ThriftCodec等

  • Serialize 数据序列化层:提供可以复用的一些工具,扩展接口为 Serialization,对应扩展实现有 DubboSerialization、FastJsonSerialization、Hessian2Serialization、JavaSerialization等,扩展接口ThreadPool对应扩展实现有 FixedThreadPool、CachedThreadPool、LimitedThreadPool 等。

综上可知Dubbo的分层架构使得Dubbo的每层的功能都是可被替换的,这使得Dubbo的扩展性极强,上面说了那么多关于扩展点的东西,那么具体什么是扩展点呢,下面看下 Dubbo 扩展点一个简单例子。以扩展点 Protocol 为例:

@SPI("dubbo")	
public interface Protocol {	
...	
}

扩展点接口的类上面都含有@SPI注解,这里注解里面的"dubbo"说明Protocol扩展接口SPI的默认实现是DubboProtocol。

如果我们想自己写一个 Protocol 扩展接口的实现类,那么我们需要在实现类所在的 Jar 包内的 META-INF/dubbo/ 目录下创建一个名字为 org.apache.dubbo.rpc.Protocol 的文本文件,然后配置它的内容为:

myprotocol=com.alibaba.user.MyProtocol

假设该实现类 MyProtocol 的内容如下:

package com.alibaba.user;	
public class MyProtocol implemenets Protocol {	
// ...	
}

那么如何使用我们自定义的扩展实现呢?Dubbo 配置模块中,扩展点均有对应配置属性或标签,如下代码通过配置标签方式指定使用哪个扩展实现:

<dubbo:protocol name="myprotocol" />

注意这里的 name 必须与 jar 包内 META-INF/dubbo/ 目录下 org.apache.dubbo.rpc.Protocol 文件中的等号左侧的key的名字一致。

3.2 Dubbo内核原理

在Dubbo中框架的可扩展性是靠适配器原理结合增强SPI机制实现的,本书中首先会讲解Dubbo的适配器原理,什么是适配器模式?比如dubbo提供的扩展接口Protocol,Protocol的定义如下:

@SPI("dubbo")	
public interface Protocol {	
    @Adaptive	
    <T> Exporter<T> export(Invoker<T> invoker) throws RpcException;	
    ....	
}

Dubbo则会使用本书介绍的动态编译技术为接口Protocol生成一个适配器类 Protocol$Adaptive的对象实例,Dubbo框架中需要使用Protocol的实例的时候实际就是使用的 Protocol$Adaptive的对象实例来获取具体SPI实现类,其代码如下:

package org.apache.dubbo.rpc;	
...	
public class Protocol$Adaptive implements Protocol {   	
 ...	
public Exporter export(Invoker invoker) throws RpcException {	
    String string;	
    ...	
    //(1)	
    URL uRL = invoker.getUrl();	
    String string2 = string = uRL.getProtocol() == null ? "dubbo" : uRL.getProtocol();	
    if (string == null) {	
        throw new IllegalStateException(new StringBuffer().append("Failed to get extension (org.apache.dubbo.rpc.Protocol) name from url (").append(uRL.toString()).append(") use keys([protocol])").toString());	
    }	
    //(2)	
    Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getExtension(string);	
    //(3)	
    return protocol.export(invoker);	
}

在dubbo框架中protocol的一个定义为:privatestaticfinalProtocolprotocol=ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();当调用 protocol.export(wrapperInvoker)时候,实际是调用的 Protocol$Adaptive的对象实例的export方法,然后后者根据wrapperInvoker中的url里面的协议类型参数执行代码(2)使用Dubbo增强SPI方法getExtension获取对应的SPI实现类,然后调用代码(3)执行具体SPI实现类的export方法。

然后本书会讲解Dubbo增强的SPI机制,本书中首先会借助 java.sql.Driver扩展接口讲解标准JDK中的SPI实现原理以及缺陷,然后讲解dubbo的增强SPI如何对其进行改进,如何实现的扩展接口之间自动IOC和扩展接口的功能增强AOP功能。

然后会讲解Dubbo使用JavaAssist减少反射调用开销:Dubbo会给每个服务提供者的实现类生产一个Wrapper类,这个wrapper类里面最终调用服务提供者的接口实现类,wrapper类的存在是为了减少反射的调用。当服务提供方接受到消费方发来的请求后需要根据消费者传递过来的方法名和参数反射调用服务提供者的实现类,而反射本身是有性能开销的,所以dubbo把每个服务提供者的实现类通过JavaAssist包装为一个Wrapper类,那么Wrapper类为何能减少反射调用那?观看本书就可以找到答案

3.3 Dubbo功能实现原理

讲解完毕支撑Dubbo框架的内核原理后,本书会先从整体剖析Dubbo服务提供端如何发布服务的,这包含发布本地服务和发布远程服务的流程, Dubbo服务导出分 本地导出与远程导出,本地导出使用了 injvm 协议,是一个伪协议,它不开启端口,不发起远程调用,只在 JVM 内直接关联,但执行 Dubbo 的 Filter 链;默认下 Dubbo 同时支持本地导出与远程导出协议,可以通过ServiceConfig的setScope方式设置,其中配置为none表示不导出服务,为remote表示只导出远程服务,为local表示只导出本地服务。

你会知道Dubbo如何实现的服务延迟发布,如何把服务实现类转换为 Wrapper 类,以便减少反射的调用,什么时候构建的dubbo的Filter链,都有哪些Wrapper类对扩展接口的实现类进行了功能增强?如何启动的NettyServer对服务进行监听,同一个机器上的多个服务提供接口是启动多个NettyServer还是一个?如何做到的?如何注册服务到服务注册中心的?服务注册到zookeeper后,其存储结构是怎么样的?

然后本书会讲解当服务提供方接受到请求后,如何进行处理的,这包含Filter链对请求的处理,以及如何找到对应的被wrapper类包装后的服务实现类,并对请求进行处理,如何实现的Dubbo的服务提供端异步执行。

然后会讲解Dubbo服务消费端的启动流程,这个过程,你会知道如何基于Proxy SPI扩展实现对服务接口进行代理。与服务提供端一样,消费端可以设置是否需要本地服务引用,你会知道在消费端如果没有指定scope类型,在启动时候会检查当前jvm内是否有导出的服务,如果有则自动开启本地引用(也就是协议类型修改为injvm),则具体调用时候会使用本地暴露的服务来提供服务,而不发起远程调用。

当具体发起远程调用时候,你会知道如何动态从服务注册中心动态订阅服务信息的,比如订阅服务提供者地址列表,服务降级信息,服务路由信息,以及Directory目录与Router路由服务,以及什么时候构建的路由规则链。

如何启动NettyClient具体发起远程调用的。然后你会知道同一个服务提供者机器可以提供多个服务,那么消费者机器需要与同一个服务提供者机器提供的多个共享连接还是与每个服务都建立一个?消费端是启动时候就与服务提供者机器建立好连接?

然后会讲解具体如何发起一次远程调用,这个过程你会知道当发起一次rpc调用时候会先经过MockInvoker进行处理,其会看是否设置了 force:return 降级策略,如果设置了则直接返回 mock 值,并不发起远程调用;否者发起远程调用,如果远程调用结果 OK,则直接返回远程调用返回的结果;如果远程调用失败了,则看当前是否设置了 fail:return 的降级策略,如果设置了,则直接返回 mock 值,否者返回调用远程服务失败的具体原因。

如果没有设置服务降级策略或者mock服务,则会基于SPI机制选择具体的集群容错策略(本文会详细讲解常见的Failover、Failfast、Failsafe、Forking、Broadcast这几种集群容错实现原理,以及讲解如何自己基于SPI实现自己的容错策略),具体集群容错策略内有会根据SPI机制选择设置的服务负载均衡策略(本文会详细介绍常见的Random、RoundRobin、LeastActive、ConsistentHash),具体负载均衡策略内会基于SPI选择设置的服务目录实现,其内部维护了所有服务提供者的服务提供者列表与路由规则,负载均衡策略则会从符合路由规则的地址列表里面选择一个invoker返回,然后最终有该invoker执行。如果执行失败了,则根据具体集群容错策略重新选择一个invoker进行执行....

然后本书会讲解Dubbo线程模型与线程池策略,Dubbo 默认的底层网络通讯使用的是 Netty ,服务提供方 NettyServer 使用两级线程池,其中 EventLoopGroup(boss) 主要用来接受客户端的链接请求,并把接受的请求分发给 EventLoopGroup(worker) 来处理,boss 和 worker 线程组我们称之为 IO 线程。

如果服务提供方的逻辑能迅速完成,并且不会发起新的 IO 请求,那么直接在 IO 线程上处理会更快,因为这减少了线程池调度与上下文切换开销。但如果处理逻辑较慢,或者需要发起新的 IO 请求,比如需要查询数据库,则 IO 线程必须派发请求到新的线程池进行处理,否则 IO 线程会被阻塞,将导致不能接收其它请求。

Dubbo中在服务提供端与消费端的IO线程对请求处理时候默认是把请求转交给dubbo框架的内部线程池来进行处理的,以便可以及时释放IO线程。

根据IO线程把什么类型的消息或者请求交给内部线程池来处理,dubbo提供了不同的线程模型,本书主要讲解Dubbo提供的线程模型AllDispatcher、DirectDispatcher、MessageOnlyDispatcher、ExecutionDispatcher、ConnectionOrderedDispatcher的实现原理,以及线程池策略FixedThreadPool、LimitedThreadPool、EagerThreadPool、CachedThreadPool的实现原理,以及如何基于SPI自定义自己的线程模型与线程池策略。

基础篇我们讲解到,基于Dubbo APi搭建Dubbo服务时候,服务消费端引入了一个 SDK 二方包,里面存放着服务提供端提供的所有接口类,泛化接口调用方式主要在服务消费端没有 API 接口类及模型类元(比如入参和出参的 POJO 类)的情况下使用。其参数及返回值中没有对应的 POJO 类,所以所有 POJO 均转换为 Map 表示。使用泛化调用时候服务消费模块不再需要引入 SDK 二方包,本书会详细介绍Dubbo中nativejava,true, bean三种泛化调用的实现。

基础篇我们讲到Dubbo提供了隐式参数传递的功能,即服务调用方可以通过RpcContext.getContext().setAttachment()方法设置附加属性键值对,然后设置的值对可以在服务提供方服务方法内获取;本书我们会详细介绍如何在在消费端设置参数,并且如何通过网络把参数传递到服务提供方,然后服务提供方如何进行获取。

正如Dubbo官网所说dubbo从2.7.0版本开始支持所有异步编程接口以CompletableFuture为基础,以便解决2.7.0之前版本异步调用的不便与功能缺失。

异步调用实现是基于 NIO 的非阻塞能力实现并行调用,服务消费端不需要启动多线程即可完成并行调用多个远程服务,相对多线程开销较小,如下图是Dubbo异步调用链路概要流程图图:640?wx_fmt=png

本书我们首先讲解dubbo服务消费端的异步调用,首先讲解2.7.0版本前的异步调用实现原理,我们会知道future调用get()方法方式实现异步缺点是当业务线程调用get()方法后业务线程会被阻塞,这不是我们想要的,所以dubbo2.7.0版本提供了在CompletableFuture对象上设置回调函数的方式,让我们实现真正的异步调用。

在Provider端非异步执行时候,其对调用方发来的请求的处理是在Dubbo内部线程模型的线程池中的线程来执行的,在dubbo中服务提供方提供的所有的服务接口都是使用这一个线程池来执行的,所以当一个服务执行比较耗时时候,可能会占用线程池中很多线程,这可能就会导致其他服务的处理收到影响。

Provider端异步执行则将服务的处理逻辑从Dubbo内部线程池切换到业务自定义线程,避免Dubbo线程池中线程被过度占用,有助于避免不同服务间的互相影响。

但是需要注意provider端异步执行对节省资源和提升RPC响应性能是没有效果的,这时是因为如果服务处理比较耗时,虽然不是使用Dubbo框架内部线程处理,但是还是需要业务自己的线程来处理,另外副作用还有会新增一次线程上下文切换(从dubbo内部线程池线程切换到业务线程),模型如下图11.2.0

640?wx_fmt=png

本书首先会讲解基于定义CompletableFuture签名的接口实现异步执行的实现原理,然后讲解使用AsyncContext实现异步执行原理,最后讲解Dubbo的异步调用与执行引入的新问题以及如何解决的,这包含引入异步调用时候等结果返回后Filter链得不到执行的问题,以及异步执行时候上下文参数传递问题。

前面章节我们介绍了服务消费端一次服务调用流程与服务提供端一次服务处理流程,但是还是有一些东西是我们没有提到的,比如服务消费端如何把服务请求信息序列化为二进制、服务提供方又是如何把消费端发送的二进制数据反序列化为可识别的POJO对象、比如Dubbo的应用层协议是怎么样的。本书我们就来一一来看dubbo是如何做这些的。

本书会首先讲解Dubbo协议,在TCP协议栈中,每层协议都有自己的协议报文格式,比如TCP协议是网络七层模型中的传输层,有TCP协议报文格式;在TCP上层是应用层,应用层协议常见的有http协议等,Dubbo协议作为建立在TCP协议之上的一种应用层协议,自然也有自己的协议包格式,Dubbo协议也是参考TCP协议栈中的协议,协议内容由header和body两部分组成,本书会详细介绍协议header中每个字段含义。然后讲解服务消费方编码原理,包含当服务消费端发送请求时候,如何把请求内容封装为Dubbo协议帧的。然后讲解服务提供方接受请求后如何对协议帧进行解码解决半包粘包问题的。

四、Dubbo-实践篇

实践篇我们来探讨如何使用Arthas和一些demo来对研究Dubbo框架实现提供便捷,并且基于Netty与CompletableFuture模拟了RPC同步与纯异步调用。

首先本书会介绍如何安装Arthas,然后讲解如何使用Arthas查看查看扩展接口适配器类的源码,查看服务提供端Wrapper类的源码,如何查询Dubbo启动后都有哪些Filter,然后通过Demo验证RoundRobin LoadBalance负载均衡原理,然后探讨如果根据IP动态路由调用Dubbo服务。

Dubbo的服务消费端基于CompletableFuture实现了功能比较丰富的纯异步调用,其实还不单单是CompletableFuture的功劳,归根到底是Netty的NIO非阻塞功能提供的底层实现,本文我们就来基于CompletableFuture与Netty来模拟下如何异步发起远程调用,以及如何使用CompletableFuture本身的功能,让多个请求的异步结果进行运算,以便加深对dubbo异步调用实现原理的理解。

五、总结

如何你对上面内容感兴趣,想深入研究,但是无从入手,那么机会来了,专栏内容包含但是不限于上述内容,大家可以扫描订阅该专栏,专栏地址 左下角阅读原文。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值