RPC的架构 | 青训营

74 阅读13分钟

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

在远程过程调用时,客户端跟服务端是不同的进程,不能通过内存来传递参数,这时候就需要客户端把参数先转成一个字节流,传给服务端后,再把字节流转成自己能读取的格式。

RPC 需要解决的问题

1、函数映射

2、数据转换成字节流

3、网络传输

image.png

RPC 概念模型

image.png

一次 RPC的完整过程

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

1、IDL (Interface description lanquage) 文件

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

2、生成代码

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

3、编解码

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

4、通信协议

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

5、网络传输

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

image.png

RPC的好处

image.png

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

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

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

可扩展性强,例如压力过大的时候可以独立扩充资源,底层基础服务可以复用,节省资源

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

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

RPC带来的问题

1、服务宕机

2、在调用过程中发生网络异常

3、请求量突增导致服务无法及时处理

分层设计

image.png

编解码层

生成代码

image.png

数据格式

1、语言特定的格式

许多编程语言都内建了将内存对象编码为字节序列的支持,例如 Java 有 iava.io.Serializable

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

2、文本格式

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

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

3、二进制编码

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

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

二进制 编码

TLV 编码

1、Tag: 标签,可以理解为类型

2、Lenght: 长度

3、Value: 值,Value 也可以是个TLV结构

TLV编码结构简单清晰,并且扩展性较好,但是由于增加了Type和length两几余信息,有额外的内存开销,特别是在大部分字段都是基本类型的情况下有不小的空间浪费

image.png

image.png

选型

1、兼容性

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

移动互联时代,业务系统需求的更新周期变得更快,新的需求不断涌现,而老的系统还是需要继续维护。如果序列化协议具有良好的可扩展性,支持自动增加新的业务字段,而不影响老的服务,这将大大提供系统的灵活度。

2、通用性

支持跨平台、跨语言

第一、技术层面,序列化协议是否支持跨平台、跨语言。如果不支持,在技术层面上的通用性就大大降低了。

第二、流行程度,序列化和反序列化需要多方参与,很少人使用的协议往往高味着昂贵的学习成本:另一方面,流行度低的协议,往往缺乏稳定而成熟的跨语言、跨平台的公共包。

3、性能

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

协议层

概念

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

1、特殊结束符

一个特殊字符作为每个协议单元结束的标示

image.png

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

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

2、变长协议

以定长加不定长的部分组成,其中定长的部分需要描述不定长的内容长度

image.png

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

协议构造

image.png

LENGTH: 数据包大小,不包含自身

HEADER MAGIC: 标识版本信息,协议解析时候快速校验

SEQUENCE NUMBER: 表示数据包的 segID可用于多路复用,单连接内递增

HEADER SIZE: 头部长度,从第14个字节开始计算一直到 PAYLOAD前

PROTOCOL ID:编解码方式,有 Binary 和Compact 两种

TRANSFORM ID: 压缩方式,如 zlib 和snappyINFO ID: 传递一些定制的 meta 信息

PAYLOAD:消息体

协议解析

image.png

网络通信层

SocketsAPI

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

image.png

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

image.png

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

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

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

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

尝试去读会得到一个EOF,并返回0。

尝试去写会触发一个SIGPIPE信号,并返回-1和ermno=EPIPE,SIGPIPE的默认行为是终止程序,所以通常我们应该忽略这个信号,避免程序终止。

如果这一端不去读写,我们可能没有办法知道对端的socket关闭了。

网络库

1、提供易用API

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

2、功能

协议支持: tcp、udp 和 uds 等

优雅退出、异常处理等

3、性能

应用层 buffer 减少 copy

高性能定时器、对象池等

关键指标

稳定性

保障策略

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

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

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

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

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

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

image.png

请求成功率

负载均衡

image.png

重试

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

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

image.png

长尾请求

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

Backup Request

image.png

image.png

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

注册中间件

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

image.png

易用性

1、开箱即用

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

2、周边工具

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

image.png

简单易用的命令行工具

1、生成服务代码脚手架

2、支持protobuf 和thrift

3、内置功能丰富的选项

4、支持自定义的生成代码插件

扩展性

1、Middleware

2、Option

3、编解码层

4、协议层

5、网络传输层

6、代码生成工具插件扩展

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

image.png

观测性

image.png

1、Log、Metric、 Tracing

2、内置观测性服务

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

高性能

1、场景

单机多机

单连接多连接

单/多client 单/多server

不同大小的请求包

不同请求类型: 例如 pingpong、streaming 等

2、目标

低延迟

高吞吐

3、手段

连接池

多路复用

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

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

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

高性能编解码协议

高性能网络库

合并部署

微服务过微,传输和序列化开销越来越大

将亲和性强的服务实例尽可能调度到同一个物理机,远程 RPC 调用优化为本地 IPC 调用

image.png

image.png

1、中心化的部署调度和流量控制

2、基于共享内存的通信协议

3、定制化的服务发现和连接池实现

4、定制化的服务启动和监听逻辑

image.png