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

63 阅读10分钟

本篇文章主要内容

基本概念

分层设计

核心指标

企业实践

RPC 框架的基本概念

RPC(Remote Procedure Call)远程过程调用,简单的理解是一个节点请求另一个节点提供的服务。

本地函数调用和 RPC 调用的区别

本地函数调用:

func main(){    var a = 2    var b = 3    result := calculate(a, b)    fmt.Println(result)    return } func calculate(x, y int) {    z := x*y    return z }

将 a 和 b 的值压栈

通过函数指针找到 calculate 函数,进入函数取出栈中的值 2 和 3,将其赋予 x 和 y。

计算 x * y,并将结果存在 z

将 z 的值压栈,然后从 calculate 返回

从栈中取出 z 返回值,并赋值给 result。

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

在本地调用中,我们只需要把参数压到栈里,然后让函数自己去栈里读就行,但是在远程过程调用时,客户端跟服务端是不同的进程,不能通过内存来传递参数,这时候需要客户端把参数先转成一个字节流,传给服务端后,再把字节流转成自己能读取的格式。

RPC 的概念模型

概念模型组成:User、User-Stub、RPC-Runtime、Server-Stub、Server 提出于1984年 Nelson的论文《Implenmenting Remote Procedure Calls》

一次 RPC 的完整过程

IDL(Interface description language)文件

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

生成代码

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

编解码

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

通信协议

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

网络传输

IO 网络模型: 网络 IO 的本质是 socket的读取 ,socket在linux系统被抽象为流,IO 可以理解为对流的操作。

阻塞 IO(blocking IO): 需要内核IO操作彻底完成后,才返回到用户空间,执行用户操作

非阻塞 IO (non-blocking IO): 不需要内核IO操作彻底完成后,才返回到用户空间。

阻塞/非阻塞指的是用户空间程序的执行状态

多路复用 IO (IO multiplexing): 通过select/epoll系统调用,单个应用程序的线程,可以不断轮询成百上千的socket连接,当某个或者某些socket网络连接有IO就绪的状态,就返回对应的可以执行的读写操作。

信号驱动 IO(signal driven IO,SIGIO): 当有输入或者数据可以写到指定的文件描述符上时,内核向请求数据的进程发送一个信号。

异步 IO(Asynchronous IO): 整个内核的数据处理过程中,包括内核将数据从网络物理设备(网卡)读取到内核缓存区、将内核缓冲区的数据复制到用户缓冲区,用户程序都不需要阻塞。

传输层协议: 传输层的主要功能是实现分布式进程之间的通信。利用网络层提供的服务,在源主机的应用进程与目的主机的应用进程建立“端—端”连接。 传输层之间传输的报文称为“传输协议数据单元(TPDU)”,TPDU有效载荷称为应用层的数据

TCP(传输控制协议): TCP是传输控制协议是一种面向连接的、可靠的、基于字节流的传输层通信协议,由 IETF 的RFC 793定义。TCP 是面向连接的、可靠的流协议。流就是指不间断的数据结构,你可以把它想象成排水管中的水流。

UDP(用户数据报协议): UDP是一个简单的面向数据报的传输层协议。提供的是非面向连接的、不可靠的数据流传输。UDP不提供可靠性,也不提供报文到达确认、排序以及流量控制等功能。

PCR 的好处

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

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

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

RPC 框架的分层设计

编解码层

以 Apache Thrift 为例 数据格式

语言特定的格式: 许多编程语言都内建了将内存对象编码为字节序列的支持,例如java.io.Serializable。这种编码形式好处是非常方便,可以用很少的额外代码实现内存对象的保存与恢复。

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

二进制编码: 具有跨语言和高性能等优点,常见有 Thift 的 BinaryProtocol等。

二进制编码

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

struct Person {    1: required string       userName,    2: optional i64          favoriteNumber,    3: optional list interests }

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

Lenght:长度

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

选型

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

通用性:支持跨平台、跨语言

性能:

空间开销 (Verbositv) , 序列化需要在原有的数据上加上描述字段,以为反序列化解析之用。巨大的的额外空间开销意味着高昂的成本。

时间开销 (Comdlexity) ,复杂的南列化协议会导致较长的解析时间 这可能会使得序列么和反南列化阶段成为数个系统的瓶颈

协议层

概念

协议是双方确定的交流语义,比如:我们设计一个字符串传输的协议,它允许客户端发送一个字符串,服务端接收到对应的字符串。

