RPC 远程过程调用详解与应用
RPC(Remote Procedure Call),即远程过程调用。
RPC 的核心目的是实现进程间通信,在分布式环境中广泛应用。
RPC 框架面向开发者屏蔽了网络底层逻辑,使远程调用可以像本地调用一样方便。
分布式通信
软件系统越来越复杂,从单机走向了分布式(集群/分布式/异地 部署),就需要支持分布式环境下的通信技术和方案。
TCP/UDP:最基础最底层的通信方式,所有应用层通信协议都基于 TCP/UDP。
CORBA:底层结构基于面向对象模型。
曾经是分布式环境解决互联的主流技术,开发和部署成本较高,现已基本被遗弃。
Web Service:基于 HTTP + XML 的标准化 Web API,传统行业的信息系统仍在广泛使用。
RESTful:基于 HTTP,可以使用 XM格式定义或JSON格式定义。
RMI(Remote Method Invocation):Java 提供的远程方法调用,。
MQ(Message Queue):消息队列(消息中间件),包括对JMS提供支持。
RPC(Remote Procedure Call):远程过程调用,使远程调用像本地调用一样方便。
RPC 概述
RPC(Remote Procedure Call)远程过程调用,在分布式应用中,RPC 是一个计算机通信协议,是一种进程间通信的模式。该协议允许运行于一台计算机的程序调用另一个地址空间(通常为一个开放网络的一台计算机)的程序,而程序员就像调用本地程序一样,无需关注底层网络通信的实现。
RPC 是一种服务器 / 客户端(Client/Server)模式,经典实现是一个通过发送请求-接受回应进行信息交互的系统。
如果涉及的软件采用面向对象编程,那么远程过程调用亦可称作远程调用或远程方法调用,例:Java RMI。
RPC 与 HTTP
HTTP 调用也是 RPC 实现的一种方式,所有的网络通信底层都是基于 TCP/UDP 协议将数据转换为二进制报文传输。
RPC | HTTP | |
---|---|---|
性能 | 可以长连接,可直接通信 可以指定序列化协议 性能消耗低,传输效率高 |
需要建立三次握手 一般是JSON或XML格式 序列化更消耗性能,消息头臃肿 |
传输协议 | 支持自定义协议, 通常支持广泛使用的RPC协议 |
只能HTTP |
负载均衡 | RPC 支持 | 使用中间件,如 Nginx。 |
应用范围 | 适用于企业内部使用 不建议传输较大的文本,视频等 |
行业标准,对内对外都适用 适用于多种异构环境 浏览器,APP接口,第三方接口调用都支持 |
传输协议:指的是应用层的协议,RPC 和 HTTP都属于应用层协议,都是基于网络层 TCP 协议。
RPC 应用场景
RPC在分布式系统中的系统环境建设和应用程序设计中有着广泛的应用,应用包括如下方面:
- 分布式操作系统的进程间通讯
- 分布式应用程序设计
- 分布式程序的调试
- 构造分布式计算的软件环境
- 远程数据库服务
RPC 调用过程
RPC 核心组件主要由 5 个部分组成:客户端,客户端存根,网络传输模块,服务端存根,服务端。
RPC 调用通常会经历 4 个环节:
建立通信:两台服务之间通信,必备条件是建立网络连接。
服务寻址:A 服务调用 B 服务,需要知道B服务的主机地址(IP),端口,方法名称。
网络传输:
- 序列化:A 服务发起一个 RPC 调用,需要将调用方法和参数数据进行序列化传输。
- 反序列化:B 服务收到请求后,需要将收到的数据进行反序列化。
服务调用:B 服务进行本地调用之后得到返回值,将返回值序列化返回给A服务,经过网络将二进制数据回传给A服务器。
一次 RPC 调用过程通常会有以下步骤:
- 客户端(Client)以本地调用的方式调用服务。
- 客户端存根(Client Stub)接收到调用后,负责将方法、入参等信息序列化(组装)成能够进行网络传输的消息体(对象数据转换为二进制)。
- 客户端存根(Client Stub)找到远程的服务地址,并且将消息通过网络发送给服务端。
- 服务端存根(Server Stub)收到消息后进行解码(反序列化操作)。
- 服务端存根(Server Stub)根据解码结果调用本地的服务。
- 服务端(Server)本地服务执行,并将结果返回给服务端存根(Server Stub)。
- 服务端存根(Server Stub)序列化结果。
- 服务端存根(Server Stub)将序列化的结果通过网络发送给消费者。
- 客户端存根(Client Stub)接收到消息,并进行解码(反序列化)。
- 服务消费方得到最终结果。
RPC 框架是把 2、3、4、7、8、9 这些步骤封装起来。
RPC 实现原理
- 动态代理:解决代理逻辑代码和业务隔离,添加增强操作。
- 反射:实现自动查找到远程方法。
- 序列化:为网络传输做好准备。
- 网络编程:建立网络通道进行内容传输。
动态代理
RPC 自动给接口生成一个代理类,在项目中引入了接口后,运行时实际绑定的是这个代理类。
例如,Dubbo 就使用 JDK 自带的动态代理
org.apache.dubbo.rpc.proxy.jdk.JdkProxyFactory
和org.apache.dubbo.rpc.proxy.javassist.JavassistProxyFactory
对字节码增加实现代理。反射
传入远程服务名和方法名,通过反射自动定位到需要被调用的方法,再传入入参,从而进行RPC调用。
可参考 Dubbo的
org.apache.dubbo.rpc.proxy.InvokerInvocationHandler
类中的实现序列化
序列化目的是将对象数据转换化可网络传输的二进制。每次RPC调用都会伴随频繁的序列化和反序列化操作。需要考虑序列化后的内容大小,序列化和反序列化的耗时和性能。
常见的序列化框架有:Hessian、Protobuf、Thrift。
网络编程
RPC 远程调用就一定会涉及到网络编程,需要可靠的通信来保障每一次调用的顺序进行。
比较流行的网络通信框架有 Netty、Mina,都出自同一个作者(Trustin Lee)
RPC 异步实现
成熟的 RPC 框架除了同步调用方式外,通常还会提供异步调用、事件通知(异步监听)、callback 调用功能。
异步调用
RPC 异步调用是指客户端发起请求后,不必等待获取结果(实际立即返回的是 null),同时可以获取一个 Future 对象,然后从 Future 中去获取结果。这样客户端不需要开启多线程就可以并行调用多个远程服务的接口。
如果一个业务需要调多个接口,多个接口之间没有顺序依赖,可以使用异步调用提高效率。
以 Dubbo 为例,使用 RpcContext 实现异步调用。
在 consumer.xml 中配置:
1 | <dubbo:reference id="asyncService" interface="org.apache.dubbo.samples.governance.api.AsyncService"> |
调用代码:
1 | // 此调用会立即返回null |
或者也可以这样调用:
1 | CompletableFuture<String> future = RpcContext.getContext().asyncCall( |
事件通知
成熟的 RPC 框架通常会提供事件通知功能,在调用之前,调用之后,出现异常时的事件通知。
在异步调用时,是通过 Future 的 get 获取结果,而 get 是阻塞的。还可以使用事件通知的方式获取结果,即在调用完之后完全不管,可以干其它事情,通过一个监听去侦听,当 RPC 框架有结果返回时会通知监听器,然后进行逻辑处理。
以 Dubbo 为例,提供了事件通知功能在调用之前、调用之后、出现异常时,会触发 oninvoke
、onreturn
、onthrow
三个事件,可以配置当事件发生时,通知哪个类的哪个方法。
服务消费者 Notify 接口:
1 | public interface Notify { |
服务消费者 Notify 实现:
1 | public class NotifyImpl implements Notify { |
服务消费者 Notify 配置
1 | <bean id ="demoNotify" class = "org.apache.dubbo.callback.implicit.NotifyImpl" /> |
callback
与 async
功能正交分解,async=true
表示结果是否马上返回,onreturn
表示是否需要回调。
两者叠加存在以下几种组合情况:
- 异步回调模式:
async=true onreturn="xxx"
- 同步回调模式:
async=false onreturn="xxx"
- 异步无回调 :
async=true
- 同步无回调 :
async=false
callback调用
成熟的 RPC 框架通常支持服务端回调客户端功能。RPC 通信基本都是长连接的,也就支持全双工通信,客户端可以调服务端,服务端同样可以调客户端。
RPC 客户端调只管发送调用请求,不用管结果响应,服务端的结果响应通过回调客户端来处理。
以 Dubbo 为例,Dubbo 的 参数回调将基于长连接生成反向代理,这样就可以从服务器端调用客户端逻辑。可以参考 dubbo 项目中的示例代码。
回调服务接口:定义了一个业务方法,增加一个回调监听器
1 | public interface PayService { |
回调监听器接口:
1 | public interface CallbackListener { |
服务提供者接口实现:
1 |
|
服务提供者配置:
1 | <bean id="payService" class="com.callback.impl.PayServiceImpl" /> |
服务消费者配置:
1 | <dubbo:reference id="payService" interface="com.callback.PayService" /> |
服务消费者调用:
1 |
|
如果服务端完成一个业务逻辑需要多多次通知,可以采用 callback 方法。这一点在异步监听中是没有办法实现的,callback 调用更新是一次性买卖,在高并发场景下建议使用监听方式,因为 callback 方式客户端会对 callback 实例的个数有限制。
RPC 常用框架
一个好的 RPC 框架要具备可靠性和易用性,可靠性方面要保证 I/O,序列化等准确处理,还要考虑网络的不确定性,心跳,网络闪断等因素;j易用性方面要考虑超时与重试机制的控制,同步和异步调用的使用。
一些优秀的开源 RPC 框架:Dubbo(阿里),BRPC(百度),gRPC(Google),Thrift(Facebook),Motan(微博)。