进程间通信是所有分布式系统的核心。分布式系统中的通信都是基于底层网络提供的底层消息传递机制。
概念
RPC 是 远程过程调用(Remote Procedure Call) 的缩写形式。Birrell 和 Nelson 在 1984 年发表于 ACM Transactions on Computer Systems 的论文《Implementing remote procedure calls》对 RPC 做了经典的诠释:
计算机 A 上的进程,调用另外一台计算机 B 上的进程,其中 A 上的调用进程被挂起,而 B 上的被调用进程开始执行。当值返回给 A 时,A 进程继续执行。调用方可以通过使用参数将信息传送给被调用方,而后可以通过传回的结果得到信息。
开发人员看不到任何消息传递过程,这对于开发人员来说是透明的。简而言之,RPC 就是“像调用本地方法一样调用远程方法”
RPC类型
异步和同步的区分在于是否等待服务端执行完成并返回结果。
同步调用
客户方等待调用执行完成并返回结果。
异步调用
客户方调用后不用等待执行结果返回,但依然可以通过回调通知等方式获取返回结果。 若客户方不关心调用返回结果,则变成单向异步调用,单向调用不用返回结果
RPC框架/协议
- 应用级的服务框架:
Dubbo/Dubbox、Google gRPC、Spring Boot/Spring Cloud - 远程通信协议:
RMI、Socket、SOAP(HTTP XML)、REST(HTTP JSON) - 通信框架:
MINA和Netty
RPC原理
组件
- 客户端(Client): 服务调用方
- 客户端存根(Client Stub): 存放服务端地址信息,将客户端的请求参数数据信息打包成网络消息,再通过网络传输发送给服务端
- 服务端存根(Server Stub): 接收客户端发送过来的请求消息并进行解包,然后再调用本地服务进行处理
- 服务端(Server): 服务的真正提供者
- Network Service: 底层传输,可以是
TCP或HTTP
步骤
这些步骤将 客户端 对 客户存根 发出的本地调用转换成对 服务端过程的本地调用,而客户端和服务端都不会意识到有中间步骤的存在。
- 客户端以正常的方式调用
client stub client stub构建一条消息并调用本地操作系统- 客户端操作系统将消息发送给服务端操作系统
- 服务端操作系统将消息发送给
server stub server stub解包参数并调用服务端方法- 服务端方法执行完成后并将结果返回给
server stub server stub将结果打包成一条消息,并调用其本地操作系统- 服务端操作系统将消息发送给客户端操作系统
- 客户端操作系统将消息发送给
client stub client stub解包结果并返回给客户端
流程图
RPC协议
协议是 RPC 的核心,它规范了数据在网络中的 传输内容和格式。除必须的请求、响应数据外,通常还会包含额外控制数据,如单次请求的序列化方式、超时时间、压缩方式和鉴权信息等。
协议的内容包含三部分:
- 数据交换格式: 定义
RPC的请求和响应对象在网络传输中的字节流内容,也叫作 序列化方式 - 协议结构: 定义包含字段列表和各字段语义以及不同字段的排列方式
- 协议通过定义规则、格式和语义来约定数据如何在网络间传输。一次成功的
RPC需要通信的两端都能够按照协议约定进行网络字节流的读写和对象转换。如果两端对使用的协议不能达成一致,就会出现鸡同鸭讲,无法满足远程通信的需求。
RPC 协议的设计需要考虑以下内容:
- 通用性: 统一的二进制格式,跨语言、跨平台、多传输层协议支持
- 扩展性: 协议增加字段、升级、支持用户扩展和附加业务元数据
- 性能:As fast as it can be
- 穿透性:能够被各种中端设备识别和转发:网关、代理服务器等 通用性和高性能通常无法同时达到,需要协议设计者进行一定的取舍。
HTTP/1.1
比于直接构建于 TCP 传输层的私有 RPC 协议,构建于 HTTP 之上的远程调用解决方案会有更好的通用性,如 WebServices 或 REST 架构,使用 HTTP + JSON 可以说是一个事实标准的解决方案。
选择构建在 HTTP 之上,有两个最大的优势:
HTTP的语义和可扩展性能很好的满足RPC调用需求。- 通用性,
HTTP协议几乎被网络上的所有设备所支持,具有很好的协议穿透性。
但也存在比较明显的问题:
- 典型的
Request – Response模型,一个链路上一次只能有一个等待的Request请求。会产生HOL。 - Human Readable Headers,使用更通用、更易于人类阅读的头部传输格式,但性能相当差
- 无直接
Server Push支持,需要使用Polling Long-Polling等变通模式
RPC框架应用原理
建立通信
首先要解决通讯的问题:即A机器想要调用B机器,首先得建立起通信连接。
通常这个连接可以是按需连接(需要调用的时候就先建立连接,调用结束后就立马断掉),也可以是长连接(客户端和服务器建立起连接之后保持长期持有,不管此时有无数据包的发送,可以配合心跳检测机制定期检测建立的连接是否存活有效),多个远程过程调用共享同一个连接。
服务寻址
要解决寻址的问题:A服务器上的应用怎么告诉底层的 RPC 框架,如何连接到B服务器(如主机或 IP 地址)以及特定的端口,方法的名称名称是什么。
通常情况下需要提供B机器(主机名或 IP 地址)以及特定的端口,然后指定调用的方法或者函数的名称以及入参出参等信息,这样才能完成服务的一个调用。
可靠的寻址方式(主要是提供 服务的发现)是
RPC的实现基石,比如可以采用Redis或者Zookeeper来注册服务等等。
服务提供者:
- 当服务提供者启动的时候,需要将自己提供的服务注册到指定的注册中心,以便服务消费者能够通过服务注册中心进行查找;
- 当服务提供者由于各种原因致使提供的服务停止时,需要向注册中心注销停止的服务;
- 服务的提供者需要定期向服务注册中心发送心跳检测,服务注册中心如果一段时间未收到来自服务提供者的心跳后,认为该服务提供者已经停止服务,则将该服务从注册中心上去掉
服务调用者:
- 服务的调用者启动的时候根据自己订阅的服务向服务注册中心查找服务提供者的地址等信息;
- 当服务调用者消费的服务上线或者下线的时候,注册中心会告知该服务的调用者;
- 服务调用者下线的时候,则取消订阅。
网络传输
序列化
当A机器上的应用发起一个 RPC 调用时,调用方法和其入参等信息需要通过底层的网络协议如 TCP 传输到B机器。由于网络协议是基于二进制的,所以传输的参数数据都需要先进行序列化(Serialize)或者编组(marshal)成二进制的形式才能在网络中进行传输。然后通过寻址操作和网络传输将序列化或者编组之后的二进制数据发送给B机器。
反序列化
当B机器接收到A机器的应用发来的请求之后,又需要对接收到的参数等信息进行反序列化操作(序列化的逆操作),即将二进制信息恢复为内存中的表达方式,然后再找到对应的方法(寻址的一部分)进行本地调用(一般是通过生成代理 Proxy 去调用,通常会有 JDK 动态代理、CGLIB 动态代理、Javassist 生成字节码技术等),之后得到调用的返回值。
服务调用
B机器进行本地调用(通过代理 Proxy 和反射调用)之后得到了返回值,此时还需要再把返回值发送回A机器,同样也需要经过序列化操作,然后再经过网络传输将二进制数据发送回A机器,而当A机器接收到这些返回值之后,则再次进行反序列化操作,恢复为内存中的表达方式,最后再交给A机器上的应用进行相关处理,一般是业务逻辑处理操作。
通常,经过以上四个步骤之后,一次完整的 RPC 调用算是完成了,另外可能因为网络抖动等原因需要重试等。