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

136 阅读7分钟

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

基本概念

远程函数调用(RPC - Remote Procedure Calls)

RPC 需要解决的问题

  1. 函数映射
  2. 数据转换成字节流
  3. 网络传输

image.png

RPC概念模型

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

image.png

一次RPC的完整过程

IDL (Interface description language)文件

  • IDL通过—种中立的方式来描述接口,使得在不同平台上运行的对象和用不同语言编写的程序可以相互通信

生成代码

  • 通过编译器工具把IDL文件转换成语言对应的静态库

编解码

  • 从内存中表示到字节序列的转换称为编码,反之为解码,也常叫做序列化和反序列化

通信协议

  • 规范了数据在网络中的传输内容和格式。除必须的请求/响应数据外,通常还会包含额外的元数据

网络传输

  • 通常基于成熟的网络库走TCP/UDP传输

image.png

RPC的好处

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

image.png

RPC带来的问题

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

小结

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

分层设计

以Apache Thrift为例

image.png

编解码层 - 生成代码

image.png

编解码层 - 数据格式

语言特定的格式

  • 许多编程语言都内建了将内存对象编码为字节序列的支持,例如Java有java.io.Serializable

文本格式

  • JSON、XML、CSV等文本格式,具有人类可读性

二进制编码

  • 具备跨语言和高性能等优点,常见有Thrift 的 BinaryProtocol,Protobuf等

编解码层 - 二进制编码

TLV编码

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

image.png

image.png

编解码层 - 选型

兼容性

  • 支持自动增加新的字段,而不影响老的服务,这将提高系统的灵活度

通用性

  • 支持跨平台、跨语言

性能

  • 从空间和时间两个维度来考虑,也就是编码后数据大小和编码耗费时长

协议层 - 概念

特殊结束符

  • 一个特殊字符作为每个协议单元结束的标示

image.png

变长协议

  • 以定长加不定长的部分组成,其中定长的部分需要描述不定长的内容长度

image.png

协议层 - 协议构造

LENGTH:数据包大小,不包含自身

HEADER MAGIC:标识版本信息,协议解析时候快速校验

SEQUENCE NUMBER:表示数据包的seqlD,可用于多路复用,单连接内递增

HEADER SIZE:头部长度,从第4个字节开始计算一直到PAYLOAD前

PROTOCOL ID:编解码方式,有 Binary和Compact 两种

TRANSFORM ID:压缩方式,如zlib和snappy

INFO ID:传递—些定制的meta信息

PAYLOAD:消息体

image.png

协议层 - 协议解析

image.png

网络通信层 - Socketes API

image.png

网络通信层 - 网络库

提供易用API

  • 封装底层Socket API
  • 连接管理和事件分发

功能

  • 协议支持: tcp、 udp和uds等
  • 优雅退出、异常处理等

性能

  • 应用层buffer 减少copy
  • 高性能定时器、对象池等

小结

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

关键指标

稳定性 - 保障策略

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

image.png

稳定性 - 长尾请求

image.png

稳定性 - 注册中间件

image.png

易用性

开箱即用

  • 合理的默认参数选项、丰富的文档

周边工具

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

image.png

扩展性

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

image.png

观测性

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

image.png

高性能

场景

  • 单机多机
  • 单连接多连接
  • 单/多client单/多server
  • 不同大小的请求包
  • 不同请求类型:例如pingpong、streaming等

目标

  • 高吞吐
  • 低延迟

手段连接池

  • 多路复用
  • 高性能
  • 偏解码协议
  • 高性能网络库

小结

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

企业实践

整体架构 - Kitex

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

image.png

自研网络库 - 背景

原生库无法感知连接状态

  • 在使用连接池时,池中存在失效连接,影响连接池的复用。

原生库存在goroutine暴涨的风险

  • 一个连接一个goroutine 的模式,由于连接利用率低下,存在大量 goroutine 占用调度开销,影响性能。

自研网络库 - Netpoll

解决无法感知连接状态问题

  • 引入epoll主动监听机制,感知连接状态

解决goroutine暴涨的风险

  • 建立goroutine池,复用goroutine

提升性能

  • Nocopy Buffer,向上层提供 NoCopy 的调用接口,编解码层面零拷贝

扩展性设计

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

image.png

image.png

性能优化 - 网络库优化

调度优化

  • epoll_wait在调度上的控制
  • gopool重用goroutine降低同时运行协程数

LinkBuffer

  • 读写并行无锁,支持nocopy 地流式读写
  • 高效扩缩容
  • Nocopy Buffer池化,减少GC

Pool

  • 引入内存池和对象池,减少GC开销

性能优化 - 编解码优化

Codegen

  • 预计算并预分配内存,减少内存操作次数,包括内存分配和拷贝lnline 减少函数调用次数和避免不必要的反射操作等 自研了Go语言实现的Thrift IDL解析和代码生成器,支持完善的Thrift IDL语法和语义检查,并支持了插件机制- Thriftgo

JIT

  • 使用J川T编译技术改善用户体验的同时带来更强的编解码性能,减轻用户维护生成代码的负担
  • 基于JT编译技术的高性能动态Thrift编解码器- Frugal

合并部署

  • 微服务过微,传输和序列化开销越来越大
  • 将亲和性强的服务实例尽可能调度到同一个物理机,远程 RPC调用优化为本地IPC调用

image.png


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

image.png

小结

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