Sorry, your browser cannot access this site
This page requires browser support (enable) JavaScript
Learn more >

GRPC 简介

gRPC:gRPC是Google公布的开源项目,基于HTTP2.0协议,并支持常见的众多编程语言。HTTP 2.0协议是基于二进制的HTTP协议的升级版本,gRPC底层使用了Netty框架。

使用gRPC, 可以一次性的在一个.proto文件中定义服务并使用任何支持它的语言去实现客户端和服务端,它们可以应用在各种场景中, gRPC解决了不同语言及环境间通信的复杂性。使用protocol buffers提供高效的序列化、简单的IDL以及容易进行接口更新。总之一句话,使用gRPC可以更容易的编写跨语言的分布式代码。

rpc工作原理

RPC(Remote Procedure Call,远程过程调用)是一种分布式计算模型,它允许程序在不同的计算机之间通过网络进行通信和交互,实现像本地调用一样的程序调用和数据传输。RPC通常用于构建分布式系统和微服务架构。

RPC的工作原理可以简单地分为以下几个步骤:

  1. 客户端调用远程服务:客户端程序调用本地接口,接口封装了需要执行的操作和参数,然后将请求通过网络发送到远程服务端。
  2. 服务端接收请求:远程服务端接收请求,解析请求的内容,根据请求的操作调用相应的方法,执行操作。
  3. 服务端返回结果:服务端将执行结果返回给客户端,客户端接收到结果后继续执行。
  4. 客户端接收结果:客户端接收到服务端返回的结果,进行处理并返回给调用者。

在这个过程中,RPC通常需要解决以下问题:

  1. 远程服务的定位:客户端需要知道远程服务的位置和地址,才能将请求发送到正确的服务端。解决这个问题可以使用命名服务(如DNS)、注册中心等技术。
  2. 数据传输和序列化:客户端和服务端之间需要进行数据传输和序列化。RPC框架通常使用网络协议(如HTTP、TCP等)进行数据传输,同时需要将数据序列化成二进制流或其他格式(Protobuf, json)。
  3. 异常处理和容错机制:由于网络和远程服务的不确定性,RPC框架需要提供异常处理和容错机制,例如超时重试、故障转移等技术,保证RPC调用的可靠性和稳定性。
  • rpc 调用基于 sdk 方式,调用方法和出入参协议固定,stub 文件本身还能起到接口文档的作用,很大程度上优化了通信双方约定协议达成共识的成本。
  • rpc 在传输层协议 tcp 基础之上,可以由实现框架自定义填充应用层协议细节,理论上存在着更高的上限。

gRPC优势

RPC(远程过程调用)之所以被称为 “远程”,是因为在微服务架构下,当服务部署到不同的服务器时,它可以实现远程服务之间的通信。从用户的角度来看,它就像一个本地函数调用。

  1. 更高的性能:gRPC使用二进制协议,可以比HTTP传输更快地传输数据,可以提高系统的响应速度和吞吐量,特别是在大数据传输和高并发的场景下更为明显。

  2. 更小的带宽占用:gRPC使用Protocol Buffers作为数据序列化协议,与XML和JSON相比,它可以将数据更紧凑地编码,从而减少网络带宽的占用,降低系统的网络开销。

  3. gRPC 基于 HTTP/2 标准设计,要比传统的http1快

  4. 更严格的接口定义:gRPC使用IDL(Interface Definition Language)定义接口,可以清晰地定义每个服务的接口、参数和返回值,并生成相应的代码,降低开发者的开发难度和出错率,同时也增加了接口的可读性和可维护性。

  5. 更多的语言支持:gRPC支持多种编程语言,包括Java、C++、Python、Go等,可以让开发者根据自己的喜好和技能选择最适合的编程语言进行开发,提高了开发效率和灵活性。

  6. 更好的扩展性和互操作性:gRPC支持负载均衡、流控制和错误处理等功能,可以让开发者轻松地构建高可用的分布式系统,同时也可以与其他系统集成,实现跨平台、跨语言的服务调用。

gRPC工作流程

