RPC 框架学习 | 青训营笔记

120 阅读6分钟

RPC 框架学习


基础概念

本地函数调用

func main() {
	var a = 2
	var b = 3
	result := calculate(a, b)
	fmt.Println(result)
	return
}

func calculate(x, y int) {
	z := x * y
	return z
}

调用过程:

  1. 将 a 和 b 的值压栈
  2. 通过函数指针找到 calculate 函数,进入函数取出栈的值 2 和 3,将其赋予 x 和 y
  3. 计算 x×yx\times y 并将结果放在 z
  4. 将 z 的值压栈,然后从 calculate 中返回
  5. 从栈中取出 z 返回值,并赋值给 result

远程函数调用(RPC - Remote Procedure Calls)

在本地函数调用的过程中增加了一层网络

RPC 需要解决的问题

  1. 函数映射
  2. 数据转换成字节流
  3. 网络传输

RPC 概念模型

RPC 原理与实现 2023-03-03 08.22.15.excalidraw.png 1984 年 NeisonFabio 论文,其中提出了 RPC 的过程由 5 个模块组成:User、User-Stub、RPC-Runtime、Server-Stub、Server。其中具体的调用过程为:

  1. Caller 端发起本地调用 User-stub ,并将参数交给 User-stub 进行打包
  2. 打包好的参数交给 RPCRuntime,并由 RPCRuntime 传输到对端。
  3. 对端 CalleeRPCRuntime 接受到数据后,进行数据解压。
  4. 数据解压后在 callee 里进行本地调用函数,并将结果类似的返回给 Caller

RPC 相关概念

IDL 文件 IDL 通过一种中立的方式来描述接口,使得在不同平台上运行的对象和使用不同语言编写的程序可以相互 通信

生成代码 通过编译器工具把 IDL 文件转换成语言对应的静态库

编译码 从内存中表示到字节序列的转换称作编码,反之为解码,也长叫做序列化和反序列化。

通信协议 规范了数据在网络中的传输内容和格式,除了必须的请求/响应数据外,通常还会包含额外的元数据

网络传输 通常基于成熟的网络库走 TCP/UDP 传输

RPC 的好处

  1. 单一职责,有利于分工协作。甚至每个服务可以使用不同的语言编写,然后独立上线调用。
  2. 可拓展性强,资源使用率更优。当服务器压力大的时候可以单独对某项服务进行扩容。底层的资源也可以进行复用。
  3. 故障隔离,服务的整体可靠性更高。当某一个服务出现故障的时候,不会影响到整体服务。

RPC 带来的问题

  1. 服务宕机
  2. 在调用过程中发生网络异常,
  3. 请求量徒增导致服务无法及时处理。 以上问题可以有专门的RPC框架解决

RPC 的分层设计

PRC 一般分为三层:编解码层、协议层和网络通信层

以 Apache Thrift 为例

Pasted image 20230303084650.png

编解码层

生成代码

客户端和服务端依赖用一份 IDL 文件生成不同语言的文件。

数据格式
  1. 语言特定的格式 许多变成语言都内建了将内存对象编码为字节序列的支持,例如 Java 中有 java.io.Serializable

  2. 文本格式 JSON, xml, csv 等文本格式,具有人类可读性,但是可能存在描述不准确的问题。

  3. 二进制编码 具有跨语言和高性能的特点,常见有 ThriftBinaryProtocal、Protobuf

二进制编码

使用 TLV 编码

  • Tag:标签,可以理解为类型
  • Length:长度
  • Value:值,Value 也可以是一个 TLV 结构
struct Person {
		1: required string userName,
		2: optional i64 favoriteNum,
		3: optional list<string> interests
}

缺点: 增加了 tag 和 length 的冗余信息,增加了内存开销。

选型
  1. 兼容性 支持自动增加新的字段,而不影响老的服务,这将提高系统的灵活性

  2. 通用性 支持跨平台、跨语言

  3. 性能 从空间和时间两个维度来考虑,也就是编码后数据大小和编码消耗时长

空间开销:序列化需要在原有的数据上加上描述字段,用来解释反序列化的时候使用,如果 序列化过程引入的额外开销过高,可能会导致过大的无网络,磁盘等各方面的压力,对于海量分布式存储系统,数据量往往以 TB 为单位,巨大的额外空间开销意味着高昂的成本

时间开销:负载的序列化协议会导致较长时间的解析时间,这可能会使得序列化和反序列化成为整个系统的速度瓶颈。

协议层

  1. 特殊结束符 一个特殊字符作为每个协议单元结束的表示,但是不能过于简单,且要防止用户传输的数据不能以同样的结束符结束,不然就会出现混乱,Http 协议头就是以回车 + 换行符号作为序列结尾。这种构造方式,很简单但是对于一个协议单元必须要全部读入才能进行处理。

  2. 变长协议 以定长加不定长的部分组成,其中定长的部分需要描述不定长的内容长度。有 header 和 payload 组成。

协构解析

内存首先先读取 MagicNumber 找到数据所在的存储位置,然后再读取 PayloadCode 读取出编码方式,通过对应的编译器,解码出传输信息。

网络通信层 - Sockets API

RPC 原理与实现 2023-03-03 09.46.28.excalidraw.png 提供易用 API

  1. 封装底层 Socket API
  2. 连接管理和事件分发

功能

  1. 协议支持:tcp、udp 和 uds 等
  2. 优雅退出,处理异常等

性能

  1. 应用层 buffer 减少 copy
  2. 高性能定时器,对象池等

RPC 性能关键指标

稳定性

为了保证系统稳定性,会使用熔断、限流、超时控制等方式来缓解服务器压力。其中

  • 熔断:保护调用方,防止被调用的服务出现问题影响整个网路
  • 限流:保护被调用方,防止大流量把服务压垮
  • 超时控制:避免浪费资源在不可用的节点上 这三种措施分别针对调用方,被调用方和资源进行,需要根据实际业务选择,也可以全都用上。另一个稳定性的重要指标是请求的成功率。为了提高请求的成功率,可以使用负载均衡和重试的策略。
  • 负载均衡:将请求通过轮询、权重选择等方式均匀的打在不同的服务器上,使得每台服务器上的请求数量均匀,防止全部压在一台服务器上导致服务器压力过大
  • 重试:当服务请求失败的时候,不急着判断失败,而是重新请求几次后才认定真正失败。提高请求的成功率 长尾请求的处理: 使用 Backup Request 策略,在发送一个请求后,如果在预计的时间内没有返回,则再次发送一次请求。提高请求的成功率

易用性

我们希望我们拿到的 RPC 框架具有以下功能:

  • 开箱即用:合理的默认参数选项,丰富的文档。不需要做过多的配置就可以实现熔断、限流、降级等等功能
  • 周边工具:RPC 框架支持生成代码工具,脚手架工具。

高性能

RPC 框架需要做到高吞吐、低延迟。使用连接池、多路复用、高性能编码协议和高性能网络库等手段,适用于单机多级、单链接多连接,不同大小的请求包等场景。