Dubbo 浅析(一)
1-分布式开发
分布式开发简介
对于完整的企业级项目而言,包含着很多的功能,从层次上可以分为控制层上的大量用户访问,业务层上的各种各样逻辑处理,数据层上数据的安全处理。
将一整套完整的服务分拆为不同的部分,交由不同的主机系统进行处理,这样的开发模式就叫做分布式开发。分布式开发的本质就是各个系统物理上是分布的,但是对于用户而言,所有的服务是一个整体。

单一应用架构(ORM) -> 垂直应用架构(MVC) -> 分布式服务架构(RPC) -> 流动计算架构(SOA)
单一节点模式
将所有的业务处理操作与前端调用放在一台主机上。

传统的垂直应用(MVC 设计模式便是最好体现)项目开发,会把业务处理与页面显示放在一个项目上运行,这样会要求项目的性能非常高。在没有上升的集群的时代,还能够勉强应付。但是,大数据时代的来临,使得单点服务器已经不能满足需求了,常规的垂直应用架构已无法应对。
对于企业级项目而言,什么地方最该使用分布式开发?
在任何项目里,业务操作是最为核心的部分,所有项目的业务处理是否完整决定整体项目是否健壮。
将前端开发与业务处理混合在一起,使得业务处理无法重用,而且又得不断重复编写重复的业务处理,系统性能非常不好。故将业务抽离出来,在一台服务器上专门进行业务处理,这样就形成了一个业务的服务器集群,这样的集群也可以叫做业务中心。
RPC
RPC(Remote Procedure Call Protocol),远程过程调用协议,是一种通过网络从远程计算机程序上请求服务的协议,而不需要了解底层网络技术。
无论 RPC(Remote Procedure Call)技术如何处理,远程业务端的服务都应该提供了远程对象供前端开发进行调用。

实际开发中,使用何种方式和协议进行远程服务提供,取决于开发团队和项目实际情况,不可一概而论。当使用 RPC 协议时,需要给客户端提供远程对象进行访问。

当庞大的业务中心组建完成后,对应的数据集群也会应运而生。每一个业务中心都有与其对应的数据库应用。如何进行数据库的架构,这就需要根据实际情况而定。现实中有用到了库表的分离设计,使用 MyCat 进行分片处理。
传统分布式开发技术
分布式的两种形式
基于远程接口的实现技术(RPC):RMI、EJB、DUBBO
基于平台的交换技术(基于 XML、JSON):WebService、RESTful 架构
1、RMI
RMI(Remote Method Invoke),远程方法调用。
RMI 是一种计算机之间对象互相调用对方函数,启动对方进程的一种机制,使用这种机制,某一台计算机上的对象在调用另外一台计算机上的方法时,使用的程序语法规则和在本地机上对象间的方法调用的语法规则一样。
最早的分布式开发技术来源于 Corba,所有语言都可以实现。在这之后,Sun 公司发布了自己的分布式开发技术:RMI。虽然 RMI 性能较差,但是实现形式与 Dubbo 相似。

RMI 有两个非常重要的概念:存根、骨架。
RMI 技术借助于接口定义完成,客户端用户接口定义形式(远程方法视图),客户端通过这个远程接口知道所有可以使用的方法有哪些。随着 JDK 版本提升,服务端的骨架会自动进行生成处理。
2、EJB
EJB(Enterprise JavaBean)是 RMI 的技术延伸。
EJB 有三个部分组成:SessionBean(业务层)、EntityBean(数据层)、MessageDrivenBean(消息驱动)。
在 RMI 的基础上,结合 Corba,形成了 EJB 技术,EJB 真正提出了业务中心的概念,也就是结合会话 bean 和实体 bean 进行业务中心定义。但是 EJB 技术过于庞大,才出现了后来的框架发展(SSH)。

3、WebService
对于分布式开发的需求一直存在,到了后来的.NET 时代,为了将 J2EE 与.NET 进行对接,产生了 WebService 技术。

