gRPC
2021-02-18 21:48:25 阿炯

gRPC (gRPC Remote Procedure Calls) 是Google发起的一个开源远程过程调用 (Remote procedure call) 框架系统。该框架基于 HTTP/2 协议传输,使用Protocol Buffers 作为接口描述语言。旨在帮助开发人员更轻松地构建分布式应用,特别是当代码可能在不同地方运行的时候。其最初由 Google 开发,用于实现远程过程调用(RPC)。如今,gRPC 是云原生计算基金会(CNCF)的一个孵化项目,这意味着它已被用于生产环境,并得到了大量贡献者的支持。


A high performance, open source universal RPC framework.


其它功能:
认证(authentication)
双向流(bidirectional streaming)
流控制(flow control)
超时(timeouts)

最常见的应用场景是:
微服务框架下,多种语言服务之间的高效交互。
将手机服务、浏览器连接至后台
产生高效的客户端库


在 gRPC 里客户端应用可以像调用本地对象一样直接调用另一台不同的机器上服务端应用的方法,使得您能够更容易地创建分布式应用和服务。与许多 RPC 系统类似,gRPC 也是基于以下理念:定义一个服务,指定其能够被远程调用的方法包含参数和返回类型。在服务端实现这个接口,并运行一个 gRPC 服务器来处理客户端调用。在客户端拥有一个存根能够像服务端一样的方法。


gRPC 客户端和服务端可以在多种环境中运行和交互 - 从 google 内部的服务器到你自己的笔记本,并且可以用任何 gRPC 支持的语言来编写。所以可以很容易地用 Java 创建一个 gRPC 服务端,用 Go、Python、Ruby 来创建客户端。此外,Google 最新 API 将有 gRPC 版本的接口,使你很容易地将 Google 的功能集成到你的应用里。

使用 protocol buffers

gRPC 默认使用 protocol buffers,这是 Google 开源的一套成熟的结构数据序列化机制当然也可以使用其他数据格式如 JSON。用 proto files 创建 gRPC 服务,用 protocol buffers 消息类型来定义方法参数和返回类型。


接口描述语言

接口描述语言Interface description language,缩写IDL,是用来描述软件组件界面的一种计算机语言。IDL通过一种中立的方式来描述接口,使得在不同平台上运行的对象和用不同语言编写的程序可以相互通信交流;比如,一个组件用C++写成,另一个组件用Java写成。

IDL通常用于远程调用软件。在这种情况下,一般是由远程客户终端调用不同操作系统上的对象组件,并且这些对象组件可能是由不同计算机语言编写的。IDL建立起了两个不同操作系统间通信的桥梁。

在IDL基础上开发出来的软件系统有Sun的ONC RPC,The Open Group的Distributed Computing Environment,IBM的System Object Model,Object Management Group的CORBA,和SOAP用于Web service。

典型的接口描述语言
IDL specification language,the original Interface Description Language.
Microsoft Interface Definition Language
HIDLHAL Interface Definition Language(硬件抽象层接口描述语言,用于Android操作系统8.0及以上版本)
Open Service Interface Definitions
Platform-Independent Component Modeling Language
Simple Object Access ProtocolSOAP
WDDX
XML-RPC,the predecessor of SOAP


Protocol Buffers

Protocol Buffers简称:ProtoBuf是一种序列化数据结构的协议。对于透过管道(pipeline)或存储资料进行通信的程序开发上是很有用的。这个方法包含一个接口描述语言,描述一些数据结构,并提供程序工具根据这些描述产生代码,用于将这些数据结构产生或解析资料流。

语言支持
proto2提供一个程序产生器,支持C++、Java和Python。

第三方实现支持JavaScript。
proto3提供一个程序产生器,支持C++、Java (包含JavaNano)、Python、Go、Ruby、Objective-C和C#,从 3.0.0 Beta 2 版开始支持JavaScript。

第三方实现支持Perl、PHP、Dart、Scala和Julia。


为何创建 gRPC
为了解 Google 开发 gRPC 的原因,来大致看一下 API 这一设计的发展过程。

