RPC原理与实现 | 青训营

81 阅读9分钟

这是我参与[第五届青训营]伴学笔记的第14天。

课程目录

  1. 基本概念
  2. 分层设计
  3. 关键指标
  4. 企业实践

01.基本概念

1.1本地函数调用

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

1.2远程函数调用

RPC 需要解决的问题

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

1.3RPC概念模型

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

理论逻辑:caller machine调用端与callee被调用端,中间是一个网络。user将本地请求送到user-stub打包,然后rpc runtime将请求送到被调用端,接收完后由server-stub解压,最后调用真正的业务逻辑,处理完后将返回结果打包,又通过网络送回调用端。

1.4一次rpc的完整过程

  • IDL (Interface description language)文件

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

  • 生成代码

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

  • 编解码

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

  • 通信协议

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

  • 网络传输

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

1.5RPC的好处

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

1.6RPC带来的问题

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

小结

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

02分层设计

2.1分层设计-以Apche Thrift为例

image.png

2.2编解码层-生成代码

client与server以来同一份IDL文件,生成各种不同语言的文件。

2.3编解码层-数据格式

  • 语言特定的格式

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

  • 文本格式

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

  • 二进制编码

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

2.4编解码层-二进制编码

TLV编码

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

2.5编解码层-选型

  1. 兼容性

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

2.6协议层-概念

  1. 特殊结束符

一个特殊字符作为每个协议单元结束的标示 2. 变长协议 以定长加不定长的部分组成,其中定长的部分需要描述不定长的内容长度

2.8协议层-协议构造

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

2.9协议层-协议解析

image.png

2.10网络通信层-sockerts API

sockets API介于应用层与传输层之间。大致过程:必须先知道IP和端口。有一个bind操作,将套接字传输到一个地址上,再listen监听更新进来的连接,然后放入队列里,可能会有阻塞,需要放入等待队列中。最后关闭套接字close,一端关闭后,若另一端仍然读则会得到一个EUF,仍然写则触发一个信号,返回错误。

2.11网络通信层-网络库

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

小结

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

03.关键指标

3.1稳定性-保障策略

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

3.2稳定性-请求成功率

  1. 负载均衡

A调用B,A与B下各有多个节点,使得分配均衡,保障下流能够处理请求并且成功率较高。 2. 重试 A调用B失败后,多次重试都失败了,才判断为失败。

3.3稳定性-长尾请求

一般是明显高于平均响应请求占比比较少的请求,例如PCD99,99%请求都可以在该时间返回。

提高请求成功率:备份请求backup request。

3.4稳定性-注册中间件

image.png

3.5易用性

  1. 开箱即用

合理的默认参数选项、丰富的文档 2. 周边工具 生成代码工具、脚手架工具

简单易用的命令行工具

  • 生成服务代码脚手架
  • 支持protobuf 和thrift
  • 内置功能丰富的选项
  • 支持自定义的生成代码插件

3.6扩展性

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

3.7观测性

  • Log(日志)、Metric(监控)、Tracing(链式跟踪)
  • 内置观测性服务

image.png

3.8高性能

场景

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

目标

  1. 高吞吐
  2. 低延迟

手段

  • 连接池
  • 多路复用
  • 高性能编解码协议
  • 高性能网络库

小结

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

04企业实践

4.1整体架构-Kitex

  • Kitex Core

核心组件

  • Kitex Byted

与公司内部基础设施集成

  • Kitex Tool

代码生成工具

4.2自研网络库-背景

  1. 原生库无法感知连接状态

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

4.3自研网络库-Netpoll

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

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

  1. 解决goroutine暴涨的风险

建立 goroutine池,复用goroutine 3. 提升性能 引入 Nocopy Buffer,向上层提供NoCopy 的调用接口,编解码层面零拷贝

4.4扩展性设计

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

image.png

4.5性能优化-网络库优化

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

4.6性能优化-编解码优化

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

4.7

合并部署

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

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

小结

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

总结

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