Dubbo
什么是 RPC
RPC(Remote Procedure Call)—远程过程调用,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。比如两个不同的服务A,B部署在两台不同的机器上,那么服务 A 如果想要调用服务 B 中的某个方法该怎么办呢?使用 HTTP请求 当然可以,但是可能会比较慢而且一些优化做的并不好。 RPC 的出现就是为了解决这个问题。
什么是分布式?
分布式或者说 SOA 分布式重要的就是面向服务,说简单的分布式就是我们把整个系统拆分成不同的服务然后将这些服务放在不同的服务器上减轻单体服务的压力提高并发量和性能。比如电商系统可以简单地拆分成订单系统、商品系统、登录系统等等,拆分之后的每个服务可以部署在不同的机器上,如果某一个服务的访问量比较大的话也可以将这个服务同时部署在多台机器上。
为什么要分布式?
从开发角度来讲单体应用的代码都集中在一起,而分布式系统的代码根据业务被拆分。所以,每个团队可以负责一个服务的开发,这样提升了开发效率。另外,代码根据业务拆分之后更加便于维护和扩展。
另外,我觉得将系统拆分成分布式之后不光便于系统扩展和维护,更能提高整个系统的性能。
Dubbo 是什么
- 一款分布式服务框架
- 高性能和透明化的RPC (Remote Procedure Call远程过程调用),远程服务调用方案
- SOA(面向服务的架构)服务治理方案
Dubbo提供了很多协议,Dubbo协议、RMI协议、Hessian协议
为什么要用 Dubbo
如果你要开发分布式程序,你也可以直接基于 HTTP 接口进行通信,但是为什么要用 Dubbo呢?
我觉得主要可以从 Dubbo 提供的下面四点特性来说为什么要用 Dubbo:
-
负载均衡——同一个服务部署在不同的机器时该调用那一台机器上的服务
-
服务调用链路生成——随着系统的发展,服务越来越多,服务间依赖关系变得错踪复杂,甚至分不清哪个应用要在哪个应用之前启动,架构师都不能完整的描述应用的架构关系。Dubbo 可以为我们解决服务之 间互相是如何调用的。
-
服务访问压力以及时长统计、资源调度和治理——基于访问压力实时管理集群容量,提高集群利用率。
-
服务降级——某个服务挂掉之后调用备用服务
另外,Dubbo 除了能够应用在分布式系统中,也可以应用在现在比较火的微服务系统中。不过,由于 Spring Cloud 在微服务中应用更加广泛,所以,我觉得一般我们提 Dubbo 的话,大部分是分布式系统的情况。
Dubbo 的架构图解
-
Provider: 暴露服务的服务提供方
-
Consumer: 调用远程服务的服务消费方
-
Registry: 服务注册与发现的注册中心
-
Monitor: 统计服务的调用次数和调用时间的监控中心
-
Container: 服务运行容器
调用关系说明:
-
服务容器负责启动,加载,运行服务提供者。
-
服务提供者在启动时,向注册中心注册自己提供的服务。
-
服务消费者在启动时,向注册中心订阅自己所需的服务。
-
注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。
-
服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。
-
服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。
重要知识点总结:
-
注册中心负责服务地址的注册与查找,相当于目录服务,服务提供者和消费者只在启动时与注册中心交互,注册中心不转发请求,压力较小
-
监控中心负责统计各服务调用次数,调用时间等,统计先在内存汇总后每分钟一次发送到监控中心服务器,并以报表展示
-
注册中心,服务提供者,服务消费者三者之间均为长连接,监控中心除外
-
注册中心通过长连接感知服务提供者的存在,服务提供者宕机,注册中心将立即推送事件通知消费者
-
注册中心和监控中心全部宕机,不影响已运行的提供者和消费者,消费者在本地缓存了提供者列表
-
注册中心和监控中心都是可选的,服务消费者可以直连服务提供者
-
服务提供者无状态,任意一台宕掉后,不影响使用
-
服务提供者全部宕掉后,服务消费者应用将无法使用,并无限次重连等待服务提供者恢复
Dubbo 工作原理
各层说明:
-
第一层:service层,接口层,给服务提供者和消费者来实现的
-
第二层:config层,配置层,主要是对dubbo进行各种配置的
-
第三层:proxy层,服务接口透明代理,生成服务的客户端 Stub 和服务器端 Skeleton
-
第四层:registry层,服务注册层,负责服务的注册与发现
-
第五层:cluster层,集群层,封装多个服务提供者的路由以及负载均衡,将多个实例组合成一个服务
-
第六层:monitor层,监控层,对rpc接口的调用次数和调用时间进行监控
-
第七层:protocol层,远程调用层,封装rpc调用
-
第八层:exchange层,信息交换层,封装请求响应模式,同步转异步
-
第九层:transport层,网络传输层,抽象mina和netty为统一接口
-
第十层:serialize层,数据序列化层。网络传输需要。
Dubbo集群的负载均衡策略
Dubbo提供了常见的集群策略实现,并预扩展点予以自行实现。
- Random LoadBalance: 随机选取提供者策略,有利于动态调整提供者权重。截面碰撞率高,调用次数越多,分布越均匀;
- RoundRobin LoadBalance: 轮循选取提供者策略,平均分布,但是存在请求累积的问题;
- LeastActive LoadBalance: 最少活跃调用策略,解决慢提供者接收更少的请求;
- ConstantHash LoadBalance: 一致性Hash策略,使相同参数请求总是发到同一提供者,一台机器宕机,可以基于虚拟节点,分摊至其他提供者,避免引起提供者的剧烈变动;
配置方式
xml 配置方式
服务端服务级别
-
<dubbo:service interface="..." loadbalance="roundrobin" />
客户端服务级别
-
<dubbo:reference interface="..." loadbalance="roundrobin" />
服务端方法级别
-
<dubbo:service interface="...">
-
<dubbo:method name="..." loadbalance="roundrobin"/>
-
</dubbo:service>
客户端方法级别
-
<dubbo:reference interface="...">
-
<dubbo:method name="..." loadbalance="roundrobin"/>
-
</dubbo:reference>
注解配置方式:
消费方基于基于注解的服务级别配置方式:
-
@Reference(loadbalance = "roundrobin")
-
HelloService helloService;
核心的配置有哪些
dubbo:service/ 服务提供者暴露服务配置
dubbo:reference/ 服务消费者引用服务配置
dubbo:protocol/ 服务提供者协议配置
dubbo:registry/ 注册中心配置
dubbo:application/ 应用信息配置
dubbo:provider/ 服务提供者缺省值配置
dubbo:consumer/ 服务消费者缺省值配置
dubbo:method/ 方法级配置
zookeeper宕机与dubbo直连的情况
zookeeper宕机与dubbo直连的情况在面试中可能会被经常问到,所以要引起重视。
在实际生产中,假如zookeeper注册中心宕掉,一段时间内服务消费方还是能够调用提供方的服务的,实际上它使用的本地缓存进行通讯,这只是dubbo健壮性的一种提现。
dubbo的健壮性表现:
-
监控中心宕掉不影响使用,只是丢失部分采样数据
-
数据库宕掉后,注册中心仍能通过缓存提供服务列表查询,但不能注册新服务
-
注册中心对等集群,任意一台宕掉后,将自动切换到另一台
-
注册中心全部宕掉后,服务提供者和服务消费者仍能通过本地缓存通讯
-
服务提供者无状态,任意一台宕掉后,不影响使用
-
服务提供者全部宕掉后,服务消费者应用将无法使用,并无限次重连等待服务提供者恢复
注册中心负责服务地址的注册与查找,相当于目录服务,服务提供者和消费者只在启动时与注册中心交互,注册中心不转发请求,压力较小。所以,我们可以完全可以绕过注册中心——采用 dubbo 直连 ,即在服务消费方配置服务提供方的位置信息。
xml配置方式:
-
<dubbo:reference id="userService" interface="com.zang.gmall.service.UserService" url="dubbo://localhost:20880" />
注解方式:
-
@Reference(url = "127.0.0.1:20880")
-
HelloService helloService;
注册中心挂了可以继续通信吗?
可以,因为刚开始初始化的时候,消费者会将提供者的地址等信息拉取到本地缓存,所以注册中心挂了可以继续通信。
服务治理
于dubbo做的分布式系统中,对各个服务之间的调用自动记录下来,然后自动将各个服务之间的依赖关系和调用链路生成出来。
服务降级
比如说服务A调用服务B,结果服务B挂掉了,服务A重试几次调用服务B,还是不行,直接降级,走一个备用的逻辑,给用户返回响应
public interface HelloService { void sayHello(); } public class HelloServiceImpl implements HelloService { public void sayHello() { System.out.println("hello world......"); } }
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:dubbo="http://code.alibabatech.com/schema/dubbo" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd"> <dubbo:application name="dubbo-provider" /> <dubbo:registry address="zookeeper://127.0.0.1:2181" /> <dubbo:protocol name="dubbo" port="20880" /> <dubbo:service interface="com.zhss.service.HelloService" ref="helloServiceImpl" timeout="10000" /> <bean id="helloServiceImpl" class="com.zhss.service.HelloServiceImpl" /> </beans> <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:dubbo="http://code.alibabatech.com/schema/dubbo" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd"> <dubbo:application name="dubbo-consumer" /> <dubbo:registry address="zookeeper://127.0.0.1:2181" /> <dubbo:reference id="fooService" interface="com.test.service.FooService" timeout="10000" check="false" mock="return null"> </dubbo:reference> </beans>
现在就是mock,如果调用失败统一返回null
但是可以将mock修改为true,然后在跟接口同一个路径下实现一个Mock类,命名规则是接口名称加Mock后缀。然后在Mock类里实现自己的降级逻辑。
public class HelloServiceMock implements HelloService {
public void sayHello() {
// 降级逻辑
}
}
失败重试和超时重试
<dubbo:reference id="xxxx" interface="xx" check="true" async="false" retries="3" timeout="2000"/>
dubbo集群容错策略
Failover- 失败自动切换,当出现失败,重试其它服务器。通常用于读操作,但重试会带来更长延迟。可通过 retries="2" 来设置重试次数(不含第一次)。
Failfast- 快速失败,只发起一次调用,失败立即报错。通常用于非幂等性的写操作,比如新增记录。
Failsafe- 失败安全,出现异常时,直接忽略。通常用于写入审计日志等操作。
Failback- 失败自动恢复,后台记录失败请求,定时重发。通常用于消息通知操作。
Forking- 并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源。可通过 forks="2" 来设置最大并行数。
Broadcast- 播调用所有提供者,逐个调用,任意一台报错则报错。通常用于通知所有提供者更新缓存或日志等本地资源信息。
dubbo动态代理策略
默认使用javassist动态字节码生成,创建代理类,但是可以通过spi扩展机制配置自己的动态代理策略。
dubbo调用
详见:https://yq.aliyun.com/articles/608811?spm=a2c4e.11155435.0.0.37a258d6B9KrwE
Dubbo 缺省协议采用单一长连接,底层实现是 Netty 的 NIO 异步通讯机制;基于这种机制,Dubbo 实现了以下几种调用方式:
- 同步调用
- 异步调用
- 参数回调
- 事件通知
同步调用
同步调用是一种阻塞式的调用方式,即 Consumer 端代码一直阻塞等待,直到 Provider 端返回为止;
通常,一个典型的同步调用过程如下:
- Consumer 业务线程调用远程接口,向 Provider 发送请求,同时当前线程处于
阻塞
状态; - Provider 接到 Consumer 的请求后,开始处理请求,将结果返回给 Consumer;
- Consumer 收到结果后,当前线程继续往后执行。
这里有 2 个问题:
- Consumer 业务线程是怎么进入
阻塞
状态的? - Consumer 收到结果后,如果唤醒业务线程往后执行的?
其实,Dubbo 的底层 IO 操作都是异步的。Consumer 端发起调用后,得到一个 Future 对象。对于同步调用,业务线程通过Future#get(timeout)
,阻塞等待 Provider 端将结果返回;timeout
则是 Consumer 端定义的超时时间。当结果返回后,会设置到此 Future,并唤醒阻塞的业务线程;当超时时间到结果还未返回时,业务线程将会异常返回。
异步调用
dubbo默认使用同步的方式调用。但在有些特殊的场景下,我们可能希望异步调用dubbo接口,从而避免不必要的等待时间,这时候我们就需要用到异步。
异步调用主要是使用future来实现,当消费端发去远程调用时候,具体会调用到DubboInvoker的doInvoke方法,doInvoke代码如下:
protected Result doInvoke(final Invocation invocation) throws Throwable { RpcInvocation inv = (RpcInvocation) invocation; 忽略Attachment设置。。。 try { // 是否异步执行 boolean isAsync = RpcUtils.isAsync(getUrl(), invocation); boolean isOneway = RpcUtils.isOneway(getUrl(), invocation);//是否单向执行 int timeout = getUrl().getMethodParameter(methodName, Constants.TIMEOUT_KEY, Constants.DEFAULT_TIMEOUT);//接口超时时间,默认1秒 if (isOneway) { boolean isSent = getUrl().getMethodParameter(methodName, Constants.SENT_KEY, false); currentClient.send(inv, isSent); RpcContext.getContext().setFuture(null); return new RpcResult(); } else if (isAsync) {//异步 ResponseFuture future = currentClient.request(inv, timeout); RpcContext.getContext().setFuture(new FutureAdapter<Object>(future)); return new RpcResult(); } else { RpcContext.getContext().setFuture(null); return (Result) currentClient.request(inv, timeout).get(); } } catch (TimeoutException e) { //异常处理。。。 } }
可以看到,如果异步执行,会直接返回一个空的RpcResult,然后用户如果需要异步执行结果,可以从RpcContext中的Future中去获取,直接用RpcContext.getContext().getFuture().get();就可以获取到执行结果。那么RpcContext是如何保证当前线程可以拿到执行结果呢?答案是ThreadLocal。我们来看看RpcContext源码如下:
private static final ThreadLocal<RpcContext> LOCAL = new ThreadLocal<RpcContext>() { @Override protected RpcContext initialValue() { return new RpcContext(); } }; private Future<?> future; public static RpcContext getContext() { return LOCAL.get(); } public <T> Future<T> getFuture() { return (Future<T>) future; }
Dubbo协议及序列化
https://www.cnblogs.com/jameszheng/p/10271341.html
Dubbo通信协议适用范围及适用场景:
(1)Dubbo协议:适用范围:传入传出参数数据包较小(建议小于100K),消费者比提供者个数多,单一消费者无法压满提供者,尽量不要用dubbo协议传输大文件或超大字符串。适用场景:常规远程服务方法调用。
(2)RMI协议:RMI协议采用JDK标准的java.rmi.*实现,采用阻塞式短连接和JDK标准序列化方式,Java标准的远程调用协议。适用范围:传入传出参数数据包大小混合,消费者与提供者个数差不多,可传文件。适用场景:常规远程服务方法调用,与原生RMI服务互操作。
(3)Hessian协议:Hessian协议用于集成Hessian的服务,Hessian底层采用Http通讯,采用Servlet暴露服务,Dubbo缺省内嵌Jetty作为服务器实现。适用范围:传入传出参数数据包较大,提供者比消费者个数多,提供者压力较大,可传文件。适用场景:页面传输,文件传输,或与原生hessian服务互操作
(4)http协议:采用Spring的HttpInvoker实现,基于http表单的远程调用协议。序列化:表单序列化(JSON)适用范围:传入传出参数数据包大小混合,提供者比消费者个数多,可用浏览器查看,可用表单或URL传入参数,暂不支持传文件。适用场景:需同时给应用程序和浏览器JS使用的服务。
(5)Webservice:序列化:SOAP文本序列化;适用场景:系统集成,跨语言调用。
(6)Thrif:Thrift是Facebook捐给Apache的一个RPC框架,当前 dubbo 支持的 thrift 协议是对 thrift 原生协议的扩展,在原生协议的基础上添加了一些额外的头信息,比如service name,magic number等
dubbo 调用过程
https://www.cnblogs.com/aspirant/p/9002663.html
参考自:https://mp.weixin.qq.com/s/5LJUK8gjsS6GZ5ud5sO65Q
https://www.cnblogs.com/java-zhao/p/8424019.html
https://www.cnblogs.com/canmeng-cn/p/10554625.html