gRPC流程分为四个步骤:定义服务生成源代码实现服务启动服务。首先,需要定义要实现的服务及其接口,使用Protocol Buffers编写接口定义文件。其次,使用编译器生成客户端和服务器端的源代码。然后,实现生成的接口。最后,启动服务器并将其部署在适当的位置。

步骤 1:从客户端发出 REST 调用。请求体通常为 JSON 格式。
步骤 2 - 4:订单服务(gRPC 客户端)接收 REST 调用,对其进行转换,然后向支付服务发出 RPC 调用。
步骤 5:gRPC 通过 HTTP2 在网络上发送数据包。由于采用了二进制编码和网络优化,gRPC 据说比 JSON 快 5 倍。
步骤 6 - 8:支付服务(gRPC 服务器)接收来自网络的数据包,解码后调用服务器应用程序。
步骤 9 - 11:结果从服务器应用程序返回,经过编码后发送到传输层。
步骤 12 - 14:订单服务接收数据包、解码并将结果发送给客户端应用程序。

http对比rpc

  • 纯裸 TCP 是能收发数据,但它是个无边界的数据流,上层需要定义消息格式用于定义消息边界。于是就有了各种协议,HTTP 和各类 RPC 协议就是在 TCP 之上定义的应用层协议。
  • RPC 本质上不算是协议,而是一种调用方式,而像 gRPC 和 Thrift 这样的具体实现,才是协议,它们是实现了 RPC 调用的协议。目的是希望程序员能像调用本地方法那样去调用远端的服务方法。同时 RPC 有很多种实现方式,不一定非得基于 TCP 协议
  • 从发展历史来说,HTTP 主要用于 B/S 架构,而 RPC 更多用于 C/S 架构。但现在其实已经没分那么清了,B/S 和 C/S 在慢慢融合。很多软件同时支持多端,所以对外一般用 HTTP 协议,而内部集群的微服务之间则采用 RPC 协议进行通讯。
  • RPC 其实比 HTTP 出现的要早,且比目前主流的 HTTP/1.1 性能要更好,所以大部分公司内部都还在使用 RPC。
  • HTTP/2.0HTTP/1.1 的基础上做了优化,性能可能比很多 RPC 协议都要好,但由于是这几年才出来的,所以也不太可能取代掉 RPC。

检测 RPC 调用

  1. 健康检查和监控: 定期探活
    在RPC调用的架构中,可以实现健康检查和监控机制,定期检测RPC服务的可用性和网络连接状态。通过监控系统的指标和警报,可以及时发现并解决网络问题。
  2. 日志记录和错误处理:
    在RPC调用的代码中,可以添加日志记录功能,以捕获网络相关的错误和异常。通过检查日志,可以查看是否有网络连接失败、超时或其他网络相关的错误信息。
  3. 监控网络延迟、丢包、带宽。

grpc通信方式

普通rpc:这就是一般的rpc调用,一个请求对象对应一个返回对象,==传统的 即刻响应的 ==
服务端流式rpc:一个请求对象,服务端可以传回多个结果对象,==入参为流==
客户端流式rpc:客户端传入多个请求对象,服务端返回一个响应结果,==出参为流==
双向流式rpc:结合客户端流式rpc和服务端流式rpc,可以传入多个对象,返回多个响应对象,==出入参均为流==

grpc通过使用流式(关键字stream)的方式,返回/接受多个实例可以用于类似不定长数组的入参和出参

protocol buffers

Protobuf是由Google开发的二进制格式,用于在不同服务之间序列化数据。是一种IDL(interface description language)语言。

  • Protocol Buffers (protobuf) 是 除了 jsonxml 之外的另一种 数据传输方式
  • 一条 数据,用 protobuf 序列化后的大小是 json10分之一, 性能却是它的 5~100倍
  • 支持多种语言
  • 缺点: 由于是 二进制格式 存储的,所以 可读性较差
  1. 体积小-无需分隔符 存储方式tag-value不需要分隔符
  2. 空白字段可以省略,若字段没有被设置字段值,那么该字段序列化时的数据是完全不存在的,即不需要进行编码,而json会传key和空值的value
  3. tag用二进制表示,tag是用字段的数字值然后转换成二进制进行表示的,比json的key用字符串表示更加省空间
  4. 编码快,tag的里面存储了字段的类型,可以直接知道value的长度,或者当value是字符串的时候,则用length存储了长度,可以直接从length后取n个字节就是value的值,而如果不知道value的长度,我们就必须要做字符串匹配。主要使用的两种编码方式就是varintZigZag

