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

76 阅读8分钟

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

内容来自掘金字节内部课:RPC 框架分层设计RPC 关键指标分析与企业实践

一、本堂课重点内容:

  1. RPC 相关的基本概念
  2. RPC 框架的分层设计
  3. 衡量 RPC 框架的一些核心指标
  4. 字节内部 RPC 框架 Kitex 实践分享

二、详细知识点介绍:

1. 基本概念

1.1 本地函数调用

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

image.png

RPC 需要解决的问题:

  1. 函数映射(本地是直接函数指针;远程调用是两个进程,地址空间不同,每个函数有自己的id)
  2. 数据转换成字节流(本地直接压栈;远程客户端与服务端是两个进程)
  3. 网络传输

1.3 RPC概念模型

image.png

1.4 一次RPC的完整过程

image.png

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

1.5 RPC的好处

image.png

  1. 单一职责,有利于分工协作和运维开发
  2. 可扩展性强(压力大时,如双11。可独立扩充资源),资源使用率更优
  3. 故障隔离,服务的整体可靠性更高

1.6 RPC带来的问题

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

这些问题都由RPC框架来处理

小结:

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

2. 分层设计

编解码层,协议层,网络通信层

2.1 分层设计 - 以Apache Thrift为例

image.png

2.2 编解码层

2.3 编解码层 - 生成代码

image.png

2.4 编解码层–数据格式

  • 语言特定的格式:许多编程语言都内建了将内存对象编码为字节序列的支持,例如Java有 java.io.Serializable(跟特定语言绑定了)
  • 文本格式:JSON、XML、CSV等文本格式,具有人类可读性(如json无法指定精度,没有模型约束,只能采用文档约束,json采用反射机制,性能较差)
  • 二进制编码:具备跨语言和高性能等优点,常见有Thrift的 BinaryProtocol,Protobuf 等

2.5 编解码层 - 二进制编码

TLV编码

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

eg.

struct Person {
    1: required string       userName,
    2: optional i64          favoriteNumber,
    3: optional list<string> interests
}

编码后字节流为

image.png

图片来自《Designing Data-intensive Application》

2.6 编解码层 - 选型

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

2.7 协议层

2.8 协议层 - 概念

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

eg. http协议 image.png

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

image.png

2.9 协议层

eg. github.com/apache/thri…

image.png

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

2.10 协议层 - 协议解析

image.png

2.11 网络通信层

2.12 网络通信层-Sockets API

image.png

image.png

图片来自www.javatpoint.com/socket-prog…

2.13 网络通信层-网络库

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

小结:

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

3. 关键指标

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

3.1 稳定性–保障策略

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

image.png

3.2 稳定性 - 请求成功率

负载均衡

image.png

重试

image.png

3.2 稳定性 - 长尾请求

备份请求(Backup Request)

image.png

图片来自www.infoq.cn/article/5fb…

3.4 稳定性一注册中间件(拦截器)

image.png

3.5易用性

  • 开箱即用:合理的默认参数选项、丰富的文档
  • 周边工具:生成代码工具、脚手架工具
    • 简单易用的命令行工具
    • 生成服务代码脚手架
    • 支持 protobuf和thrift
    • 内置功能丰富的选项
    • 支持自定义的生成代码插件

image.png

3.6 扩展性

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

image.png

3.7 观测性

  • Log、 Metric、Tracing
  • 内置观测性服务(如linux系统:top命令等)

image.png

3.8高性能

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

小结:

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

4. 企业实践

整体架构 自研网络库 扩展性设计 性能优化 合并部署

4.1 整体架构-Kitex

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

image.png

github.com/cloudwego/k…

4.2 自研网络库-背景

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

4.3 自研网络库- Netpoll

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

4.4 扩展性设计

image.png

4.5 性能优化–网络库优化

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

4.6 性能优化–编解码优化

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

4.7 合并部署

问题:微服务过微,传输和序列化开销越来越大

将亲和性强的服务实例尽可能调度到同一个物理机,远程RPC调用优化为本地IPC调用

image.png

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

image.png

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

小结:

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