消息切分

特殊结束符:一个特殊字符作为每个协议单元结束的标示。除此之外必须要防止用户传输的数据不能同结束符相同,否则就会出现紊乱。

message body \r\n message body \r\n

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

length message body length message body

协议构造

LENGTH:字段 32 bits,数据包大小,不包含自身长度。

LEADER MAGIC:字段 16 bits,值为0×1000,用于标识 协议版本信息,协议解析的时候可以快速效验。

SEQUENCE NUMBER 字段 32 bits,表示数据包的 seqld,可用于多路复用,最好确保单个连接内递增。

HEADER SIZE:字段 16 bits,等于头部长度字节数/4,头部长度计算从第 14 个字节开始计算一直到 PAYLOAD 前。

PROTOTCOL ID:字段 uint8 编码

Binary = 0

Compact = 2

INFO ID:字段 uint8 编码 用于传递一些定制的 meta 的信息

PAYLOAD 消息体

网络通信层

Sockets API 

网络库

提供易用 API

封装底层 Socket API

连接管理和事件分发

功能

协议支持:tcp、udp 和 uds 等

优雅退出、异常处理等

性能

应用层 buffer 减少 copy

高性能定时器、对象池等

RPC 框架的核心指标

稳定性

保障策略

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

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

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

请求成功率 

负载均衡:重试会加大下游的负载。

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

长尾请求: 一般是指明显高于均值的那部分占比较小的请求。常见于网络抖动、GC、系统调度

易用性

开箱即用

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

周边工具

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

扩展性

Middleware

Option

编解码层

协议层

网络传输层

代码生成工具插件扩展

观测性

Log、Metric、Tracing

内置观测性服务

高性能

场景

单机多机

单连接多连接

单/多client 单/多server

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

目标

高吞吐

低延迟

手段

连接池

多路复用

高性能编解码协议

高性能网络库

Kitex 的企业实践分享

整体架构 - Kitex

Kitex Core 核心组件:定义框架的层次结构、接口、还有接口的默认实现。如 client 和 server 是对用户暴露的,client/server option 的配置都是在这两个 package 中提供的,还有 client/server 的初始化。

client/server 下面的是框架治理层面的功能模块和交互信息,remote是与对端交互的模块,包含编解码是网络通信。

Kitex Byted 与公司内部基础设施集成:是对字节内部的扩展,byted 部分是在生成代码中初始化 client 和 cerver 时通过 suite 集成进来的,这样实现的好处是与字节内部特性解耦,方便后续开源拆分。

Kitex Tool 代码生成工具:里面包括idl解析、效验、代码生成、插件支持、自更新等。

自研网络库

背景

原生库无法感知连接状态

在使用连接池时,池中存在失效连接,影响连接池的复用。

原生库存在 goroutine 暴涨的风险

一个连接一个 goroutine 的模式,由于连接利用率低下,存在大量 goroutine 占用调度开销,影响性能。

Netpoll

引入 epoll 主动监听机制,感知连接状态

建立 goroutine 池,复用 goroutine

引入 Nocopy Buffer,向上层提供 NoCopy 的调用接口,编解码层面零拷贝。

扩展性设计

支持多协议,也支持灵活的自定义协议扩展

编解码支持thrift、Protobuf

应用层协议支持TTHeader、Http2、也支持裸的thrift协议

传输层目前支持TCP

性能优化

网络库优化

调度优化

epoll_wait 在调度上的控制

gopool 重用 goroutine 降低同时运行协程数

LinkBuffer

读写并行无锁,支持 nocopy 地流式读写

高效扩缩容

Nocopy Buffer 池化,减少 GC

Pool

引入内存池和对象池,减少GC 开销

编解码优化

Codegen

预计算并预分配,减少内存操作次数,包括内存分配和拷贝

lnline 减少函数调用次数和避免不必要的反射操作等

自研了 Go 语言实现的 Thrift IDL 解析和代码生成器,支持完善的 Thrift IDL 语法和语义检查,并支持了插件机制 - Thriftgo

JIT

使用 JIT 编译技术改善用户体验的同时带来了更强的编解码性能,减轻用户维护生成代码的负担。

基于 JIT 编译技术的高性能动态 Thrift 编解码器 - Frugal

合并部署

微服务过微,引入的额外的传输序列化开销越来越大。

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