[02实践应用 - RPC框架] 14 - RPC原理与实现 | 青训营笔记

35 阅读6分钟

这是我参与「第五届青训营」伴学笔记创作活动的第14天。今天的内容是关于远程函数调用 RPC 的。

1 基本概念

本地函数调用流程:将变量值压栈 -> 找到子函数并传参 -> 执行子函数并存储返回值 -> 将返回值压栈并传回主函数 -> 从栈中取出返回值并赋予对应变量

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

需要解决的问题:函数映射、数据转化成字符流、网络传输

1.2 RPC 的远程调用模型

模型构成:User, User-Stub, RPC-Runtime, Server-Stub, Server

在 caller machine (调用端)中包含 user、user-stub 和 RPC-Runtime 模块,在 callee machine (背调用端)中包含 RPC-Runtime、server-stub 和 server 模块,调用端和被调用端之间由网络连接。数据从 caller machine 中的 user 在本地调用 user-stub,将参数打包传给 RPC-Runtime,再通过网络传给 callee machine 的 RPC-Runtime。callee machine 端的 RPC-Runtime 将数据表传给 server-stub 解压,再去调用真正的业务逻辑 server。server 处理好数据后同样采用打包、通过网络传输、解压将结果传回。

1.3 一次 RPC 的完整过程

基本概念

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

本质上,从 caller 向 callee 传输数据的过程中数据经历了编码-网络协议打包-网络传输-解包-解码的过程。

1.4 RPC 的优点

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

1.5 RPC 的问题

  • 服务宕机对方应如何处理
  • 调用过程中网络异常
  • 请求量突增导致服务无法及时处理

2 分层设计

image.png

2.1 编解码层

对应 Apache 的 service client/processer + read/write + TProtocol 三层,其中前两层用于生成代码。通过 IDL 文件控制生成的代码。

IDL 文件的数据格式

常见的文件格式有语言特定的格式(简洁但依赖语言)、文本格式(人类可读但描述不严谨)和二进制编码(跨语言、高性能)。

在 Thrift 中常用 BinaryProtocol、Protobuf 等。以 TLV 编码为例,TLV 编码包含 tag、length和value(可以仍是TLV结果,即可以嵌套)。

选型考量

在选择编码时,主要考虑兼容性(便于维护)、通用性(技术上跨语言跨平台、使用量、开发者学习成本)和性能(时空开销)。

2.2 协议层

对应 Apache 的 TTransport。

常见协议格式

  • 特殊结束符:以一个特殊字符作为一个协议单元的结束(如 HTTP 以 "\r\n" 结尾)
  • 变长协议:以定长加不定长的部分组成,其中定长部分需要描述不定长的长度

以 Apache 的 THeader 为例:

image.png

  • LENGTH:数据包大小
  • HEADER MAGIC:版本标志信息
  • SEQUENCE NUMBER:数据包的 seqID,可以用于多路复用,单链接内递增
  • HEADER SIZE:头部长度,从第14字节开始计算到 PAYLOAD 之前
  • PROTOCOL ID:编解码方式,有 binary 和 compact 两种
  • TRANSFORM ID:压缩方式,如zlib、snappy
  • INFO ID:传递一些订制的 meta 信息
  • PAYLOAD:消息体

协议解析

image.png

2.3 网络通信层

对应 Apache 的 Network IO。

Sockets API

介于应用层和传输层之间。

image.png

在实际应用中多用网络库。对上层提供易用 API (封装底层 socket API,连接管理和事件分发)、支持常见协议(tcp、udp、uds 等)和退出、常见异常处理等、提高性能(应用层 buffer 减少 copy,使用高性能定时器,对象池等)

3 关键指标

3.1 稳定性

保障策略

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

提高请求成功率的方法负载均衡、重试。

使用 backup request 备份请求来解决长尾请求(明显高于平均请求时间的请求)。

在实践中通过注册中间件实现以上的保障。

3.2 易用性

  • 开箱即用:合理的默认参数选项,无需过多配置,提供丰富的文档
  • 周边工具:提供生成代码工具、脚手架工具

3.3 扩展性

  • Middleware:middleware 会被构造成一个有序调用链逐个执行,比如服务发现、路由、负载均衡、超时控制等
  • Option:作为初始化参数
  • 核心层是支持扩展的:编解码、协议、网络传输层
  • 代码生成工具也支持插件扩展

image.png

3.4 观测性

通过 Log、Metric 和 Tracing 进行观察,还可以提供一些内置观测服务。

3.5 高性能

高性能目标可以分为高吞吐、低延时。常用的解决方案有:

  • 连接池和多路复用:复用连接,减少频繁建联带来的开销
  • 高性能编解码协议:Thrift、Protobuf、Flatbuffer 和 Cap'n Proto 等
  • 高性能网络库:Netpoll 和 Netty 等

4 字节跳动的企业实践

4.1 整体架构 Kitex

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

4.2 自研网络库

研发原因:原生库无法感知连接状态、原生库存在 goroutine 暴增的风险。

解决方案 Netpool:引入 epool 主动监听,感知连接状态;建立 goroutine 池增强复用;引入 NoCopy Buffer,向上提供 NoCopy 接口,实现编解码层面零拷贝(内存层面)。

4.3 扩展性设计

支持多协议:

  • Interation: Ping-Pong, Streaming, Oneway
  • Codec: Thrift, Protobuf
  • Applicationg Layer Protocol: TTHeader, HTTP2
  • Transport Layer: TCP, UDP, RDMA

4.4 性能优化

网络优化:调度优化(epool wait 在调度上控制,gopool 增加 goroutine 的复用)、LinkBuffer (读写并行无锁,支持 nocopy 的流式读写;高效扩缩容;NoCopy Buffer 池化,减少 GC)、Pool(引入内存池和对象池,减少 GC)

编解码优化:CodeGen(预计算预分配内存,减少内存分配拷贝操作带来的性能开销;Inline 减少函数调用次数,避免不必要的反射操作;Thriftgo 完善了 Thrift IDL 解析和代码生成)、JIT(Just in time 即时编译。减轻用户维护生成代码的负担 Frugal)

4.5 合并部署

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

解决方案:将强依赖的服务统计部署,有效减少资源消耗。