WebService 的好处在于使用了 XML 作为数据交换,整合了所有平台。但是,执行非常慢。WebService 进行处理的时候往往结合 WEB 容器去实现。WebService 支持的框架 CXF 后续演化产生了 jersey(Rest 架构)。
WebService 继续进行革新,产生了 SOA(Service-Oriented Architecture)和 ESB(Enterprise Service Bus)概念。
4、RESTful 架构
在之后发展到了 RESTful 架构,利用 json 或者 xml 实现数据交互,可以避免 webservice 的性能问题,很多开源框架大量使用。
2-Dubbo
Dubbo
Dubbo 是一个分布式服务框架,致力于提供高性能和透明化的 RPC 远程服务调用方案,是阿里巴巴 SOA 服务化治理方案的核心框架。
Dubbo 在整个开发过程中以接口(远程接口)为主提供服务。
与其他的 RPC 系统一样,dubbo 是以服务为基础的,具体的方法通过参数和返回类型实现远程调用。服务器端,实现接口和运行 dubbo 服务器去处理客户端调用。在客户端,拥有一个与服务器端提供的方法。
Dubbo 架构

节点角色
Dubbo 服务中有两个非常重要的角色:提供者(provider)和消费者(consumer)。provider 提供具体的业务实现类,consumer 依据远程接口调用远程对象。
Container
RPC 服务运行的容器,负责启动、加载、运行服务提供者;
Provider
暴露服务的服务提供方,在启动时,向注册中心注册自己提供的服务。在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心;
Registry
服务注册与发现的注册中心,消费方订阅服务后,注册中心返回服务提供者地址列表给消费者,如有变更,注册中心将基于长连接推送变更数据给消费者;
Consumer
调用远程服务的服务消费方,在启动时,向注册中心订阅自己所需的服务。获取服务提供者地址列表后,从中基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心;
Monitor
统计服务的调用次数和调用时间的监控中心。
Dubbo 整体架构分层

图例说明
图中左边淡蓝背景的为服务消费方使用的接口,右边淡绿色背景的为服务提供方使用的接口,位于中轴线上的为双方都用到的接口。
图中从下至上分为十层,各层均为单向依赖,右边的黑色箭头代表层之间的依赖关系,每一层都可以剥离上层被复用,其中,Service 和 Config 层为 API,其它各层均为 SPI。
图中绿色小块的为扩展接口,蓝色小块为实现类,图中只显示用于关联各层的实现类。
图中蓝色虚线为初始化过程,即启动时组装链,红色实线为方法调用过程,即运行时调时链,紫色三角箭头为继承,可以把子类看作父类的同一个节点,线上的文字为调用的方法。
对于开发者而言,最为重要的部分就是业务处理,而业务处理的重点在于接口。同时,Dubbo 需要借助一个注册中心,注册中心负责 Dubbo 所有元数据的提供,依靠这些元数据,才可以找到 Dubbo 服务。Dubbo 也提供了一个监控工具,监控所有的 Dubbo 服务。当然,没有注册中心,可以通过直连进行远程请求。
Dubbo 分层
服务接口层(service):与实际业务相关,根据服务提供方和服务消费方的业务,设计对应的接口和实现。这是实际开发中最为重要的部分。
配置层(config):对外配置接口,以 serviceConfig 和 referenceConfig 为中心,可以直接 new 配置类,也可以通过 Spring 解析配置生成配置类。
服务代理层(proxy):服务接口透明代理,生成服务的客户端 stub 和服务器端 skeleton,以 serviceProxy 为中心,扩展接口为 ProxyFactory。
服务注册层(registry):封装服务地址的注册与发现,以服务 URL 为中心,扩展接口为 RegistryFactory、Registry 和 RegistryService。没有服务注册中心时,服务提供者直接暴露服务。
集群层(cluster):封装多个提供者的路由及负载均衡,并桥接注册中心,以 Invoke 为中心,扩展接口为 cluster、directory、router 和 loadbalance。将多个服务提供方组成为一个服务提供方,实现对服务消费方来透明,只需要与一个服务提供放进行交互。
监控层(monitor):RPC 调用次数和调用时间监控,以 statistics 为中心,扩展接口为 monitorfactory、monitor 和 monitorservice。
远程调用层(protocol):封装 RPC 调用,以 invocation 和 result 为中心,扩展接口为 protocol、invoker 和 exporter。protocol 是服务域,它是 invoker 暴露和引用的主功能入口,它负责 invoker 的生命周期管理。invoker 是实体域,它是 dubbo 的核心模型,其他模型都向它靠扰,或转换成它,它代表一个可执行体,可向它发起 invoke 调用,它有可能是一个本地的实现,也可能是一个远程的实现,也可能一个集群实现。
信息交换层(exchange):封装请求响应模式,同步转异步,以 request 和 response 为中心,扩展接口为 exchanger、exchangechannel、exchangeclient 和 exchangeserver。
网络传输层(transport):抽象 mina 和 netty 为统一接口,以 message 为中心,扩展接口为 channel、transport、client、server 和 codec。
数据序列化层(serialize):可复用的一些工具,扩展接口为 serialization、objectinput、objectoutput 和 threadpool。
依赖关系