底层

Protobuf 是一系列键值对。消息的二进制版本只使用字段的标签作为,每个字段的名称和声明类型只能在解码结束时通过引用消息类型的定义来确定。

  • varint和ZigZag的编码方式
  • 隔断冗余信息的剔除
  • tag-value方式的存储,tag采用二进制进行存储

Protobuf协议实现原理 | 学习笔记 (haohtml.com)

和json编码比较而言,protobuf首先就去除了{}””,等标点符号,其次完全没有字段名,而是仅保留了字段的编号。因此protobuf的编码结果会比json的编码结果更小

原始的json数据:

1
{"name":"personJson","id":15,"email":"personJson@google.com"}

按照之前定义的.proto文件,name编号为1,id编号为2,email编号为3,编码后的结果若翻译成可读的文字来说如下

1
1personJson2153personJson@google.com

即1号字段的值是personJson,2号字段的值是15,3号字段的值是personJson@google.com

数字编码Varint、数据长度

在网络上传递编码成字节的数据时,由于网络传输半包、粘包等等各种因素的存在,如何确定整个数据的长度就是最首要的任务。因为数据大小的不确定性,无法约定固定的字节表示数据长度。例如有时候数据量小于255个字节,那么一个字节就能表示长度,若超过了65535,那么就需要3个字节才能表示数据长度了。

为了解决这个问题,采用了varint编码方式。它约定,每个表示数字字节的最高位若为1,则说明该数字还需要读取下一个字节。若字节的最高位位0,则表示数字的字节读取完毕。而剩下的7位则记录具体的数字。

Varint 是一种紧凑的表示数字的方法。它用一个或多个字节来表示一个数字,值越小的数字使用越少的字节数。这能减少用来表示数字的字节数。

比如对于 int32 类型的数字,一般需要 4 个 byte 来表示。但是采用 Varint,对于很小的 int32 类型的数字,则可以用 1 个 byte 来表示。当然凡事都有好的也有不好的一面,采用 Varint 表示法,大的数字则需要 5 个 byte 来表示。从统计的角度来说,一般不会所有的消息中的数字都是大数,因此大多数情况下,采用 Varint 后,可以用更少的字节数来表示数字信息

Varint 中的每个 byte 的最高位 bit 有特殊的含义,如果该位为 1,表示后续的 byte 也是该数字的一部分;如果该位为 0,则结束。其他的 7 个 bit 都用来表示数字。因此小于 128 的数字都可以用一个 byte 表示。大于 128 的数字,比如 300,会用两个字节来表示:

1010 1100 0000 0010

另外如果从数据大小角度来看,这种表示方式比真正要代表的数据多了一个bit, 所以其实际传输大小就多14%(1/7 = 0.142857143),对于这一点我们需要有所了解。

数据编号Message Buffer

消息经过序列化后会成为一个二进制数据流,该流中的数据为一系列的 Key-Value 对。如下图所示:

采用这种 Key-Pair 结构无需使用分隔符来分割不同的 Field。对于可选的 Field,如果消息中不存在该 field,那么在最终的 Message Buffer 中就没有该 field,这些特性都有助于节约消息本身的大小。
Key 用来标识具体的 field,在解包的时候,客户端创建一个结构对象,Protocol Buffer 从数据流中读取并反序列化数据,并根据 Key 就可以知道相应的 Value 应该对应于结构体中的哪一个 field。

定义模型的.proto文件中,有看到每个字段后面会跟着一个数字。这不表示这个字段的默认值,而是表示这个字段的编号。当编码完成后的字节中,每一个字段的数据之前都会有几个字节表示该数据的编号。

