文章目录
简介
目前的工作主要是做一些业务功能的开发,涉及到的系统架构以all-in-one的单体架构为主,也少量涉及分布式系统,对于涉及到的分布式系统,几乎都是以dubbo为基础构建的,日常也主要是对其进行维护以及对开发新的功能,之前对dubbo的使用也不是特别深入,应用场景也不复杂。dubbo作为目前最流行的分布式微服务架构之一,是非常值得好好去研究一下的。
《深入理解Apache Dubbo与实战》这本书主要是对dubbo的架构及原理进行讲解,中间穿插了一些实战案例,通过这本书可以更好的理解dubbo,理解其设计理念。两位作者商宗海和林琳,商宗海是dubbo的技术负责人,在dubbo开源到apache后,其本人称为了PMC Member,林琳也是dubbo的核心贡献者,这两位目前都就职于蚂蚁金服。
架构演进
在将dubbo前,我们先来简单回顾一下应用系统架构的演进,这是一个老生常谈的问题了。
对于一些比较简单的应用,我们通常采用all-in-one的单体架构,这种架构对于开发和部署都比较友好,能够在项目前期快速的响应需求,但随着业务的发展,系统中的功能越来越多,服务的体积也越来越庞大,另外团队成员也越来越多,这时候就会代理很多问题,比如代码重复问题,牵一发而动全身以及单次部署时间越来越长等,这时候就需要考虑对服务进行拆分。在拆分时,可以根据系统不同的特点采用横向拆分或纵向拆分。如果系统中各个子模块的业务边界比较清楚,内部不存在跨模块的相互调用的话,可以按照纵向拆分,把每一个模块拆分成一个独立的服务,单独维护部署,但是如果模块之间耦合度比较高的话,就可以考虑按照功能横向拆分,把每一个功能模块独立成一个服务,每个服务交给一个独立的小组负责,这样,可以很大程度的提高团队的开发效率,其实这时候系统架构就演进到了我们常说的SOA架构。如果架构再往前演进的话,就到了微服务架构,我觉得微服务架构可以简单的理解为拆分粒度更小的服务化,微服务架构由于对目前比较流程的敏捷开发和DevOps比较友好,所以也很流行,但是具体采用哪种架构,还是要根据具体的场景来决定。
理解RPC与SOA
在最开始接触dubbo的时候,认为dubbo就是架构,后来随着工作经验的增加,理解了dubbo本身其实是一种用来构建分布式系统的框架,通过dubbo构建的分布式系统遵循SOA架构,即面向服务的架构。那么,dubbo所要解决的问题就是SOA架构的中的问题,我个人理解,dubbo解决了soa架构中的两大核心问题:RPC调用和服务治理。
RPC是远程过程调用的缩写,它指的是一种基于网络的跨进程调用,在java中实现rpc的方式主要有两种,一种是基于jdk RMI实现,一种是基于自定义协议和反射实现,在几乎所有的rpc框架中都是采用第二种方式实现。RPC解决了分布式系统的核心的远程服务调用的问题。
但是SOA架构中的另一个重要功能就是服务治理,服务治理包括服务注册与发现,服务的限流、降级,服务监控等。这些功能dubbo也都有提供。
所以说,dubbo是一个分布式服务框架,基于dubbo构建的分布式系统基于SOA架构,也就是说dubbo解决了SOA架构中的核心问题。
下面,我们从dubbo的世界里暂时跳出来,看看一个通用的分布式服务框架都应该具有哪些功能。
在最上层是业务层,也就是具体的业务服务接口;下层是proxy代理层,proxy的主要作用是屏蔽rpc框架的底层细节,避免rpc框架侵入业务代码;下面的两层我理解成框架特性层,主要包含了负载均衡、超时重试、集群容错以及服务路由;再往下,就属于rpc层了,分为协议层、通信框架层和传输层,这几层包含了rpc框架核心,包括协议、序列化/反序列化等,总是这里的主要作用是把消息按照指定的格式发送到provider或返回给consumer。
dubbo架构简介
下面再来看看dubbo,下图是dubbo的总体抽象架构
这里包含了四个部分,分别是服务提供者,服务消费者,注册中心,监控中心,包含了SOA架构中的两个核心部分:RPC与服务治理。服务提供者异步向注册中心;服务消费者异步从注册中心拉取服务提供者信息,并接受注册中心的变更通知;服务消费者和服务提供者异步向监控中心上报数据;服务消费者同步调用服务提供者;
下面再从微观层面看看dubbo在代码层面的架构设计
对于dubbo来说这是一张非常经典的架构图,我们可以看到,完全满足一个分布式服务框架所应该具有的功能。
下面简单按照我个人的理解简单解释一下
- service:业务层,包含了业务接口及具体实现;
- proxy:服务代理层,封装底层实现细节,使业务层对rpc过程无感知;
- registry:注册层,负责与注册中心进行交互,进行服务注册于发现、接收注册中心消息变更通知;
- cluster:集群容错层,提供了dubbo在集群层面的核心功能,包括集群容错、负载均衡、服务路由、超时重试等;
- monitor:监控层。负责上报数据到监控中心;
- protocol:协议层,这里封装了rpc调用的完整逻辑;
- exchange:信息交换层,把对API接口的调用转换成-Request-Response模式;
- transport:网络传输层,即Netty或Mina;
- Serializable:序列化,将消息进行序列化或反序列化;
SPI扩展机制
上面的代码架构是纵向观察dubbo的结构,但从横向看,dubbo会呈现出一个不一样的风景。
在横向上,dubbo采用的是一种为内核架构,内核基于SPI扩展点机制将不同的组件组合在一起,形成一个完整的分布式服务框架,同时,用户还可以基于SPI标准实现自己的扩展组件,这些组件通过SPI可以很容易的整合到dubbo中,下图是我对dubbo微内核和spi机制的理解
提到SPI,如果不了解dubbo的同学可能没有听过,或者有的同学对Java的SPI有一些了解,SPI利用策略模式,将接口与实现进行解耦合,用法如下
- 创建接口和实现类
public interface IHelloService {
String hello(String name);
}
public class HelloService1 implements IHelloService {
@Override
public String hello(String name) {
return "HelloService1:" + name;
}
}
public class HelloService2 implements IHelloService {
@Override
public String hello(String name) {
return "HelloService2:" + name;
}
}
- 在META-INF/services中创建以接口全名称为名字的文件
com.learn.dubbo.chapter1.echo.server.spi.IHelloService
- 在文件中制定具体的实现类,多个实现类用换行符分隔
com.learn.dubbo.chapter1.echo.server.spi.HelloService1
com.learn.dubbo.chapter1.echo.server.spi.HelloService2
- 使用Java SPI api调用实现类
ServiceLoader<IHelloService> helloServices = ServiceLoader.load(IHelloService.class);
for (IHelloService helloService : helloServices) {
System.out.println(helloService.hello("zhangsan"));
}
输出:
HelloService1:zhangsan
HelloService2:zhangsan
Java SPI存在两个主要问题,一是不能按需加载实现类,只能全部加载,二是对异常不友好,如果实例化对象出现异常 通过SPI相关的api没办法感知异常,Dubbo并没有直接使用Java SPI来实现自己的扩展点机制,而是自己实现了一个与Java SPI类似但功能更强的Dubbo SPI,具体体现在一下几个方面
- 按需实例化接口的实现类
- 提供了IoC和AOP功能
- 能够基于参数动态加载获取实现类的对象
- 对异常友好
下面用具体事例看一下
@SPI("helloService1") //指定默认的SPI接口实现
public interface IHelloService {
//通过URL中的helloService参数对应的值进行自适应,主要为了配置setter实现依赖注入
@Adaptive("helloService")
String hello(URL url, String name);
}
//第一个实现类
public class HelloService1 implements IHelloService {
@Override
public String hello(URL url, String name) {
return "HelloService1:" + name;
}
}
//第二个实现类
public class HelloService2 implements IHelloService {
@Override
public String hello(URL url, String name) {
return "HelloService2:" + name;
}
}
//第三个实现类
public class MyHelloService implements IHelloService {
private IHelloService helloService;
//这里有一个setter依赖注入,具体注入的对象是由URL中的参数决定的
public void setHelloService(IHelloService helloService) {
this.helloService = helloService;
}
@Override
public String hello(URL url, String name) {
System.out.println("my hello service ");
return helloService.hello(url, name);
}
}
//这是一个具有AOP功能的实现类
public class HelloServiceWrapper implements IHelloService{
private IHelloService helloService;
//构造方法中如果右依赖的扩展点,就会为对每个实例进行包装
public HelloServiceWrapper(IHelloService helloService) {
this.helloService = helloService;
}
@Override
public String hello(URL url, String name) {
System.out.println("before...");
String result = helloService.hello(url, name);
System.out.println("after...");
return result;
}
}
//配置文件 com.learn.dubbo.chapter1.echo.server.spi.IHelloService
helloService1=com.learn.dubbo.chapter1.echo.server.spi.HelloService1
helloService2=com.learn.dubbo.chapter1.echo.server.spi.HelloService2
myHelloService=com.learn.dubbo.chapter1.echo.server.spi.MyHelloService
wrapper=com.learn.dubbo.chapter1.echo.server.spi.HelloServiceWrapper
//调用
IHelloService helloService = ExtensionLoader.getExtensionLoader(IHelloService.class).getDefaultExtension();
URL url1 = new URL("","",0);
System.out.println(helloService.hello(url1,"lisi"));
IHelloService helloService2 = ExtensionLoader.getExtensionLoader(IHelloService.class).getExtension("helloService2");
URL url2 = new URL("","",0);
System.out.println(helloService2.hello(url2,"wangwu"));
IHelloService helloService3 = ExtensionLoader.getExtensionLoader(IHelloService.class).getExtension("myHelloService");
Map<String, String> p3 = new HashMap<>();
p3.put("helloService","helloService2");
URL url3 = new URL("","",0, p3);
helloService3.hello(url3, "zhaoliu");
下面在说说Dubbo SPI中涉及到的注解
- @SPI 标记一个借口为SPI扩展点
- @Adaptive 实现基于URL总线的自适应注入机制
- @Active
下面说说getExtension(String name)原理,这个方法的作用是根据指定的名称获取扩展点实现类,主要分为以下几步
- 加载配置文件,通过反射得到对应的Class对象并进行缓存
- 创建Class对应的实例化对象
- 进行setter依赖注入
- 进行Aop包装处理
加载配置文件时,dubbo兼容了java spi,会从/META-INF/services, /META-INF/dubbo, /META-INF/dubbo/internal
三个目录下读取,读取完成后还会对对应的Class对象进行缓存,缓存时,会区分普通扩展类、自适应扩展类、包装扩展类和Activate扩展类,以方便后续处理
紧接着就会通过名称确定对应的Class对象,然后对其进行实例化。
然后处理setter依赖注入,这里的原理很简单,就是根据setter方法规则,截取setter方法的set后剩下的字符串作为待注入的扩展实例名称,通过ExtensionFactory根据这个名称来获取实例,然后通过反射调用setter方法,将这个依赖的实例注入进去。
最后处理包装类,也即是AOP特性,这里也很简单,由于在加载class时,dubbo根据实现类是否包含一个依赖其他扩展点的构造器来判断是不是包装扩展类,如果是,则单独缓存,当进行AOP处理时,就会遍历在加载配置时得到的所有wrapper,然后通过反射调用构造器,层层注入 实现AOP。
下面看看getAdaptiveExtension()的原理,该方法可以通过URL总线完成自适应加载扩展点,先看看如何使用
@SPI
public interface AdaptiveService {
@Adaptive("impl")
void adaptive(URL url, String msg);
}
public class AdaptiveServiceImpl implements AdaptiveService {
@Override
public void adaptive(URL url, String msg) {
System.out.println("msg");
}
}
配置文件:com.learn.dubbo.chapter1.echo.server.spi.AdaptiveService
impl1=com.learn.dubbo.chapter1.echo.server.spi.AdaptiveServiceImpl
调用
AdaptiveService adaptiveService = ExtensionLoader.getExtensionLoader(AdaptiveService.class).getAdaptiveExtension();
Map<String, String> p5 = new HashMap<>();
p5.put("impl","impl1");
URL url5 = new URL("","",0, p5);
adaptiveService.adaptive(url5, "aaaaa");
输出:
aaaaa
可以看到,这里是通过在url设置与@Adaptive中指定的key的值来动态觉得最终加载那个扩展类的,其实,底层实现的原理很简单,简单来说,就是dubbo底层动态生成了Xxx$Adaptive类,在这个类中,会从url中获取key对应的值,然后通过getExtension(String name)来获取指定的扩展点,下面是dubbo动态生成的代码对应的字符串
package com.learn.dubbo.chapter1.echo.server.spi;
import com.alibaba.dubbo.common.extension.ExtensionLoader;
public class AdaptiveService$Adaptive implements com.learn.dubbo.chapter1.echo.server.spi.AdaptiveService {
public void adaptive(com.alibaba.dubbo.common.URL arg0, java.lang.String arg1) {
if (arg0 == null) throw new IllegalArgumentException("url == null");
com.alibaba.dubbo.common.URL url = arg0;
String extName = url.getParameter("impl");
if(extName == null)
throw new IllegalStateException("Fail to get extension(com.learn.dubbo.chapter1.echo.server.spi.AdaptiveService) name from url(" + url.toString() + ") use keys([impl])");
com.learn.dubbo.chapter1.echo.server.spi.AdaptiveService extension = (com.learn.dubbo.chapter1.echo.server.spi.AdaptiveService)ExtensionLoader.getExtensionLoader(com.learn.dubbo.chapter1.echo.server.spi.AdaptiveService.class).getExtension(extName);
extension.adaptive(arg0, arg1);
}
}
然后dubbo会动态对这个字符串进行编译,最终返回这个代理类的实例给客户端。dubbo中提供了三种编译方式,分别是jdk 编译、javaassist编译和ActiveCompiler编译,其中javaassist是默认的编译方式(@SPI(“javaassist”)),可以通过<dubbo:application compiler="jdk">
来修改。
最后,看一下getActivateExtension是怎么实现的
@SPI
public interface IActivateService {
void active();
}
@Activate(order = 1, group = "test")
public class ActiveService1 implements IActivateService {
@Override
public void active() {
System.out.println("Active1");
}
}
@Activate(order = 2, group = "test")
public class ActivateService2 implements IActivateService {
@Override
public void active() {
System.out.println("