Dubbo 相关支持

Dubbo 架构特点
1)、连通性
1、注册中心负责服务地址的注册与查找,相当于目录服务,服务提供者和消费者只在启动时与注册中心交互,注册中心不转发请求,压力较小;
2、注册中心,服务提供者,服务消费者三者之间均为长连接,监控中心除外。长连接,指在一个连接上可以连续发送多个数据包,在连接保持期间,如果没有数据包发送,需要双方发链路检测包;
3、注册中心通过长连接感知服务提供者的存在,服务提供者宕机,注册中心将立即推送事件通知消费者;
4、注册中心和监控中心全部宕机,不影响已运行的提供者和消费者,消费者在本地缓存了提供者列表;
5、新的服务注册到注册中心以后,注册中心会将这些服务通过 notify 到消费者;
6、监控中心负责统计各服务调用次数,调用时间等,统计先在内存汇总后每分钟一次发送到监控中心服务器,并以报表展示;
7、注册中心和监控中心都是可选的,服务消费者可以直连服务提供者;
8、服务提供者向注册中心注册其提供的服务,并汇报调用时间到监控中心,此时间不包含网络开销;
9、服务消费者向注册中心获取服务提供者地址列表,并根据负载算法直接调用提供者,同时汇报调用时间到监控中心,此时间包含网络开销;
10、消费者订阅 subscribe 服务,如果没有订阅到自己想获得的服务,它会不断的尝试订阅;
11、在实际调用过程中,Provider 的位置对于 Consumer 来说是透明的,上一次调用服务的位置(IP 地址)和下一次调用服务的位置,是不确定的。这个地方就是实现了软负载;
2)、健壮性
1、监控中心宕掉不影响使用,只是丢失部分采样数据;
2、数据库宕掉后,注册中心仍能通过缓存提供服务列表查询,但不能注册新服务;
3、注册中心对等集群,任意一台宕掉后,将自动切换到另一台;
4、注册中心全部宕掉后,服务提供者和服务消费者仍能通过本地缓存通讯;
5、服务提供者无状态,任意一台宕掉后,不影响使用;
6、服务提供者全部宕掉后,服务消费者应用将无法使用,并无限次重连等待服务提供者恢复。
3)、伸缩性
1、注册中心为对等集群,可动态增加机器部署实例,所有客户端将自动发现新的注册中心;
2、服务提供者无状态,可动态增加机器部署实例,注册中心将推送新的服务提供者信息给消费者。
4)、升级性
当服务集群规模进一步扩大,带动 IT 治理结构进一步升级,需要实现动态部署,进行流动计算,现有分布式服务架构不会带来阻力。下图是未来可能的一种架构:


