远程过程调用(Remote Procedure Call)

一个人是否了解RPC的原理和技术细节,问2个问题就可以知道,
1、描述一下RPC的通信流程。
2、在通信时有哪些步骤,分别用到了什么技术?

这个题没有标准答案,这个需要自己理解以后才能描述。


(1) 什么是RPC

RPC 的全称是 Remote Procedure Call,即远程过程调用。

用户视角RPC调用

像调用本地函数一样,调用一个远端服务。
什么是“远程”,为什么“远”?
先来看下什么是,即本地函数调用

(1.1) 本地函数调用

下面是一个两数相加的本地函数

int result = Add(1, 2);
public static int Add(int x, int y) {
    return x + y;
}

本地函数调用

这行代码的时候,到底发生了什么?
(1)传递两个入参;
(2)调用了本地代码段中的函数,执行运算逻辑;
(3)返回一个出参;
这三个动作,都发生在同一个进程空间里,这是本地函数调用。

有办法,调用一个跨进程的函数呢?

(1.2) 远程过程调用

远程函数调用,进程部署在另一台服务器上。

最容易想到的,两个进程约定一个协议格式,使用Socket通信,来传输:
(1)入参;
(2)调用哪个函数;
(3)出参;
如果能够实现,那这就是“远程”过程调用。

远程过程调用

问题
Socket通信只能传递连续的字节流,如何将入参、函数都放到连续的字节流里呢?

 

(1.2.1) 请求报文

假设,设计一个请求报文:
请求报文

(1)前3个字节填入函数名“add” 字符串类型,占用大小 3字节 ;
(2)中间4个字节填入第一个参数“1”,int类型 4字节;
(3)末尾4个字节填入第二个参数“2”,int类型 4字节;
加起来共11字节

 

(1.2.2) 响应报文

同理,可以设计一个响应报文:
响应报文

 

(1.2.3) 调用方处理逻辑

调用方的代码变为

// 把请求序列化成二进制  并且按照协议组装
reqMsg = WriteMessage("add", 1, 2);
// 发送请求
SendRequest_ToService_B(reqMsg);
// 接收返回结果
respMsg = RecieveRespnse_FromService_B();
// 把二进制的数据按照协议解析 并把结果部分反序列化
int result = readMessage(respnse);

 

(1.2.4) 服务方处理逻辑

服务方的代码变为

// 接收请求
reqMesg = RecieveRequest();
// 按照协议解析请求 把参数名称反序列化
functionName = ReadMsg(reqMesg);
// 把参数部分反序列化
args = ReadMsg(reqMesg);
// 本地函数调用
result = Add(1, 2);
// 将结果序列化成二进制 并按照协议组装
respMsg = WriteMsg(result);
// 发送返回结果
SendResponse(respMsg);

 

(1.2.5) 此时rpc调用整体流程

rpc调用整体流程
调用方与服务方的处理步骤都是非常清晰。

这个过程存在最大的问题是什么呢?
调用方太麻烦了,每次都要关注很多底层细节:
(1)入参到字节流的转化,即序列化应用层协议细节;
(2)socket发送,即网络传输协议细节;
(3)socket接收;
(4)字节流到出参的转化,即反序列化应用层协议细节;

能不能调用层不关注这个细节?
可以,RPC框架就是解决这个问题的,它能够让调用方“像调用本地函数一样调用远端的函数(服务)”。

(1.2.6) rpc-client-server

RPC框架的职责是什么?
RPC框架,要向调用方屏蔽各种复杂性,要向服务提供方也屏蔽各类复杂性:
(1)服务调用方client感觉就像调用本地函数一样,来调用服务;
(2)服务提供方server感觉就像实现一个本地函数一样,来实现服务;
所以整个RPC框架又分为client部分与server部分,实现上面的目标,把复杂性屏蔽,就是RPC框架的职责。

rpc客户端服务端职责

调用方和服务方职责
(1) 调用方,传入参数,执行调用,拿到结果;
(2) 服务方,收到参数,执行逻辑,返回结果;

RPC框架的职责是,中间大蓝框的部分:
(1) client端:序列化、反序列化、连接池管理、负载均衡、故障转移、队列管理,超时管理、异步管理等等;
(2) server端:服务端组件、服务端收发包队列、io线程、工作线程、序列化反序列化等;


(2) RPC应用

RPC应用案例


(3) RPC核心原理

一个RPC核心通信会涉及到哪些步骤呢?

RPC调用流程