RPC 是设计和构建 API 的最早方法之一,允许编写代码,就好像它将在本地计算机上运行一样,即使实际上可能需要调用在其他机器(通常在您的本地网络)上运行的服务。在实践中,这支持开发人员使用直接操作命令(例如 SendUserMessages、addEntry 等),而不必考虑网络细节。RPC 消息具有轻量化、高效等特征,但却与底层系统紧密耦合,因此难以集成和变更,并很有可能泄露系统的细节。

REST API 架构问世后为使用 GET、POST、PUT 及 DELETE 等通用 HTTP 方法访问数据和资源提供了一种统一的方式,化解了其中一些挑战。尽管 REST 简化了数据访问,但 API 返回的元数据往往多于所需的数量。而且由于 REST API 还需要更多的网络信息(例如,请求发送的目的地),因此不如 RPC 轻量化和高效。

gRPC 有何优势
通过采用新技术,gRPC 更新了 RPC 旧方法,以实现互操作性并提高效率。如今,它被大量用于为微服务架构开发 API。其优势包括:

性能 – 默认使用 HTTP/2 作为其传输协议和 Protocol Buffers,这使其性能要高于 REST 和 JSON 通信。

流式传输 – 支持事件驱动型架构的数据流,例如服务器端流式传输、客户端流式传输及用于同时发送客户端请求和服务器响应的双向流式传输。

互操作性 – 内置代码生成能力,支持广泛的编程语言,包括 C++、Java、Python、PHP、Go、Ruby、C#、Node.js 等。

安全性 – 提供可插式身份验证、链路追踪、负载均衡及健康检查,可提高安全防护和弹性。

云原生 – 支持基于容器的部署,并可兼容 Kubernetes 和 Docker 等现代云技术。

总之,gRPC 提供了一个灵活的高性能框架,非常适合高度分布式微服务架构中的服务间通信。

了解 gRPC 之基本概念

其优势和好处主要源于以下两种技术的采用:
1.用于信息结构化的 Protocol Buffers
2.作为传输层的 HTTP/2

用于结构化消息的 Protocol Buffers
gRPC 使用 Protocol Buffers(或 “Protobuf”)来定义服务和消息,而非利用 XML 或 JSON。它是一种语言中立的机制,用于对服务将要相互发送的结构化消息进行序列化处理。

类似于 REST API 的 OpenAPI 规范的概念,gRPC 中的 API 契约在 .proto 文本文件中实现。开发人员可在该文件中定义其所需的数据结构化方式。然后 protoc 编译器会自动将 .proto 文本文件编译成支持的语言。在运行时,消息被压缩并以二进制格式发送。这提供了两大优势:
1).gRPC 占用的 CPU 较少,因为数据以二进制格式表示,降低了消息的大小。

2).模式进行了明确定义,可确保消息在客户端和服务器之间顺畅交换,从而减少错误。

作为传输层的 HTTP/2
过去 REST API 使用 HTTP/1.1 作为传输层,虽然也可通过 HTTP/2 传送。然而 gRPC 只使用 HTTP/2,这带来了一些关键优势;其中一个优势是能够使用二进制发送通信。此外 HTTP/2 支持处理多个并行请求,而不是一次处理一个请求。通信也是双向的,这意味着单个连接即可同时发送请求和响应。

总体而言,HTTP/2 有助于提高性能,并降低网络利用率,这在繁忙的微服务架构中尤其重要。然而它也有一些局限性。现代 Web 浏览器通常不支持 HTTP/2,因此可能需要使用 Nginx 等反向代理来交付应用。

gRPC 与 REST:比较
如今 REST 是最主要的 API 设计风格,因此可用作与 gRPC 进行对比。REST 和 gRPC 都是为 Web 应用和微服务构建 API 的有效方法,难分优劣。不过,了解二者之间的主要区别对选择最适合作业的工具而言大有裨益。

它们之间的一些主要区别包括以下方面:协议、数据格式、流式传输、API 设计、性能、错误处理、语言支持。

协议
虽然 REST API 能够利用 HTTP/2,RESTful 服务通常使用基于文本的 HTTP/1.1 作为传输层,而 gRPC 则完全依靠 HTTP/2 — 这是一种更高效的二进制协议,支持请求头压缩和单个 TCP 连接上多路复用等特性。

