您的位置:首页 > 其它

RPC框架(四)dubbo源码分析--dubbo基础预热

2018-03-05 18:00 330 查看
一、前言

二dubbo架构

三dubbo特性
3.1、连通性

3.2、健状性

3.3、伸缩性

四、zookeeper 注册中心流程

五、dubbo框架深入设计

六、动态编程:Javassist

七、用到的设计模式
7.1、工厂模式

7.2、装饰器模式

7.3、观察者模式

7.4、动态代理模式

八、SPI和扩展点
8.1、JAVA自带的SPI

8.2、dubbo框架做的修改

8.3、Protocol的扩展点文件

8.4、扩展点的加载方式
8.4.1、名称加载扩展点

8.4.2、加载激活扩展点

8.4.3、加载自适应扩展点

RPC框架(二)dubbo简介我们以示例的方式讲解了阿里dubbo服务治理框架基本使用。接下来将对dubbo的主要模块的设计原理和源码进行讲解,从而帮助读者理解dubbo是如何工作的。关于dubbo框架的使用可以参考dubbo的官方文档 http://dubbo.io/books/dubbo-user-book/

一、前言

dubbo代码量看起来不大,但实际上涉及到的知识点非常多; 因此建议对 java 开发处

于初学者就 不要看这么复杂的东西了;如果要看,需要按如下知识点去储备知识:

Java 语言编程知识(《java 编程思想》)

Spring 框架开发及应用,这里重点推荐夏昕写的两本《SpringGuide》、《Spring 实战》、牛逼一点的可以看看《Spring 源码解析》

Java 网络编程 《Java_TCPIP_Socket 网络编程.pdf》对 java 的 socket 编程有非常深入的介绍;《Java 网络编程》,另外这一部分一定要弄清楚 NIO,以及比较流行的网络编程框架,比如 mina, netty ,自己跑几个例子看看;可以参考我的IO系列文章。IO系统专栏

Java rpc 机制 从功能来看, dubbo 就是一个 rpc 框架,但是他实际上包含 rpc 相关的所有东 西,其底层实现还是这些java rpc 机制 ,参考我的RPC简介

Java 其他内容 此外,dubbo还涉及到其他一些 java的内容,重点包括:序列化、SPI、代理、Classloader、ScriptEngine 等东西;

设计模式, 这一部分是所有软件架构都会涉及到的内容。参考我的设计模式专栏

二dubbo架构



节点角色说明
Provider暴露服务的服务提供方
Consumer调用远程服务的服务消费方
Registry服务注册与发现的注册中心
Monitor统计服务的调用次数和调用时间的监控中心
Container服务运行容器
调用关系说明

服务容器负责启动,加载,运行服务提供者。

服务提供者在启动时,向注册中心注册自己提供的服务。

服务消费者在启动时,向注册中心订阅自己所需的服务。

注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。

服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。

服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。

三dubbo特性

dubbo 架构具有以下几个特点,分别是连通性、健壮性、伸缩性、以及向未来架构的升级性。

3.1、连通性

注册中心负责服务地址的注册与查找,相当于目录服务,服务提供者和消费者只在启动时与注册中心交互,注册中心不转发请求,压力较小。

监控中心负责统计各服务调用次数,调用时间等,统计先在内存汇总后每分钟一次发送到监控中心服务器,并以报表展示。

服务提供者向注册中心注册其提供的服务,并汇报调用时间到监控中心,此时间不包含网络开销。

服务消费者向注册中心获取服务提供者地址列表,并根据负载算法直接调用提供者,同时汇报调用时间到监控中心,此时间包含网络开销。

注册中心,服务提供者,服务消费者三者之间均为长连接,监控中心除外。

注册中心通过长连接感知服务提供者的存在,服务提供者宕机,注册中心将立即推送事件通知消费者。

注册中心和监控中心全部宕机,不影响已运行的提供者和消费者,消费者在本地缓存了提供者列表。

注册中心和监控中心都是可选的,服务消费者可以直连服务提供者。

3.2、健状性

监控中心宕掉不影响使用,只是丢失部分采样数据。

数据库宕掉后,注册中心仍能通过缓存提供服务列表查询,但不能注册新服务。

注册中心对等集群,任意一台宕掉后,将自动切换到另一台。

注册中心全部宕掉后,服务提供者和服务消费者仍能通过本地缓存通讯。