(3.1) 动态代理

动态代理生成的代理类,隐藏了复杂的序列化、连接池管理、负载均衡、故障转移、队列管理、超时管理、异步管理、线程管理 等

对于研发人员来说,这样做要掌握太多的 RPC 底层细节,需要手动写代码去构造请求、调用序列化,并进行网络调用,整个 API 非常不友好。

采用动态代理的技术,通过字节码增强对方法进行拦截增强,以便于增加需要的额外处理逻辑。

由服务提供者给出业务接口声明,在调用方的程序里面,RPC 框架根据调用的服务接口提前生成动态代理实现类,并通过依赖注入等技术注入到声明了该接口的相关业务逻辑里面。该代理实现类会拦截所有的方法调用,在提供的方法处理逻辑里面完成一整套的远程调用,并把远程调用结果返回给调用方,这样调用方在调用远程方法的时候就获得了像调用本地接口一样的体验。

(3.2) 序列化

平时编程时通常使用“对象”来进行数据

public class User {
    private Long id;
    private String name;
    private Integer age;
}

当需要对数据进行存储或者传输时,“对象”就不这么好用了,往往需要把数据转化成连续空间的“二进制字节流”。

一些典型的场景是:
(1) 数据库索引的磁盘存储:数据库的索引在内存里是b+树,但这个格式是不能够直接存储到磁盘上的,所以需要把b+树转化为连续空间的二进制字节流,才能存储到磁盘上;
(2) 缓存的KV存储:redis/memcache是KV类型的缓存,缓存存储的value必须是连续空间的二进制字节流,不能是对象;
(3) 数据的网络传输:socket发送的数据必须是连续空间的二进制字节流,不能是对象;

序列化(Serialization),就是将”对象”形态的数据转化为”连续空间二进制字节流”形态数据的过程。

反序列化
把二进制的消息体逆向还原成请求对象。这个过程叫作反序列化

怎么进行序列化

序列化方式有很多,比如 JSON、XML、CSV、二进制 等等。

(3.3) 协议

趣谈网络协议 - 第32讲 | RPC协议综述:远在天边,近在眼前

数据包识别建立一些“指示牌”,并在上面标明数据包的类型和长度,这样就可以正确的解析数据了。确实可以,并且我们把数据格式的约定内容叫做协议

大多数的协议会分成两部分,分别是数据头和消息体。数据头一般用于身份识别,包括协议标识、数据大小、请求类型、序列化类型等信息;消息体主要是请求的业务参数信息和扩展属性等。

根据协议格式,服务提供方就可以正确地从二进制数据中分割出不同的请求来。

(3.4) 网络通信-IO模型

04 | 网络通信:RPC框架在网络通信上更倾向于哪种网络IO模型?


(4) RPC高级特性

RPC架构图

(4.1) 服务发现

08 | 服务发现:到底是要CP还是AP?

(4.2) 健康监测

(4.3) 路由策略

(4.4) 负载均衡

(4.5) 异常重试

(4.6) 优雅关闭

(4.7) 优雅启动

(4.8) 熔断限流

(4.9) 业务分组

(5) RPC高级篇

(5.1) 异步RPC

(5.2) RPC安全

(5.3) 分布式环境下如何快读定位问题

(5.4) 动态分组

(5.5) 泛化调用

(5.6) 兼容多种RPC协议

RPC框架能够帮助我们解决系统拆分后的通信问题,并且能让我们像调用本地一样去调用远程方法。利用 RPC 我们不仅可以很方便地将应用架构从“单体”演进成“微服务化”,而且还能解决实际开发过程中的效率低下、系统耦合等问题,这样可以使得我们的系统架构整体清晰、健壮,应用可运维度增强。

参考资料

[1] RPC 实战与核心原理 - 01 | 核心原理:能否画张图解释下RPC的通信流程?
[2] 必须知道的RPC内核细节(值得收藏)!!!
[3] 趣谈网络协议 - 第32讲 | RPC协议综述:远在天边,近在眼前

[4] RPC 实战与核心原理 - 02 | 协议:怎么设计可扩展且向后兼容的协议?
[5] RPC 实战与核心原理 - 03 | 序列化:对象怎么在网络中传输?
[6] RPC 实战与核心原理 - 04 | 网络通信:RPC框架在网络通信上更倾向于哪种网络IO模型?
[7] RPC 实战与核心原理 - 05 | 动态代理:面向接口编程,屏蔽RPC处理流程