数据格式
REST API 通常使用 JSON 作为收发数据的数据格式。JSON 是基于文本的格式,易于读写,并得到了广泛支持。gRPC API 使用 Protobuf,它采用二进制格式,可提供更小的有效载荷并实现更快的交互。但 Protobufs 本身可读性差。

流式传输
REST API 支持 “请求 - 响应” 模式,对流式传输的支持有限。相比之下,gRPC API 通过 HTTP/2 进行传送,并支持多种通信模式,包括一元流式传输(请求 - 响应)、服务器流式传输、客户端流式传输和双向流式传输。

API 设计
REST 是一种以资源为中心的模式,支持 GET、POST、PUT 和 DELETE 等标准 HTTP 方法。每个请求都必须包含处理该请求所需的所有信息。此外,API 契约通常使用 OpenAPI 规范编写而成,客户端和服务器的编码需要单独完成。相比之下,gRPC 是一种以服务为中心的模式,其中消息和服务在 .proto 文件中进行定义。该文件可用于为 API 客户端和服务器生成代码。

性能
REST 可能会慢些,因为它通过 HTTP/1.1 传输基于文本的数据。每个请求都需要进行一次 TCP 握手,这可能会导致一些延迟。而 gRPC 支持通过 HTTP/2 传输多个数据流,因此多个客户端可同时发送多个请求,而无需新建 TCP 连接。它还利用了 HTTP/2 的特性,例如请求头压缩。

错误处理
REST 使用标准 HTTP 状态代码进行错误处理。相比之下,gRPC 支持以更高的细粒度来定义错误状态代码,并确保其保持一致。默认的 gRPC 模型非常有限,通常使用更丰富的错误模型(由 Google 开发)进行扩展。

语言支持
REST 得到了几乎所有语言的广泛支持,但并未提供任何内置代码生成功能。实现完全由开发人员自己解决。而 gRPC 凭借其 protoc 编译器为多种编程语言提供了原生代码生成功能。

应使用 gRPC 而非 REST 吗?
综上所述,在 gRPC 和 REST 之间如何选择取决于需完成的项目。gRPC 为分布式应用中的服务提供了一种高效、高性能的通信方式。不过它无法被 Web 浏览器及其他客户端直接读取,并需要使用 API 网卡或反向代理(如 Nginx)与前端客户端进行交互。对于事件驱动型微服务架构中的内部 API 而言,它是不二之选。

与之相比,REST 已被广泛采用,并得到了几乎所有语言的支持。它具有人类和机器可读性,因为数据使用 JSON 或 XML 进行交换。此外,其入门学习难度要低得多,并得到许多 Web 浏览器的支持,因此非常适合公开暴露的 API。

gRPC 微服务架构
由于性能以及在语言支持方面的灵活性,gRPC 是微服务架构中通信的最佳选择之一。开发人员可以轻松构建并生成以其首选语言运行的 gRPC 客户端和服务器。由于 gRPC 以二进制格式描述了 API 契约,因此微服务可独立于用于构建它们的语言进行通信。最常见的基于 gRPC 的微服务架构之一是将 API 网关部署在微服务的前面,然后通过它来处理所有内部通信。API 网关处理来自 HTTP/1.1 的传入请求,并通过 HTTP/2 将其作为 gRPC 请求代理到微服务。

gRPC 安全防护问题
随着 gRPC 日益普及,开发人员和安全运维团队需要确保部署有效的安全防护解决方案。由于其消息采用二进制格式,因此对于采用 ASCII 通信的设备和工具而言,可能会遇到问题。其 API 也容易受到许多最常见的威胁。访问控制、加密及运行时防护等标准 API 安全防护实践在基于 gRPC 的架构中同样重要。

gRPC 安全防护建议
gRPC 应用和 API 需要采用整体安全防护方法。确保 gRPC 安全的一些最佳实践包括:
1.模式验证 – 通过验证 gRPC 消息中的每个字段是否具有正确的类型和预期内容来阻止恶意利用。