而Key也是由以下两部分组成

Key 的定义如下: (field_number << 3) | wire_type

可以看到 Key 由两部分组成。第一部分是 field_number。第二部分为 wire_type。表示 Value 的传输类型。

一个字节的低3位表示数据类型,其它位则表示字段序号。

Type Meaning Used For
0 Varint int32, int64, uint32, uint64, sint32, sint64, bool, enum
1 64-bit fixed64, sfixed64, double
2 Length-delimi string, bytes, embedded messages, packed repeated fields
3 Start group Groups (deprecated)
4 End group Groups (deprecated)
5 32-bit fixed32, sfixed32, float

Protobuf fixed32类型和int32类型有什么区别?

Protobuf 中的 fixed32 类型和 int32 类型之间有以下区别:

  1. 数据范围:
    • fixed32 类型是一个无符号的 32 位整数,取值范围为 0 到 2^32-1。
    • int32 类型是一个带符号的 32 位整数,取值范围为 -2^31 到 2^31-1。
  2. 内存占用:
    • fixed32 类型始终占用 4 个字节的内存空间,无论存储的值是多少。
    • int32 类型使用变长编码进行存储,根据存储的值的大小动态选择所需的字节数,通常情况下会占用更少的内存空间。
  3. 符号位处理:
    • fixed32 类型是无符号的,不包含符号位。
    • int32 类型是带符号的,包含一个符号位,用于表示正数、负数或零。

选择使用 fixed32 还是 int32 取决于你的数据的性质和需求:

  • 如果你的数据范围是非负数,并且需要精确表示 0 到 2^32-1 之间的整数,那么可以选择 fixed32 类型。
  • 如果你的数据范围包括正数、负数和零,并且希望节省存储空间,可以选择 int32 类型。同时,使用变长编码可以在存储较小的值时节省空间。

解码

  1. 在消息流中每个Tag都是varint,编码方式为:field_num << 3 | wire_type。即,Tag由 .proto文件中字段的编号(field_num) 和 **传输类型(wire_type)**两部分组成。

  2. 注:Tag也是Varints编码,其后三位是传输类型(wire_type),之前的数值为是字段编号(field_num)。

  3. 注意并不是说Tag只能是一个字节,这里说了Tag也是用Varint编码,显然使用Varint编码方式几千/几万的字段序号(field_num)都是可以被表示的。

  4. 在对一条消息(message)进行编码的时候是把该消息中所有的key-value对序列化成二进制字节流;key和value分别采用不同的编码方式。

  5. 消息的二进制格式只使用消息字段的字段编号(field_num)作为Tag(key/键)的一部分,字段名和声名类型只能在解析端通过引用参考消息类型的定义(即.proto文件)才能确定。

  6. 解码的时候解码程序(解码器)读入二进制的字节流,解析出每一个key-value对; 如果解码过程中遇到识别不出来的filed_num就直接跳过。这样的机制保证了即使该消息(message)添加了新的字段,也不会影响旧的编/解码程序正常工作。

序号的作用

  • 每个字段有唯一编号,在二进制流中标识字段,可以看后面protobuf 编解码原理去了解字段的作用。
  • 消息被使用了,字段就不能改了,改了会造成数据错乱(常见坑),服务器和客户端很多bug,是proto buffer 文件更改,未使用更改后的协议导致。
  • 1 到 15 范围内的字段编号需要一个字节进行编码,编码结果将同时包含编号和类型
  • 16 到 2047 范围内的字段编号占用两个字节。因此,非常频繁出现的 message 元素保留字段编号 1 到 15。
  • 字段最小数字为1,最大字段数为2^29 - 1。(原因在编码原理那章讲解过,字段数字会作为key,key最后三位是类型)

