RPC框架之Dubbo
问题1:为什么要把系统拆分成分布式的?为啥要用dubbo?
1.为什么要将系统进行拆分?
-
要是不拆分系统,一个大系统几十万行代码,很多人共同维护一份代码,简直是悲剧;
-
拆分了以后,一个大系统拆分成很多小服务,平均每个系统也就几万行代码,每个服务部署到单独的机器
2.如何进行服务拆分?
大部分系统,是要进行多轮拆分的,第一次拆分就可能将原来的多个模块拆分开来。
但是后来可能每个系统都变得很复杂了,每个模块拆分出来的服务又要继续拆分。
3.拆分后可以不用dubbo吗?
当然可以,大不了各个系统之间,直接基于springmvc,就通过纯httpj接口互相通信。但是这里肯定是由问题的,因为HTTP接口通信维护起来成本很高,要考虑超时重试,负载均衡等各种问题。
所以dubbo说白了,就是一个rpc框架,就是本地就是进行接口调用,但是dubbo会代理这个调用请求,跟远程机器网络通信,处理负载均衡,服务上下线自动感知,超时重试等问题,就不用我们自己做,交给dubbo。
问题2:说一下dubbo的工作原理,注册中心挂了可以继续通信吗?说一下一次rpc请求的流程?
1.dubbo工作原理
2.dubbo分层
-
第一层:service层,需要服务提供方和消费方来实现
-
第二层:config层,配置层,主要是对dubbo的各种配置
-
第三层:proxy层,服务代理层,透明生成客户端的stub和服务端的skeleton
-
第四层:registry层,服务端的注册与发现
-
第五层:cluster层,集群层,封装多个服务提供者的路由以及负载均衡,将多个实例组合成一个服务
-
第六层:monitor层,监控层,对rpc接口的调用次数和调用时间进行监控
-
第七层:protocol层,远程调用层,封装rpc调用
-
第八层:exchange层,信息交换层,封装请求响应模式,同步转异步
-
第九层:transport层,网络传输层,抽象mina和netty为统一接口
-
第十层:serilize层,数据序列化层
2.注册中心挂了可以继续通信吗?
是可以的,因为客户端第一次从注册中心获取服务端的服务后,会缓存在自己本地,下一次调用服务端不用去请求注册中心,因此注册中心挂了是可以继续通信的
问题3:dubbo都支持哪些通信协议和序列化协议
1.dubbo支持不同的协议
-
dubbo协议
默认就是走dubbo协议,单一长链接,NIO异步通信
(NIO通信原理:NIO采用了Reactor模式(类似于观察者模式,不同之处在于Reactor模式可以监听多个主题),通过一个多路复用器来监听多个客户端的网络句柄,一旦监听到客户端的请求消息,将对应的请求消息转发给对应的Handler(业务处理类))
适合场景:传输数据量较小,但是并发很高
-
rmi协议
走java二进制序列化,多个短连接,适合消费者和提供者数量差不多,适用于文件的传输,一般较少
-
hessian协议
走hessian序列化协议,多个短连接,适用于提供者数量和消费者数量还多,适用于文件传输,一般较少使用
-
http协议
走json序列化
-
webservice
走soap文本序列化
问题4:dobbo负载均衡策略和集群容错策略?动态代理策略?
dobbo负载均衡策略
1.random loadbalance
默认情况下,dubbo是random load balcance 随机调用实现负载均衡,可以对provider不同的实例设置不同的权限,会按照权重来负载均衡,权重越大分配流量越高,一般就用这个默认的就可以了。
2.roundrobin loadbalance
这个默认就是均匀地将流量打到各个机器上去,但是如果各个机器的性能都不一样,容易导致性能差的机器负载过高而宕机。此时需要调整权重,让性能差一点的机器承载权重少一些
3.leastactive loadbalance
这个就是自动感知一下,如果某个机器性能差,那么接收的请求越少,约不活跃,此时就会给不活跃的性能差的机器更少的请求
4.consistanthash loadbalance
一致性hash算法,相同的参数的请求一定分配到一个provider上去,provider挂掉的时候,会基于虚拟节点均匀分配剩余的流量,抖动不会太大。如果需要的不是随机负载均衡,是要一类请求都到这个节点上,那就走这个一致性hash策略
dobbo集群容错策略
1.failover cluster模式
失败自动切换,自动重试其他机器,dubbo默认使用这种策略
2.failfast cluster模式
一次失败就立即失败,常见于写操作
3.failsafe cluster模式
出现异常就忽略,常用于不重要的接口调用,比如日志记录
4.failbackc cluster模式
失败了后台自动记录请求,然后定时发送,比较适合消息队列这种情况
5.focking cluster模式
并行的调用多个provider,只要一个成功就立即返回
6.broadcast cluster模式
逐个调用所有的provider
dubbo的动态代理策略
默认使用 javassist动态字节码生成,创建代理类
问题5:你了不了解spi机制呢?如何基于spi机制对dubbo进行扩展?
SPI机制
-
简单来说,就是service provider interface;比如有一个接口A,现在这个接口A有三个实现类,那么在运行的时候对接口到底用哪个实现类呢?这就需要SPI,需要根据指定的配置和默认的配置,去找到对应的实现类加载进来,然后用这个实现类的对象
例如:接口A --> 实现类A1 实现类A2 实现类A3
配置一下,接口A= 实现类A2
在系统运行的时候,会加载这个配置,用实现类A2实例化出对象来提供服务
-
比如说你有一个工程Demo ,里面有一个接口A,接口A在工程里是没有实现的--> 系统在运行的时候,怎么给接口A选择一个实现类呢?
-
你就可以自己搞一个jar包,META-INF/services/,上放一个文件,文件名就是接口名,接口A,接口A的实现类=com.ultrapower.service.实现类A2.让工程A来依赖这个jar包,然后在系统运行的时候,工程跑起来,对接口A,就会扫描自己依赖的所有jar包,在每个jar包里找找,有没有 META-INF/service 文件夹?如果有,在里面找,有没有接口A这个名字的文件?如果有,在里面找找有没有接口A的实现类是你的jar包里的那个类
-
Dubbo的SPI机制
dubbo也用了spi的思想,不过没有用到jdk的spi机制,是自己实现的一套SPI机制
1 Protocol protocol=ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension()
-
Protocol接口,dubbo需要判断一下,在系统运行的时候,应该选用这个Protocol接口的哪个实现类来实例化对象来使用呢?
-
他会去找一个你配置的Protocol,它就会将你配置的Protocol实现类,加载到 jvm中来,然后会实例化对象,就会用到你提供的哪个Protocol接口实现类
-
微内核,可插拔,大量的组件,Protocol负责RPC调用,你可以实现自己的rpc调用组件,实现Protocol接口,给自己一个实现类即可
-
这行代码就是Dubbo中大量使用的,就是对很多组件,都保留一个接口和多个实现,然后在运行的时候动态的根据配置去找对应的实现类,如果没有配置,那就走默认的实现类。
1 2 @SPI("dubbo") 3 public interface Protocol{ 4 /** 5 * 获取缺省端口,当用户没有配置端口时使用。 6 * 7 * @return 缺省端口 8 */ 9 int getDefaultPort(); 10 11 /** 12 * 暴露远程服务:<br> 13 * 1. 协议在接收请求时,应记录请求来源方地址信息:RpcContext.getContext().setRemoteAddress();<br> 14 * 2. export()必须是幂等的,也就是暴露同一个URL的Invoker两次,和暴露一次没有区别。<br> 15 * 3. export()传入的Invoker由框架实现并传入,协议不需要关心。<br> 16 * 17 * @param <T> 服务的类型 18 * @param invoker 服务的执行体 19 * @return exporter 暴露服务的引用,用于取消暴露 20 * @throws RpcException 当暴露服务出错时抛出,比如端口已占用 21 */ 22 @Adaptive 23 <T> Exporter<T> export(Invoker<T> invoker) throws RpcException; 24 25 /** 26 * 引用远程服务:<br> 27 * 1. 当用户调用refer()所返回的Invoker对象的invoke()方法时,协议需相应执行同URL远端export()传入的Invoker对象的invoke()方法。<br> 28 * 2. refer()返回的Invoker由协议实现,协议通常需要在此Invoker中发送远程请求。<br> 29 * 3. 当url中有设置check=false时,连接失败不能抛出异常,并内部自动恢复。<br> 30 * 31 * @param <T> 服务的类型 32 * @param type 服务的类型 33 * @param url 远程服务的URL地址 34 * @return invoker 服务的本地代理 35 * @throws RpcException 当连接服务提供方失败时抛出 36 */ 37 @Adaptive 38 <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException; 39 40 /** 41 * 释放协议:<br> 42 * 1. 取消该协议所有已经暴露和引用的服务。<br> 43 * 2. 释放协议所占用的所有资源,比如连接和端口。<br> 44 * 3. 协议在释放后,依然能暴露和引用新的服务。<br> 45 */ 46 void destroy(); 47 48 }
在dubbo自己的jar里,在META-INF/dubbo/internal/com.alibaba.dubbo.rpc.Protocol文件中
dubbo=com.alibaba.dubbo.rpc.DubboProtocol
http=com.alibaba.dubbo.rpc.HttpProtocol
hessian=com.alibaba.dubbo.rpc.hessianProtocol
所有说这里就是dubbo默认的配置,其实就是一个Protocol接口,@SPI("dubbo")说的就是,通过SPI机制来提供实现类,实现类是通过dubbo作为默认key去配置文件里找的,配置文件的名称和接口名的全路径名时一样的,通过dubbo作为key 可以找到默认的实现类就是 com.alibaba.dubbo.rpc.DubboProtocol
dubbo默认的网络通信协议,就是dubbo协议,用的 DubboProtocol
如果想要动态替换默认的实现类,需要使用@Adapter接口,Protocol接口中,有两个方法加了@Adapter注解,就是说那俩接口会被代理实现。
比如:这个Protocol接口搞了俩@Adapter 注解标注了方法,在运行的时候,就会针对Protocol生成代理类,这个代理类的俩方法里面会代理代码,代理代码会在运行时候动态根据url中的protocol来获取那个key,默认是dubbo,你也可以自己指定别的key,那么就会获取别的实现类的实例了。
通过这个URL的参数不同,就可以控制动态的使用不同的组件实现类
问题6:如何基于dubbo做服务治理,服务降级,失败重试以及超时重试?
1.服务治理
1.调用链路生成
一个大型的分布式系统,由大量的服务组成。这些服务之间是如何调用的呢?调用链路是啥?
那就需要基于dubbo做的分布式系统中,对各个服务之间的调用自动记录下来,然后自动将各个服务的依赖关系和调用链路生成出来,做成一张图,大家可以看到的
2.服务访问压力以及时长统计
需要自动统计各个接口之间的访问次数和调用延迟,而且要分成两个级别。一个级别是接口粒度,就是每个服务额每个接口每天被调用的次数,TP50 ,TP90,TP99,三个档次的请求延迟分别是多少;第二个级别是从源头入口,一个完整的请求链路经过几十个服务之后,完成一次请求,每天全链路走多少次,全链路请求延迟的TP50,TP90,TP99
分别是多少?
这些东西都搞定以后,后面才看当前系统的压力到底在哪里?如何来扩容和优化?
3.其他的
服务分层(避免循环依赖),调用链路失败监控和报警,服务鉴权,每个服务 的可用性的监控
2.服务降级
比如服务A调用服务B,结果服务B挂掉了,服务A重试几次调用服务B,还是不行,直接降级,走一个备用逻辑,给用户返回响应
问题7:分布式服务接口的幂等性如何设计?
所谓幂等性,就是说一个接口,多次发起同一个请求,你这个接口得保证结果是准确的,比如不能多扣款,不能多插入一条数据,不能将统计值多加1。这就是幂等性。
保证幂等性的手段如下:
-
对于每次请求必须有一个全局唯一的标识
-
每次处理完请求后,必须有一个记录标识这个请求处理完了,比如常见的方案是在mysql 中记录个啥状态,比如支付之前记录一条这个订单的支付流水,而且支付流水采用orderid 作为 唯一键。只有成功插入这个支付流水,才可以执行实际的支付扣款。
-
每次接收到了请求需要进行判断之前是否处理过的逻辑处理,比如说吗,如果有一个订单已经支付了,就已经有一个支付流水了,那么如果重复发送请求,则此时先插入支付流水,然而orderid已经存在了,唯一键约束生效,报错插入失败,然后你就不用扣款了
-
一般生产上可以用redis中的 set来保证幂等
问题8:如何设计一个类似于Dubbo的rpc框架
简单梳理一下思路:
-
上来就要把服务到注册中心注册,就应该有一个注册中心,保留各个服务的地址信息,就可以用zookeeper来做。
-
然后消费者要去注册中心去拿服务信息,每个服务可能会存在于有多台机器
-
接着你就该发起一次请求,如何发呢? 基于动态代理,面向接口获取到一个动态代理,这个动态代理就是本地接口的一个代理,然后这个代理会找到服务对应的机器地址
-
然后找哪台机器发送请求? 这里会有一个负载均衡算法,比如可以使用随机轮训的算法
-
找到一台机器,如何发送给他? 可以用netty,nio的方式,第二个问题发送什么格式?可以是序列化格式或者json格式等
-
服务起那边一样,需要针对你的服务生成一个动态代理,监听某个网络端口,然后代理你本地的服务代码,接收到请求,就代理本地的服务方法。