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

116 阅读8分钟

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

01 基本概念

1.1 本地函数调用

本地函数调用

  1. 函数名称
  2. 参数
  3. 返回值

1.2 远程函数调用RPC

RPC需要解决的问题

  1. 函数映射(怎么调用这个函数)
  2. 数据转换成字节流(参数和返回值怎么传递)
  3. 网络传输(怎么保证网络高效稳定传输)

1.3 RPC概念模型

image.png

1.4 一次RPC的完整过程

image.png 分别需要 IDL 生成代码 编解码 通信协议:规范了数据在网络传输中的内容和格式。除必须的请求和响应数据外,还需要包含额外的元数据 网络传输:tcp,udp

1.5 RPC的好处

  1. 单一职责,有利于分工协作和运维开发
  2. 可扩展性强,资源利用率高
  3. 故障隔离,服务的整体的稳定性更强

1.6 RPC带来的问题

  1. 服务宕机,对方应该怎么处理
  2. 在调用过程中发生网络异常,然后保证消息的可达性?
  3. 请求量激增导致服务无法及时处理,有哪些应对措施?

RPC框架来解决

02 分层设计

编码解码、协议层、网络通信

image.png

2.1 编码解码

2.1.1 生成代码

image.png

2.1.2 数据格式

  • 语言特定的格式 例如 java的java.io.Serializable
  • 文本格式 JSON XML CSV 文本格式,人类可读
  • 二进制编码 Thrift的BinaryProtocol,Protobuf

2.1.3 二进制编码

TLV 编码 Tag:标签,可以理解为类型 Length:长度 Value:值,Value也可以是个TLV结构

image.png

image.png 缺点:增加了Type和Length两个冗余的信息,有额外的内存开销,特别是大部分字段都是基本类型的情况下有不小的内存空间浪费。

2.1.4 选型

  • 兼容性 在移动互联的时代,业务需求变更的周期变得更快,新的需求不断涌现,老的需求还要继续维护。如果序列化协议有好的扩展性,支持自动增加新的业务字段,而不影响老的服务,那么将大大提高系统的灵活性。
  • 通用性
  1. 技术层面,序列化协议是否支持跨平台、跨语言
  2. 流行程度
  • 性能
  1. 空间开销:在元数据的下,尽可能的减少额为的存储信息。要不然网络和磁盘会有大的压力。
  2. 时间开销:复杂的序列化协议可能造成大的时间开销,成为系统瓶颈。

2.2 协议层

2.2.1 概念

  • 特殊的结束符 一个特殊字符作为每个协议单元结束的标示 image.png
  • 变长协议 以定长加不定长的部分组成,其中定长的部分需要描述不定长的内容长度

image.png

2.2.2 协议构造

image.png

  • LENGTH 数据包的大小,不包含自身
  • HEADER MAGIC 标识版本协议,用于快速验证
  • SEQUENCE NUMBER 表示数据包的seqid,用于多路复用,单连接内递增
  • HEADER SIZE 头部长度
  • PROTOCOL ID 编码方式
  • TANSFORM ID 压缩方式
  • INFO ID 传递一些定制的meta信息
  • PAYLOAD 消息体

2.2.3 协议解析

image.png

2.3 网络通信层

2.3.2 Sockets API

image.png

2.3.3 网络库

  • 提供易用的API 封装底层Socket API 连接管理和事件分发
  • 功能 协议支持tcp,udp,uds 优雅退出、异常处理
  • 性能 应用层buffer减少copy 高性能定时器、对象池等

03 关键指标

稳定性、易用性、扩展性、观测性、高性能

3.1 稳定性

3.1.1 保障策略

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

服务A调用服务B,服务B又调用服务C,而这时服务C又响应超时了,服务B就一直等待,而这时服务A又频繁的调用服务B,服务B就会应为堆积了大量的请求而导致宕机,而造成服务雪崩的问题。

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

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

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

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

image.png

3.1.2 请求成功率

image.png

image.png

因为重试有放大故障的风险 重试会加大直接下游的负载,假设服务A调用服务B,重试次数设置为r,当B高负载时很可能调用不成功,这时A重试调用B,B的被调用量快速增大,最坏的情况可能放大r倍,不仅不能成功反而可能会挂掉。

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

3.1.3 长尾请求

backup request 备份请求

image.png

image.png

长尾请求,一般是指明显高于均值的那部分占比较小的请求。业界关于延迟有一个常用的标准,P99标准,P99标准是指,当个请求的响应时间从小到大排序,顺序处于99%位置的就是P99。后面的百分之1就是长尾请求。在复杂的请求环境中,长尾请求总是存在的,造成这个的原因有很多,如网络抖动,GC,系统调用。

我们现设值一个时间t3(小于超时时间,一般为p99)当请求发送出去后,超过t3时间没有返回,那我们直接重试请求,这样同时运行的有两个请求,其中任意一个返回就可以结束这次请求,这样整体的耗时时间为t4。这种相对于超时之后再请求的方式大大的减少了整体的时延。

3.1.4 注册中间件

image.png

3.2 易用性

  • 开箱即用 合理的默认参数选项,丰富的文档
  • 周边工具 生成代码工具,脚手架工具

image.png

image.png

3.3 扩展性

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

image.png

3.4 观测性

image.png

除了传统的Log、Metric、Tracing三件套外,对于框架来说还是不够,还需要框架自身需要暴露,例如当前的环境变量,配置,client,service初始化参数,缓存信息等。

3.5 高性能

image.png

image.png 一般来说低延迟更重要

image.png

04 企业实践

整体架构、自研网络库、扩展性设计、性能优化、合并部署

4.1 整体架构

image.png

4.2 自研网络库

4.2.1 背景

  • 原生库无法感知连接状态 在使用连接池时,池中存在无效连接,影响连接池的复用。
  • 原生库存在goroutine暴涨的风险 一个连接一个gorountine的模式,由于连接利用率低下,存在goruntine占用调度开销,影响开销。
  1. go net使用epoll et,netpoll使用lt
  2. netpoll在大包场景下会占用更多的内存
  3. go net只有一个epoll时间循环(因为et模式被唤醒的少,且事件循环内无需负责写,所以干的活少),而netpoll允许有多个事件循环(循环内需要负责读写,干的活多,读写越重,越需要开更多loops)
  4. go net一个连接一个goroutine,netpoll连接数和gorouitne数量没有关系,和请求数有一定关系
  5. go net不支持零拷贝,甚至用户想要实现bufferedConnect这类缓存,还会产生二次拷贝。netpoll支持管理一个buffer池直接交给用户,且上层可以不使用read接口而使用特定的零拷贝接口对buffer进行管理,实现零拷贝能力的传递。

4.2.2 netpoll

  • 解决了无法感知连接状态问题 引入epoll主动监听机制,感知连接状态
  • 解决goroutine暴涨的风险 建立gorutine池,复用goroutine
  • 提升性能 引入nocopy buffer,向上层提供nocopy的调用接口,编解码层面的零拷贝

4.3 扩展性设计

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

image.png

image.png

4.4 性能优化

4.4.1 网络库优化

  • 调度优化 epoll_wait在调度上优化 gopool重用goroutine降低同时运行协程数
  • LinkBuffer 读写并行无锁,支持nocopy流式读写 高效扩缩容 nocopy buffer池化,减少gc
  • Pool 引入内存池和对象池,减少gc

4.4.2 编解码优化

  • codegen
  1. 预计算并分配内存,减少内存的操作次数,包括内存的分配和拷贝
  2. inline减少函数调用次数和不必要的反射操作
  3. 自研了go语言实现的thrift idl解析和代码生成器
  • jit
  1. 使用jit编译技术改善了用户体验,并且减少了用户维护生成代码的负担
  2. 基于jit编码技术的高性能动态thrift编解码器

4.5 合并部署

微服务过微,传输和序列化开销越来越大,将亲和性强的服务实例尽可能调度到同一台物理机,转化为ipc调用