初探RPC框架的秘密 | 青训营

67 阅读10分钟

基本概念

远程过程调用(Remote Procedure Call,简称RPC)是一种常用的分布式计算通信协议,它允许一个程序通过网络来调用另一个程序中的子程序。RPC使得不同计算机上运行的程序能够进行数据交换并执行远程任务,从而实现分布式计算。

以下是RPC的基本概念:

  1. 客户端和服务器:RPC系统中,调用程序的程序被称为客户端,而提供服务的程序被称为服务器。客户端通过调用服务器上的远程过程来执行服务器上的操作。
  2. 接口:为了使客户端和服务器之间的通信标准化,RPC系统通常定义了一组接口。这些接口描述了客户端可以调用的远程过程以及它们的参数和返回类型。
  3. 协议:RPC系统使用一种协议来规定客户端和服务器之间的通信方式。协议包括了如何建立连接、如何交换数据、如何处理错误等细节。
  4. 序列化和反序列化:由于客户端和服务器通常运行在不同的硬件和操作系统上,它们使用的数据表示可能不同。因此,在RPC中,需要进行数据序列化和反序列化,将数据转换为可以在网络上传输的格式,然后在另一端将其转换回原始格式。
  5. 网络传输:RPC系统通过网络将序列化后的请求发送到服务器,服务器接收到请求后进行反序列化,然后执行相应的操作,并将结果序列化后通过网络返回给客户端。
  6. 异步调用和同步调用:根据客户端和服务器之间的通信方式,RPC可以有两种模式:异步调用和同步调用。在异步调用中,客户端发送请求后可以继续执行其他任务,而不需要等待服务器响应;在同步调用中,客户端需要等待服务器响应后才能继续执行其他任务。
  7. 安全性和可靠性:RPC系统需要确保数据传输的安全性和可靠性,包括数据加密、身份验证、错误处理等。

总的来说,RPC提供了一种灵活和强大的方式来实现分布式计算,使得不同计算机能够像在同一台计算机上一样调用彼此的程序和资源。

RPC需要解决的问题

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

客户端怎么把参数值传给远程的函数呢?

  • 在本地调用中,我们只需要把参数压到栈里,然后让函数自己去栈里读就行。
  • 但是在远程过程调用时,客户端跟服务端是不同的进程,不能通过内存来传递参数。这时候就需要客户端把参数先转成一个字节流,传给服务端后,再把字节流转成自己能读取的格式。


RPC概念模型

一次完整的RPC过程

IDL(Interface description language)文件

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

生成代码

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

编解码

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

通信协议

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

网络传输

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

远程调用需要使用IDL文件来描述方法和参数,以便服务双方能够按照规范进行调用。双方依赖同一份IDL文件,并使用工具生成对应的生成文件。用户代码需要依赖生成代码,因此可以将它们看做一个整体。

RPC的好处

  1. 单一职责,有利于分工协作和运维开发
  2. 可扩展性强,资源使用率更优
  3. 故障隔离,服务的整体可靠性更高

RPC带来的问题

  1. 服务宕机,对方应该如何处理?
  2. 在调用过程中发生网络异常,如何保证消息的可达性?
  3. 请求量突增导致服务无法及时处理,有哪些应对措施?

分层设计

分层设计-以Apache Thrift为例

编解码层-生成代码

不同的端依赖同一份IDL文件生成不同语言的代码

编解码层-数据格式

  1. 语言特定的格式:这种格式通常与特定的编程语言深度绑定,比如Java的java.io.Serializable。这种编码形式方便快捷,可以用很少的额外代码实现内存对象的保存与恢复。但是,它也有一些问题,如安全性、兼容性等。
  2. 文本格式:包括JSON、XML、CSV等。这些格式都具有良好的人类可读性。然而,对于数字处理,这些格式存在一些问题。例如,XML和CSV不能区分数字和字符串,而JSON虽然能区分字符串和数字,但不能指定精度。在处理大量数据时,这个问题可能会更严重。此外,由于没有强制模型约束,通常只能通过文档进行约定,这可能会给调试带来一些不便。
  3. 二进制编码:这种格式具有跨语言和高性能等优点。常见的二进制编码包括Thrift、BinaryProtocol和Protobuf等。
  4. TLV编码:这种编码方式包括标签(tag)、长度(length)和值(value)。其中,标签表示数据的类型,长度表示数据的长度,值可以嵌套其他TLV。然而,TLV编码需要额外的空间来存储标签和长度。

