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

256 阅读5分钟

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

RPC

  1. 需要解决的问题
    • 函数映射

    • 网络传输

    • 数据转换成字节流

image.png 《Implementing Remote Procedure Calls》

  • User
  • User-Stub
  • RPC-Runtime
  • Server-Stub
  • Server

一次RPC的完整过程

image.png

  • IDL文件 Interface description Language 需要有一种方式声明方法及其参数,因此使用IDL文件来描述接口,各接口遵循该规范,可以让不同平台的对象和不同语言的程序相互通信
  • 生成代码 编译器工具把IDL文件转换为语言对应的静态库
  • 编解码 编码是内存中的格式转换为字节序列,反之为解码。
  • 网络传输 TCP/UDP
  • 通信协议 规范了数据在网络中的传输格式和内容,除必须的请求/响应数据外,可以还会包含额外元数据。

好处

  • 单一职责
  • 可扩展性强,资源利用率高
  • 故障隔离,可靠性高

带来的问题

  • 依赖服务宕机后,如何处理?
  • 调用过程中出现异常,如何保证可达性?
  • 请求量突增导致服务无法及时处理,如何应对?

分层设计

以Apache Thrift为例。

image.png

编码层

客户端和服务端对于同一份IDL文件生成不同语言不同场景下的CodeGen。

  • 语言格式特定:内建了将内存对象编码为字节序列的支持,如java.io.Serializable
  • 文本格式:如Json、Xml、Csv
  • 二进制编码:跨语言,高性能,如Thrift的BinaryProtocal、Google的Protobuf。 以BinaryProtocal为例,它的底层实现方式是TLV编码

TLV编码

  • Tag
  • Length
  • Value.也可以嵌套TLV
struct Person{
   1: required string           userName,
   2: optional i64          favoriteNumber,
   3: optional list<string>   interests
}

image.png

协议层

  • 特殊结束符:以特殊字符作为协议单元结束标志。如HTTP协议
  • 变长协议:以定长+不定长的部分组成,定长的部分需要描述不定长部分的长度

协议构造

image.png

  • LENGTH
  • HEADER MAGIC:标识版本信息,协议解析时快速效验
  • SEQUENCE NUMBER: 标识数据包seqid,可用于多路复用(多个请求流在发送,单连接内递增
  • HEADER SIZ:头部长度,从第14个字节计算到PAYLOAD前
  • PROTOCAL_ID:编解码方式,Binary/Compact
  • TRANSFORM_ID,压缩方式,zlib/snappy
  • INFO I:传递定制meta信息
  • PAYLOAD: 消息体 那么 框架如何解析协议?
    首先 框架从内存读取部分数据(MAGIC NUMBER),知道是什么类型的协议。而后读取编码方式,知道用什么解码,解码后交给对方处理。
    -----Peek--->MagicNumber-----Peek--->PayloadCodec---Decode--->Payload

网络通信层(Socket API

SOCKET API介于应用层和传输层中间。

image.png socket创建套接字会有bind操作,会把套接字返回到ip+port,而后去listen,并把监听到的信息放在队列。队列有一定的长度限制(BACKLOAD),LINUX默认128。客户端发起请求会进行connect(),得到客户端的ip后使用read和write进行通信。默认阻塞默认读取数据。读取数据完成使用close关闭套接字。如果一方关闭,另一方尝试读会返回EOF,尝试写会返回错误。
一般会使用封装好的网络库作为rpc的网络通信层。

关键指标

稳定性(使用降级

  • 熔断 避免雪崩,保护调用方
  • 限流
  • 超时控制 稳定性的一个指标是请求成功率。可以通过负载均衡和重试来提高。
    还有长尾请求,明显高于平均响应时间但是占比较小的请求。有个指标就是PC99,后面的1%就是长尾请求。
    如何提高长尾请求的成功率?
    Backup Request.发送请求后设置一个时间阈值,如果在时间阈值内没有返回,再次发送请求,一般会很快得到返回。
    以上是提高稳定性的策略,那么框架如何应用这些策略?
    一般框架使用中间件方式,将这些策略设置为可选的配置。

易用性

  • 开箱即用,合理的参数配置,丰富的文档。
  • 生成代码工具/脚手架工具。

扩展性

需要尽可能提供多的扩展点。

  • 中间件.有序调用链添加策略

image.png

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

观测性

  • Log
  • Metric 监控qps或延迟
  • Tracing

高性能

  • 目标: 高吞吐,低延迟
  • 场景,不同场景下表现不同
    • 单机多机
    • 单链接多连接
    • 单/多client 单/多server
    • 不同大小请求包
    • 不同请求类型,例如pingpong/streaming
  • 优化手段都是通用的
    • 连接池
    • 多路复用
    • 高性能编解码协议
    • 高性能网络库

企业实践

Kitex

  • Kitex Core 核心组件
  • Kitex Byted 与公司内部基础设施集成
  • Kitex Tool 代码生成工具 image.png

自研网络库的原因:

  • 原生库go/net无法感知连接状态,使用连接池时池中出现失效连接,影响连接池复用。 * 原生库中,一个连接一个Goroutinue,连接利用率低下,存在大量goroutinue占用调度开销,影响性能。 自研网络库-epoll
  • 引入epoll主动监听机制,感知连接状态
  • 建立goroutine池,复用goroutine
  • 引入Nocopy Buffer,向上层提供NoCopy的调用接口,编解码界面零拷贝

性能优化之网络库优化

  • 调度优化
    • epoll_wait
    • gopool重用goroutinue
  • LinkBuffer
    • 读写并行无锁,支持nocopy流式读写
    • 高效扩缩容
    • Nocopy Buffer池化,减少GC
  • Pool
    • 引入内存池和对象池

性能优化之编解码优化

  • Codegen
    • 预结算并预分配内存,减少内存操作次数,包括内存分配和拷贝
    • Inline减少次数调用次数,避免不必要反射
    • ThriftGo
  • JIT(Just in time
    • Frugal

合并部署

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

  • 中心化的部署调度和流量控制
  • 基于共享内存的通信协议
  • 定制化的服务发现和连接池实现
  • 定制化的服务启动和监听逻辑