RPC框架|青训营笔记

108 阅读11分钟

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

1.基本概念

RPC(Remote Procedure Calls远程过程调用),区别于本地函数调用

1.RPC需要解决的问题

1.函数映射(本地是通过函数指针直接指定的,而远程调用是两个进程,地址空间完全不一样,所以需要给每个函数一个自己的id)

2.数据转换成字节流(本地直接把参数压栈,而远程调用是两个进程,不能通过内存传递参数,需要客户端将参数转为字节流传给服务端,再把字节流转换成服务端能读取的一个格式)

3.网络传输(保证网络高效稳定地传输数据)

2.RPC概念模型

image.png RPC的过程由5个模型组成:

1.User

2.User-Stub

3.RPC-Runtime

4.Server-Stub

5.Server

3.一次RPC的完整过程

image.png IDL文件(Interface description language)

通过一种中立的方式来描述接口,使得在不同平台上运行的对象和用不同语言编写的程序可以相互通信 用于描述远程可调用的方法及方法参数,服务双方都通过约定的规范进行调用,即都依赖于同一份远程调用文件

生成代码 通过编译工具把IDL文件转成语言对应的静态库,生成对应的生成文件,具体调用的时候用户代码依赖生成代码,所以可以把用户代码和生成代码看成一个整体

编解码

编码:从内存中表示到字节序列的转换(序列化)

解码:从字节序列到内存中表示(反序列化)

解决了跨语言的数据交换格式

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

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

4.RPC的好处

1.单一职责,有利于分工协作和运维开发

开发(可采用不同的语言),部署以及运维(上线独立)都是独立的

2.可扩展性强,资源使用率更优

eg.压力过大时可以独立扩充资源,底层基础服务可以复用,节省资源

3.故障隔离,服务的整体可靠性更高

某个模块发生故障,不会影响整体的可靠性 image.png

5.RPC的问题->通过RPC框架处理

1.服务宕机,对方应该如何处理

2.调用过程中发生网络异常,如何保证消息可达

3.请求量突增导致服务无法及时处理,有哪些应对措施

2.RPC框架的分层设计

以Apache Thrift为例

image.png

1.编解码层

image.png

image.png

数据格式:

1.语言特定格式

编程语言内建将内存对象编码为字节序列的支持

eg.Java有java.io.Serializable

缺点:跟语言绑定,兼容性较差

2.文本格式

JSON,XML,CSV等文本格式,具有可读性

缺点:描述不够精确,没有模型强制约束,性能差等 eg.JSON可以区分字符串和数字但是无法区分整数和浮点数,也不能处理精度 没有模型强制约束,实际操作中往往只能采用文档方式进行约定 JSON序列化反序列化往往需要采用反射的机制,性能较差

3.二进制编码 实现可以有很多种,TLV编码和Varint编码 具备跨语言和高性能等优点,常见有Thrift的BinaryProtocol,Protobuf等 image.png 用feild tag 取代了key的字符串(eg.1代替userName),从而减小总体的大小 但由于增加了Type和length两个额外信息,在大部分字段都是基本类型的情况下空间浪费较大

选型

1.兼容性

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

2.通用性

支持跨平台、跨语言

3.性能

从空间(编码后数据大小)和时间(编码耗费时长)两个维度来考虑

2.协议层

image.png

1.特殊结束符

一个特殊字符作为每个协议单元结束的标示 image.png eg.HTTP协议以回车(CR)加换行(LF)结尾 过于简单,对于一个协议单元必须要全部读入才能进行处理,同时要防止用户传输数据不能同结束符相同,否则会出现紊乱

2.变长协议 image.png 由hedaer和playload组成,通常定长部分需要描述不定长部分的长度,使用比较广泛

协议构造 apache swift

image.png

  • LENGTH字段 32bits,包括数据包剩余部分的字节大小,不包含LENGTH自身长度
  • HEADER MAGIC字段 16bits,值为: 0x1000, 用于标识协议版本信息,协议解析的时候可以快速校验
  • FLAGS字段 16bits, 为预留字段,暂未使用,默认值为0x0000
  • SEQUENCE NUMBER字段 32bits,表示数据包的seqld,可用于多路复用,最好确保单个连接内递增
  • HEADER SIZE字段 16bits,等于头部长度字节数/4,头部长度计算从第14个字节开始计算,一直到 PAYLOAD前(备注: header 的最大长度为64K)
  • PROTOCOL ID字段 uint8编码,取值有: ~ ProtocollDBinary = 0 ProtocollDCompact = 2
  • NUM TRANSFORMS字段 uint8编码,表示TRANSFORM个数
  • TRANSFORM ID字段 uint8编码,具体取值参考下文,表示压缩方式zlib or snappy
  • INFO ID字段 uint8编码,具体取值参考下文,用于传递一些定制的meta信息 -** PAYLOAD**消息内容

协议解析

image.png 魔数:可以知道是什么协议

PayloaadCodec:编解码方式

3.网络通信层

image.png

Sockets API网络通信层

dbe21ad81454eefb7aed8ce11c69b580_b424f5d4-4b14-4d9e-8be6-8881cf97e857.png