总的来说,选择哪种数据格式取决于具体的应用场景和需求,需要根据数据量、处理速度、兼容性、可读性等多个因素进行综合考虑。

编解码层-选型

  1. 兼容性:支持自动增加新的字段,而不影响老的服务,这将提高系统的灵活度
  2. 通用性:支持跨平台、跨语言
  3. 性能:从空间和时间两个维度来考虑,也就是编码后数据大小和编码耗费时长

协议层

协议是双方确定的交流语义

网络通信层

Sockets API是一种通用的网络编程接口,它可以在网络通信层中使用,实现不同主机之间的数据通信。Sockets API提供了一组函数和数据结构,用于创建和管理网络连接,发送和接收数据等操作。

以下是一些常见的Sockets API函数:

  1. socket():创建一个新的socket,返回一个socket描述符(socket descriptor)。
  2. bind():将socket绑定到指定的IP地址和端口号。
  3. listen():使socket进入被动状态,等待远程主机的连接请求。
  4. accept():接受远程主机的连接请求,并创建一个新的socket用于与远程主机通信。
  5. connect():向远程主机发起连接请求。
  6. send()和recv():发送和接收数据。
  7. close():关闭socket。

关键指标

稳定性; 易用性; 扩展性; 观测性; 高性能

稳定性

保障策略

请求成功率

  • 负载均衡
  • 重试

长尾请求

备份请求(backup request)是指那些明显高于均值并且占比较小的请求。在业界,延迟有一个常用的P99标准,即99%的请求延迟必须满足一定的耗时要求,而1%的请求会超过这个耗时,这1%被认为是长尾请求。这个概念在处理高并发、分布式系统、网络流量控制等场景中非常重要,因为它可以帮助我们识别和处理那些极少数但可能导致系统瓶颈或故障的长尾请求。

注册中间件

使用中间件可以帮助实施各种稳定性策略,例如:

  1. 负载均衡:中间件可以通过在多个服务器之间分配请求来平衡负载。当某个服务器过载时,中间件可以将请求转发到其他可用的服务器上,以确保应用程序的稳定性和性能。
  2. 故障转移:中间件可以在应用程序出现故障时提供故障转移功能。当某个服务器发生故障时,中间件可以将请求重定向到其他可用的服务器上,以确保应用程序的持续可用性。
  3. 容错和恢复:中间件可以提供容错和恢复机制,以处理应用程序中的错误和异常。例如,它可以捕获异常并将错误信息记录到日志文件中,以便进行故障排除和诊断。
  4. 数据缓存:中间件可以使用缓存来存储经常访问的数据,以减少对数据库的访问次数,从而降低数据库的负载和提高应用程序的性能。
  5. 安全性和认证:中间件可以提供安全性和认证功能,以保护应用程序免受网络攻击和未经授权的访问。例如,它可以验证用户的身份和权限,以确保只有授权用户能够访问应用程序。

易用性

  • 开箱即用:合理的默认参数选项、丰富的文档
  • 周边工具:生成代码工具、脚手架工具

扩展性

  1. 请求发起:当一次请求发起时,首先会经过治理层面。
  2. 中间件执行:治理相关的逻辑被封装在middleware中,这些middleware会被构造成一个有序调用链逐个执行。这个调用链可能包括服务发现、路由、负载均衡、超时控制等middleware。
  3. 进入remote模块:在middleware执行后,请求会进入到remote模块,该模块负责与远端进行通信。

观测性

  • Log、Metric、Tracing
  • 内置状态观测性服务

高性能

企业实践

整体架构- Kitex

Core是框架的主干逻辑,定义了框架的层次结构、接口和默认实现。Client和Server是对用户暴露的,提供了Client/Server Option的配置和初始化。Client/Server下面是框架治理层面的功能模块和交互元信息,Remote是与对端交互的模块,包括编解码和网络通信。Byted是对字节内部的扩展,集成了内部的二方库和非通用的实现,通过suite集成进Client和Server中,与字节的内部特性解耦,方便后续开源拆分。Tool是与生成代码相关的实现,包括IDL解析、校验、代码生成、插件支持和自更新等,未来生成代码逻辑还会做一些拆分,便于给用户提供更友好的扩展。