2.数据屏蔽 – 屏蔽或阻止敏感数据(如信用卡号码和社会保险号)离开系统。

3.速率限制 – 严格限制请求的大小和数量,以防止资源耗尽型 DoS 攻击。

4.访问控制 – 在授予客户端对服务的访问权限之前执行身份认证和授权。

5.加密 – 使用传输层安全(TLS)保护传输中的消息。

最终,应验证 API 网关、Web 应用防火墙(WAF)及其他 API 管理和安全防护工具能否保护生产环境中的 gRPC 应用和 API。这些工具应能够为每个服务导入 .proto 文件,以用于对 gRPC 应用和 API 实施安全防护。


全面简述一个 RPC 框架

开发人员要能清楚的了解 RPC 框架所具备的要素,掌握 RPC 框架中涉及的服务注册发现、负载均衡、序列化协议、RPC 通信协议、Socket 通信、异步调用、熔断降级等技术,就可以全方位的提升基本素质。另外目前市面上也有非常多优秀的框架,GitHub 上也有相关源码,但好记性不如烂笔头,只有自己真正了解并且动手去尝试实现一个 RPC 框架,才是去掌握这门技术的最优路径。

1、介绍

研究一个概念或者框架,带着三个 W 去考虑,可能会对其有更加深刻的了解:

1)What,什么是 RPC 框架,RPC 是 Remote Procedure Call 的简称,远程过程调用,那么什么叫远程过程调用呢,你可以理解为我们调用外部(远程)服务就像调用自己本地方法一样。

2)Where,RPC 框架用在什么地方,在分布式系统和微服务盛行的今天,各业务系统会被独立拆分出来成为一个个独立的 web 应用,应用之间的交互和数据传输就成了必不可少的一环,RPC 就是为了实现独立服务之间远程交互的框架。

3)Why,为什么需要一个 RPC 框架,服务之间的调用需要各种场景和因素的考虑,内部原理非常复杂和繁琐,同时在集群情况下,服务的负载均衡,熔断,限流等都是需要去考虑的,这时候就需要一个集服务注册发现、负载均衡、序列化协议、RPC 通信协议、Socket 通信、异步调用、熔断降级等技术为一体的技术去完成这些公共功能,RPC 框架就是在这种情况下应运而生。

目前比较主流的 RPC 框架包括谷歌开源的 GRPC、阿里巴巴的 Dubbo、Netflix 的 SpringCloud 等。

2、RPC 框架基本组成

RPC 框架需要的最基本的三个要素:
ServiceProvider: 服务提供方,提供相关服务接口。
ServiceConsumer: 服务消费方,消费服务提供方的接口。
Registry: 注册中心,用于进行服务的注册、发现、治理、高可用。

基于三个最基本要素,还会延伸出包括负载均衡器、熔断降级器、通信协议组件、序列化协议等等组件。一个最简单的 RPC 调用模型图如下所示:


下面做一些名词的介绍和解释:

2.1 注册中心

注册中心是 RPC 框架中的管理者和协调者角色,虽然在远程过程调用中服务消费者会不经过注册中心,会直接向服务提供者发送请求,但是随着我们的服务方越来越多,每个服务的实例也不断变化的,且每个服务的地址,端口等信息是需要通知到消费方的,所以我们需要一个类似 “管家” 的角色,来负责管理服务注册和发现的工作,这个 “管家” 我们称之为注册中心。

一个合格的注册中心需要具备包括缓存和持久化服务提供方数据,动态更新服务提供者信息,动态监听服务提供方节点变化,推送节点变化到消费方,查询服务提供方数据等功能。

目前市面上比较流行的注册中心有:Zookper、 Nacos、Consul、Eurake 等,针对于上面功能的实现方式也有所不同,以下是注册中心的对比:
  Zookeeper Nacos Consul Eurake
一致性协议 CP CP + AP CP AP
雪崩保护
多数据中心 不支持 支持 支持 支持
自动注销实例 支持 支持 支持 支持


2.2 服务提供方(RPC 服务端)

