RPC
英文原义:Remote Procedure Call Protocol
中文释义:(RFC-1831)远过程调用协议
注解:一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。RPC协议假定某些传输协议的存在,如TCP或UDP,为通信程序之间携带信息数据。在OSI网络通信模型中,RPC跨越了传输层和应用层。RPC使得开发包括网络分布式多程序在内的应用程序更加容易。
RPC采用客户机/服务器模式。请求程序就是一个客户机,而服务提供程序就是一个服务器。首先,调用进程发送一个有进程参数的调用信息到服务进程,然后等待应答信息。在服务器端,进程保持睡眠状态直到调用信息的到达为止。当一个调用信息到达,服务器获得进程参数,计算结果,发送答复信息,然后等待下一个调用信息,最后,客户端调用过程接收答复信息,获得进程结果,然后调用执行继续进行。
目前,有多种 RPC 模式和执行。最初由 Sun 公司提出。IETF ONC 宪章重新修订了 Sun 版本,使得 ONC PRC 协议成为 IETF 标准协议。现在使用最普遍的模式和执行是开放式软件基础的分布式计算环境(DCE)。
RPC 调用分类
RPC 调用的分类方式有很多种。
从通信协议层面可以分为:
基于 HTTP 协议的 RPC;
基于二进制协议的 RPC;
基于 TCP 协议的 RPC。
从是否跨平台可分为:
单语言 RPC,如 RMI, Remoting;
跨平台 RPC,如 google protobuffer, restful json,http XML。
从调用过程来看,可以分为同步通信RPC和异步通信RPC:
同步 RPC:指的是客户端发起调用后,必须等待调用执行完成并返回结果;
异步 RPC:指客户方调用后不关心执行结果返回,如果客户端需要结果,可用通过提供异步 callback 回调获取返回信息。大部分 RPC 框架都同时支持这两种方式的调用。
RPC 框架结构
一个完整的 RPC 框架的架构主要模块如图所示。
RPC 服务方的主要职责是提供服务,供客户端调用访问,服务端会通过一个接收器接受客户端的调用请求,根据相应的 RPC 协议进行解码获取调用方法以及相关参数,当调用完成后,服务器端通过后台处理模块处理完成并将结果返回给客户端。
对于客户端来说,服务调用完全透明,像调用本地服务一样调用远程方法,客户端调用服务时候通过一个远程连接和服务端建立通道,并通过相应的协议进行编码,将调用的方法和相关参数发送给服务方。
上 手 篇RPC 模块详解
下面我们根据上面的RPC的架构图,对图中的各个模块进行拆解,并解释每个模块的作用。
服务端(Server):RPC 服务的提供者,负责将 RPC 服务导出;
客户端 (Client):RPC 服务的消费者,负责调用 RPC 服务;
代理(Proxy):通过动态代理,提供对远程接口的代理实现;
执行器(Invoker):对于客户端:主要负责服务调用的编码,调用请求发送和等待结果返回;对于服务方:负责处理调用逻辑并返回调用结果;
协议管理(Protocol):协议管理组件,负责整个 RPC 通信协议的编/解码;
连接端口(Connector):负责维持客户方和服务方的长连接通道;
后台处理(Processor):负责整个调用服务中的管理调度,包括线程池,分发,异常处理等;
连接通道(Channel):客户端和服务器端的数据传输通道。
具体到 JAVA 平台来说,其中的3,4通常使用动态代理实现,5,6,7,8使用 NIO 或者一些高性能 NIO 框架,如 mina,netty 实现。
最简单的 RPC JAVA 实现
在进一步拆解了组件并划分了职责之后,这里以一个最简单 Java RPC 框架实现为例,对 RPC 具体逻辑进行分析。
RPC 框架服务发布代码:
服务端发布服务的代码如上,首先校验传入的端口和服务是否合法,然后开启一个 socket 监听,这儿为了简便,没有采用 NIO 方式,同时直接采用 java 的序列化方式,将传入的数据通过反射取出调用的方法和参数,本地执行后将运行结果通过 socket 套接字返回给客户端。
框架中客户端调用的代码中,首先校验对应的端口和主机是否合法,然后通过动态代理生成一个代理对象,在代理对象的方法中,拦截调用,通过建立 socket 连接,将方法和参数传递到远端执行并获取远程执行返回结果。
RPC 调用测试:
如上图所示,服务器端发布一个接口服务 HelloService,客户端成功通过 RPC 调用。
思 考 篇
自定义 RPC 协议
协议头
在上面的示例程序当中,我们仅仅是完成了一个基本的远程调用,并没有实现 RPC 框架中的很多组件功能,从最简单的代码版本中我们可以发现,发起一个 RPC 调用,需要传输的最基本数据如下:
接口方法:包括接口的名字和相应的方法名字;
方法参数:包括参数的类型和取值;
附件参数,包括调用接口版本,接口超时时间等等。
因此,如果要自定义协议实现 RPC,我们必须再协议的消息体中包含这部分数据,另外,我们需要定义一些协议元数据,这些元数据通常放在协议头中,和包含必要参数的协议体一期组成了自定义消息。
元数据通常会包含以下字段,大部分字段只需要1-2位:
magic: 魔数,方便协议解码
header_size: 协议头大小,便于解码,同时可用用于处理TCP粘包问题
id :消息 id,用来标示这次调用version: 接口版本
type:消息类型,可用包括普通调用消息,心跳,控制消息
status:消息状态,是否首次处理或者已经处理
body_size: 消息体长度
serialize_type:消息体序列化类型
body:具体消息
具体消息
消息内容在网络上传输需要对其进行编码,这个编码的过程就是序列化过程,显然,对于网络传输的数据,在能够保证信息足够解码的情况下,序列化的大小越小,传输的开销就越小,效率就越高,目前 JAVA 平台常用的序列化方式有:xml,json ,binary(包括 thrift; hession; kryo 等)。
在 RPC 调用中我们推荐使用二进制方式进行序列化,在大部分的测试中,二进制方式序列化具有相当好的表现,另外一个比较有意思的地方是,每一次 JDK 版本的升级,JAVA 自带的序列化方式的效率都有提升。
服务端调用优化
从前面的示例代码中,我们仅仅简单的考虑了实现了组件中的服务端和客户端,并没有考虑效率问题,在一个完整的 RPC 框架中,我们需要考虑实现并优化调用的每一个地方,同时,为了符合业务需求,需要有很高的可靠性和容错机制。
具体来说,在动态代理模块,我们不会采用 java 自带的动态接口,而是会采用一些性能更高的三方库,在连接通道和连接模块,我们会采用更优秀的三方NIO,如 netty 来实现,在后端处理模块,我们也不会仅仅是执行结果并返回,要考虑更多的东西:
并发控制:当多个请求并发处理的时候,如何管理和控制线程池和超时等待时间;
版本隔离:当服务有多个版本的时候,如何让不同的调用者能够调用正确的服务;
服务路由:当服务提供者有多台机器的时候,如何提高系统负载均衡,路由到正确的服务端;
服务降级:当多个服务重要性有不同的时候,如果保证核心业务的稳定性,适当的降低非核心业务优先级;
服务监控和报警:服务出现异常情况时候,运维和对应的系统负责人能够第一时间得到告警和错误信息。
以上的思考大部分要结合运维层面一起考虑,但是 RPC 框架本身也要提供足够的支持才能保证它足够的健壮性。
领取专属 10元无门槛券
私享最新 技术干货