RPC是什么
RPC是远程过程调用(Remote Procedure Call)的缩写形式。
RPC的概念与技术早在1981年由Nelson提出。
1984年,Birrell和Nelson把其用于支持异构型分布式系统间的通讯。Birrell的RPC 模型引入存根进程( stub) 作为远程的本地代理,调用RPC运行时库来传输网络中的调用。Stub和RPC runtime屏蔽了网络调用所涉及的许多细节,特别是,参数的编码/译码及网络通讯是由stub和RPC runtime完成的,因此这一模式被各类RPC所采用。
简单来说,就是“像调用本地方法一样调用远程方法”。
RPC原理
核心是代理机制。
- 本地代理存根: Stub
- 本地序列化反序列化
- 网络通信
- 远程序列化反序列化
- 远程服务存根: Skeleton
- 调用实际业务服务
- 原路返回服务结果
- 返回给本地调用方
1. 设计
RPC是基于接口的远程服务调用。
本地应用程序与远程应用程序,分别需要共享: POJO实体类定义,接口定义
远程->服务提供者,本地->服务消费者。
2. 代理
RPC是基于接口的远程服务调用。
Java下,代理可以选择动态代理,或者AOP实现。
3. 序列化
序列化和反序列化的选择:
1、语言原生的序列化,RMI,Remoting
2、二进制平台无关,Hessian,avro,kyro,fst等 3、文本,JSON、XML等
4. 网络传输
最常见的传输方式:
- TCP/SSL
- HTTP/HTTPS
5.查找实现类
通过接口查找服务端的实现类。
一般是注册方式,例如 dubbo 默认将接口和实现类配置到Spring。
服务注册
服务提供者启动时:
- 将自己注册到注册中心(比如zk实现)的临时节点。
- 停止或者宕机时,临时节点消失。 注册的数据格式:
- 节点key,代表当前服务(或者服务+版本)
- 多个子节点,每一个为一个提供者的描述信息
服务发现
服务消费者启动时,
- 从注册中心代表服务的主节点拿到多个代表提供者的临时节点列表,并本地缓存。
- 根据router和loadbalance算法从其中的某一个执行调用。
- 如果可用的提供者集合发生变化时,注册中心通知消费者刷新本地缓存的列表。
例如zk可以使用curator作为客户端操作。
服务集群
多个服务提供者都提供了同样的服务,这时应该如何处理? 对于完全相同能力的多个服务,我们希望他们能一切协同工作,分摊处理流量。
- 路由
- 负载均衡
服务路由
跟网关的路由一样
1、比如基于IP段的过滤,
2、再比如服务都带上tag,用tag匹配这次调用范围。
服务负载均衡(Service LoadBalance)
跟Nginx的负载均衡一样。 多个不同策略,原理不同,目的基本一致(尽量均匀):
1、Random(带权重)
2、RoundRobin(轮询)
3、LeastActive(快的多给)
4、ConsistentHashLoadBalance(同样参数请求到一个提供者)
服务过滤
所有的复杂处理,都可以抽象为管道+过滤器模式(Channel+Filter)
这个机制是一个超级bug的存在,可以用来实现额外的增强处理(类似AOP),也可以中断当前处理流程,返回特定数据。
对比考虑一下,我们NIO网关时的filter,servlet的filter等。
为什么需要服务流控(Flow Control)
稳定性工程:
1、我们逐渐意识到一个问题:系统会故障是正常现象,就像人会生病
2、那么在系统出现问题时,直接不服务,还是保持部分服务能力呢?
系统的容量有限。
保持部分服务能力是最佳选择,然后在问题解决后恢复正常状态。
响应式编程里,这就是所谓的回弹性(Resilient)。
需要流控的本质原因是,输入请求大于处理能力。
流控有三个级别:
1、限流(内部线程数,外部调用数或数据量)
2、服务降级(去掉不必要的业务逻辑,只保留核心逻辑)
3、过载保护(系统短时间不提供新的业务处理服务,积压处理完后再恢复输入请求)
我们自己如何设计一个RPC框架,从哪些方面考虑?
基于共享接口还是IDL?
动态代理 or AOP?
序列化用什么?文本 or 二进制?
基于TCP还是HTTP?
Dubbo的主要功能
Apache Dubbo 是一款高性能、轻量级的开源 Java 服务框架
六大核心能力:
面向接口代理的高性能RPC调用,智能容错和负载均衡,服务自动注册和发现,高度可 扩展能力,运行期流量调度,可视化的服务治理与运维。
Dubbo的主要功能
基础功能:
RPC调用
- 多协议(序列化、传输、RPC)
- 服务注册发现
- 配置、元数据管理 框架分层设计,可任意组装和扩展。
扩展功能:
集群、高可用、管控 - 集群,负载均衡
- 治理,路由
- 控制台,管理与监控
灵活扩展+简单易用,是Dubbo成功的秘诀。
Dubbo整体架构
- 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
Dubbo SPI的应用
本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类。这样可以在运行时,动态为接口替换实现类。
在Java中SPI是被用来设计给服务提供商做插件使用的。基于策略模式 来实现动态加载的机制 。我们在程序只定义一个接口,具体的实现交个不同的服务提供者;在程序启动的时候,读取配置文件,由配置确定要调用哪一个实现;
通过 SPI 机制为我们的程序提供拓展功能,在dubbo中,基于 SPI,我们可以很容易的对 Dubbo 进行拓展。例如dubbo当中的protocol,LoadBalance等都是通过SPI机制扩展。启动时装配,并缓存到ExtensionLoader中。
Java SPI和Dubbo SPI对比
- 原始的JDK SPI不支持缓存: Dubbo设计了缓存对象-cachedInstances 是一个 new ConcurrentHashMap<String, Holder>()
- 原始JDK SPI不支持默认值: Dubbo设计默认值 -@SPI("dubbo")代表默认的spi对象,例如 Protocol的@SPI("dubbo")就是DubboProtocol通过ExtensionLoader.getExtensionLoader(Protocol.class).getDefaultExtension()拿默认对象
- JDK要使用For循环判断对象: Dubbo设计的getExtension灵活方便,动态获取spi对象,例如 ExtensionLoader.getExtensionLoader(Protocol.class).getExtension(spi的key)来提取对象
- 原始JDK不支持AOP功能: Dubbo设计增加了AOP功能,在cachedWrapperClasses,在原始类,包装了XxxxFilterWrapper XxxxListenerWrapper,例如 ProtocolFilterWrapper
- 原始JDK不支持IOC功能: Dubbo设计增加了IOC,通过构造函数注入代码,例如 wrapperClass.getConstructor(type).newInstance(instance) ,首先获得自适应扩展点的Class对象,然后通过反射获取实例,将对象创建的权力交给框架,这就是控制反转
- 对@Reference标注的接口查看是否合法,检查该接口是不是存在泛型
- 在系统中拿到dubbo.resolve.file这个文件,这个文件是进行配置consumer的接口的。将配置好的consumer信息存到URL中
- 将配置好的ApplicationConfig、ConsumerConfig、ReferenceConfig、MethodConfig,以及消费者的IP地址存到系统的上下文中
- 接下来开始创建代理对象进入到ReferenceConfig的createProxy 。这里还是在ReferenceConfig类中。上面的那些配置统统传入该方法中。上面有提到resolve解析consumer为URL,现在就根据这个URL首先判断是否远程调用还是本地调用。
4.1 若是本地调用,则调用 InjvmProtocol 的 refer 方法生成 InjvmInvoker 实例
4.2 若是远程调用,则读取直连配置项,或注册中心 url,并将读取到的 url 存储到 urls 中。然后根据 urls 元素数量进行后续操作。若 urls 元素数量为1,则直接通过 Protocol 自适应拓展类即RegistryProtocol类或者DubboProtocol构建 Invoker 实例接口,这得看URL前面的是registry://开头还是以dubbo://。若 urls 元素数量大于1,即存在多个注册中心或服务直连 url,此时先根据 url 构建 Invoker。然后再通过 Cluster 合并即merge多个 Invoker,最后调用 ProxyFactory 生成代理类。 - RegistryProtocol:在refer方法中首先为 url 设置协议头,然后根据 url 参数加载注册中心实例。然后获取 group 配置,根据 group 配置决定 doRefer 第一个参数的类型。doRefer 方法创建一个 RegistryDirectory 实例,然后生成服务消费者链接,通过registry.register方法向注册中心注册消费者的链接,然后通过directory.subscribe向注册中心订阅 providers、configurators、routers 等节点下的数据。完成订阅后,RegistryDirectory 会收到这几个节点下的子节点信息。由于一个服务可能部署在多台服务器上,这样就会在 providers 产生多个节点,这个时候就需要 Cluster 将多个服务节点合并为一个,并生成一个 Invoker。同样Invoker创建过程先不分析,后面会拿一章专门介绍。
- ProxyFactory:Invoker 创建完毕后,接下来要做的事情是为服务接口生成代理对象。有了代理对象,即可进行远程调用。代理对象生成的入口方法为的getProxy。获取需要创建的接口列表,组合成数组。而后将该接口数组传入 Proxy 的 getProxy 方法获取 Proxy 子类,然后创建 InvokerInvocationHandler 对象,并将该对象传给 newInstance 生成 Proxy 实例。InvokerInvocationHandler 实现 JDK 的 InvocationHandler 接口,具体的用途是拦截接口类调用。可以理解为AOP或拦截器。也就是在获取该对象之前会调用到Proxy实例而不会调用到服务提供者对应的类。至于如何创建proxy实例,请看后面源码的注释。
- Router : 选取此次调用可以提供服务的invoker集合
- LoadBalance:从上述集合选取一个作为最终调用者 Random,RoundRobin
- failover:失效转移 Fail-Over的含义为“失效转移”,是一种备份操作模式,当主要组件异常时,其功能转移到备份组件。其要点在于有主有备,且主故障时备可启用,并设置为主。如Mysql的双Master模式,当正在使用的Master出现故障时,可以拿备Master做主使用
- failfast:快速失败
从字面含义看就是“快速失败”,尽可能的发现系统中的错误,使系统能够按照事先设定好的错误的流程执行,对应的方式是“fault-tolerant(错误容忍)”。以JAVA集合(Collection)的快速失败为例,当多个线程对同一个集合的内容进行操作时,就可能会产生fail-fast事件。例如:当某一个线程A通过iterator去遍历某集合的过程中,若该集合的内容被其他线程所改变了;那么线程A访问集合时,就会抛出ConcurrentModificationException异常(发现错误执行设定好的错误的流程),产生fail-fast事件。 - failback:失效自动恢复
Fail-over之后的自动恢复,在簇网络系统(有两台或多台服务器互联的网络)中,由于要某台服务器进行维修,需要网络资源和服务暂时重定向到备用系统。在此之后将网络资源和服务器恢复为由原始主机提供的过程,称为自动恢复 - failsafe:失效安全 Fail-Safe的含义为“失效安全”,即使在故障的情况下也不会造成伤害或者尽量减少伤害。维基百科上一个形象的例子是红绿灯的“冲突监测模块”当监测到错误或者冲突的信号时会将十字路口的红绿灯变为闪烁错误模式,而不是全部显示为绿灯。
Dubbo服务如何暴露
在暴露服务的时候,要获取到invoker,再用exporter来暴露执行器。Exporter会用两个,dubboExporter和registryExporter。DubboExporter来开启netty服务器,registryExporter用来注册,服务(执行器)和对应的url地址,注册到注册表里。
首先ServiceConfig类拿到对外提供服务的实际类ref,然后将ProxyFactory类的getInvoker方法使用ref生成一个AbstractProxyInvoker实例,到这一步就完成具体服务到invoker的转化。接下来就是Invoker转换到Exporter的过程。
Dubbo处理服务暴露的关键就在Invoker转换到Exporter的过程
Dubbo服务如何引用
当我们使用@Reference注解将对应服务注入到其他类中这时候Spring会第一时间调用getObject方法,而getObject中只有一个方法就是get()。这里可以理解为消费者开始引入服务了。
饿汉式:在 Spring 容器调用 ReferenceBean 的 afterPropertiesSet 方法时引用服务。
懒汉式:在 ReferenceBean 对应的服务被注入到其他类中时引用。Dubbo默认使用懒汉式。
ReferenceConfig:通过get方法其实是进入到ReferenceConfig类中执行init()方法。在这个方法里主要做了下面几件事情:
集群与路由
泛化引用
GenericService
当我们知道接口、方法和参数,不用存根方式,而是用反射方式调用任何服务。
Dubbo最佳实践
建议将服务接口、服务模型、服务异常等均放在 API 包中,因为服务模型和异常也是 API 的一部分,这样做也符合分包原则:重用发布等价原则(REP),共同重用原则 (CRP)。
服务接口尽可能大粒度,每个服务方法应代表一个功能,而不是某功能的一个步骤,否 则将面临分布式事务问题,Dubbo 暂未提供分布式事务支持。
服务接口建议以业务场景为单位划分,并对相近业务做抽象,防止接口数量爆炸。 不建议使用过于抽象的通用接口,如:Map query(Map),这样的接口没有明确语义, 会给后期维护带来不便。
Dubbo参数配置
通用参数以 consumer 端为准,如果consumer端没有设置,使用provider数值
建议在 Provider 端配置的 Consumer 端属性有:
timeout:方法调用的超时时间
retries:失败重试次数,缺省是2
loadbalance:负载均衡算法,缺省是随机 random。
actives:消费者端的最大并发调用限制,即当 Consumer 对一个服务的并发调用到上限后,新 调用会阻塞直到超时,可以配置在方法或服务上。
建议在 Provider 端配置的 Provider 端属性有:
threads:服务线程池大小
executes:一个服务提供者并行执行请求上限,即当 Provider 对一个服务的并发调用达到上限 后,新调用会阻塞,此时 Consumer 可能会超时。可以配置在方法或服务上。
幂等
根据业务特点,针对特定的业务类型,添加业务操作日志。比如定时为某些用户下发订单的场景,可以将用户订购信息添加到单独日志表中或者redis中,且这些日志信息应该是跟业务无关的,只用来做防止重复订购的校验,使用完后可以定时清理掉,或者自动失效,避免堆积太多的垃圾数据。消息的结构可以包括:用户标识、业务标识、操作时间、操作结果,其中业务类型就表示这是定制化的重复校验,用来保证业务上的幂等。
重试
要谨慎的选择重试策略和集群方式。
对于系统间调用链比较短的场景,可以取消重试,然后整个数据流向设计成快速失败的(failefast)。
但是必须有其他的容错机制,这里容错机制不仅仅是微服务架构上的容错,也是业务流程整体设计上的容错,例如:每个请求都带有请求日志,记录请求状态和时间。对于异常的请求,可以手动重试,也可以自动重试,或者将整个过程回滚,这就是业务流程设计上的容错。微服务架构容错机制则是重试并添加熔断器,重试可以用前提是所有接口都是幂等的,但是熔断器也是个鸡肋,很难自动控制,若果熔断策略选用不当还会起反作用,甚至不如APM监控+分布式配置功能开关组合策略。所以对于完整性要求比较的高的业务场景,可以取消重试,去掉熔断器,但是要在业务流程的入口处加上限流机制,防止过载。
failover&failfast&failback&failsafe
本文基于网络文章整理