其需要对外提供服务接口,一个服务方需要包括启动连接注册中心,注册相关信息到注册中心,提供服务下线和更新机制,维护服务名和服务的映射,序列化和反序列化,启动通信等。

目前服务提供方有两种服务提供维度,基于接口的服务提供和基于服务的服务提供,Dubbo3.0 之前是基于接口维度做的服务注册,Dubbo3.0 之后渐渐向服务维度的服务注册发现靠拢,SpringCloud 是基于服务来进行注册发现的,在云原生和容器化越来越火的今天,基于服务可以更好的适配容器化和云原生。

2.3 服务消费方(RPC 消费端)

服务消费方需要具备可以从注册中心拉取服务列表,缓存服务列表,动态监听和更新服务列表的功能,还需要具备针对于服务的负载均衡策略,序列化和反序列化,根据约定的通信协议进行调用等。

目前 Dubbo 是基于代理和 Spring 的 BeanDefination 来实现的,在消费启动的时候会去扫描基于自定义注解或配置的信息,然后生成一个相应的代理对象,注册到 Spring 容器中,在调用的时候直接通过代理类进行相关一系列调用。SpringCloud 是基于 HTTP 协议和增强版的 RestTemplate 来实现的,内部实现了 Ribbon 的负载均衡,消费方可以通过 Feign 或者 RestTemplate 实现远程调用。

2.4 通讯框架

通讯框架是服务之间进行 IO 交互和传输的保证,消费端需要通过通讯框架和提供方进行交互,获取数据,目前市面上主流的基于 Java 的 NIO 通讯框架就是 Netty。

2.5 通讯协议

通讯协议是消费端和服务端约定好一种交互协议,当消费端拿到服务端提供的 IO 流之后,需要根据通讯协议获取具体的数据内容,目前 TCP 通讯协议比较通用的是 HTTP、FTP、SMTP 协议等,SpringCloud 是基于 HTTP 的通讯协议实现的,Dubbo 内部自己集成了一套通讯协议。

业界的主流协议的解决方案可以归纳如下:
消息定长,例如每个报文的大小为固定长度 100 字节,如果不够用空格补足。
在包尾特殊结束符进行分割。
将消息分为消息头和消息体,消息头中包含表示消息总长度(或者消息体长度)的字段。

通过对比,我们发现第三点是最为灵活和可拓展的,一般推荐都会使用第三种。

2.6 序列化

2.6.1 概念介绍

序列化(serialization)就是将对象序列化为二进制形式(字节数组),一般也将序列化称为编码(Encode),主要用于网络传输、数据持久化等。

反序列化(deserialization)则是将从网络、磁盘等读取的字节数组还原成原始对象,以便后续业务的进行,一般也将反序列化称为解码(Decode),用于网络传输对象的解码,以便完成远程调用。

2.6.2 序列化协议

XML & SOAP
XML 是一种常用的序列化和反序列化协议,具有跨机器,跨语言等优点。XML 历史悠久,其 1.0 版本早在 1998 年就形成标准,并被广泛使用至今,目前金融和银行行业使用较多。

JSON
JSON 全称 (Javascript Object Notation) 起源于弱类型语言 Javascript, 它的产生来自于一种称之为”Associative array” 的概念,其本质是就是采用”Attribute-value” 的方式来描述对象。实际上在 Javascript 和 PHP 等弱类型语言中,类的描述方式就是 Associative array。JSON 的具有数据简单,可接受程度高,结构简洁,序列化包体小等特点,目前大部分的公司都是使用这种序列化协议来实现的。

Protobuf

由谷歌开发的一款高性能序列化框架,是一个纯粹的展示层协议,可以和各种传输层协议一起使用,目前支持 Java、C++、Python 等多种语言,他具有更小的数据量,更快的解析速度,简单的调用等特点,目前得物使用的 Dubbo2.7.7 版本 默认使用这种序列化协议。

2.7 负载均衡