套接字层介于应用层和传输层之间

1dccf43ee2c921069d530a576617bc7a_0a44a149-ab2e-44ce-ae5b-c9d233a625b4.png socket通过一个四元组唯一确定(源IP,源端口号,目的IP,目的端口号)

socket创建套接字,bind将socket绑定到ip地址和端口,listen监听进来的连接

connect:客户端向服务器发起连接

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

backlog:当客户端连接时,服务器可能由于正在处理逻辑而未调用accept接受连接,会导致连接被挂起,内核维护连接的挂起队列,backlog指定挂起连接队列的长度,accept从队列中取出连接并接收它,连接就从挂起队列中删除。如果队列未满,客户端调用connect马上成功,如果满了会阻塞等待队列未满(实际在Linux中测试,不会是这样的结果),Linux的backlog默认是128。

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

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

socket关闭套接字:当一端关闭后,另一端读写情况 尝试读会得到一个EOF,并返回0 尝试去写会触发一个SIGPIPE信号,并返回-1和error=ePIPE,SIGPIPE的默认行为是终止程序,所以我们应该忽略这个信号,避免程序终止。如果这一端不去读写,可能无法知道对端socket关闭

通常采用封装好的网络库

1.提供易用API

封装底层SocketAPI

连接管理和事件分发

2.功能

协议支持tcp,udp和uds等

优雅退出,异常处理等

3.性能

应用层buffer减少copy

高性能定时器、对象池

3.关键指标

image.png

1.稳定性

(1)保障策略 image.png

  • 熔断:保护调用方,防止被调用的服务出现问题而影响整个链路

服务A调用服务B,服务B的业务逻辑又调用到服务C

如果服务C响应超时,服务B依赖服务C,服务B的业务逻辑就会一直等待

这时候服务A继续频繁调用服务B,B可能因为堆积了大量请求而导致服务宕机,导致了服务雪崩的问题

  • 限流:保护被调用方,防止大流量把服务压垮

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

  • 超时控制:避免浪费资源在不可用节点上

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

(2)请求成功率 提高请求成功率的方式:

1.负载均衡

image.png

2.重试

image.png

重试有放大故障的风险

重试会直接加大下游的负载

eg.A服务调用B服务,重试次数设置为r(包括首次请求)

当B高负载时很可能调用不成功,此时A调用失败重试B,B服务的被调用量快速增大,最大情况下可能放大到r倍,可能不但不成功,直接把B打挂

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

(3)长尾请求

长尾请求一般是指响应耗时明显高于均值的那部分占比较小的请求。

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

ce6427312712ae453f928bc73ae1da7b_f092cc74-bdf9-4074-9a70-aad18a4846fb.png

2be7cd5f0d3f6080a916aac049414be6_f0699d94-c3f6-41ab-aca8-6e8d8003f557.png

我们预先设定一个阈值t3 (比超时时间小,通常建议是RPC请求延时的pct99 ),当Req1发出去后超过t3时间都没有返回,那我们直接发起重试请求Req2,这样相当于同时有两个请求运行。

然后等待请求返回,只要Resp1或者Resp2任意一个返回成功的结果, 就可以立即结束这次请求,这样整体的耗时就是t4,它表示从第一个请求发出到第一个成功结果返回之间的时间,相比于等待超时后再发出请求,这种机制能大大减少整体延时。

(4)注册中间件

框架通过注册中间件的方式将前面讲的具体的措施策略进行实现 创建时就可以通过可选方式将这些功能(超时,熔断,重试等)加上 58dcbf7960c23fd11f5178ab6599dad0_9e741021-f9ae-4ac1-9ff0-cf944ddc9acb.png Kitex Client和Server的创建接[ ]均采用Option模式,提供了极大的灵活性,很方便就能注入这些稳定性策略

2.易用性

3.扩展性

log、Metric、Tracing

4.观测性

image.png 高吞吐:时间单位内尽可能的处理更多的请求 低延迟:请求发出去,返回时间尽可能短

5.高性能

4.企业实践

image.png

1.整体架构

Kitex Core 核心组件 Kitex Byted 与公司内部基础设施集成 Kitex Tool 代码生成工具 image.png 原生库: 1.原生库无法感知连接状态,池中存在失效连接,影响连接池的复用 2.原生库存在goroutine暴涨的风险,一个连接一个goroutine的模式,由于连接利用率底下,存在大量goroutine占用调度开销,影响性能 自研网络库:Netpoll 1.解决无法感知连接状态的问题 引入epoll主动监听机制,感知连接状态 2.解决goroutine暴涨的风险 建立goroutine池,复用goroutine 3.提升性能 引入Nocopy Buffer,向上提供Nocopy的调用接口,编解码层面零拷贝 (1)网络库优化 image.png (2)编解码优化 image.png 支持多协议,支持灵活的自定义协议扩展

合并部署 缺点: 微服务过微->传输和序列化开销越来越大 解决: 将亲和性强的服务实例尽可能调度到同一个物理机,远程RPC调用优化为本地IPC调用