远程过程调用RPC 框架详解

178 阅读7分钟

1.RPC简介

  • 相比本地函数调用,RPC调用需要解决的问题

    • 函数映射 解决怎么告诉函数要调用它:本地函数在同一进程当中,因此同一地址空间直接用函数指针,远程函数调用采用函数ID找到远程函数

    • 数据转换成字节流,而本地调用压栈即可

    • 网络传输

1.1 一次 RPC 的完整过程

调用->打包数据->传输->接受->解压数据->服务端

  • IDL (Interface description language)文件
    • IDL通过一种中立的方式来描述接口,使得在不同平台上运行的对象和用不同语言编写的程序可以相互通信
  • 生成代码
    • 通过编译器工具把IDL文件转换成语言对应的静态库
  • 编解码
    • 从内存中表示到字节序列的转换称为编码,反之为解码,也常叫做序列化和反序列化
  • 通信协议
    • 规范了数据在网络中的传输内容和格式。除必须的请求/响应数据外,通常还会包含额外的元数据
  • 网络传输
    • 通常基于成熟的网络库走TCP/UDP传输

image.png

1.2 RPC优点

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

1.3 RPC 带来的问题

  • 服务宕机如何感知?
  • 遇到网络异常应该如何应对?
  • 请求量暴增怎么处理?

由下面的RPC框架解决

2.分层设计

2.1 Apache Thrift

2.2 编解码层

上面的图中生成代码和Thrift协议层都属于编解码层

生成代码

Client和Server通过同一份IDL文件生成代码,这个代码可以是不同语言的

数据格式

  • 语言特定格式:例如 java.io.Serializable(缺陷:语言兼容性)
  • 文本格式:例如 JSON、XML、CSV 等(缺陷:无强模型约束,性能差)
  • 二进制编码:常见有 Thrift 的 BinaryProtocol,Protobuf,实现可以有多种形式,例如 TLV 编码 和 Varint 编码

二进制编码

TLV编码

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

选型要点

  • 兼容性

  • 通用型

  • 性能

    • 空间开销
    • 时间开销
  • 生成代码和编解码层相互依赖,框架的编解码应当具备扩展任意编解码协议的能力

2.3 协议层

特殊结束符 一个特殊字符作为每个协议单元结束的标示(如HTTP的\r\n)

变长协议 以定长(lenght)加不定长(message body)的部分组成,其中定长的部分需要描述不定长的内容长度

  • 以 Thrift 的 THeader 协议为例

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

2.4 网络通信层

SocketsAPI

在网络五层模型中处于应用层、传输层中间

socket编程需要知道ip和端口,bind把socket绑定在一个地址上后,listen监听静态连接放到队列里面(队列长度最大backlog,超过会发生阻塞,Linux为128),accept接收connect请求,没有请求的时候处于阻塞状态等待连接。

读写IO默认为阻塞IO,一边读而另外一边没有写会一直处于阻塞装填

  • 阻塞 IO 下,耗费一个线程去阻塞在 read(fd) 去等待用足够多的数据可读并返回。
  • 非阻塞 IO 下,不停对所有 fds 轮询 read(fd) ,如果读取到 n <= 0 则下一个循环继续轮询。

第一种方式浪费线程(会占用内存和上下文切换开销)
第二种方式浪费 CPU 做大量无效工作
而基于 IO 多路复用系统调用实现的 Poll 的意义在于将可读/可写状态通知和实际文件操作分开,并支持多个文件描述符通过一个系统调用监听以提升性能。
网络库的核心功能就是去同时监听大量的文件描述符的状态变化(通过操作系统调用),并对于不同状态变更,高效,安全地进行对应的文件操作。

一端关闭之后另一端还尝试读,会得到EOF,返回0
尝试写会触发SIGPIPE信号返回错误码为-1和errno=EPIPE,这个信号默认行为是终止程序,通常忽略该信号

网络库

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

2.5 小结

  • 1.RPC框架主要核心有三层:编解码层、协议层和网络通信层
  • 2.二进制编解码的实现原理和选型要点
  • 3.协议的一般构造,以及框架协议解析的基本流程
  • 4.网络库的基本架构,以及选型时要考察的核心指标

3.关键指标

3.1 稳定性

保障策略

  • 熔断:保护调用方,防止被调用的服务出现问题而影响到整个链路
    • 一个服务A调用服务B时,服务B的业务逻辑又调用了服务C,而这时服务C响应超时了,由于服务B依赖服务C,C超时直接导致B的业务逻辑一直等待,而这个时候服务A继续频繁地调用服务B,服务B就可能会因为堆积大量的请求而导致服务宕机,由此就导致了服务雪崩的问题
  • 限流:保护被调用方.防止大流量把服务压垮
    • 当调用端发送请求过来时,服务端在执行业务逻辑之前先执行检查限流逻辑,如果发现访问量过大并且超出了限流条件,就让服务端直接降级处理或者返回给调用方一个限流异常
  • 超时控制:避免浪费资源在不可用节点上
    • 当下游的服务因为某种原因响应过慢,下游服务主动停掉一些不太重要的业务,释放出服务器资源,避免浪费资源

从某种程度上讲超时、限流和熔断也是一种服务降级的手段 。

请求成功率

-   负载均衡
-   重试(有放大故障的风险,要防止重试风暴)

长尾请求

长尾请求一般是指明显高于均值的那部分占比较小的请求。关于延迟有一个常用的P99标准,P99单个请求响应耗时慢于所有请求的99%,造成长尾请求原因常见的有网络抖动,GC,系统调度。

解决: BackupRequest

我们预先设定一个阈值T3(建议是RPC请求延时的pct99)当Req1发出去后超过T3时间都没有返回,那我们直接发起 重试请求Req2,这样相当于同时有两个请求运行。然后等待请求返回,只要Resp1或者Resp2任意一个返回成功的结果,就可以立即结束这次请求.

相比于等待超时后再发出请求,这种机制能大大减少整体延时。

注册中间件

有注册中间件可以方便地注入超时、熔断、重试、限流、负载均衡、BackRequest等稳定性策略

3.2 易用性

  • 开箱即用

    • 合理的默认参数选项、丰富的文档
  • 周边工具

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

3.3 扩展性

  • Middleware:一次请求发起首先会经过治理层面,治理相关的逻辑被封装在middleware中,这些middleware会被构造成一个有序调用链逐个执行,比如服务发现、路由、负载均衡、超时控制等,mw执行后就会进入到remote模块,完成与远端的通信
  • Option:作为初始化参数
  • 核心层是支持扩展的:编解码、协议、网络传输层
  • 代码生成工具也支持插件扩展

3.4 观测性

  • 三件套:Log、Metric 监控和 Tracing跟踪链路上的情况

  • 内置观测性服务,用于观察框架内部状态

    • 当前环境变量
    • 配置参数
    • 缓存信息
    • 内置 pprof 服务用于排查问题

3.5 高性能

  • 连接池和多路复用:复用连接,减少频繁建联带来的开销
  • 高性能编解码协议:Thrift、Protobuf、Flatbuffer 和 Cap'n Proto 等
  • 高性能网络库:Netpoll 和 Netty 等

3.6 小结

  1. 框架通过中间件来注入各种服务治理策略,保障服务的稳定性
  2. 通过提供合理的默认配置和方便的命令行工具可以提升框架的易用性
  3. 框架应当提供丰富的扩展点,例如核心的传输层和协议层
  4. 观测性除了传统的Log、Metric和Tracing之外,内置状态暴露服务也很有必要
  5. 性能可以从多个层面去优化,例如选择高性能的编解码协议和网络库

4.实践

  1. 框架文档 Kitex
  1. 网络库 Netpoll,背景:

    a. 原生库无法感知连接状态
    b. 原生库存在 goroutine 暴涨的风险

  1. 扩展性:支持多协议,也支持灵活的自定义协议扩展
  1. 性能优化,参考 字节跳动 Go RPC 框架 KiteX 性能优化实践

    a. 网络优化

    i.  调度优化  
    ii.  LinkBuffer 减少内存拷贝,从而减少 GC  
    iii.  引入内存池和对象池
    复制代码
    

    b. 编解码优化

    i.  Codegen:预计算提前分配内存,inline,SIMD等
    ii.  JIT:无生产代码,将编译过程移到了程序的加载(或首次解析)阶段,可以一次性编译生成对应的 codec 并高效执行
    复制代码
    
  1. 合并部署

    a. 微服务过微,引入的额外的传输和序列化开销越来越大
    b. 将强依赖的服务统计部署,有效减少资源消耗

5.一些问题

  1. 行业内各个流行的 RPC 框架的优劣对比
  1. 从第三章节 RPC 的核心指标来看,Kitex 还有哪些功能是欠缺或者需要加强的?
  1. 了解微服务的新趋势 ServiceMesh,以及 RPC 框架和 ServiceMesh 的关系
  1. 关于 RPC 框架,业界有哪些新的趋势和概念?
  1. Netpoll 的优势在哪?相比其他高性能网络库例如 Netty 还有什么不足?
  1. Flatbuffer 和 Cap'n Proto 等编解码协议为什么高性能?