负载均衡是保证服务提供方在多实例的情况下保证负载的均衡的一种策略,目前大体有如下几种负载均衡策略:
1)轮训,采用计数器的方式,根据计数器的值和实例数量进行取余。
2)随机,采用随机请求的方式,随机一个 Random 的数值,根据 random 进行取余。
3)加权轮训,采用权重的方式,给每一个实例配置不同的权重比例,通过比例选择合适的实例。
4) 一致性 Hash,采用 Hash 环的方式,大体的实现思路是通过寻址的方式找到就近的一个节点,具体可以自行网上搜索一下相关文档理解。

3、实现

既然是当作练手实现一个 RPC 框架,所以会尽量借鉴当前主流的框架,技术选型方面:
注册中心:选择 Zookeeper 来实现。
服务提供方:选择基于服务维度来实现服务发现,目前主流框架都是基于这个来做的。
服务消费方:选择 Dubbo 类似的基于代理 Spring 的 BeanDefination 来实现。
通讯框架:Netty。
通讯协议:采用上述的第三种方式。
序列化协议:支持 JSON 和 Protobuf。
负载均衡:实现轮训和随机。

当然,上述的组件都是使用 SpringBoot 的 SPI 方式来进行选择的,后续如果需要进行拓展和按不同配置加载,可以通过配置的方式来实现插件的可插拔。先贴上项目整体结构:


整体代码结构如上,下面针对模块来讲解:

3.1 注册中心

因为是以可拓展和接口方式实现的,所以定义了一些接口,通过不同的实现来进行区分,后期可以通过不同的配置来选择合适的注册中心。

3.2 服务提供方

服务提供方通过使用 Spring 的事件来进行监听,同时根据声明式的注解来进行解析和注册,同时通过组装元数据,将自己的服务注册到注册中心,完成服务提供方的服务注册,同时启动 Netty 的客户端,来进行客户端的监听,从而进行服务调用,具体流程图如下:


3.3 服务消费方

服务消费方的逻辑稍微复杂一些,需要通过自动装配来创建新的 BeanDefination, 然后从所有的 Bean 中找到相关的带有 RPC 注解的参数,重写 BeanDefination,重写的 Bean 需要构建新的元数据和存入客户端缓存,然后通过动态代理的方式,创建一个代理对象,对象使用负载均衡在服务发现的时候选择一个地址,通过 Netty 的方式进行通信和数据交互,最后返回相关数据对象,具体的流程如下图:


负载均衡实现代码:(SPI 可拓展模式)

目前采用轮训和随机的方式实现的,后期可根据接口进行拓展。

3.4 通讯框架

可使用 Netty4 实现了通讯框架,采用了 NettyClient 和 NettyServer 来实现。

3.5 通讯协议

目前使用了自定义的通信协议来实现:
第一个字节是魔法数,比如我定义为 0X35。
第二个字节代表协议版本号,以便对协议进行扩展,使用不同的协议解析器。
第三个字节是请求类型,如 0 代表请求 1 代表响应。
第四个字节表示消息长度,即此四个字节后面此长度的内容是消息 content。

3.6 序列化协议

目前支持 JSON 和 ProtoBuf,后期可以通过 SPI 进行拓展和可配置。

小结

上文从整体名词介绍、内部组件组件介绍等两个方面阐述了 RPC 的框架模型,从技术选型、具体代码等实现了一个 RPC 框架并应用到项目中。目前 RPC 框架整体搭建完成,可以正常通过注解接入业务方使用,但还有很多细节需要更仔细地去考虑和思索,比如系统的可拓展性和框架的整体性能等。希望通过阅读本篇文章,可以让读者对 RPC 有更清晰的了解,让自己的基础技能有一个更高的提升。

gRPC 正被开发人员及 Netflix 和 Lyft 等大公司大量用于微服务架构中的内部通信。不过其既无法替代 REST API,也不是构建 API 的更好方式;只有当主要为内部微服务环境构建 API 并需要高效、实时通信能力时,它才是一个可供考虑的备选方案。

展望未来,鉴于其性能优势和易开发性,gRPC 可能会在云原生应用中愈加受到青睐。与此同时,需要公开暴露 API 的开发人员将继续在其应用中使用 REST。REST 也将继续存在于云原生环境中,因为它具有向后兼容性并可深度集成现有 API 基础架构和运维环境。


官方主页:https://grpc.io/