RPC
RPC【Remote Procedure Call】是指远程过程调用,是一种进程间通信方式,他是一种技术的思想,而不是规范。它允许程序调用另一个地址空间(通常是共享网络的另一台机器上)的过程或函数, 而不用程序员显式编码这个远程调用的细节。即程序员无论是调用用本地的还是远程的函数,本质上编写的调用代码基本相同。
RPC两个核心模块: 通讯, 序列化
决定RPC的效率: 通信效率, 序列化及反序列化效率
RPC框架:如 dubbo, gRPC, Thrift, HSF
Dubbo
概述
Apache dubbo 是一款高性能、 轻量级的开源 java RPC 框架,它提供了三大核心能力:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现。
特性
面向接口代理的高性能RPC调用
服务自动注册与发现
运行期流量调度
智能负载均衡
高度可扩展能力
可视化的服务治理与运维
总体架构
图例说明:
-
图中小方块 Protocol, Cluster, Proxy, Service, Container, Registry, Monitor 代表层或模块,蓝色的表示与业务有交互,绿色的表示只对 Dubbo 内部交互。
-
图中背景方块 Consumer, Provider, Registry, Monitor 代表部署逻辑拓扑节点。
-
图中蓝色虚线为初始化时调用,红色虚线为运行时异步调用,红色实线为运行时同步调用。
-
图中只包含 RPC 的层,不包含 Remoting 的层,Remoting 整体都隐含在 Protocol 中。
节点角色说明
节点 | 角色说明 |
---|---|
Provider | 暴露服务的服务提供方 |
Consumer | 调用远程服务的服务消费方 |
Registry | 服务注册与发现的注册中心 |
Monitor | 统计服务的调用次数和调用时间的监控中心 |
Container | 服务运行容器 |
连通性
-
注册中心负责服务地址的注册与查找,相当于目录服务,服务提供者和消费者只在启动时与注册中心交互,注册中心不转发请求,压力较小
-
监控中心负责统计各服务调用次数,调用时间等,统计先在内存汇总后每分钟一次发送到监控中心服务器,并以报表展示
-
服务提供者向注册中心注册其提供的服务,并汇报调用时间到监控中心,此时间不包含网络开销
-
服务消费者向注册中心获取服务提供者地址列表,并根据负载算法直接调用提供者,同时汇报调用时间到监控中心,此时间包含网络开销
-
注册中心,服务提供者,服务消费者三者之间均为长连接,监控中心除外
-
注册中心通过长连接感知服务提供者的存在,服务提供者宕机,注册中心将立即推送事件通知消费者
-
注册中心和监控中心全部宕机,不影响已运行的提供者和消费者,消费者在本地缓存了提供者列表
-
注册中心和监控中心都是可选的,服务消费者可以直连服务提供者
健壮性
-
监控中心宕掉不影响使用,只是丢失部分采样数据
-
数据库宕掉后,注册中心仍能通过缓存提供服务列表查询,但不能注册新服务
-
注册中心对等集群,任意一台宕掉后,将自动切换到另一台
-
注册中心全部宕掉后,服务提供者和服务消费者仍能通过本地缓存通讯
-
服务提供者无状态,任意一台宕掉后,不影响使用
-
服务提供者全部宕掉后,服务消费者应用将无法使用,并无限次重连等待服务提供者恢复
伸缩性
-
注册中心为对等集群,可动态增加机器部署实例,所有客户端将自动发现新的注册中心
-
服务提供者无状态,可动态增加机器部署实例,注册中心将推送新的服务提供者信息给消费者
升级性
未来可能的一种架构
节点角色说明
节点 | 角色说明 |
---|---|
Deployer | 自动部署服务的本地代理 |
Repository | 仓库用于存储服务应用发布包 |
Scheduler | 调度中心基于访问压力自动增减服务提供者 |
Admin | 统一管理控制台 |
Registry | 服务注册与发现的注册中心 |
Monitor | 统计服务的调用次数和调用时间的监控中心 |
详设代码架构
图例说明:
-
图中左边淡蓝背景的为服务消费方使用的接口,右边淡绿色背景的为服务提供方使用的接口,位于中轴线上的为双方都用到的接口。
-
图中从下至上分为十层,各层均为单向依赖,右边的黑色箭头代表层之间的依赖关系,每一层都可以剥离上层被复用,其中,Service 和 Config 层为 API,其它各层均为 SPI。
-
图中绿色小块的为扩展接口,蓝色小块为实现类,图中只显示用于关联各层的实现类。
-
图中蓝色虚线为初始化过程,即启动时组装链,红色实线为方法调用过程,即运行时调用链,紫色三角箭头为继承,可以把子类看作父类的同一个节点,线上的文字为调用的方法。
各层说明
-
Config 配置层:对外配置接口,以
ServiceConfig
,ReferenceConfig
为中心,可以直接初始化配置类,也可以通过 spring 解析配置生成配置类 -
Proxy 服务代理层:服务接口透明代理,生成服务的客户端 Stub 和服务器端 Skeleton, 以
ServiceProxy
为中心,扩展接口为ProxyFactory
-
Registry 注册中心层:封装服务地址的注册与发现,以服务 URL 为中心,扩展接口为
RegistryFactory
,Registry
,RegistryService
-
Cluster 路由层:封装多个提供者的路由及负载均衡,并桥接注册中心,以
Invoker
为中心,扩展接口为Cluster
,Directory
,Router
,LoadBalance
-
Monitor 监控层:RPC 调用次数和调用时间监控,以
Statistics
为中心,扩展接口为MonitorFactory
,Monitor
,MonitorService
-
Protocol 远程调用层:封装 RPC 调用,以
Invocation
,Result
为中心,扩展接口为Protocol
,Invoker
,Exporter
-
Exchange 信息交换层:封装请求响应模式,同步转异步,以
Request
,Response
为中心,扩展接口为Exchanger
,ExchangeChannel
,ExchangeClient
,ExchangeServer
-
Transport 网络传输层:抽象 mina 和 netty 为统一接口,以
Message
为中心,扩展接口为Channel
,Transporter
,Client
,Server
,Codec
-
Serialize 数据序列化层:可复用的一些工具,扩展接口为
Serialization
,ObjectInput
,ObjectOutput
,ThreadPool
关系说明
-
在 RPC 中,Protocol 是核心层,也就是只要有 Protocol + Invoker + Exporter 就可以完成非透明的 RPC 调用,然后在 Invoker 的主过程上 Filter 拦截点。
-
图中的 Consumer 和 Provider 是抽象概念,只是想让看图者更直观的了解哪些类分属于客户端与服务器端,不用 Client 和 Server 的原因是 Dubbo 在很多场景下都使用 Provider, Consumer, Registry, Monitor 划分逻辑拓扑节点,保持统一概念。
-
而 Cluster 是外围概念,所以 Cluster 的目的是将多个 Invoker 伪装成一个 Invoker,这样其它人只要关注 Protocol 层 Invoker 即可,加上 Cluster 或者去掉 Cluster 对其它层都不会造成影响,因为只有一个提供者时,是不需要 Cluster 的。
-
Proxy 层封装了所有接口的透明化代理,而在其它层都以 Invoker 为中心,只有到了暴露给用户使用时,才用 Proxy 将 Invoker 转成接口,或将接口实现转成 Invoker,也就是去掉 Proxy 层 RPC 是可以 Run 的,只是不那么透明,不那么看起来像调本地服务一样调远程服务。
-
而 Remoting 实现是 Dubbo 协议的实现,如果你选择 RMI 协议,整个 Remoting 都不会用上,Remoting 内部再划为 Transport 传输层和 Exchange 信息交换层,Transport 层只负责单向消息传输,是对 Mina, Netty, Grizzly 的抽象,它也可以扩展 UDP 传输,而 Exchange 层是在传输层之上封装了 Request-Response 语义。
-
Registry 和 Monitor 实际上不算一层,而是一个独立的节点,只是为了全局概览,用层的方式画在一起。
模块分包
模块说明:
-
dubbo-common 公共逻辑模块:包括 Util 类和通用模型。
-
dubbo-remoting 远程通讯模块:相当于 Dubbo 协议的实现,如果 RPC 用 RMI协议则不需要使用此包。
-
dubbo-rpc 远程调用模块:抽象各种协议,以及动态代理,只包含一对一的调用,不关心集群的管理。
-
dubbo-cluster 集群模块:将多个服务提供方伪装为一个提供方,包括:负载均衡, 容错,路由等,集群的地址列表可以是静态配置的,也可以是由注册中心下发。
-
dubbo-registry 注册中心模块:基于注册中心下发地址的集群方式,以及对各种注册中心的抽象。
-
dubbo-monitor 监控模块:统计服务调用次数,调用时间的,调用链跟踪的服务。
-
dubbo-config 配置模块:是 Dubbo 对外的 API,用户通过 Config 使用Dubbo,隐藏 Dubbo 所有细节。
-
dubbo-container 容器模块:是一个 Standlone 的容器,以简单的 Main 加载 Spring 启动,因为服务通常不需要 Tomcat/JBoss 等 Web 容器的特性,没必要用 Web 容器去加载服务。
整体上按照分层结构进行分包,与分层的不同点在于:
-
Container 为服务容器,用于部署运行服务,没有在层中画出。
-
Protocol 层和 Proxy 层都放在 rpc 模块中,这两层是 rpc 的核心,在不需要集群也就是只有一个提供者时,可以只使用这两层完成 rpc 调用。
-
Transport 层和 Exchange 层都放在 remoting 模块中,为 rpc 调用的通讯基础。
-
Serialize 层放在 common 模块中,以便更大程度复用。
详设中的call调用链(——>Call)
暴露服务时序
展开总设计图右边服务提供方暴露服务的蓝色初始化链,时序图如下:
引用服务时序
展开总设计图左边服务消费方引用服务的绿色初始化链,时序图如下:
领域模型
在 Dubbo 的核心领域模型中:
-
Protocol 是服务域,它是 Invoker 暴露和引用的主功能入口,它负责 Invoker 的生命周期管理。
-
Invoker 是实体域,它是 Dubbo 的核心模型,其它模型都向它靠拢,或转换成它,它代表一个可执行体,可向它发起 invoke 调用,它有可能是一个本地的实现,也可能是一个远程的实现,也可能一个集群实现。
-
Invocation 是会话域,它持有调用过程中的变量,比如方法名,参数等。
基本设计原则
-
采用 Microkernel + Plugin 模式,Microkernel 只负责组装 Plugin,Dubbo 自身的功能也是通过扩展点实现的,也就是 Dubbo 的所有功能点都可被用户自定义扩展所替换。
-
采用 URL 作为配置信息的统一格式,所有扩展点都通过传递 URL 携带配置信息。
dubbo开发
高可用
1. zookeeper注册中心宕机,还可以消费dubbo暴露的服务
监控中心宕掉不影响使用,只是丢失部分采样数据
数据库宕掉后,注册中心仍能通过缓存提供服务列表查询,但不能注册新服务。
注册中心对等集群,任意一台宕掉后,将自动切换到另一台
注册中心全部宕掉后,服务提供者和服务消费者仍能通过本地缓存通讯。
服务提供者无状态,任意一台宕掉后,不影响使用
服务提供都全部宕掉后,服务消费者应用将无法使用,并无限次重连等待服务提供者恢复
dubbo也可以直接配置本地直连
@DubboReference(url="127.0.0.1:20882")(url是服务端地址)
负载均衡
在集群负载均衡时,Dubbo提供了多种均衡策略,缺省为random随机调用.
负载均衡策略
Random LoadBalance
随机,按权重设置随机概率: 在一个截面上碰撞的概率高,但调用量越大分布越均匀,而且按概率使用权重后也比较均匀,有利于动态调整提供者权重。
RoundRobin LoadBalance
轮循,按公约后的权重设置轮循比率:存在慢的提供者累积请求的问题,比如:第二台机器慢,但没挂,当请求调到第二台时就卡住,久而久之,所有请求都卡在调到第二台上。
@DubboReference(loadbalance="roundRobin") //配置
LeastActive LoadBalance
最少活跃调用数,相同活跃数据的随机,活跃数指调用前后计数差。使慢的提供者收到更少请求,因为越慢的提供者的调用前后计数差会越大。
ConsistentHash LoadBalance
一致性Hash,相同参数的请求总是发到同一提供者。当某一台提供者挂时,原本发往该提供者的请求,基于虚拟节点,平摊到其它提供者,不会引起剧烈变动。算法参考:http://en.wikipedia.org/wiki/Consistent_hashing
缺省只对第一个参数 Hash,如果要修改,请配置 <dubbo:parameter key="hash.arguments" value="0,1" />
缺省用 160 份虚拟节点,如果要修改,请配置 <dubbo:parameter key="hash.nodes" value="320" />
配置
以下将以轮询的方式举例,可以根据自己实际需要修改成其他策略。
1. 服务端服务级别
1.1 XML配置方式:
<dubbo:service interface="..." loadbalance="roundrobin" />
1.2 yml配置(SpringBoot的application.yml):
dubbo:
provider:
loadbalance: roundrobin
1.3 基于注解的配置(SpringBoot):
具体可以查看dubbo注解:@Service
@Service(version = "${product.service.version}",loadbalance="roundrobin")
public class ProductServiceImpl implements ProductService {
@Override
public String helloDubbo() {
return "hello,spring-boot dubbo";
}
}
2. 客户端服务级别
2.1 XML配置方式:
<dubbo:reference interface="..." loadbalance="roundrobin" />
2.2 yml配置(SpringBoot)
dubbo:
consumer:
loadbalance: roundrobin
2.3 基于注解的配置(SpringBoot):
具体可以查看注解:@Reference
@RestController
public class ProductController {
@Reference(version = "${product.service.version}",loadbalance="roundrobin")
private ProductService productService;
@RequestMapping(value = "test")
public String test() {
return productService.helloDubbo();
}
}
3. 服务端方法级别
3.1 XML配置:
<dubbo:service interface="...">
<dubbo:method name="..." loadbalance="roundrobin"/>
</dubbo:service>
4. 客户端方法级别
4.1 XML配置:
<dubbo:reference interface="...">
<dubbo:method name="..." loadbalance="roundrobin"/>
</dubbo:reference>
配置好之后,运行两个服务提供者实例,分别配置不同dubbo.protocol.port 端口:8898、8899
启动消费端,访问两次:http://localhost:8888/test,输出显示如下:
hello,spring-boot dubbo,port:8898
hello,spring-boot dubbo,port:8899
服务降级
当服务压力剧增的情况下,根据实际业务情况及流量,对一些服务和页面有策略的不处理或换种简单的方式处理,从而释放服务器资源以保证核心变易正常动作或高效动作。
可以通过服务降级功能临时屏蔽某个出错的非关键 服务,并定义降级后的返回策略。向注册中心写入动态配置覆盖规则:
RegistryFactory registryFactory = ExtensionLoader.getExtensionLoader(RegistryFactory.class).getAdaptiveExtension();
Registry registry = registryFactory.getRegistry(URL.valueOf("zookeeper://10.20.153.10:2181"));
registry.register(URL.valueOf("override://0.0.0.0/com.foo.BarService?category=configurators&dynamic=false&application=foo&mock=force:return+null"));
其中:
mock=force:return+null 表示消费方对该服务的方法调用都直接返回null值,不发起远程调用。用来屏蔽不重要服务不可用时对调用方的影响。
mock=fail:return+null 表示消费方对该服务的方法调用在失败后,再返回null值,不抛异常。用来容忍
集群容错
在集群调用失败时,Dubbo 提供了多种容错方案,缺省为 failover 重试。
2.1 Failover Cluster(缺省配置) 失败自动切换,当出现失败,重试其它服务器。通常用于读操作,但重试会带来更长延迟。可通过 retries=“2” 来设置重试次数(不含第一次)。
重试次数配置如下:
<dubbo:service retries="2" />
<dubbo:reference retries="2" />
<dubbo:reference><dubbo:method name="findFoo" retries="2" />
</dubbo:reference>
源码分析:
copyinvokers 变量,候选的 Invoker 集合。 调用父 #checkInvokers(copyinvokers, invocation) 方法,校验候选的 Invoker 集合非空。如果为空,抛出 RpcException 异常。 获得最大可调用次数:最大可重试次数 +1 。默认最大可重试次数Constants.DEFAULT_RETRIES = 2 。 le 变量,保存最后一次调用的异常。 invoked 变量,保存已经调用的 Invoker 集合。 providers 变量,保存已经调用的网络地址集合。 failover 机制核心实现:如果出现调用失败,那么重试其他服务器。 超过最大调用次数,抛出 RpcException 异常。该异常中,带有最后一次调用异常的信息。 2.2 Forking Cluster 并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源。可通过 forks=“2” 来设置最大并行数。
源码分析:
count 变量,异常计数器。 ref 变量,阻塞队列。通过它,实现线程池异步执行任务的结果通知,非常亮眼。 循环 selected 集合,提交线程池,发起 RPC 调用。 从 ref 队列中,阻塞等待,直到获得到结果或者超时。至此,ForkingClusterInvoker 实现了并行调用,且只要一个成功即返回。当然,还有一个隐性的,所有都失败才返回。 处理等待的“结果”。
2.3 Failfast Cluster 快速失败,只发起一次调用,失败立即报错。通常用于非幂等性的写操作,比如新增记录。
2.4 Failsafe Cluster 失败安全,出现异常时,直接忽略。通常用于写入审计日志等操作。
2.5 Failback Cluster 失败自动恢复,后台记录失败请求,定时重发。通常用于消息通知操作。
2.6 Broadcast Cluster 广播调用所有提供者,逐个调用,任意一台报错则报错。通常用于通知所有提供者更新缓存或日志等本地资源信息。
现在广播调用中,可以通过 broadcast.fail.percent 配置节点调用失败的比例,当达到这个比例后,BroadcastClusterInvoker 将不再调用其他节点,直接抛出异常。 broadcast.fail.percent 取值在 0~100 范围内。默认情况下当全部调用失败后,才会抛出异常。 broadcast.fail.percent 只是控制的当失败后是否继续调用其他节点,并不改变结果(任意一台报错则报错)。broadcast.fail.percent 参数 在 dubbo2.7.10 及以上版本生效。
Broadcast Cluster 配置 broadcast.fail.percent。
broadcast.fail.percent=20 代表了当 20% 的节点调用失败就抛出异常,不再调用其他节点。
@reference(cluster = "broadcast", parameters = {"broadcast.fail.percent", "20"})
源码分析:
集群模式配置
按照以下示例在服务提供方和消费方配置集群模式
<dubbo:service cluster="failsafe" />
<dubbo:reference cluster="failsafe" />
Hystrix
Hystrix最全详解(看这篇就够了) – mikechen
dubbo原理汇总
netty
Netty基本原理:(Java开发中Netty线程模型原理解析-JavaEE资讯-博学谷)
BossGroup
BossGroup用来接受客户端发来的连接WorkerGroup则负责对完成TCP三次握手的连接进行处理。
Boss NioEventLoop线程的执行步骤:
(1)处理accept事件与client建立连接, 生成NioSocketChannel。
(2)将NioSocketChannel注册到某个worker NIOEventLoop上的selector
(3)处理任务队列的任务 即runAllTasks。
Worker NioEventLoop线程的执行步骤:
(1)轮询注册到自己Selector上的所有NioSocketChannel的read和write事件。
(2)处理read和write事件在对应NioSocketChannel处理业务。
(3)#runAllTasks处理任务队列TaskQueue的任务,一些耗时的业务处理可以放入TaskQueue中慢慢处理这样不影响数据在pipeline中的流动处理。
Worker NIOEventLoop处理NioSocketChannel业务时,使用了pipeline (管道),管道中维护了handler处理器链表用来处理channel中的数据。
ChannelPipeline
Netty将Channel的数据管道抽象为ChannelPipeline,消息在ChannelPipline中流动和传递。ChannelPipeline持有I/O事件拦截器ChannelHandler的双向链表,由ChannelHandler对I/O事件进行拦截和处理,可以方便的新增和删除ChannelHandler来实现不同的业务逻辑定制不需要对已有的ChannelHandler进行修改能够实现对修改封闭和对扩展的支持。
ChannelPipeline是一系列的ChannelHandler实例,流经一个Channel的入站和出站事件可以被ChannelPipeline 拦截。每当一个新的Channel被创建了,都会建立一个新的ChannelPipeline并绑定到该Channel上,这个关联是永久性的;Channel既不能附上另一个ChannelPipeline也不能分离当前这个。这些都由Netty负责完成,而无需开发人员的特别处理。
根据起源一个事件将由ChannelInboundHandler或ChannelOutboundHandler处理,ChannelHandlerContext实现转发或传播到下一个ChannelHandler。一个ChannelHandler处理程序可以通知ChannelPipeline中的下一个ChannelHandler执行。Read事件(入站事件)和write事件(出站事件)使用相同的pipeline,入站事件会从链表head 往后传递到最后一个入站的handler出站事件会从链表tail往前传递到最前一个出站的 handler,两种类型的 handler 互不干扰。
(看不懂)ChannelInboundHandler回调方法:
(看不懂)ChannelOutboundHandler回调方法:
异步非阻塞
写操作:通过NioSocketChannel的write方法向连接里面写入数据时候是非阻塞的,马上会返回即使调用写入的线程是我们的业务线程。Netty通过在ChannelPipeline中判断调用NioSocketChannel的write的调用线程是不是其对应的NioEventLoop中的线程,如果发现不是则会把写入请求封装为WriteTask投递到其对应的NioEventLoop中的队列里面,然后等其对应的NioEventLoop中的线程轮询读写事件时候,将其从队列里面取出来执行。
读操作:当从NioSocketChannel中读取数据时候并不是需要业务线程阻塞等待,而是等NioEventLoop中的IO轮询线程发现Selector上有数据就绪时,通过事件通知方式来通知业务数据已就绪,可以来读取并处理了。
每个NioSocketChannel对应的读写事件都是在其对应的NioEventLoop管理的单线程内执行,对同一个NioSocketChannel不存在并发读写,所以无需加锁处理。
使用Netty框架进行网络通信时,当我们发起I/O请求后会马上返回,而不会阻塞我们的业务调用线程;如果想要获取请求的响应结果,也不需要业务调用线程使用阻塞的方式来等待,而是当响应结果出来的时候,使用I/O线程异步通知业务的方式,所以在整个请求 -> 响应过程中业务线程不会由于阻塞等待而不能干其他事情。
netty + dubbo源码解析
学习Dubbo-带你通过源码看看dubbo对netty的使用 - 知乎 (zhihu.com)
最后,来一张示意图做个调用的总结: