RPC原理与实践 | 青训营笔记

79 阅读6分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 11 天。

1 基本概念

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

  • 函数映射
  • 数据转换成字节流
  • 网络传输

1.2 一次 RPC 的完整过程

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

1.3 RPC的好处

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

1.3 RPC 带来的问题将由 RPC 框架来解决

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

2 分层设计

img

2.1 编解码层

  • 生成代码

    • Client 和 Server 依赖同一份IDEL文件,基于语言类型生成代码
  • 数据格式

    • 语言特定格式:例如 java.io.Serializable,方便使用但是与特定编程语言深度绑定,兼容性差。
    • 文本格式:例如 JSON、XML、CSV 等。具有人类可读性,但是数字编码可能存在歧义。
    • 二进制编码:具备跨语言和高性能等优点。常见有 Thrift 的 BinaryProtocol,Protobuf,实现可以有多种形式,例如 TLV 编码 和 Varint 编码
  • 选型考察点

    • 兼容性:支持自动增加新字段而不影响老服务。
    • 通用型:支持跨平台、跨语言
    • 性能
      • 空间开销(编码后数据大小)
      • 时间开销(编码耗时)
  • 生成代码和编解码层相互依赖,框架的编解码应当具备扩展任意编解码协议的能力

img

2.2 协议层

  • 以 Thrift 的 THeader 协议为例

img

  • 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 消息内容

  • 协议解析

img

2.3 网络通信层

网络库:

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

img

  • 阻塞 IO 下,耗费一个线程去阻塞在 read(fd) 去等待用足够多的数据可读并返回。

  • 非阻塞 IO 下,不停对所有 fds 轮询 read(fd) ,如果读取到 n <= 0 则下一个循环继续轮询。

第一种方式浪费线程(会占用内存和上下文切换开销),第二种方式浪费 CPU 做大量无效工作。而基于 IO 多路复用系统调用实现的 Poll 的意义在于将可读/可写状态通知和实际文件操作分开,并支持多个文件描述符通过一个系统调用监听以提升性能。

网络库的核心功能就是去同时监听大量的文件描述符的状态变化(通过操作系统调用),并对于不同状态变更,高效,安全地进行对应的文件操作。

3 关键指标

3.1 稳定性

  • 保障策略
    • 熔断:保护调用方,防止被调用的服务出现问题而影响到整个链路
    • 限流:保护被调用方,防止大流量把服务压垮
    • 超时控制:避免浪费资源在不可用节点上

img

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

  • 请求成功率

    • 负载均衡
    • 重试
  • 长尾请求:即明显高于均值的那部分占比较小的请求

    • BackupRequest

img

  • 注册中间件

3.2 易用性

  • 开箱即用:合理的默认参数选项、丰富的文档

  • 周边工具:生成代码工具、脚手架工具

3.3 扩展性

  • Middleware:middleware 会被构造成一个有序调用链逐个执行,比如服务发现、路由、负载均衡、超时控制等

  • Option:作为初始化参数

  • 核心层是支持扩展的:编解码、协议、网络传输层

  • 代码生成工具也支持插件扩展

img

3.4 观测性

  • 三件套:Log、Metric 和 Tracing

img

  • 内置观测性服务,用于观察框架内部状态
    • 当前环境变量
    • 配置参数
    • 缓存信息
    • 内置 pprof 服务用于排查问题

3.5 高性能

  • 连接池和多路复用:复用连接,减少频繁建联带来的开销

  • 高性能编解码协议:Thrift、Protobuf、Flatbuffer 和 Cap'n Proto 等

  • 高性能网络库:Netpoll 和 Netty 等

4 企业实践——字节内部 Kitex

  • 框架文档 Kitex

  • 自研网络库 Netpoll,背景:

    • 原生库无法感知连接状态
    • 原生库存在 goroutine 暴涨的风险
  • 扩展性:支持多协议,也支持灵活的自定义协议扩展

  • 性能优化,参考 字节跳动 Go RPC 框架 KiteX 性能优化实践

    • 网络优化
      • 调度优化
      • LinkBuffer 减少内存拷贝,从而减少 GC
      • 引入内存池和对象池
    • 编解码优化
      • Codegen:预计算提前分配内存,inline,SIMD等
      • JIT:无生产代码,将编译过程移到了程序的加载(或首次解析)阶段,可以一次性编译生成对应的 codec 并高效执行
  • 合并部署

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