1.1 开篇说明

dubbo是一个分布式服务框架,致力于提供高性能透明化RPC远程调用方案,提供SOA服务治理解决方案。本文旨在将对dubbo的使用和学习总结起来,深入源码探究原理,以备今后可以作为借鉴用于工作之中。

由于dubbo各个分层都是很多扩展,比如注册中心有redis、zookeeper选项,通信模块有netty、mina,序列化有hession、hession2、java序列化等,本文不能面面俱到,重点阐述主线流程,注册中心选择zookeeper(client选择curator),通信选择netty,协议选择dubbo,序列化选择hession2,容器选择Spring。本文不会照搬官网文档,而是由浅入深阐述dubbo框架。

欢迎各位大佬进群共同交流学习,我们的交流分享群:1149778920 暗号:CSDN
博主在这里给大家整理了包括但不限于:JAVA基础和进阶类、Spring、Spring boot、Spring MVC、MyBatis、MySQL、JVM等各种资料有,免费分享给各位进群的小伙伴

在这里插入图片描述

1.2 为什么要服务化

为什么要做服务化?

随着业务发展,应用的功能和涵盖的业务越来越大,造成复杂度越来越高,代码量跟着加大,开发人员在发布环节会遇到前后端协调和代码冲突导致发布失败,在开发过程中由于代码的臃肿而不得不背负较大的负担降低开发效率,每个开发人员没有具体分工不能够做到业务模块责任到人,单个应用包含了不同业务一方业务出现问题影响其他业务的正常服务,大量业务柔和在一起无法有效做到容量规划,造成数据库连接和分布式缓存的浪费。
因此,将应用拆分,并抽取出核心服务来解决上述问题,还要考虑负载均衡、服务监控、高可用性、服务隔离与降级、路由策略、完善的容错机制、序列化方案的选择、通信框架的选择、开发人员对底层细节无感知、服务升级兼容性等问题。Dubbo满足了以上所有需求。

1.3 深入浅出dubbo

本节从零基础服务调用到dubbo演进过程,如果对dubbo功能已经很熟悉,可以忽略。

1)初期一个简单的服务调用方式
调用方与服务方约定请求参数字段和请求结果字段,服务方启动一个tomcat+springmvc,监听80端口,调用方通过httpclient发起http请求,服务方返回json或xml数据结果,调用方拿到http响应结果解析结果数据,一次服务调用结束。
那么问题来了,过了几个月,核心服务越来越多,业务被拆的越来越精细,配置文件中由服务方提供的url地址越来越多,运维忙于架设负载均衡设备,部署新的软负载服务nginx,lvs,haproxy,甚至每个服务的内部域名的变化或url路径变化都要增加运维人员的人工成本。

2)通过注册中心发现服务、client端完成负载均衡
服务方在启动tomcat后,向注册中心注册自己的服务列表,包括服务器ip、port,以及代表服务的唯一标识,比如以格式/a_service/ip_port,/b_service/ip_port存储在注册中心。
调用方在启动后,去注册中心寻找a服务的地址列表,并且订阅/a_service,当a服务列表变更就会将变更消息推给调用方。接下来地址列表得到了,调用方创建多个httpClient实例,每个实例对应一个服务器ip_port,每次发起调用,从httpclient实例列表中随机选择一个,发起调用请求。当服务方某台服务器出现宕机或者网络故障,调用方会从收到由注册中心推送过来的通知消息,进而将出现故障的ip_port对应的httpclient从列表中移出;当服务方新增加服务器时,调用方同样会收到通知消息,进而新建httpclient实例,加入httpclient列表。由此我们增加了注册中心集群,在服务方调用方加入注册中心客户端,解决了1)提出的问题。

那么问题来了?服务方提供的服务器硬件配置不一样,性能也不一样,希望通过设置服务器权重的方式,权重高的希望收到更多的请求,希望有轮询的负载均衡方式,于是有了负载均衡策略的需求。

3) 负载均衡策略模块
服务方将权重信息写入注册中心,调用方取到后根据自己或者服务方建议的负载均衡策略从httpclient列表中选择一个实例,进而发起http请求。
那么问题来了?负载均衡虽然解决了均衡压力的问题,但如果服务方与调用方之间网络出现闪断造成请求失败怎么办,如果能够重试就好了,但又不能对所有的请求都开启重试机制,有些写请求比如账户充值,肯定不能重试多次,于是需要一个集群容错模块。

4)集群容错模块
对于不同服务选择不同的容错机制,比如非幂等的写操作选择failfast–失败立即报错,对于响应快速的读操作选择failover—重试其它服务器,如果调用方无法容忍因为服务调用的阻塞也可以选择failfast,对于消息通知可以选择failback—失败定时重发,对于审计日志可以选择failsafe—失败直接忽略。
那么问题来了?如何解决不同机房调用的问题,读写分离的问题,当线上服务方某个网段服务器出现问题,需要立即隔离掉。于是需要一个路由策略模块。
5)路由策略
通过web界面管理端可以直接操作注册中心,管理员添加路由规则,调用方订阅路由规则节点,当发生变更时调用方收到通知修改本地路由策略。

