【青训营笔记】- 「RPC原理与实现」

79 阅读9分钟

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

前言

当我们的业务越来越多、应用也越来越多时,自然的,我们会发现有些功能已经不能简单划分开来或者划分不出来。此时,可以将公共业务逻辑抽离出来,将之组成独立的服务Service应用 。而原有的、新增的应用都可以与那些独立的Service应用 交互,以此来完成完整的业务功能。HTTP可以用作交互的协议,但是由于其性能较差,因此更多的是选择RPC这种协议。RPC框架就是实现以上结构的有力方式。

一、基本概念

1.1 本地函数调用

  1. 将 a 和 b 的值压栈
  2. 通过函数指针找到 calculate 函数,进入函数取出栈中的值 2 和 3,将其赋予 x 和 y
  3. 计算 x * y ,并将结果存在 z
  4. 将z 的值压栈,然后从 calculate 返回
  5. 从栈中取出 z 返回值,并赋值给 result image.png

1.2 远程函数调用(RPC)

  • RPC 需要解决的问题
  1. 函数映射
  2. 数据转换成字节流
  3. 网络传输 image.png

1.3 RPC概念模型

image.png

1984 年 Nelson 发表了论文《Implementing Remote Procedure Calls》其中提出了 RPC 的过程由 5个模型组成: User、User-Stub、RPC-Runtime、Server-Stub、Server

1.4 一次RPC的完整过程

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

image.png

1.5 RPC的好处

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

1.6 RPC带来的问题

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

1.7 小结

  1. 本地函数调用和 RPC 调用的区别: 函数映射、数据转成字节流、网络传输
  2. RPC 的概念模型: User、User-Stub、RPC-Runtime、Server-Stub、Server
  3. 一次 PRC 的完整过程,并讲解了 RPC 的基本概念定义
  4. RPC 带来好处的同时也带来了不少新的问题,将由 RPC 框架来解决

二、分层设计

2.1 以 Apache Thrift 为例

image.png

2.2 编解码层

  • Service.client(processor), read/write, TProtocal 三个部分可以归为编解码层

2.2.1 生成代码

image.png

  • 以IDL文件作为一些方法的描述,并做一些约束。
  • 语言特定的格式:许多编程语言都内建了将内存对象编码为字节序列的支持,例如 Java 有 java.io.Serializable
  • 文本格式:JSON、XML、CSV 等文本格式,具有人类可读性
  • 二进制编码:常见有 Thrift 的 BinaryProtocol,Protobuf 等具备跨语言和高性能等优点

2.2.2 二进制编码 - TLV编码

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

image.png IDL文件

image.png 编码后文件

2.2.3 编码选型准则

  • 兼容性:支持自动增加新的字段,而不影响老的服务,这将提高系统的灵活度
  • 通用性:支持跨平台、跨语言
  • 性能:从空间和时间两个维度来考虑,也就是编码后数据大小和编码耗费时长

2.3 协议层

  • 图中的TTransport image.png

2.3.1 概念

  • 特殊结束符:一个特殊字符作为每个协议单元结束的标示
    • 如HTTP协议中的CRLF
  • 变长协议:以定长加不定长的部分组成,其中定长的部分需要描述不定长的内容长度
    • 如字节流中的第一位标识后续字节的长度

2.3.2 协议构造

image.png

  • LENGTH: 数据包大小,不包含自身
  • HEADER MAGIC:标识版本信息,协议解析时候快速校验
  • SEQUENCE NUMBER: 表示数据包的 seqID可用于多路复用,单连接内递增
  • HEADER SIZE: 头部长度,从第14个字节开始计算一直到 PAYLOAD前
  • PROTOCOL ID: 编解码方式,有 Binary 和Compact 两种
  • TRANSFORM ID: 压缩方式,如 zlib 和 snappy
  • INFO ID: 传递一些定制的 meta 信息
  • PAYLOAD: 消息体

2.3.3 协议解析

image.png

2.4 网络通信层

  • 图中的NetworkIO image.png

2.4.1 Socket API

image.png

2.4.2 网络库

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

2.5 小结

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

三、关键指标

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

3.1 稳定性

3.1.1 保障策略

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

3.1.2 请求成功率

  • 负载均衡
  • 重试 image.png image.png

3.1.3 长尾请求

备份请求:在超过一定长尾请求判断阈值之后不管第一次的请求没有返回,直接再次重试发送一个新的请求 比等待第一次的长尾请求返回结果再次发送新的请求更快

3.2 易用性

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

image.png

3.3 拓展性

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

image.png

3.4 观测性

  • Log、Metric、Tracing
  • 内置观测性服务

image.png

3.5 高性能

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

3.6 小结

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

四、企业实践

4.1 整体架构 - Kitex

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

image.png

4.2 自研网络库 - 背景

  • 原生库无法感知连接状态
    • 在使用连接池时,池中存在失效连接,影响连接池的复用
  • 原生库存在 goroutine 暴涨的风险
    • 一个连接一个 goroutine 的模式,由于连接利用率低下,存在大量 goroutine 占用调度开销,影响性能。

4.3 自研网络库 - NetPoll

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

4.4 拓展性设计

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

image.png

image.png

4.5 性能优化 - 网络库优化

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

4.5 性能优化 - 编解码优化

  • Codegen
    • 预计算并预分配内存,减少内存操作次数,包括内存分配和拷贝
    • Inline 减少函数调用次数和避免不必要的反射操作等
    • 自研了 Go 语言实现的 Thrift IDL 解析和代码生成器,支持完善的 Thrift IDL 语法和语义检查,并支持了插件机制 - Thriftgo
  • JIT
    • 使用 JIT 编译技术改善用户体验的同时带来更强的编解码性能,减轻用户维护生成代码的负担
    • 基于 JIT 编译技术的高性能动态 Thrift 编解码器 - Frugal

4.6 合并部署

  • 微服务过微,传输和序列化开销越来越大
  • 将亲和性强的服务实例尽可能调度到同一个物理机,远程 RPC 调用优化为本地 IPC 调用
  • 中心化的部署调度和流量控制
  • 基于共享内存的通信协议
  • 定制化的服务发现和连接池实现
  • 定制化的服务启动和监听逻辑

image.png

结果:某抖音服务,30% 合并流量,服务端 CPU减少19%,延迟 TCP99 减少 29%

4.7 小结

  1. 介绍了 Kitex 的整体架构
  2. 介绍了自研网络库 Netpoll 的背景和优势
  3. 从扩展性和性能优化两个方面分享了相关实践
  4. 介绍了内部正在尝试落地的新的微服务形态: 合并部署

总结

  1. 从本地函数调用引出 RPC 的基本概念
  2. 重点讲解了 RPC 框架的核心的三层,编解码层、协议层和网络传输层
  3. 围绕 RPC 框架的核心指标,例如稳定性、可扩展性和高性能等,展开讲解相关的知识
  4. 分享了字节跳动高性能 RPC 框架 Kitex 的相关实践