RPC | 豆包MarsCode AI刷题

90 阅读6分钟

深入浅出 RPC 框架

01、基本概念

1.1、本地函数调用

func main() {
    a, b := 2, 3
    ret := calculate(a, b)
    fmt.Println(ret)
    return
}
func calculate(x, y int) int {
    z := x * y
    return z
}

本地调用流程:

  1. a, b的值压栈
  2. 通过函数指针找到 calculate函数,进入函数取出栈中的值2, 3,将其赋值给x, y
  3. 计算x*y,并将结果存在z
  4. z的值压栈,然后从calculate返回
  5. 从栈中取出z的值,并赋值给calculate

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

image-20241115105848189.png

RPC需要解决的问题:

  1. 函数映射:
    1. 我们怎么告诉支付服务我们要调用付款这个函数,而不是充值或其他函数呢?在本地调用中,函数是直接通过函数指针来指定的,我们调用哪个方法,编译器就自动帮我们调用它相应的函数指针。
    2. 在远程调用中,函数指针是不行的,因为两个服务不是一个进程,地址空间不同。所以函数都有一个自己的ID,在做RPC的时候要附上这个ID,以及ID和函数的对照关系表,通过ID找到对应的函数并执行。
  2. 数据转换成字节流
    1. 在本地调用中,我们将参数压到栈里面,让被调用函数自己去读,但是RPC中两个服务不是一个进程,不能通过内存来传递参数。这时候就需要客户端先把参数转换成一个字节流,然后传递给客户端,客户端再将其转换成自己能够读取的格式。
  3. 网络传输

1.3、RPC概念模型

image-20241115110410956.png

1.4、一次RPC的完整过程

  • IDL (Interface description language): IDL是通过一种中立的方式来==描述接口==,使得在不同平台上运行的对象和用不同语言编写的程序可以互相通信。
    • 相比本地调用,我们不知道远程调用都有哪些方法,有什么参数,所以需要有一种方式来描述或声明有我有哪些方法,参数是什么样子的,这个描述文件就是IDL文件。
  • 生成代码:通过编译工具把IDL文件转换成语言对应的静态库
  • 编解码:将字节流转换成自己能够读取的格式称为解码,反之是编码。
  • 通信协议:规范了数据在网络中传输的格式。
  • 网络传输:通常基于成熟的网络库走TCP/UDP协议。

1.5、RPC优点与缺点

优点

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

缺点

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

02、分层设计

以Apache Thrift为例

image-20241115112032263.png

2.1、编解码层

生成代码

image-20241115112454947.png

编码的数据格式:

  • 语言特定格式:如Java的java.io.Serializable
  • 文本格式:JSON、XML
  • 二进制编码:常见的有Binary ProtocalProtobuf

二进制编码

TLV编码为例:

  • T-Tag:标签,可以理解为类型。
  • L-Length:长度
  • V-Value:值,可以是单个值,也可以是TLV结构

image-20241115112828020.png

下图是上面一个二进制编码

image-20241115112837108.png

2.2、协议层

  • 以特殊字符结尾
message body\r\nmessage body\r\n
  • 变长协议:定长+不定长部分组成,定长部分需要描述不定长的内容长度
lengthmessage bodylengthmessage body

image-20241115113135101.png

  • Length:数据包大小,不包含本身
  • HEADER MAGIC: 标识版本信息
  • SEQUENCE NUMBER:标识数据包的seqID,可用于多路复用
  • HEADER SIZE:头部长度,从第14个字节开始计算,一直到PAYLOAD前
  • PROTOCOL ID:编解码方式,有Binary与Compact两种
  • TRANSFORM ID:压缩方式,如zlib和snappy
  • INFO ID:传递一些定制的meta信息
  • PAYLOAD:消息体

协议解析:

image-20241115113530720.png

2.3、网络通信层 Sockets API

image-20241115113612422.png

socket函数创建一个套接字,bind 将一个套接字绑定到一个地址上。listen 监听进来的连接,backlog的含义有点复杂,这里先简单的描述:指定挂起的连接队列的长度,当客户端连接的时候,服务器可能正在处理其他逻辑而未调用accept接受连接,此时会导致这个连接被挂起,内核维护挂起的连接队列,backlog则指定这个队列的长度,accept函数从队列中取出连接请求并接收它,然后这个连接就从挂起队列移除。如果队列未满,客户端调用connect马上成功,如果满了可能会阻寒等待队列未满(实际上在Linux中测试并不是这样的结果,这个后面再专门来研究)。Linux的backLog默认是128。

03、关键指标

稳定性:

  • 熔断:保护调用方;

    • 服务A调用服务B,服务B调用服务C,如果服务器C响应超时,由于B依赖于服务C,导致B一致等待,这个时候服务A频繁的调用服务B,服务B就可能因为堆积大量的请求而导致服务宕机。
  • 限流:保护被调用方

    • 当调用端请求发送过来的时候,服务端在执行业务逻辑之前先检查限流逻辑,如果发现访问量过大并且超出限流条件,就让服务端直接降级,或返回服务端一个限流异常。
  • 超时:当下游服务因为某种原因响应过慢,下游服务主动停掉一些不太重要的业务,释放出服务器资源,避免浪费资源。

  • 负载均衡:

  • 重试:www.infoq.cn/article/5fb…

  • 长尾请求:一般是指明显高于均值的那部分占比较小的请求,业界关于延迟有一个常用的P99标准,P99单个请求响应耗时从小到大排列,顺序处于99%位置的值就是P99值,后面的1%就是长尾请求。

正常的长尾请求:

image-20241115153148816.png 我们假设一个阈值t3t3 ,建议是RPC请求延时的P99,当Req1发出超过t3时间后没有返回,我们直接发起重试请求Req2,这样相当于同时有两个请求运行。然后等待请求返回,只要Req1或Req2任意一个返回成功的结果,就可以立即结束这次请求,如下:这样整体耗时就是t4

image-20241115153355629.png