在 Protocol Buffers(protobuf)中,每个字段后面的序号(Field Number)用于标识和区分不同的字段。每个字段都必须有一个唯一的序号,它在定义消息结构时起到以下几个作用:

  1. 标识字段:
    序号用于唯一标识消息结构中的每个字段。通过序号,可以清楚地指定消息中的特定字段,从而在序列化和反序列化过程中准确地读取和写入对应的值。
  2. 向后兼容性:
    序号在 Protocol Buffers 的版本演进中起到重要作用。当你需要向现有的消息结构中添加新字段时,可以使用未使用的序号,这样旧版本的解析器仍然能够正确解析旧的消息,并忽略它们不了解的新字段。
  3. 优化编码:
    序号还用于优化编码和消息的大小。Protocol Buffers 使用了一种紧凑的二进制编码格式,通过使用序号而不是字段名来进行编码,可以减少序列化后的消息大小,提高网络传输效率。

需要注意的是,序号的分配应该是有规划和稳定的。一旦为字段分配了序号,就应该保持稳定,不要更改或重复使用序号,以确保向后兼容性和正确的解析。

原始的json数据:

1
{"name":"personJson","id":15,"email":"personJson@google.com"}

按照之前定义的.proto文件,name编号为1,id编号为2,email编号为3,编码后的结果若翻译成可读的文字来说如下

1
1personJson2153personJson@google.com

即1号字段的值是personJson,2号字段的值是15,3号字段的值是personJson@google.com

拦截器Interceptor

拦截器的作用,是在执行核心业务方法的前后,创造出一个统一的切片,来执行所有业务方法锁共有的通用逻辑. 此外,还能够通过这部分通用逻辑的执行结果,来判断是否需要熔断当前的执行链路,以起到所谓的”拦截“效果.

拦截器是gRPC生态中的中间件,可以对RPC的请求和响应进行拦截处理,而且既可以在客户端进行拦截,也可以对服务器端进行拦截。

拦截器可以做统一接口的认证工作,再也不需要每一个接口都做一次认证了,多个接口多次访问,只需要在统一个地方认证即可

分类:

  • 按功能分:
    1. 一元拦截器UnaryInterceptor
    2. 流式拦截器StreamInterceptor
  • 按端角度分:
    1. 客户端拦截器ClientInterceptor
    2. 服务端拦截器ServerInterceptor

常见:身份认证、日志框架

序列化和反序列化

  • 序列化: 将数据结构或对象转换成二进制串的过程
  • 反序列化:将在序列化过程中所生成的二进制串转换成数据结构或者对象的过程

性能

性能包括两个方面,时间复杂度和空间复杂度:

第一、空间开销(Verbosity), 序列化需要在原有的数据上加上描述字段,以为反序列化解析之用。如果序列化过程引入的额外开销过高,可能会导致过大的网络,磁盘等各方面的压力。对于海量分布式存储系统,数据量往往以TB为单位,巨大的的额外空间开销意味着高昂的成本。

第二、时间开销(Complexity),复杂的序列化协议会导致较长的解析时间,这可能会使得序列化和反序列化阶段成为整个系统的瓶颈。

JSON

总的来说,采用JSON进行序列化的额外空间开销比较大,对于大数据量服务或持久化,这意味着巨大的内存和磁盘开销,这种场景不适合。没有统一可用的IDL降低了对参与方的约束,实际操作中往往只能采用文档方式来进行约定,这可能会给调试带来一些不便,延长开发周期。 由于JSON在一些语言中的序列化和反序列化需要采用反射机制,所以在性能要求为ms级别,不建议使用。

go中解析json就需要反射

Protobuf

Protobuf具有广泛的用户基础,空间开销小以及高解析性能是其亮点,非常适合于公司内部的对性能要求高的RPC调用。由于Protobuf提供了标准的IDL以及对应的编译器,其IDL文件是参与各方的非常强的业务约束,另外,Protobuf与传输层无关,采用HTTP具有良好的跨防火墙的访问属性,所以Protobuf也适用于公司间对性能要求比较高的场景。由于其解析性能高,序列化后数据量相对少,非常适合应用层对象的持久化场景。

Protobuf 是一系列键值对。消息的二进制版本只使用字段的标签作为,每个字段的名称和声明类型只能在解码结束时通过引用消息类型的定义来确定。

评论