服务提供者无状态,任意一台宕掉后,不影响使用。

服务提供者全部宕掉后,服务消费者应用将无法使用,并无限次重连等待服务提供者恢复。

3.3、伸缩性

注册中心为对等集群,可动态增加机器部署实例,所有客户端将自动发现新的注册中心。

服务提供者无状态,可动态增加机器部署实例,注册中心将推送新的服务提供者信息给消费者。

四、zookeeper 注册中心流程



流程说明:

服务提供者启动时: 向
/dubbo/com.foo.BarService/providers
目录下写入自己的 URL 地址。

服务消费者启动时: 订阅
/dubbo/com.foo.BarService/providers
目录下的提供者 URL 地址。并向
/dubbo/com.foo.BarService/consumers
目录下写入自己的 URL 地址。

监控中心启动时: 订阅
/dubbo/com.foo.BarService
目录下的所有提供者和消费者 URL 地址。

五、dubbo框架深入设计



在这个文章中,我还会有多出引用dubbo官网的用户手册和技术手册。dubbo团队对文档的维护是做得比较到位,一点是我非常钦佩的。我们先来看看dubbo官方文档中,对于上图中各层的功能描述:

config:配置层,对外配置接口,以ServiceConfig,ReferenceConfig为中心,可以直接new配置类,也可以通过spring解析配置生成配置类。

proxy:服务代理层,服务接口透明代理,生成服务的客户端Stub和服务器端Skeleton,以ServiceProxy为中心,扩展接口为ProxyFactory。

registry:注册中心层,封装服务地址的注册与发现,以服务URL为中心,扩展接口为RegistryFactory,Registry,RegistryService。

cluster:路由层,封装多个提供者的路由及负载均衡,并桥接注册中心,以Invoker为中心,扩展接口为Cluster,Directory,Router,LoadBalance。

monitor:监控层,RPC调用次数和调用时间监控,以Statistics为中心,扩展接口为MonitorFactory,Monitor,MonitorService 。

protocol:远程调用层,封将RPC调用,以Invocation,Result为中心,扩展接口为Protocol, Invoker, Exporter。

exchange:信息交换层,封装请求响应模式,同步转异步,以Request,Response为中心,扩展接口为Exchanger,ExchangeChannel,ExchangeClient,ExchangeServer。

transport:网络传输层,抽象mina和netty为统一接口,以Message为中心,扩展接口为Channel,Transporter,Client,Server,Codec。

serialize:数据序列化层,可复用的一些工具,扩展接口为Serialization,ObjectInput,ObjectOutput,ThreadPool。

六、动态编程:Javassist

