RPC框架 | 青训营

102 阅读7分钟

基本概念

  1. 本地函数调用(举例说明)

image.png

  1. 远程函数调用(RPC——Remote Procedure Calls)
    1. 比如一个远程支付操作

image.png

  1. RPC需要解决的问题(和本地调用的区别)
    1. 函数映射
    2. 数据转换成字节流
    3. 网络传输
  2. RPC概念模型

image.png

  1. 一次RPC完整流程

image.png
相关概念

  1. IDL文件——IDL通过一种中立的方式来描述接口,使得在不同平台上运行的对象和用不同语言编写的程序可以相互通信
  2. 生成代码——通过编译器工具把IDL文件转换成语言对应的静态库
  3. 编解码——从内存中表示到字节序列的转换成为编码,反之为解码,也常叫做序列化和反序列化
  4. 通信协议——规范了数据在网络中的传输内容和格式。除必须的请求/响应数据外,通常还会包含额外的元数据
  5. 网络传输——通常基于成熟的网络库走TCP/UDP传输
  6. RPC的好处
    1. 单一职责,有利于分工协作和运维开发
    2. 可扩展性强,资源使用率更优
    3. 故障隔离,服务的整体可靠性更高
  7. RPC带来的问题——>RPC框架
    1. 服务宕机,如何处理
    2. 调用过程中发生网络异常,如何保证消息的可达性
    3. 请求量突增导致服务无法及时处理,如何应对

分层设计(RPC框架)

分层模型——以Apache Thrift为例
一个RPC通信需要经过:生成代码、编解码层、协议层、网络传输层
其中属于框架的部分有三层:编解码层、协议层、网络传输层
image.png
编解码层

  1. 生成代码(编解码的前提)

image.png

  1. 数据格式(包括以下三方面)
    • 语言特定格式:许多编程语言都内建了将内存对象编码为字节序列的支持,例如Java有java.io.Serializable
    • 文本格式:JSON、XML、CSV等文本格式,对人来说具有可读性
    • 二进制编码:具备跨语言和高性能等优点,常见有Thrift的BinaryProtocol,Protobuf等(下面将举例展开讲解其实现原理和选型要点)
  2. 二进制编码(以TLV编码为例)
    1. Tag:标签/类型
    2. Length:长度
    3. Value:值(可以是TLV结构)
  3. 二进制编码选型(考虑以下几个方面)
    • 兼容性(支持自动增加新的字段,而不影响老的服务,将提高系统灵活度)
    • 通用性(支持跨平台、跨语言)
    • 性能(从时间和空间两个维度来考虑,也就是编码后数据大小和编码耗费时长)
  4. 协议层
    1. 部分结构
      • 特殊结束符(\r\n)——一个特殊字符作为每个协议单元结束的标示

image.png

  - 变长协议——以定长+不定长的部分组成,其中定长的部分需要描述不定长部分的内容长度

image.png

  1. 协议构造

屏幕截图 2023-08-10 163659.png

  1. 协议解析

image.png
流程:

  1. 网络通信层
    1. Sockets API
      1. Sockets API 连接应用层和传输层

image.png

  2. 一次连接所需的Sockets API大致如下![image.png](https://cdn.nlark.com/yuque/0/2023/png/38431927/1691657845427-a044d218-d560-4db4-956a-b8c3c65fa5b1.png#averageHue=%23fafbc9&clientId=ud85e8a35-6a9f-4&from=paste&height=479&id=u91b5f33a&originHeight=599&originWidth=767&originalType=binary&ratio=1.25&rotation=0&showTitle=false&size=270635&status=done&style=none&taskId=u552163bd-40e1-4dc5-8de1-83c605c0806&title=&width=613.6)

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

关键指标

稳定性

  1. 保障策略
    1. 熔断:保护调用方,防止被调用的服务出现问题而影响整个链路
    2. 限流:保护被调用方,防止大流量把服务压垮
    3. 超时控制:避免浪费资源在不可用节点上image.png
  2. 请求成功率
    1. 负载均衡(提高成功率)

image.png

  1. 重试(请求失败重试,因为可能服务处理较慢并非异常)

image.png

  1. 长尾请求(设定PCT99——99%的情况下一次请求响应的时间)
    1. 正常情况下 b. 存在长尾请求的情况下(保证稳定性,t3=PCT99)

image.png

  1. 注册中间件(将以上提到的保证稳定性的策略都集合到中间件里,并注册中间件)

image.png

易用性

  1. 开箱即用
    1. 框架有较为丰富的文档,明了易懂
    2. 默认参数选项合理,如下所示,提供了合理的功能

image.png

  1. 周边工具
    1. 生成代码的工具:生成代码插件、编解码等
    2. 脚手架工具

扩展性

  1. 中间件:功能可扩展
  2. Option:参数可扩展(实际上对应的也是功能)
  3. 编解码层:支持更多类型编解码
  4. 协议层:支持更多协议
  5. 网络传输层:对接更多网络库
  6. 代码生成工具插件

观测性

  1. Log(日志记录)、Metric(监视)、Tracing(链路跟踪)
  2. 内置观测性服务

image.png

高性能

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

企业实践

整体架构——kitex

代码生成工具 核心组件 与公司内部基础设施集成
image.png

自研网络库

  1. 背景
    1. 原生库无法感知连接状态(在使用连接池时,池中存在失效连接,影响连接池的复用)
    2. 原生库存在goroutine暴涨的风险(一个连接一个goroutine,连接利用率低,大量goroutine占用调度开销,影响性能)
  2. Netpoll

实现的功能

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

扩展性设计

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

性能优化

  1. 网络库优化
    1. 调度优化
      1. epoll_wait在调度上的控制
      2. gopool 重用goroutine降低同时运行协程数
    2. LinkBuffer
      1. 读写并行无锁,支持nocopy地流式读写
      2. 高效扩缩容
      3. Nocopy Buffer池化,减少GC
    3. Pool
      1. 引入内存池和对象池,减少GC开销
  2. 编解码优化
    1. Codegen
      1. 预计算并预分配内存,减少内存操作次数,包括内存分配和拷贝
      2. Inline减少函数调用次数和避免不必要的反射操作等
      3. 自研的Go语言实现的Thrift IDL解析和代码生成器,支持完善的Thruft IDL语法和语义检查,并支持了插件机制——Thriftgo
    2. JIT
      1. 使用JIT编译技术改善用户体验的同时带来更强的编解码性能,减轻用户维护生成代码的负担
      2. 基于JIT编译技术的高性能动态Thrift编解码器——Frugal

合并部署

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

  1. 合并部署内部设定
    1. 中心化的部署调度和流量控制
    2. 基于共享内存的通信协议
    3. 定制化的服务发现和连接池实现
    4. 定制化的服务启动和监听逻辑
  2. 落地效果

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