3-暴露服务
解析服务
提供方暴露服务和消费方引用服务都会涉及到如何解析服务,这里简单介绍这一个过程。
1、基于 dubbo.jar 内的 META-INF/spring.handlers 配置,Spring 在遇到 dubbo 名称空间时,会回调 DubboNamespaceHandler。
2、所有 dubbo 的标签,都统一用 DubboBeanDefinitionParser 进行解析,基于一对一属性映射,将 XML 标签解析为 Bean 对象。
3、在 ServiceConfig.export() 或 ReferenceConfig.get() 初始化时,将 Bean 对象转换 URL 格式,所有 Bean 属性转成 URL 的参数。
4、然后将 URL 传给 协议扩展点,基于扩展点的 扩展点自适应机制,根据 URL 的协议头,进行不同协议的服务暴露或引用。
暴露服务
1、只暴露服务端口
在没有注册中心,直接暴露提供者的情况下,ServiceConfig 解析出的 URL 的格式为:
dubbo://service-host/com.foo.FooService?version=1.0.0
基于扩展点自适应机制,通过 URL 的 dubbo:// 协议头识别,直接调用 DubboProtocol 的 export() 方法,打开服务端口。
2、向注册中心暴露服务
在有注册中心,需要注册提供者地址的情况下,ServiceConfig 解析出的 URL 的格式为:
registry://registry-host/com.alibaba.dubbo.registry.RegistryService?export=URL.encode("dubbo://service-host/com.foo.FooService?version=1.0.0")
基于扩展点自适应机制,通过 URL 的 registry:// 协议头识别,就会调用 RegistryProtocol 的 export() 方法,将 export 参数中的提供者 URL,先注册到注册中心。
再重新传给 Protocol 扩展点进行暴露: dubbo://service-host/com.foo.FooService?version=1.0.0,然后基于扩展点自适应机制,通过提供者 URL 的 dubbo:// 协议头识别,就会调用 DubboProtocol 的 export() 方法,打开服务端口。
其中 export()方法,调用 openServer(URL url)方法,通过 createServer(URL url)创建了 ExchangeServer 交换层服务器,以此打开服务端口。
服务提供方暴露服务的蓝色初始化链,时序图如下。

4-引用服务
引用服务
1、直连引用服务
在没有注册中心,直连提供者的情况下,ReferenceConfig 解析出的 URL 的格式为:dubbo://service-host/com.foo.FooService?version=1.0.0。
基于扩展点自适应机制,通过 URL 的 dubbo:// 协议头识别,直接调用 DubboProtocol 的 refer() 方法,返回提供者引用。
2、从注册中心发现引用服务
在有注册中心,通过注册中心发现提供者地址的情况下,ReferenceConfig 解析出的 URL 的格式为:
registry://registry-host/com.alibaba.dubbo.registry.RegistryService?refer=URL.encode("consumer://consumer-host/com.foo.FooService?version=1.0.0")
基于扩展点自适应机制,通过 URL 的 registry:// 协议头识别,就会调用 RegistryProtocol 的 refer() 方法,基于 refer 参数中的条件,访问注册中心查询提供者 URL,如: dubbo://service-host/com.foo.FooService?version=1.0.0。
基于扩展点自适应机制,通过提供者 URL 的 dubbo:// 协议头识别,就会调用 DubboProtocol 的 refer() 方法,得到提供者引用。
通过 refer()方法,创建了 rpc 的 invoker 对象,使用 ConcurrentHashSet 存储。
然后 RegistryProtocol 将多个提供者引用,通过 Cluster 扩展点,伪装成单个提供者引用返回。
服务消费方引用服务的蓝色初始化链,时序图如下。

5-调用链
远程调用细节
1、服务提供者暴露一个服务的详细过程

