深入浅出RPC框架 | 青训营笔记

105 阅读8分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的第7篇笔记。

RPC-Remote Procedure Calls,指的是函数的远程调用

1.基本概念

1.1本地函数调用

本地main函数调用calculate函数,通过函数指针找到calculate函数,然后通过压栈得到函数的返回值

  • 对于参数和返回值放在栈/寄存器中,执行程序时在栈中读取相应的数据
  • 在编译器优化后有可能不需要额外的存储而是将调用的函数进行inline操作

image.png

1.2远程函数调用(RPC)

网上商城和支付服务部署在不同的机器上,中间隔着一个网络,由于处在不同的进程,地址空间不同,所以不可以使用函数指针,也不可以使用内存来传递返回值,所以通过函数的ID找到映射的函数,通过字节流来获得参数 网络传输需要保证高效稳定的传输数据 image.png

RPC理论模型

image.png

RPC实际请求过程

image.png

相比本地函数调用,远程调用的话我们不知道对方有哪些方法,以及参数长什么样,所以需要有一种方式来描述或者说声明我有哪些方法,方法的参数都是什么样子的,这样的话大家就能按照这个约定来调用,这个描述文件就是IDL文件

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

编码只是解决了跨语言的数据交换格式,但是如何通讯呢?需要制定通讯协议,以及数据如何传输?我的网络模型如何呢?那就是这里的transfer 要做的事情。

RPC的好处

image.png

RPC的问题

image.png

2.分层设计

image.png

2.1编解码层

生成代码

image.png

数据格式

语言特定编码格式:这种编码形式好处是非常方便,可以用很少的额外代码实现内存对象的保存与恢复,这类编码通常与特定的编程语言深度绑定,其他语言很难读取这种数据。如果以这类编码存储或传输数据,那你就和这门语言绑死在一起了。安全和兼容性也是问题

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

二进制编码:实现可以有很多种,TLV 编码 和 Varint 编码 image.png

2.2协议层

image.png 协议是双方确定的交流语义,比如:我们设计一个字符串传输的协议,它允许客户端发送一个字符串,服务端接收到对应的字符串。这个协议很简单,首先发送一个4字节的消息总长度,然后再发送1字节的字符集charset长度,接下来就是消息的payload,字符集名称和字符串正文。

特殊结束符:过于简单,对于一个协议单元必须要全部读入才能够进行处理,除此之外必须要防止用户传输的数据不能同结束符相同,否则就会出现紊乱

HTTP 协议头就是以回车(CR)加换行(LF)符号序列结尾

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

协议构造 image.png

协议解析 image.png

2.3网络通信

Sockets API

image.png image.png 套接字编程中的客户端必须知道两个信息:服务器的 IP 地址,以及端口号。

socket函数创建一个套接字,bind将一个套接字绑定到一个地址上。listen监听进来的连接,backloq的含义有点复杂,这里先简单的描述:指定挂起的连接队列的长度,当客户端连连接的时候,服务器可能正在处理其他逻辑而未调用accept接受连接,此时会导致这个连接被挂起,内核维护挂起的连接队列,backloq则指定这个队列的长度,accept函数从队列中取出连接请求并接收它,然后这个连接就从挂起队列移除。如果队列未满,客户端调用connect马上成功,如果满了可能会阳寒等待队列未满(实际上在Linux中测试并不是这样的结果,这个后面再专门来研究。Linux的backlog默认是128,通常情况下,我们也指定为128即可。

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

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

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

网络库选型的核心指标

image.png

3.关键指标

3.1稳定性

保障策略

image.png

熔断:

一个服务A调用服务B时,服务B的业务辑又调用了服务C,而这时服务响应超时了,由干服务B依赖服务C,C超时自接导致B的业务辑一直等待,而这个时候服务A继续频繁地调用服务B,服务B就可能会因为堆积大量的请求而导致服务宕机,由此就导致了服务雪崩的问题

限流:

当调用端发送请求过来时,服务端在执行业务逻辑之前先执行检查限流逻辑,如果发现访问量过大并且超出了限流条件,就让服务端直接降级处理或者返回给调用方一个限流异常

超时:

当下游的服务因为某种原因响应过慢,下游服务主动停掉一些不太重要的业务,释放出服务器资源,避免浪费资源

请求成功率

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

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

长尾请求

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

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

注册中间件

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

3.2易用性

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

3.3扩展性

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

3.4观测性

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