RPC框架理论学习笔记 | 青训营笔记

76 阅读8分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 13 天

1 基本概念

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

RPC需要解决的问题:

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

1.1 一次RPC的完整流程

IDL (Interface description language)文件

  • IDL通过一种中立的方式来描述接口,使得在不同平台上运行的对象和用不同语言编写的程序可以相互通信
  • 相比本地函数调用,远程调用的话我们不知道对方有哪些方法,以及参数长什么样,所以需要有一种方式来描述或者说声明我有哪些方法,方法的参数都是什么样子的,这样的话大家就能按照这个来调用,这个描述文件就是 IDL 文件。

生成代码

  • 通过编译器工具把IDL文件转换成语言对应的静态库
  • 刚才我们提到服务双方是通过约定的规范进行远程调用,双方都依赖同一份IDL文件,需要通过工具来生成对应的生成文件,具体调用的时候用户代码需要依赖生成代码,所以可以把用户代码和生成代码看做一个整体。

编解码

  • 从内存中表示到字节序列的转换称为编码,反之为解码,也常叫做序列化和反序列化
  • 编码只是解决了跨语言的数据交换格式,但是如何通讯呢?需要制定通讯协议,以及数据如何传输?我的网络模型如何呢?那就是这里的 transfer 要做的事情。

通信协议

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

网络传输

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

1.2 RPC的优缺点

优点:

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

缺点:

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

2 分层设计

image.png

2.1 编解码层

生成代码

image.png

数据格式

语言特定的格式

  • 许多编程语言都内建了将内存对象编码为字节序列的支持,例如Java有java.io.Serializable
  • 这种编码形式好处是非常方便,可以用很少的额外代码实现内存对象的保存与恢复,这类编码通常与特定的编程语言深度绑定,其他语言很难读取这种数据。如果以这类编码存储或传输数据,那你就和这门语言绑死在一起了。安全和兼容性也是问题

文本格式

  • JSON、XML、CSV等文本格式,具有人类可读性
  • 文本格式具有人类可读性,数字的编码多有歧义之处,比如XML和CSV不能区分数字和字符串,JSON虽然区分字符串和数字,但是不区分整数和浮点数,而且不能指定精度,处理大量数据时,这个问题更严重了;没有强制模型约束,实际操作中往往只能采用文档方式来进行约定,这可能会给调试带来一些不便。 由于JSON在一些语言中的序列化和反序列化需要采用反射机制,所以在性能比较差;

二进制编码

  • 具备跨语言和高性能等优点,常见有Thrift的 BinaryProtocol,Protobuf 等

  • 实现可以有很多种,TLV 编码 和 Varint 编码

选型

兼容性

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

通用性

  • 支持跨平台、跨语言

性能

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

2.2 协议层

协议的解析过程:

image.png

2.3 网络通信层--网络库

提供易用API

  • 封装底层Socket API
  • 连接管理和事件分发

功能

  • 协议支持: tcpudpuds
  • 优雅退出、异常处理等

性能

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

3 关键指标

3.1 稳定性

保障策略

  • 熔断:保护调用方,防止被调用的服务出现问题而影响到整个链路
    • 一个服务 A 调用服务 B 时,服务 B 的业务逻辑又调用了服务 C,而这时服务 C 响应超时了,由于服务 B 依赖服务 C,C 超时直接导致 B 的业务逻辑一直等待,而这个时候服务 A 继续频繁地调用服务 B,服务 B 就可能会因为堆积大量的请求而导致服务宕机,由此就导致了服务雪崩的问题
  • 限流:保护被调用方,防止大流量把服务压垮
    • 当调用端发送请求过来时,服务端在执行业务逻辑之前先执行检查限流逻辑,如果发现访问量过大并且超出了限流条件,就让服务端直接降级处理或者返回给调用方一个限流异常
  • 超时控制:避免浪费资源在不可用节点上
    • 当下游的服务因为某种原因响应过慢,下游服务主动停掉一些不太重要的业务,释放出服务器资源,避免浪费资源

从某种程度上讲超时、限流和熔断也是一种服务降级的手段

请求成功率

注意,因为重试有放大故障的风险,首先,重试会加大直接下游的负载。如下图,假设 A 服务调用 B 服务,重试次数设置为 r(包括首次请求),当 B 高负载时很可能调用不成功,这时 A 调用失败重试 B ,B 服务的被调用量快速增大,最坏情况下可能放大到 r 倍,不仅不能请求成功,还可能导致 B 的负载继续升高,甚至直接打挂。

防止重试风暴,限制单点重试和限制链路重试

长尾请求

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

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

注册中间件

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

3.2 易用性

开箱即用

  • 合理的默认参数选项、丰富的文档

周边工具

  • 生成代码工具、脚手架工具

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

3.3 扩展性

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

3.4 观测性

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

3.5 高性能

这里分两个维度,高性能意味着高吞吐和低延迟,两者都很重要,甚至大部分场景下低延迟更重要。

多路复用可以大大减少了连接带来的资源消耗,并且提升了服务端性能,我们的测试中服务端吞吐可提升30%。

  • 调用端向服务端的一个节点发送请求,并发场景下,如果是非连接多路复用,每个请求都会持有一个连接,直到请求结束连接才会被关闭或者放入连接池复用,并发量与连接数是对等的关系。

  • 而使用连接多路复用,所有请求都可以在一个连接上完成,大家可以明显看到连接资源利用上的差异