Note07 深入浅出RPC框架 | 青训营

123 阅读20分钟

目录

  • 01基本概念
  • 02分层设计
  • 03关键指标

01基本概念

1.1 本地函数调用

image.png

以下是调用过程的重新表述:

  1. 将变量a和b的值压入栈中。
  2. 使用函数指针找到calculate函数,并进入该函数。从栈中取出值2和3,将它们分别赋给变量x和y。
  3. 计算表达式x * y,并将结果保存在变量Z中。
  4. 将变量Z的值压入栈中,然后从calculate函数中返回。
  5. 从栈中取出保存的值Z,并将其赋给变量result。

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

需要解决的问题:

  1. 函数映射 需要告诉服务要调用哪个函数,因此函数有自己的ID,在做RPC的时候附上这个ID,并且还要有一个ID和函数的对照关系表。
  2. 数据转换成字节流 客户端把参数先转成一个字节流,传给服务端后,再把字节流转成自己能读取的格式。
  3. 网络传输 需要保证网络高效稳定地传输数据。

1.3 RPC概念模型

image.png

①发起本地调用
②数据打包
③数据传送给对端
④数据接收
⑤解压数据
⑥数据处理
⑦结果数据打包
⑧传送返回数据
⑨接收数据
⑩解压数据
⑪数据结果返回

1984年Nelson发表了论文《Implementing Remote Procedure Calls》,其中提出了RPC的过程由5个模型组成: User、User Stub、RPC Runtime、Server- Stub、Server。

  • IDL文件(接口描述语言文件)通过一种中立的方式描述接口,从而实现在不同平台和使用不同编程语言的程序之间进行通信的约定调用规范。
  • 生成代码是通过编译器工具将IDL文件转换为语言特定的静态库,将用户代码与生成的代码视为一个整体,用户代码需要依赖生成的代码。
  • 编解码是将数据从内存表示形式转换为字节序列的过程,称为编码;将字节序列还原为内存表示形式的过程称为解码,也被称为序列化和反序列化。
  • 通信协议规定了数据在网络中传输的内容和格式。除了必要的请求和响应数据外,通常还包含额外的元数据。
  • 网络传输通常基于成熟的网络库,使用TCP或UDP协议进行数据传输。

image.png

1.5 RPC的好处

  1. 单一职责:RPC有利于分工协作和开发运维的独立性。不同团队可以使用不同的编程语言进行开发,而部署和运维也可以独立进行。

  2. 可扩展性强:RPC的可扩展性使得资源利用更加高效。当系统压力增大时,可以独立扩展资源,而底层的基础服务可以被复用,从而达到节省资源的目的。

  3. 故障隔离:RPC的架构支持故障隔离。当某个模块发生故障时,不会对整体系统产生影响,因此整个服务在可靠性上更加稳定可靠。

  4. 当服务宕机时,对方可以采取以下处理措施:及时通知维护人员或运维团队,尝试重新启动服务或修复故障,同时可以使用负载均衡机制将请求转发至备用服务,确保服务的连续可用性。

  5. 在调用过程中发生网络异常时,可以采取以下措施确保消息的可达性:使用超时机制来控制调用的最大等待时间,当超时发生时,可以重新尝试调用或者返回错误信息。另外,还可以实现请求重试机制,当网络异常时,尝试重新发送请求,直到成功或达到最大重试次数。

  6. 当请求量突增导致服务无法及时处理时,可以采取以下应对措施:增加服务的横向扩展,即增加服务器数量来分担负载;利用缓存机制将经常访问的数据缓存起来,减少对后端服务的请求压力;使用限流策略来控制并发请求数量,防止服务过载;通过分布式消息队列来异步处理请求,降低压力并提高处理效率。

