RPC框架|青训营笔记

72 阅读6分钟

RPC框架|青训营笔记

这是我参与【第五届青训营】伴学笔记创作活动的第13天。

一、重点内容

  • RPC框架分层设计
  • RPC关键指标分析
  • 企业实践

二、详细知识点

1. 基本概念

1.1 本地函数调用

  • 将a、b值压栈
  • 通过函数指针找到calculate函数,进入函数取出值2,3赋予x,y
  • 计算x*y,存储至z
  • 将z值压栈,从calculate返回
  • 从栈中取出z返回值,赋给result Screen Shot 2023-02-09 at 9.32.38 PM.png

1.2 远程函数调用(Remote Procedure Calls)

  • 需要解决的问题
    • 函数映射
    • 数据转换成字节流
    • 网络传输

1.3 RPC概念模型

Screen Shot 2023-02-09 at 9.35.39 PM.png

  • 由5个模型组成
    • User
    • User-Stub
    • RPC-runtime
    • Server-Stub
    • Server

1.4 一次RPC的完整过程

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

1.5 RPC优势

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

1.6 RPC带来的问题

  • 问题
    • 服务宕机如何处理?
    • 调用过程中发生网络异常如何保证消息可达性?
    • 请求量突增导致服务无法及时处理,如何应对?
  • 解决:RPC框架

2. 分层设计

2.1 分层设计

Screen Shot 2023-02-09 at 9.44.02 PM.png

2.2 编解码层

  • 生成代码
    • 客户端和服务端依赖同一份IDL文件
    • 生成不同语言的CodeGen
  • 数据格式
    • 语言特定格式:编程语言内建内存对象编码为字节序列的支持
      • java.io.Serializable
      • 语言特定,其他无法读取
    • 文本格式:JSON、XML、CSV 人类可读性
      • 约束较为宽松
    • 二进制编码
      • 跨语言、高性能
      • Thrift binaryProtocol、Protobuf等
  • 二进制编码
    • TLV编码:Tag-Length-Value
      • 标签:类型
      • Length:长度
      • Value:值(也可以是TLV结构)
  • 编解码选型
    • 兼容性:需要支持自动增加新字段而不影响老服务,增加灵活度
    • 通用性:支持跨平台、跨语言
    • 性能:空间、时间,编码后数据大小、编码耗费时长

2.7 协议层

  • 概念
    • 特殊结束符:一个特殊字符作为每个协议单元结束 CRLF
    • 变长协议:以定长加不定长的部分组成,定长部分描述不定长内容长度
  • 协议构造
    • LENGTH:数据包大小,不包含自身
    • HEADER MAGIC:标识版本信息,协议解析时快速校验
    • SEQUENCE NUMBER:表示数据包seqID,多路复用,单连接内递增
    • HEADER SIZE:头部长度,从第14字节开始计算一直到PAYLOAD前
    • PROTOCOL ID:压缩方式,如zlib和snappy
    • INFO ID:传递一些定制meta信息
    • PAYLOAD:消息体
  • 协议解析
    • 从内存中读取MagicNumber
    • 读取编解码方式PayloadCodec
    • 解码Payload

2.8 网络通信层

  • Sockets API
  • 网络库
    • 提供易用API
      • 封装底层SocketAPI
      • 连接管理和时间分法
    • 功能
      • 协议支持:tcp/udp,uds
      • 优雅退出,异常处理等
    • 性能
      • 应用层buffer减少copu
      • 高性能定时器、对象池

3. 关键指标

3.1 稳定性

  • 保障策略
    • 熔断:保护调用方,防止被调用的服务出现问题影响整个链路
    • 限流:保护被调用方,防止大流量把服务压垮
    • 超时控制:避免浪费资源在不可用节点上
  • 请求成功率:通过负载均衡+重试提高成功率
  • 长尾请求:使用backup request,在发送一个请求后经过t再次发送一个请求(根据经验应该在t内收到回复),可以减小请求延时
  • 注册中间件

3.2 易用性

  • 开箱即用
    • 合理的默认参数选项
    • 丰富的文档
  • 周边工具
    • 生成代码工具
    • 脚手架工具
    • 生成服务代码脚手架,支持protobuf和thrift,支持功能丰富,支持自定义生成代码插件

3.3 扩展性

  • 尽可能多的扩展点:Middleware、Option、编解码层、协议层、网络传输层、代码生成工具插件扩展

3.4 观测性

  • log Metric Tracing
  • 内置观测性服务
  • 分析上述内容获得问题

3.5 高性能

  • 场景
    • 单机多机
    • 单连接多连接
    • 单/多client,单、多server
    • 不同大小请求包
    • 不同请求类型:pingpong、streaming
  • 目标
    • 高吞吐
    • 低延迟
  • 手段
    • 连接池
    • 多路复用
    • 高性能编解码协议
    • 高性能网络库

三、实践例——字节内部实践

1. 整体架构

1.1 Kitex

  • core:核心组件
  • byted:与内部基础设施集成
  • tool:代码生成工具 Screen Shot 2023-02-09 at 10.09.49 PM.png

2. 自研网络库

  • 问题
    • 原生库无法感知连接状态:使用连接池时池中存在失效连接,影响连接池复用
    • 原生库存在Goroutine暴涨风险:一个连接一个Goroutine,连接利用率低下,大量goroutine占用调度开销影响性能
  • Netpoll
    • 引入epoll主动监听机制感知连接状态
    • 建立goroutine池,复用goroutine
    • 提升性能,引入nocopy buffer,向上提供NoCopy调用接口,实现编解码层面零拷贝

3.扩展性设计

  • 支持多协议
  • 支持自定义协议扩展

4. 性能设计优化

  • 网络库优化
    • 调度优化:epoll_wait调度上控制,gopool复用goroutine
    • LinkBuffer:读写并行无锁,支持nocopy流式读写;高效扩缩容,Nocopy buffer池化,减少GC
    • Pool:引入内存、对象池,减少GC开销
  • 编解码优化
    • codegen
      • 预计算预分配内存减少内存操作次数
      • Inline减少函数调用次数和不必要反射操作
      • 自研go语言实现Thrift IDL解析和代码生成器,支持完善的Thrift IDL语法和语义检查,支持插件机制 Thriftgo
    • JIT
      • 使用JIT编译技术改善用户体验同时带来强大编解码性能,减轻用户维护生成代码的负担
      • 基于JIT编译技术的高性能动态Thrift编解码器-Frugal

5. 合并部署

  • 问题:微服务过微导致传输和序列化开销越来越大
  • 解决方法:
    • 将亲和性强的服务实例尽可能调度一个物理机
    • 远程RPC调用优化为本地IPC
  • 框架进行改造
    • 中心化的部署调度和流量控制
    • 基于共享内存的通信协议
    • 定制化服务发现和连接池实现
    • 定制化的服务启动和监听逻辑

四、个人总结

本节课主要介绍了RPC的基础定义、框架的分层设计,核心三层(编解码、协议和网络传输层),以及RPC框架的核心指标。在今后的学习中还需要结合Kitex的代码实例进行理解。