那么问题来了?添加这些路由规则需要一个管理端,这个管理端由于与注册中心建立连接,还可以方便的进行权重修改、负载均衡策略变更、容错机制变更等

6)管理端
具有良好web界面的管理端。
在这里插入图片描述
那么问题来了?只有管理端,但不能看到服务调用情况,无法做容量规划,比如调用次数,响应时间,QPS,服务依赖关系,服务方有几台,调用方有几台机器,同时提供哪些服务,我总不能每次都跳到注册中心集群敲命令看吧,何况以上服务统计信息敲命令也看不到。那么因此需要一个监控中心集群,调用方和服务端定期上报监控中心服务的请求与返回数据,监控中心通过计算用曲线图展示出来。

7)监控中心
在这里插入图片描述
调用方和服务方引入监控中心client,client定期将服务数据上报到监控中心集群,监控中心提供web界面,使应用负责人可以登录查看。监控中心因为也是提供统计数据收集服务,所以同样可以作为服务方接收来自普通服务方和普通调用方的统计上报请求。在代码实现上,我们可以在发起调用和接收到结果之间做一层拦截比如monitorFilter,在发起调用前纪录下时间戳,在收到响应结果纪录下时间戳,然后算出时间差发送到监控中心。

那么问题来了?注册中心、监控中心、负载均衡策略、容错机制、路由规则都有了,但是现在每次调用服务都要写一堆httpclient相关代码,调用前要组织httpclient要求的请求对象request,结果返回后要解析httpclient封装的response对象,除此之外还要负载均衡策略、容错的代码封装在外面,这不仅没有减少开发成本反而增加了,如果每次服务调用都像本地调用一样,服务化对开发者无感知就好了。于是需要一个代理对象,来封装底层细节,让通信细节、路由、负载均衡对开发者不可见。

8)代理对象
上面说到了我们需要一个服务调用做一个代理,代码看起来应该是酱紫的:
清单1.DemoService.javapackageTest;public interface DemoService{String sayHello(String name);}
清单2.DemoService$Proxy.java

Public class DemoService$Proxy implements DemoService{
   Private java.lang.reflect.InvocationHandler handler;
   Public static java.lang.reflect.Method[] methods;
   Public java.lang.String interfaceName;
   Public DemoService$Proxy(String interfaceName,java.lang.reflect.InvocationHandlerarg1){
          this.handler=arg1;
          this.interfaceName=interfaceName;
   }

   Public java.lang.String sayHello(java.lang.String arg0){
          Object[]args=new Object[1];
          args[0]= arg0;
          Object ret =handler.invoke(interfaceName, methods[0],args);
                 return(java.lang.String)ret;
   }
}

DemoService$Proxy即为代理类,但需要传递一些必须字段比如类名、方法名、方法参数类型、参数值(这里的实现是与dubbo的代理类有出入的,比如handler里面invoker属性已经有了interfaceName属性)。其中handler隐藏了所有远程调用细节,包括负载均衡、路由、容错、通信。动态代理可以借助很多开源类库都可以实现比如javassist,asm,glib,jdk自带的,那到底要选择哪个呢,当然是哪个性能好易用性好选哪个,为了保证高性能就要避免反射,首先jdk自带的方案排除了,dubbo推荐使用javassit,一方面是它性能好比glib好,另一方面它可以拼接java源码动态编译生成字节码而asm需要框架开发人员熟悉class字节码结构开发成本较高,关于几种动态代理的方案对比可以看dubbo作者的博客—动态代理方案性能对比(http://javatar.iteye.com/blog/814426).
问题又来了?现在调用方通过代理对象来调用远程服务,不需要关注通信协议,已经可以作为一个RPC框架来使用了,但是传输参数是一个复杂对象而不是一个个基本类型参数该怎么办?这就需要引入序列化,业界流行的序列化协议有java序列化,hession,hession2,json序列化,protobuf,thrift。除此之外,我们需要一个高性能的通信框架,比如netty、mina。

9)通信与序列化模块
有了NIO通信框架,不再是httpclient和tomcat,性能得到了很大的提升;但同时带来连接管理的工作量,第一,NIO没有BIO那样可以方便设置读超时时间,超时管理是必不可少的(不然堆内存溢出);第二,client-server建立长连接,server端要定期扫描所有连接,关闭空闲连接;第三,为了维持长连接,client会定期发送心跳给server,发心跳也能及时检测与server的连接状态(当网络断开而FIN消息未能发出,client不知道连接关闭导致操作失败;通过定期传输接收数据,在遇到IO异常比如ClosedChannelException时就可以判断连接失效,发起关闭连接操作).
server端由tomcat改为netty,接收到调用方发过来的类名、方法名、参数等数据,一般情况下需要通过反射调用最终服务代码,但是反射性能很差,我们需要对每个服务都动态生成一个Wrapper类(通过拼接源码,借助javassist动态编译),避免反射,代码看起来是酱紫的:
清单 Wrapper.java

 Public class Wrapper0{    
    public Object invokeMethod(Object object, String method, Class[]parameterTypes,Object[]parameterValues)throwsjava.lang.reflect.InvocationTargetException{
com.test.DemoServiceImpl w; 
     try{ 
          w =(com.test.DemoServiceImpl)object;  
       }catch(Throwable e){ 
          throw new IllegalArgumentException(e);
}   
      try{if("sayHello".equals( method )&¶meterTypes.length==1){    
          return w.sayHello((java.lang.String)parameterValues[0]);}}
      catch(Throwable e){   
         throw newjava.lang.reflect.InvocationTargetException(e);
    }
}

以上代码与dubbo真实代码有些出入,这么写为的是更能方便易懂。

那么问题又来了?之前用http协议传输的,client只需要在url中指定路径,spring mvc通过url path找到方法反射调用。现在server使用netty,而调用方需要多种服务,服务方又暴露了多种服务不同服务还有不同方法,调用方应该怎么传数据才能让服务方知道它需要的服务呢?这就需要约定一种协议,协议规定了发出何种控制信息,接收方收到信息做出什么样的动作做出什么响应。

10)协议