01.小结

  1. 本地函数调用和RPC调用的区别在于:

    • 函数映射:本地函数调用是直接通过函数名进行调用,而RPC调用需要通过远程接口的标识符(通常是函数名)进行调用。
    • 数据转成字节流:本地函数调用直接传递函数参数,而RPC调用将参数进行编码转换为字节流进行网络传输。
    • 网络传输:本地函数调用在本地执行,而RPC调用需要通过网络将请求发送到远程服务端,并接收远程服务端的响应。
  2. RPC的概念模型包括以下角色:

    • User(用户):发起RPC调用的客户端。
    • User-Stub(用户本地存根):客户端用于封装RPC调用的本地代理,将调用转发给RPC-Runtime。
    • RPC-Runtime(RPC运行时):提供RPC调用的运行时环境,负责远程调用的管理和协调。
    • Server-Stub(服务端存根):远程服务端的本地代理,接收来自RPC-Runtime的请求并将其转发给实际的服务端。
    • Server(服务端):实际的远程服务端,执行请求并返回响应给Server-Stub。
  3. 完整的RPC过程包括以下步骤:

    • 用户调用User-Stub的本地方法。
    • User-Stub将调用封装成RPC请求消息,并通过RPC-Runtime进行网络传输。
    • RPC-Runtime将请求消息发送到远程服务端的Server-Stub。
    • Server-Stub解析请求消息,并将请求转发给实际的服务端。
    • 服务端执行方法逻辑,生成响应结果。
    • 服务端将响应结果返回给Server-Stub。
    • Server-Stub将响应结果封装成RPC响应消息,并通过RPC-Runtime进行网络传输。
    • RPC-Runtime将响应消息发送给User-Stub。
    • User-Stub解析响应消息,将结果返回给用户。
  4. 尽管RPC带来了许多好处,但也引入了一些新问题。为解决这些问题,RPC框架提供了以下功能:

    • 可扩展性:支持系统的横向扩展和服务的动态发现。
    • 故障处理:提供故障隔离、重试和容错机制,提高系统的可靠性。
    • 压力限制:实现限流、熔断和排队等机制,防止服务过载。
    • 安全性:提供身份验证、授权和数据加密等安全保护措施。
    • 监控和追踪:支持对RPC调用的监控、日志记录和分布式追踪,以便进行性能分析和故障排查。

02分层设计

2.1分层设计-以Apache Thrift为例

image.png

2.2编解码层

生成代码: 生成代码是根据同一份IDL文件(即接口描述文件)为不同的编程语言生成相应的代码,以实现对应语言的调用约束和规范。

数据格式: 数据格式可分为以下几种:

  1. 语言特定的格式: 许多编程语言提供了将内存对象编码为字节序列的支持,例如Java的java.io.Serializable。这种格式具有方便性,可以使用少量额外的代码来实现内存对象的序列化和反序列化。然而,这种格式通常与特定的编程语言强相关,其他语言很难读取这种数据,而且存在安全和兼容性问题。

  2. 文本格式: JSON、XML、CSV等文本格式具有人类可读性。但是,XML和CSV无法区分数字和字符串,JSON也不能准确区分整数和浮点数,没有指定精度的能力。在处理大量数据时,这些问题会更加突出。此外,这些文本格式没有强制的模型约束,常常需要通过文档方式进行约定,这可能会给调试带来不便。在一些语言中,JSON的序列化和反序列化需要采用反射机制,导致性能较差的问题存在。

  3. 二进制编码: 二进制编码具有跨语言和高性能等优点,常见的有Thrift的BinaryProtocol、Protobuf等。这些二进制编码实现有多种,例如TLV编码和Varint编码。其中,TLV编码是一种标签-长度-值的格式,Tag表示标签类型,Length表示数据的长度,Value表示实际的数据值。这种格式在跨语言通信和性能方面有一定的优势。

struct Person {
    1: required string       userName,
    2: optional 164          favor iteNumber,
    3: optional list<string> interests
}

image.png

  • 选型 兼容性

支持自动增加新的字段,而不影响老的服务,这将提高系统的灵活度。

通用性

支持跨平台、跨语言(①技术层面,序列化协议要支持跨平台、跨语言。②流行程度,可以判断此协议是否成熟,同时序列化和反序列化需要多方参与,很少人使用的协议往往意味着昂贵的学习成本。)

