后端 与 RPC | 青训营笔记

103 阅读6分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 13 天,本次学习了RPC框架,对基本的原理有了一定的掌握,同时还学习了字节的开源RPC框架Kitex,下面是我的收获

RPC框架

1.1 基本概念

1.1.1 本地函数调用

本地函数调用通俗的来说就是程序内部调用,对于参数的和返回值少的情况会直接将其存放在寄存器,而不需要压栈弹栈的过程,甚至可以直接做lnline操作不需要调用

1.1.2 远程函数调用

对于远程函数调用,因为两个进程的地址空间是完全不一样的,所以函数需要自己的ID,在RPC调用的时候附上自己的ID,然后通过映射表找到对应的函数并执行

RPC需要解决的问题

  • 函数映射
  • 数据转换成字节流
  • 网络传输

1.1.3 RPC概念模型

2023-02-23-22-08-24-image.png

1.1.4 一次RPC的完整过程

  • IDL (Interface description language)文件

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

  • 生成代码 通过编译器工具把,IDL文件转换成语言对应的静态库
  • 编解码 从内存中表示到字节序列的转换称为编码,反之为解码,也常叫做序列化和反序列化
  • 通信协议 规范了数据在网络中的传输内容和格式。除必须的请求/响应数据外,通常还会包含额外的元数据
  • 网络传输 通常基于成熟的网络库走TCP/UDP传输

1.1.5 RPC的好处

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

1.1.6 RPC带来的问题

  • 服务宕机
  • 在调用过程中发生网络异常
  • 请求量突增导致服务无法及时处理

1.2 分层设计

1.2.1 Apache Thrift

image.png

1.2.2 编解码层

生成代码

Client和Server依赖同一份IDL文件来生成不同语言的CodeGen

数据格式

  • 语言特定的格式

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

  • 文本格式

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

  • 二进制编码

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

二进制编码

  • TLV编码

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

选型

  • 兼容性

    支持自动增加新的字段,而不影响老的服务,这将提高系统的灵活度

  • 通用性

    支持跨平台、跨语言

  • 性能

    从空间和时间两个维度来考虑,也就是编码后数据大小和编码耗费时长

1.2.3 协议层

概念

  • 特殊结束符

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

  • 变长协议

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

协议构造

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

1.2.4 网络通信层

Sockets API

首先socket函数创建一个套接字,bind将一个套接字绑定到一个地址上,然后listen监听进来的连接,当客户端连接的时候,服务器因为困难在处理其他逻辑而未调用accept接受连接,此时会导致这个连接被挂起,内核维护挂起的连接队列,backlog则指定这个队列的长度,accept函数从队列中取出连接请求并接收它,然后该连接就从挂起队列移除;如果队列未满。客户端调用connect马上成功,如果满了可能会阻塞等待 队列未满

网络库

  • 提供易用API

    封装底层Socket API连接管理和事件分发

  • 功能

    协议支持: tcp、udp 和uds等优雅退出、异常处理等

  • 性能

    应用层 buffer 减少copy高性能定时器、对象池等

1.3 关键指标

1.3.1 稳定性

保障策略

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

请求成功率

分为负载均衡和重试,而重试会有放大故障的风险

长尾请求

长尾请求是指,请求响应时间明显高于均值的那部分占比较小的请求

注册中间件

中间件即提供超时、熔断、重试、限流、负载均衡、BackupRequest功能的部件

1.3.2 易用性

  • 开箱即用

    合理的默认参数选项、丰富的文档

  • 周边工具

    生成代码工具、脚手架工具

1.3.3 拓展性

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

1.3.4 观测性

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

1.3.5 高性能

目标

  • 高吞吐
  • 低延迟

手段

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

场景

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

1.4 企业实践

1.4.1 整体架构 - Kitex

2023-02-23-22-50-17-image.png

1.4.2 自研网络库

背景

  • 原生库无法感知连接状态

    在使用连接池时,池中存在失效连接,影响连接池的复用。

  • 原生库存在goroutine暴涨的风险 一个连接一个goroutine 的模式,由于连接利用率低下,存在大量goroutine占用调度开销,影响性能。

Netpoll

  • 解决无法感知连接状态问题

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

  • 解决goroutine暴涨的风险建立goroutine 池,复用goroutine

  • 提升性能

    引入 Nocopy Buffer,向上层提供NoCopy的调用接口,编解码层面零拷贝

1.4.3 性能优化

网络库优化

  • 调度优化

    epoll_wait在调度上的控制

    gopool重用goroutine降低同时运行协程数

  • LinkBuffer

    读写并行无锁,支持nocopy地流式读写

    高效扩缩容 Nocopy Buffer 池化,减少GC

  • Pool

    引入内存池和对象池,减少GC开销

编解码优化

  • Codegen

    预计算并预分配内存,减少内存操作次数,包括内存分配和拷贝

    lnline减少函数调用次数和避免不必要的反射操作等

    自研了Go语言实现的Thrift IDL解析和代码生成器,支持完善的Thrift IDL语法和语义检查,并支持了插件机制- Thriftgo

  • JIT

    使用JIT编译技术改善用户体验的同时带来更强的编解码性能,减轻用户维护生成代码的负担

    基于JIT编译技术的高性能动态 Thrift 编解码器– Frugal

1.4.4 合并部署

微服务过微将导致传输和序列化开销越来越大

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