上图是服务提供者暴露服务的主过程:
首先 ServiceConfig 类拿到对外提供服务的实现类 ref(如:HelloWorldImpl),然后通过 ProxyFactory 类的 getInvoker 方法使用 ref 生成一个 AbstractProxyInvoker 实例,到这一步就完成具体服务到 Invoker 的转化。接下来就是 Invoker 转换到 Exporter 的过程。
Dubbo 处理服务暴露的关键就在 Invoker 转换到 Exporter 的过程,上图中的红色部分。下面我们以 Dubbo 和 RMI 这两种典型协议的实现来进行说明:
Dubbo 的实现
Dubbo 协议的 Invoker 转为 Exporter 发生在 DubboProtocol 类的 export 方法,它主要是打开 socket 侦听服务,并接收客户端发来的各种请求,通讯细节由 Dubbo 自己实现。
RMI 的实现
RMI 协议的 Invoker 转为 Exporter 发生在 RmiProtocol 类的 export 方法,它通过 Spring 或 Dubbo 或 JDK 来实现 RMI 服务,通讯细节这一块由 JDK 底层来实现,这就省了不少工作量。
服务消费者消费一个服务的详细过程

上图是服务消费的主过程:
首先 ReferenceConfig 类的 init 方法调用 Protocol 的 refer 方法生成 Invoker 实例(如上图中的红色部分),这是服务消费的关键。接下来把 Invoker 转换为客户端需要的接口(如:HelloWorld)。
关于每种协议如 RMI/Dubbo/Web service 等它们在调用 refer 方法生成 Invoker 实例的细节和上一章节所描述的类似。
满眼都是 Invoker
由于 Invoker 是 Dubbo 领域模型中非常重要的一个概念,很多设计思路都是向它靠拢。这就使得 Invoker 渗透在整个实现代码里,对于刚开始接触 Dubbo 的人,确实容易给搞混了。 下面我们用一个精简的图来说明最重要的两种 Invoker:服务提供 Invoker 和服务消费 Invoker:

为了更好的解释上面这张图,我们结合服务消费和提供者的代码示例来进行说明:
服务消费者代码:
上面代码中的 DemoService 就是上图中服务消费端的 proxy,用户代码通过这个 proxy 调用其对应的 Invoker,而该 Invoker 实现了真正的远程服务调用。
服务提供者代码:
上面这个类会被封装成为一个 AbstractProxyInvoker 实例,并新生成一个 Exporter 实例。这样当网络通讯层收到一个请求后,会找到对应的 Exporter 实例,并调用它所对应的 AbstractProxyInvoker 实例,从而真正调用了服务提供者的代码。Dubbo 里还有一些其他的 Invoker 类,但上面两种是最重要的。
消费者调用服务链,如下图所示。

6-快速启动
快速启动
Dubbo 采用全 Spring 配置方式,透明化接入应用,对应用没有任何 API 侵入,只需用 Spring 加载 Dubbo 的配置即可,Dubbo 基于 Spring 的 Schema 扩展进行加载。
服务提供者
1、定义服务接口
该接口需单独打包,在服务提供方和消费方共享。
DemoService.java
package com.alibaba.dubbo.demo;
public interface DemoService {
String sayHello(String name);
}
2、在服务提供方实现接口
对服务消费方隐藏实现。
DemoServiceImpl.java
package com.alibaba.dubbo.demo.provider;
import com.alibaba.dubbo.demo.DemoService;
public class DemoServiceImpl implements DemoService {
public String sayHello(String name) {
return "Hello " + name;
}
}
3、用 Spring 配置声明暴露服务
provider.xml
4、加载 Spring 配置
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Provider {
public static void main(String[] args) throws Exception {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(new String[] {"http://10.20.160.198/wiki/display/dubbo/provider.xml"});
context.start();
System.in.read(); // 按任意键退出
}
}
服务消费者
1、通过 Spring 配置引用远程服务
consumer.xml:
2、加载 Spring 配置,并调用远程服务
也可以使用 IoC 注入。
Consumer.java
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.alibaba.dubbo.demo.DemoService;
public class Consumer {
public static void main(String[] args) throws Exception {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(new String[] {"http://10.20.160.198/wiki/display/dubbo/consumer.xml"});
context.start();
DemoService demoService = (DemoService)context.getBean("demoService"); // 获取远程服务代理
String hello = demoService.sayHello("world"); // 执行远程方法
System.out.println( hello ); // 显示调用结果
}
}
评论