性能

从空间和时间两个维度来考虑,也就是编码后数据大小和编码耗费时长。

2.5 协议层

  • 概念 特殊结束符

一个特殊字符作为每个协议单元结束的标示。(缺点:过于简单,对于一个协议单元必须要全部读入才能够进行处理,除此之外必须要防止用户传输的数据不能同结束符相同,否则就会出现紊乱。 HTTP协议头就是以回车(CR)加换行(LF)符号序列结尾。)

变长协议

以定长加不定长的部分组成,其中定长的部分需要描述不定长的内容长度。(一般都是自定义协议,有header和payload组成,使用比较广泛)

  • 协议构造: image.png

    • LENGTH(32bits):数据包大小,不包含自身长度
    • HEADER MAGIC(16bits):标识版本信息,协议解析时
    • 候快速校验
    • SEQUENCE NUMBER(32bits):表示数据包的seqlD,
    • 可用于多路复用,单连接内递增
    • HEADER SIZE(16bits):头部长度,从第14个字节开始计算一直到PAYLOAD前
    • PROTOCOL ID(unit8编码):编解码方式,取值有ProtocollDBinary = 0;ProtocollDCompact = 2两种。
    • NUM TRANSFORMS(uint8编码):表示TRANSFORM个数
    • TRANSFORM ID(unit8编码):压缩方式,如zlib和snappy
    • INFO ID(unit8编码):传递一些定制的meta信息
    • PAYLOAD:消息体
  • 协议解析

image.png

2.3 网络通信层:

  • Sockets API 介于通信层和应用层之间 image.png 套接字编程中的客户端必须知道两个信息:服务器的IP地址、端口号。

socket函数创建一个套接字, bind将一个套接字绑定到一个地址上。listen 监听进来的连接,backlog指定挂起的连接队列的长度,当客户端连接的时候,服务器可能正在处理其他逻辑而未调用accept接受连接,此时会导致这个连接被挂起,内核维护挂起的连接队列,backlog则指定这个队列的长度,accept函数从队列中取出连接请求并接收它,然后这个连接就从挂起队列移除。

如果队列未满,客户端调用connect马上成功,如果满了可能会阻塞等待队列未满(实际上在Linux中测试并不是这样的结果,还需专门研究)。Linux的backlog默认是128, 通常情况下,我们也指定为128即可。

connect客户端向服务器发起连接,accept 接收一个连接请求,如果没有连接则会直阻塞直到有连接进来。 得到客户端的fd之后,就可以调用read, write函数和客户端通讯,读写方式和其他I/O类似。

read从fd读数据,socket默认是阻塞模式的,如果对方没有写数据,read会一直阻塞着。

write写fd写数据,socket默认是阻塞模式的,如果对方没有写数据,write会一直阻塞着。

socket关闭套接字,当另-端socket关闭后, 这一端读写的情况:

- 尝试去读会得到一个EOF, 并返回0。
- 尝试去写会触发一个SIGPIPE信号, 并返回-1和ermno=EPIPE, SIGPIPE的默认行为是终止程序,所以通常我们应该忽略这个信号,避免程序终止。
- 如果这一端不去读写,我们可能没有办法知道对端的socket关闭了。
  • 网络库 提供易用API

    • 封装底层Socket API
    • 连接管理和事件分发 功能
    • 协议支持: tcp、 udp 和uds等
    • 优雅退出、异常处理等

性能

- 应用层buffer减少copy
- 高性能定时器、对象池等

02.小结

  1. RPC框架的主要核心由三层组成:

    • 编解码层:负责将数据在应用层与网络层之间进行编码和解码转换,使得数据能够在网络中传输。
    • 协议层:定义了通信双方之间的通信规范和协议,包括请求和响应消息的结构和格式。
    • 网络通信层:提供底层的网络通信能力,负责实现数据的传输和接收功能,如使用Socket进行TCP或UDP通信。
  2. 二进制编解码的实现原理和选型要点: 二进制编解码是将数据在内存和网络之间进行转换的过程。选型要点包括:

    • 性能:选用高效的编解码算法和库,尽量减少编解码过程对性能的影响。
    • 跨语言支持:选择支持多种编程语言的二进制编解码方案,以便在跨语言的RPC调用中能够进行数据传输。
    • 灵活性和扩展性:考虑选择支持自定义结构和类型的二进制编解码方案,便于应对复杂的数据结构和业务需求。
  3. 协议的一般构造和框架协议解析的基本流程: 一般来说,协议由请求和响应消息构成,其中消息包含了一系列的字段和值,用于在服务端和客户端之间传递数据。框架协议解析的基本流程包括:

    • 解析请求:框架从网络接收到请求消息后,会解析请求消息的各个字段和值,以便对请求进行处理。
    • 处理请求:框架根据解析的请求内容,调用相应的服务端方法来处理请求,并生成响应结果。
    • 构造响应:框架将处理结果封装成响应消息的字段和值,并进行编码转换,以便发送给客户端。
    • 发送响应:框架通过网络将编码后的响应消息发送给客户端,完成一次RPC调用的过程。
  4. Socket API的调用流程和选型网络库时要考察的核心指标: Socket API的调用流程通常包括创建Socket、绑定地址、监听连接、接收请求、发送响应等步骤。在选型网络库时,要考察的核心指标包括:

    • 性能:网络库的性能指标,如吞吐量、延迟等,以确保库的性能能够满足需求。
    • 支持的协议和传输方式:网络库是否支持TCP或UDP等协议以及可靠传输或无连接传输等方式,以适应具体的业务需求。
    • 跨平台支持:网络库的跨平台能力,是否支持不同操作系统和编程语言,以便在多平台环境中进行开发和部署。
    • 可靠性和稳定性:网络库的稳定性和错误处理能力,以保证在网络传输中能够处理异常情况和故障恢复。
    • 功能扩展和社区支持:网络库是否具有功能扩展性,并且有活跃的社区支持和维护,以便获取更新和解决问题。

03关键指标

3.1 稳定性-保障策略

  • 熔断:保护调用方,防止被调用的服务出现问题而影响到整个链路
  • 限流:保护被调用方,防止大流量把服务压垮(降级处理/返回限流异常)
  • 超时控制:避免浪费资源在不可用节点上(超时主动停掉不太重要的业务) (以上三种都是快速返回,避免资源浪费在不可调用的请求上,也是服务降级的手段)

3.2稳定性-请求成功率

  • 负载均街
  • 重试(会加大直接下游的负载,有放大故障的风险) 防止重试风暴,限制单点重试和限制链路重试。

3.3稳定性-长尾请求

长尾请求一般是指明显高于均值的那部分占比较小的请求。业界关于延迟有一个常用的P99标准, P99 单个请求响应耗时从小到大排列,顺序处于99%位置的值即为P99值,那后面这1%就可以认为是长尾请求。在较复杂的系统中,长尾延时总是会存在。造成这个的原因非常多,常见的有网络抖动,GC,系统调度。 image.png

我们预先设定一个阈值 t3 (比超时时间小,通常建议是RPC请求延时的pct99),当Req1发出去后超过t3时间都没有返回,那我直接发起重试请求Req2,这样相当于同时有两个请求运行。然后等待请求返回,只要Resp1或者Resp2任意一个返回成功的结果, 就可以立即结束这次请求,这样整体的耗时就是t4,它表示从第一个请求发出到第一个成功结果返回之 间的时间,相比于等待超时后再发出请求,这种机制能大大减少整体延时。

3.4稳定性-注册中间件

image.png Kitex Client和Server的创建接口]均采用Option模式,提供了极大的灵活性,很方便就能主入这些稳定性策略

3.5易用性

开箱即用: 合理的默认参数选项、丰富的文档

周边工具: 生成代码工具、脚手架工具

Kitex使用Suite来打包自定义的功能,提供「一键配置基础依赖」的体验

diff
复制代码
- 生成服务代码脚手架
- 支持protobuf 和thrift
- 内置功能丰富的选项
- 支持自定义的生成代码插件

3.6扩展性

  • Middleware
  • Option
  • 编解码层
  • 协议层
  • 网络传输层
  • 代码生成工具插件扩展

image.png 一次请求发起首先会经过治理层面, 治理相关的逻辑被封装在middleware中,这些middleware会被构造成一个有序调用链逐个执行,比如服务发现、路由、负载均衡、超时控制等,mw执行后就会进入到remote模块,完成与远端的通信。

3.7观测性

Log、Metric、Tracing

内置观测性服务

image.png 除了传统的Log、Metric、Tracing 三件套之外,对于框架来说可能还不够,还有些框架自身状态需要暴露出来,例如当前的环境变量、配置、Client/Server初始化参数、缓存信息等。

3.8 高性能

高性能可以分为两个方面,即高吞吐和低延迟。这两个方面在许多场景下都非常重要,尤其是低延迟,在许多情况下更为关键。

多路复用是一种技术,它可以显著减少连接所带来的资源消耗,并提高服务端的性能。我们的测试结果显示,通过多路复用,服务端的吞吐量可以提升30%。

在并发场景下,调用端向服务端的一个节点发送多个请求。在非连接多路复用的情况下,每个请求都需要与服务端建立一个连接,并且连接在请求结束之前不会被关闭或放入连接池进行复用。因此,并发量与连接数是一一对应的关系。

相比之下,使用连接多路复用的话,所有的请求可以在一个连接上完成。这样可以更有效地利用连接资源,并且明显减少了连接带来的差异。

03.小结

- 框架通过中间件来注入各种服务治理策略,保障服务的稳定性。
- 通过提供合理的默认配置和方便的命令行I具可以提升框架的易用性。
- 框架应当提供丰富的扩展点,例如核心的传输层和协议层。
- 观测性除了传统的Log、Metric 和Tracing之外,内置状态暴露服务也很有必要。
- 性能可以从多个层面去优化,例如选择高性能的编解码协议和网络库。

课程总结

  • 从本地函数调用引出RPC的基本概念: 我们可以从本地函数调用开始引出RPC的基本概念。在本地函数调用中,我们直接通过函数名进行调用,并传递参数来获取结果。然而,在分布式系统中,不同的服务可能运行在不同的计算机上,无法直接进行本地函数调用。为了解决这个问题,引入了RPC(远程过程调用)的概念,它允许不同的服务通过网络进行通信和相互调用。

  • 重点讲解了RPC框架的核心的三层:编解码层、协议层和网络传输层:

    • 编解码层:负责将数据在应用层与网络层之间进行编码和解码转换,使得数据能够在网络中传输。
    • 协议层:定义了通信双方之间的通信规范和协议,包括请求和响应消息的结构和格式。
    • 网络传输层:提供底层的网络通信能力,负责实现数据的传输和接收功能,如使用Socket进行TCP或UDP通信。这三层协同工作,使得RPC框架能够实现远程服务的调用和通信。
  • 围绕RPC框架的核心指标,如稳定性、可扩展性和高性能等,展开讲解相关的知识:

    • 稳定性:RPC框架应具备高可靠性和容错机制,能够处理网络故障和异常情况,并保障系统稳定运行。
    • 可扩展性:RPC框架应支持系统的横向扩展和服务的动态发现,以应对高并发和大规模的应用场景。
    • 高性能:RPC框架应具备高吞吐量和低延迟的特性,对网络传输和数据编解码进行优化,以提供快速响应和高效的数据传输。
    • 安全性:RPC框架应提供身份验证、数据加密和授权等安全保护机制,以保障通信安全和数据隐私。
    • 监控和追踪:RPC框架应支持对调用进行监控和追踪,记录关键指标和日志信息,便于性能分析、故障排查和系统优化。

通过对这些核心指标的考虑和优化,RPC框架能够提供可靠、高效且安全的远程服务调用功能,满足分布式系统的需求。