既然是远程过程调用,肯定方法名、类名、参数类型、参数值这些少不了,通过这些信息,服务方就可以很容易映射到具体方法。除此之外,我通过传递某些头信息,可以控制服务端不返回结果,比如消息通知。

总结:以上即是dubbo几大核心组件:按照角色来划分分为

provider(服务提供方,对应前文的服务方)
consumer(服务消费方,对应前文的调用方)
monitor center(监控中心)
registry center(注册中心,接下来我们以zookeeper为例子说明)
admin web console(管理端,用于修改路由、修改配置,最终作用于注册中心)
在这里插入图片描述
更细致的组件关系图:按功能来划分

  • directory (负责从zookeeper中心生成的provider列表)
  • router (路由)
  • fault-tolerantStrategy(容错策略)
  • loadBalance(负载均衡)
  • monitorFilter(监控拦截)
  • zookeeperClient(Zoookeeper客户端,我们使用zookeeper做例子)
  • proxy(代理对象)
  • nettyClient(我们以netty作为通信框架)
  • nettyServer(我们以netty作为通信框架)
  • Hession2Serialization(我们选hession2作为序列化方案)

在这里插入图片描述

1.4dubbo SPI扩展框架

dubbo作为一个开源RPC框架,实现的功能也比较多,而不同的组件有各种不一样的方案,
用户会根据自己的情况来选择合适的方案。比如序列化、通信框架、注册中心都有不同方案可选,负载均衡,路由规则都有不同的策略,dubbo采用微内核+插件式扩展体系,获得极佳的扩展性。
dubbo没有借助spring,guice等IOC框架来管理,而是运用JDK SPI思路自己实现了一个IOC框架,减少了对外部框架的依赖,更多dubbo框架设计原则可以看dubbo作者的分享《框架设计原则》

dubbo扩展框架特性

  • 内嵌在dubbo中

  • 支持通过SPI文件声明扩展实现(interfce必须有@SPI注解),格式为extensionName=extensionClassName,extensionName类似于spring的beanName

  • 支持通过配置指定extensionName来从SPI文件中选出对应实现ExtensionLoader.getExtensionLoader(RegistryFactory.class).getExtension(“Zookeeper”)
    类似于spring的beanFactory.getBean(“xxx”);
    ExtensionLoader是扩展机制能够实现的核心类,类似于spring的beanFactory,只不过ExtensionLoader里面只存放单一类型的所有单例实现,存放dubbo扩展的”bean”容器是有多个ExtensionFactory组成的。

  • 支持依赖注入,注入来源(ExtensionFactory)可以自己定义,比如可以来自于SPI,也可以来自于spring容器,ExtensionFactory也是一个扩展,可以自己扩展。查找方式是通过set${ExtName}方法名(ExtName可以替换为任意扩展名称)来注入相关类型对应extName的扩展,找不到就不注入。

  • 可以指定或动态生成自适应扩展类,通过interface方法里@Adaptive注解指定的value值作为key,从配置中(com.alibaba.dubbo.common.URL)获取key对应的extName值,找到对应扩展再调用具体方法实现方法调用自适应

  • 对于拥有构造方法参数为interface类型的扩展,按照顺序依次包装最终扩展实现类,比如ProtocolListenerWrapper–>ProtocolFilterWrapper—->DubboProtocol

  • 可以通过对同一类型不同扩展类名添加@Activate注解,基于@Activate属性group和value获取指定group、指定参数名的扩展

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