Javassist是一个开源的分析、编辑和创建Java字节码的类库。是由东京工业大学的数学和计算机科学系的 Shigeru Chiba (千叶 滋)所创建的。它已加入了开放源代码JBoss 应用服务器项目,通过使用Javassist对字节码操作为JBoss实现动态”AOP”框架。(摘自百度百科

说人话就是:在JAVA应用程序运行时,按照要求动态生成class并且完成实例化。我们都知道在JAVA语言中我们可以通过“反射”机制在JAVA应用程序运行时加载业已存在的class文件。但前提有两个:即这个class是已经在编译时存在的,另外这个class在JAVA-Classloader可以识别的路径上。

Javassist组件给了我们一个新的选择,在编译期不需要有这个java文件的存在也不需要这个java文件编译好一个class。而是运行时再生成class。使用的代码示例如下:

ClassPool pool = ClassPool.getDefault();

// 创建一个名叫Foo的类
CtClass cc = pool.makeClass("Foo");

// 创建一个名叫‘getInteger’的方法
CtMethod mthd = CtNewMethod.make("public Integer getInteger() { return null; }", cc);
cc.addMethod(mthd);

// 创建一个名叫i的属性,它的类型是‘int’
CtField f = new CtField(CtClass.intType, "i", cc);
point.addField(f);

// 实例化这个类
clazz = cc.toClass();
Object instance = class.newInstance();


七、用到的设计模式

7.1、工厂模式

ServiceConfig 中有个字段,代码是这样的:

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


dubbo 里有很多这种代码。这也是一种工厂模式,只是实现类的获取采用了 jdkspi (下面会讲到)的机制。这么实现的优点是可扩展性强,想要扩展实现,只需要在 classpath 下增加个文件就可以了,代码零侵入。另外,像 Adaptive 实现,可以做到调用时动态决定调用哪个实现,但是由于这种实现采用了动态代理,会造成代码调试比较麻烦,需要分析出实际调用的

实现类。

7.2、装饰器模式

dubbo 在启动和调用阶段都大量使用了装饰器模式。以 Provider 提供的调用链为例,具

体的调用链代码是在 ProtocolFilterWrapper 的 buildInvokerChain 完成的,具体是将注解中含

有 group=provider 的 Filter 实现,按照 order 排序,最后的调用顺序是

EchoFilter-》 ClassLoaderFilter-》 GenericFilter-》 ContextFilter-》 ExceptionFilter-》
TimeoutFilter-》 MonitorFilter-》 TraceFilter


更确切地说,这里是装饰器和责任链模式的混合使用。例如, EchoFilter 的作用是判断

是否是回声测试请求,是的话直接返回内容,这是一种责任链的体现。而像 ClassLoaderFilter

则只是在主功能上添加了功能,更改当前线程的 ClassLoader,这是典型的装饰器模式。

7.3、观察者模式

dubbo 的 provider 启动时,需要与注册中心交互,先注册自己的服务,再订阅自己的服

务,订阅时,采用了观察者模式,开启一个 listener。注册中心会每 5 秒定时检查是否有服

务更新,如果有更新,向该服务的提供者发送一个 notify 消息, provider 接受到 notify 消息

后,即运行 NotifyListener 的 notify 方法,执行监听器方法。

7.4、动态代理模式

dubbo 扩展 jdkspi 的类 ExtensionLoader 的 Adaptive 实现是典型的动态代理实现。 dubbo

需要灵活地控制实现类,即在调用阶段动态地根据参数决定调用哪个实现类,所以采用先生

成代理类的方法,能够做到灵活的调用。生成代理类的代码是 ExtensionLoader 的

createAdaptiveExtensionClassCode 方法。代理类的主要逻辑是,获取 URL 参数中指定参数的

值作为获取实现类的 key。

八、SPI和扩展点

SPI:Service Provider Interface。一个接口如果存在多个实现,那么我们必须依靠new关键字来告诉调用者这个接口的具体实现;用new关键的位置和时机都是非常重要的,因为这代表者调用者需要了解‘具体实现’;

Spring框架使用‘bean’配置关键字的形式帮我们解决了new关键字的问题,让调用者本身不需要关注所调用接口的具体实现。但是在和Spring框架相对独立的dubbo框架中,如何达到这样的效果呢?

这里要进行一下说明:网上很多帖子提到dubbo和Spring是可以无缝结合的,但是又没有分析dubbo框架为什么可以和Spring框架无缝结合;这让很多读者认为BUDDO框架是基于Spring开发的。但如果您研究过dubbo的源代码(或者读过dubbo相关技术文档),您就会发现。dubbo和Spring完全是两个不同的技术组件,所谓无缝结合只是指dubbo的service层、包括config层可以被Spring托管而已(实际上这和dubbo框架的核心实现没有半毛钱关系);但是这两个美丽的软件,采用的设计思路却是非常的一致:教科书似的设计模式应用。

dubbo框架扩展了(或者说另外实现了)基于标准JAVA的“服务自动发现”机制;为了说清楚dubbo是如何找到某个内部接口的实现类的,我们首先就要讲清楚JAVA的SPI机制,并且再对dubbo进行了哪些扩展进行一些必要的说明。

8.1、JAVA自带的SPI

对于JAVA中的接口和实现,我们一般情况下(或者说您在学习JAVA的时候),会采用如下的方式进行定义和使用(上文已经做了详细注释,这里的代码把注释精简了):

业务接口定义(BusinessInterface):

public interface BusinessInterface {
public void dosomething(String username);
}


业务接口的真实实现类(RealBusinessImpl):

public class RealBusinessImpl implements BusinessInterface {
public void dosomething(String username) {
System.out.println("正在为用户:" + username + ",进行真实的业务处理。。。");
}

public static void main(String[] args) throws RuntimeException {
BusinessInterface realBusiness = new RealBusinessImpl();
realBusiness.dosomething("yinwenjie");
}
}


实际上,从JDK1.5版本开始,您无需使用new关键字指定具体的实现类。您可以在META-INF/searvices文件夹下建立一个名叫xxxx.BusinessInterface的文件(注意xxxx代表您的包名,整个文件名与BusinessInterface接口的完整类名相同),然后在文件内容中书写“xxxxx.RealBusinessImpl”(注意是完整BusinessInterface接口实现类的名字)。保存这个文件后,您就可以通过JDK提供的java.util.ServiceLoader工具类实例化这个接口了。代码片段如下:

.....
ServiceLoader<BusinessInterface> interface = ServiceLoader.load(BusinessInterface.class);
// 这样写的原因是,您可以一次指定这个接口的多个具体实现
Iterator<BusinessInterface> iinterface= interface.iterator();
if (iinterface.hasNext()) {
BusinessInterface interfaceItem = iinterface.next();
interfaceItem.dosomething("yinwenjie");
}
...


8.2、dubbo框架做的修改

在dubbo框架中,主要作者william.liangf和ding.lid对JDK提供的SPI机制进行了修改(更准确的说法是“新建”):在META-INF/dubbo文件夹下,使用K-V的方式描述要创建的具体类,这种方式在dubbo框架中被称为“扩展点”。

dubbo框架中对“扩展点”功能支持在com.alibaba.dubbo.common.extension包中,主要的类为ExtensionLoader。在这个包中,作者也对为什么要扩展JDK的SPI功能进行了说明:



8.3、Protocol的扩展点文件

可以通过在classpath的META-INF/dubbo/internal/、META-INF/dubbo/、META-INF/services/目录下放置文件来定义扩展点,文件名称为组件接口的类全名,文件内容为扩展名=实现类名的形式,例如Protocol的扩展点文件(文件名称com.alibaba.dubbo.rpc.Protocol):



8.4、扩展点的加载方式

框架通过ExtensionLoader负责加载/存储扩展点实例,每个组件接口有一个对应的ExtensionLoader实例负责加载该组件的扩展点,有三种方式加载:

名称加载扩展点

加载激活扩展点

加载自适应扩展点

8.4.1、名称加载扩展点

对应com.alibaba.dubbo.common.extension.ExtensionLoader.getExtension方法

这是最直观的方式,在初始化时框架会读取classpath下所有扩展点文件形成多个键值对存在在扩展点对应的ExtensionLoader中,比如要获取名称为dubbo的Protocol扩展点,那么就会返回一个DubboProtocol实例,但是框架还有种特殊的扩展点叫Wrapper扩展点,即实现类有一个参数并且参数类型是扩展点接口类型的构造函数的扩展点,这类扩展点一般做一些适配和封装性的工作。当有Wrapper扩展点时,获取任何名称的扩展点都会返回Wrapper实例,Wrapper实例封装真实的扩展实现,当有多个Wrapper实现时,会将Wrapper封装Wrapper,形成一个链式的结构,比如获取名称是dubbo的Protocol扩展点,通过ExtensionLoader.getExtension(“dubbo”)获取,ExtensionLoader会返回一个ProtocolListenerWrapper实例,ProtocolListenerWrapper实例持有一个ProtocolFilterWrapper实例,ProtocolListenerWrapper实例持有持有一个DubboProtocol实例,对象引用关系:ProtocolListenerWrapper->ProtocolFilterWrapper->DubboProtocol。

8.4.2、加载激活扩展点

对应com.alibaba.dubbo.common.extension.ExtensionLoader.getActivateExtension方法

在应用进行dubbo调用时,dubbo框架会根据条件加载一些自动激活的扩展点,最典型就是Filter组件,dubbo接口调用时会激活应用配置和框架内置的Filter调用链。自动激活的扩展点一般通过两种形式定义:

通过Activate注解,框架启动时ExtensionLoader会加载所有带Activate注解的接口实现,并存在一个Map中,key是扩展点实现的类名首字母小写,如果实现类的结尾是接口名称,截掉这个接口名称,比如MonitorFilter实现,处理之后key就是monitor。

在查找被激活的扩展点时,如果Activate注解设置了group和values属性,要根据这两个属性进行过滤,group属性有两个枚举值:provider、consumer,设置了group=”provider”则调用时提供端会加载此扩展点,设置了group=”consumer”则调用时消费端会加载此扩展点,如果设置了group={”provider”,

”consumer”}则提供端和消费端都会加载此扩展点,当然还要有个前提条件,就是应用层没有禁用次扩展点,禁用的方式就是在配置时扩展点名称前面加个-号,比如设置了filter=”-monitor”,那么不会激活MonitorFilter这个扩展点。如果values属性设置了值,那么框架会检查封装此次调用所有信息的URL的parameters中是否在values中的key,如果有的话也激活该扩展点。

应用中显示设置的扩展点,如果Filter,那么使用这可以通过给接口设置filter属性来达到激活扩展点的目的。

8.4.3、加载自适应扩展点

对应com.alibaba.dubbo.common.extension.ExtensionLoader.getAdaptiveExtension方法

每一次dubbo接口调用都是框架中的多个不同的组件来合作完成的,这时候完成核心工作的组件也是通过ExtensionLoader来加载的,加载的扩展点由调用上下文决定,这种扩展点加载方式叫自适应加载。Dubbo框架通过Adaptive注解来定义自适应扩展点,同一个扩展点接口的实现中最多只能有一个实现类可以定义Adaptive注解,定义了Adaptive注解的扩展点就是被加载到的自适应扩展点Map中,如果所有实现都没有使用Adaptive注解,那么接口需要使用SPI注解,并且设置value属性,value属性值就是默认扩展点的名称,如果URL中未指定加载哪个扩展点则加载默认扩展点。同时需要扩展的方法也打上Adaptive注解。自适应扩展点加载逻辑是:在URL中获取扩展点名称,这个名称一般放在Url的某个属性或者parameters的某个key中,加入URL中没有取到响应的名称取默认值,就是SPI注解中指定的value属性。每个组件的自适应扩展点的加载都是通过该接口的一个代理实现来完成,框架为了消除重复代码,该代理类并没有对应的静态代码,代码是dubbo框架通过一个代码模板在运行时生成的,然后通过Compiler组件把生成的代码编译并加载成代理类Class(相关代码见
com.alibaba.dubbo.common.extension.ExtensionLoader.createAdaptiveExtensionClassCode()
这个方法),例如,当要加载Cluster自适应组件时,dubbo框架会在生成下面这段代码,然后把它当做Cluster组件的自适应加载代理类:

package com.alibaba.dubbo.rpc.cluster;
import com.alibaba.dubbo.common.extension.ExtensionLoader;
public class Cluster$Adpative implements com.alibaba.dubbo.rpc.cluster.Cluster {
public com.alibaba.dubbo.rpc.Invoker join(
com.alibaba.dubbo.rpc.cluster.Directory arg0)
throws com.alibaba.dubbo.rpc.cluster.Directory {
if (arg0 == null)
throw new IllegalArgumentException(
"com.alibaba.dubbo.rpc.cluster.Directory argument == null");
if (arg0.getUrl() == null)
throw new IllegalArgumentException(
"com.alibaba.dubbo.rpc.cluster.Directory argument getUrl() == null");
com.alibaba.dubbo.common.URL url = arg0.getUrl();
String extName = url.getParameter("cluster", "failover");
if (extName == null)
throw new IllegalStateException(
"Fail to get extension(com.alibaba.dubbo.rpc.cluster.Cluster) name from url("
+ url.toString() + ") use keys([cluster])");
com.alibaba.dubbo.rpc.cluster.Cluster extension = (com.alibaba.dubbo.rpc.cluster.Cluster) ExtensionLoader
.getExtensionLoader(com.alibaba.dubbo.rpc.cluster.Cluster.class)
.getExtension(extName);
return extension.join(arg0);
}
}


从上面的代码也可以看出,扩展点名称从URL中获取,加入URL中没有指定对应的名称,则去默认扩展实现,Cluster组件的SPI注解value=failover,所以上面代码的extName默认值是failover。

/**
* Cluster. (SPI, Singleton, ThreadSafe)
* <p>
* <a href="http://en.wikipedia.org/wiki/Computer_cluster">Cluster</a>
* <a href="http://en.wikipedia.org/wiki/Fault-tolerant_system">Fault-Tolerant</a>
*
* @author william.liangf
*/
@SPI(FailoverCluster.NAME)  //看这里
public interface Cluster {

/**
* Merge the directory invokers to a virtual invoker.
*
* @param <T>
* @param directory
* @return cluster invoker
* @throws RpcException
*/
@Adaptive
<T> Invoker<T> join(Directory<T> directory) throws RpcException;

}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: