chapter-1 gRPC的概述与发展
一 gRPC概述
gRPC (the “g” stands for something different in every gRPC release) is an inter-process communication technology that allows you to connect, invoke, operate, and debug distributed heterogeneous applications as easily as making a local function call.
gRPC 是一个高性能、开源、通用的RPC框架,是一种进程间通讯技术,由Google推出,基于HTTP2协议标准设计开发,默认采用Protocol Buffers数据序列化协议,支持多种开发语言。gRPC提供了一种简单的方法来精确的定义服务,并且为客户端和服务端自动生成可靠的功能库。 在gRPC客户端可以直接调用不同服务器上的远程程序,使用姿势看起来就像调用本地程序一样,轻松的实现远端服务的连接、调用、操作和调试,很容易去构建分布式应用和服务。和很多RPC系统一样,服务端负责实现定义好的接口并处理客户端的请求, 客户端根据接口描述直接调用需要的服务。客户端和服务端可以分别使用gRPC支持的不同语言实现。
Using that service definition, you can generate the server-side code known as a server skeleton, which simplifies the server-side logic by providing low-level communication abstractions. Also, you can generate the client-side code, known as a client stub, which simplifies the client-side communication with abstractions to hide low-level communication for different programming languages. The methods that you specify in the service interface definition can be remotely invoked by the client side as easily as making a local function invocation. The underlying gRPC framework handles all the complexities that are normally associated with enforcing strict service contracts, data serialization, network communication, authentication, access control, observability, and so on.
通过IDL(interface definition language )进行服务定义,生成服务端架构的服务代码,它通过提供低等级的通讯抽象去简化服务端逻辑;同样,可以根据客户端存根生成客户端的代码,通过抽象简化去隐藏不同编程语言的低级通讯。通过服务接口定义的方法可以实现通过客服端很轻松的就像在本地调用函数一样进行远程调用。gRPC底层框架处理了所有强制严格的服务契约、数据序列化、网络通讯、服务认证、访问控制、服务观测等等通常有关联的复杂性。
二 服务定义
protocol buffer
gRPC使用的是protocol buffer作为服务描述语言
示例
通常使用*.proto文件进行服务定义,例如下面定义:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17syntax = "proto3";
package api;
service ProductInfo {
rpc addProduct(Product) returns (ProductID);
rpc getProduct(ProductID) returns (Product);
}
message Product {
string id = 1;
string name = 2;
string description = 3;
}
message ProductID {
string value = 1;
}
三 gRPC服务端
生成服务架构代码
1
protoc --plugin=protoc-gen-go=/usr/local/bin/protoc-gen-go --go-grpc_out . --go_out . product.proto
生成结构如下api目录:
服务端要求
- 重写服务基类实现服务架构的服务逻辑
- 运行gRPC服务并监听处理客户端请求,返回响应;
服务端案例实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51package main
/*
@Time : 2020/12/17 13:24
@File : main.go
@Software: GoLand
*/
// 服务端需要引如生成的服务骨架
// 重写基类 实现业务
// 运行项目监听请求处理请求返回响应
import (
"context"
"fmt"
pb "gRPCChapter1/api"
"google.golang.org/grpc"
"log"
"net"
)
const port = "127.0.0.1:9090"
// 可以自定义一个结构体,然后实现该接口的全部方法即可, 将来实现将DTO对象转化DO领域对象
type product struct{}
var ProductServer = product{}
func (s product) AddProduct(ctx context.Context, in *pb.Product) (*pb.ProductID, error) {
// 处理业务逻辑,此处简单输出即可
fmt.Printf("新增商品成功...\n")
fmt.Printf("商品ID:%s, 商品名称:%v\n", in.Id, in.Name)
p := pb.ProductID{Value: in.Id}
return &p, nil
}
func (s product) GetProduct(ctx context.Context, in *pb.ProductID) (*pb.Product, error) {
// 业务逻辑的处理
log.Printf("获取商品...\n")
log.Printf("查询商品ID: %v\n", in.Value)
p := pb.Product{Id: "1", Name: "手机", Description: "测试手机"}
return &p, nil
}
func main() {
lis, _ := net.Listen("tcp", port)
s := grpc.NewServer()
pb.RegisterProductInfoServer(s, ProductServer)
fmt.Printf("ProductServer register success! listen to %v\n", port)
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v\n", err)
}
}
四 gRPC客户端
Go客户端实现与服务端相似,只要进行客户端代码存根调用即可:
示例代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54package main
/*
@Time : 2020/12/17 13:24
@File : main.go
@Software: GoLand
*/
import (
"context"
pb "gRPCChapter1/api"
"google.golang.org/grpc"
"log"
"time"
)
// gRPC客户端与服务端相似,使用客户端存根就可以实现服务调用
const (
ADDRESS = "127.0.0.1:9090"
)
func main() {
// 创建连接
client, err := grpc.Dial(ADDRESS, grpc.WithInsecure())
if err != nil {
log.Fatal("grpc create client failed! err: ", err)
}
// 关闭链接
defer client.Close()
// 创建客户端
cli := pb.NewProductInfoClient(client)
// 调用函数
var product pb.Product = pb.Product{
Id: "1",
Name: "iphone12",
Description: "新款手机",
}
// 创建请求超时上下文
timeOut, chancel := context.WithTimeout(context.Background(), time.Second)
defer chancel()
productId, err := cli.AddProduct(timeOut, &product)
if err != nil {
log.Fatalf("AddProduct failed: %v\n", err)
}
log.Printf("add product id: %v\n", productId.Value)
// 继续调用获取商品信息
pd, err := cli.GetProduct(timeOut, productId)
if err != nil {
log.Fatalf("GetProduct failed: %v\n", err)
}
log.Printf(pd.String())
}分别启用服务端与客户端结果如下:
客户端:
服务端:
五 Python客户端通信
Python插件配置
1
2
3
4# 生成python代码的脚手架用来转换IDL
pip install grpcio-tools -i https://pypi.tuna.tsinghua.edu.cn/simple
# gRPC插件支持
pip install grpcio -i https://pypi.tuna.tsinghua.edu.cn/simpleIDL接口服务进行转换Python服务
1
python3 -m grpc_tools.protoc --python_out=. --grpc_python_out=. -I. product.proto
生成的文件结构如下:
上图中_pb2.py与_pb2_grpc.py就是根据proto文件生成的客户端与服务端的相对应代码的存根,只要对逻辑或调用进行实现即可,其中_pb2.py是用来和 protobuf 数据进行交互,_pb2_grpc.py是用来和 grpc 进行交互。
Python客户端调用Go服务端服务
在main.py文件中,我们将会对Go服务端的服务进行调用,其示例代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24import grpc
import product_pb2
import product_pb2_grpc
def run():
# 创建一个gRPC的链接
channel = grpc.insecure_channel('localhost:9090')
# 创建一个客户端
stub = product_pb2_grpc.ProductInfoStub(channel)
# 调用客户端的新增方法
response = stub.addProduct(product_pb2.Product(
name="iphone12",
description="新款手机",
id="1"))
print("add product: response\n", response)
product_info = stub.getProduct(product_pb2.ProductID(value=response.value))
print("get product: response\n", str(product_info))
if __name__ == '__main__':
run()运行main.py文件后,得到如下信息,和Go客户端调用信息一致,也就是不管客户端还是服务端,只需要按照protobuf协议进行转换。其实现了高度抽象,可以实现跨语言间的低耦合通信。
六 客服端与服务端消息流动
When a gRPC client invokes a gRPC service, the client-side gRPC library uses the protocol buffer and marshals the remote procedure call protocol buffer format, which is then sent over HTTP/2. On the server side, the request is unmarshaled and the respective procedure invocation is executed using protocol buffers. The response follows a similar execution flow from the server to the client. As the wire transport protocol, gRPC uses HTTP/2, which is a high-performance binary message protocol with support for bidirectional messaging.
当gRPC的客户端调用gRPC服务,客户端侧gRPC库使用协议缓存进行封送协议缓冲区格式的远程调用程序,然后通过HTTP2发送。在服务端侧,接收到的请求被解组然后使用协议缓存进行各自程序调用。服务端到客户端响应的流动也使用了相同的方式。gRPC使用了高性能、支持双向信息传递的二进制消息协议HTTP2进行有线传输协议。
Marshaling is the process of packing parameters and a remote function into a message packet that is sent over the network, while unmarshaling unpacks the message packet into the respective method invocation.
封送: 就是将参数和远端函数打包到通过网络发送的消息包中的过程;
解组: 就是将消息包解包到相应的方法进行调用。
七 IPC发展历程
传统RPC
传统的RPC通常是构建C-S架构应用中的进程间通讯,客户端通过远程调用方法实现了像本地调用函数一样。早期受欢迎的技术是Common Object Request Broker Architecture 和RMI,用来建立与服务或应用的联系。
其存在的缺陷:极度复杂、建立在TCP通讯协议之上、妨碍内部操作、众多臃肿规范。
SOAP
由于传统RPC存在的局限性,Simple Object Access Protocol(SOAP)简单对象访问协议被微软、IBM等大厂进行设计并进行了大力推广。SOAP协议是一种标准的面相服务架构的通讯技术,用于服务间基于XML结构体数据交换,并且使用任何底层通信协议(通常是HTTP)进行通信。
可以使用SOAP协议定义服务接口、服务操作、并且可以通过调用这些操作使用有关联的XML格式消息。SOAP是一种非常受欢迎的技术,但是由于复杂的消息格式、构建也受到SOAP协议复杂的规范约束,阻碍了构建分布式应用的灵活性。因此,在当代分布式应用的开发环境下,SOAP网络服务技术也被当作了历史遗留技术。大多数分布式应用架构都开始使用REST架构风格,而不是使用SOAP协议。
REST
概述
REST is the foundation of the resource-oriented architecture (ROA), where you model distributed applications as a collection of resources and the clients that access those resources can change the state (create, read, update, or delete) of those resources.
REST是面相资源架构体系的基础,在这里,分布式应用模型被作为一种资源的集合,客户端可以访问这些资源、可以改变这些资源的状态(CRUD)。
REST的实际实现是HTTP协议,在HTTP中,你可以将RESTful风格服务应用建模为可以使用唯一标识符URL访问的资源的集合。资源状态的更改,就是基于HTTP请求动作实现。
使用HTTP或JSON的REST架构风格构建应用程序已经成为微服务的实现方式,但是,在急剧增长的微服务数量或网络交互,RESTful风格的服务架构已经不能满足预期的要求。并且其存在两个关键限制。
这些限制阻碍了他们使用基于现代微服务应用程序使用消息传输协议的能力。
局限性
Inefficient text-based message protocols
从本质上来说,REST架构服务是基于文本传输协议的HTTP1.x之上,并且携带了人类可读的类似于JSON的文本格式。当进行服务端间进行通信时,使用JSON类型文本格式是非常低效的。因为,作为服务端间的通信,没有必要使用可读的文本格式。
服务端源应用程序生成二进制内容发送到服务器,然后将二进制结构转化为文本,然后通过文本形式通过网络发送到另外一台机器,目标服务端接收解析并转换回二进制结构返回。相反,我们可以很轻松发送二进制格式数据,并能够映射服务和消费的业务逻辑。通常为了便于可读性,服务端与客户端直接使用JSON进行传输。
Lacks strongly typed interfaces between apps
越来越多的服务通过网络进行交互,这些服务可能是不同语言技术开发的,一个好的、稳定健硕的服务类型定义非常的重要。虽然在RESTful 服务中已经存在很多服务定义技术,比如说OpenAPI/Swagger服务,但都是事后定义的,在设计之初、并没有紧密的集成底层架构和消息传递协议。
在构建分散应用中就会导致很多的不兼容、运行时错误、以及内部交互的问题。因此,具备当代强类型服务定义技术和生成多语言服务侧-客户端侧核心代码的框架技术时非常有必要的。
REST architectural style is hard to enforce
真正的RESTful架构服务在实现阶段是很难落地实施的,目前普遍存在的所谓的RESTful服务应用,仅仅是网络公开的HTTP服务,并没有真正的正确遵循RESTful风格。
gRPC
Inception of gRPC
Google最早使用了一个叫Stubby的通用RPC框架,用来连接上千个使用不同技术构建的运行在多个数据中心的微服务。它的核心RPC层设计是用来每秒处理百亿个网络请求,Stubby具有很多良好的特性,但是它并不是标准化的,不能作为通用型的框架使用,因为它与Google内部的基础设施耦合的太过于紧密。
在2015年,Google发行了gRPC并作为了一个开源的RPC框架,gRPC是标准的、通用型的、跨平台的基础框架,gRPC的目的是提供与Stubby相同可伸缩性、性能和功能,并且是面向整个社区。
自此以后,gRPC的受欢迎程度随着Netflix, Square, Lyft, Docker, Cisco, and CoreOS等大公司的采用在过去的几年里大幅度的增长。后来,gRPC也加入到云原生计算基金会,并从CNCF生态系统项目中获得很大的吸引力。
Why gRPC?
gRPC被设计成一种互联网规模的进程间通信技术,可以克服传统进程间通信技术的大部分缺点。
Advantages of gRPC
It’s efficient for inter-process communication
gRPC没有使用传统的JSON和XML文本格式,而是使用了基于协议缓存区的二进制协议与gRPC的服务端、客户端通讯。gRPC在HTTP2的基础上实现了协议缓冲区,使得进程间的通讯更快。这使得gRPC成为目前最高效的IPC技术。
It has simple, well-defined service interfaces and schema
gRPC采用了合约至上的方式开发应用,即先定义服务接口,然后在此基础上进行细节实现。
因此,与用于RESTful服务定义的OpenAPI/Swagger和用于SOAP web服务的WSDL不同,gRPC提供了一种简单但一致、可靠和可扩展的应用程序开发体验。
It’s strongly typed
gRPC是强类型的。一旦使用协议缓冲定义gRPC的服务,gRPC服务合同便清晰的定义将用于应用间通讯的类型。由于强类型定义,使得分布式应用程序开发变得更加稳定,因为静态类型有助于克服在构建跨多个团队和技术的云原生应用程序时可能遇到的大多数运行时和互操作性错误。
It’s polyglot
gRPC设计用于多种编程语言。带有协议缓冲的gRPC服务的定义与语言无关,可以使用任何语言与存在的gRPC服务或者客户端进行交互。
It has duplex streaming
gRPC自身就支持客户端和服务端的流媒体,这是服务定义本身所固有的。这使得开发流式服务和流式客户端更加容易。此外,与传统的RESTful消息传递风格相比,gRPC构建传统的请求-响应式消息传递和服务端与客户端间流式传递的能力是一个关键优势。
It has built-in commodity features
gRPC内置了如用户认证、加密、弹性(过期与超时)、元数据交互、压缩、负载均衡、与服务发现等很多有价值的特性。
It’s integrated with cloud native ecosystems
gRPC是CNCF的一部分,很多的现代框架和技术为gRPC提供现成的本地支持。例如,CNCF下的许多项目(如Envoy)支持gRPC通信协议;对于观测和监控等跨领域功能,gRPC被很多工具支持(例如,使用 Prometheus监控gRPC应用程序)。
It’s mature and has been widely adopted
gRPC作为进程间通讯技术,已经是很成熟,并被广泛的采用。在Google内部已经进行全面的测试,gRPC在许多大公司如Square, Lyft, Netflix, Docker, Cisco, and CoreOS中被广泛的使用。
Disadvantages of gRPC
It may not be suitable for external-facing services
gRPC不太适合面向外部的服务。当通过互联网向外部服务机公开应用程序与服务时,gRPC通讯协议就不太适合,因为很多的外部消费者对gRPC和REST/HTTP服务模式很陌生。gPRC服务的契约驱动和强类型定义的特性,阻碍了向外部公开服务的灵活性,并且消费者获得的控制权也大大的降低。但后面通过gRPC的网关设计,用来解决这类问题。
Drastic service definition changes are a complicated development process
服务更改在现代服务间通讯用例中是非常常见的。如果gRPC服务定义发生巨大改变时,就增加了向下兼容的难度。通常需要重新生成服务端与客户端的代码,很可能加大整个开发生命周期复杂化。正常情况下,gRPC服务定义的修改都是在不违反服务契约的条件下进行,只要没有引入破坏性的变更,gRPC就可以使用不同版本的proto的客户机和服务器进行交互操作。因此,在大多数情况下不需要重新生成代码。
The ecosystem is relatively small
gRPC的生态系统和传统REST/HTTP协议相比较小,移动应用程序和浏览器对gRPC的支持目前处于初级阶段。
gRPC与其他协议的对比
Apache Thrift
概述
Apache Thrift和gRPC相似是RPC框架。它使用Thrift的IDL描述接口定义函数与数据类型,然后通过Thrift的编译环境生成各种语言类型的接口文件,用户可以根据自己的需要采用不同的语言开发客户端代码和服务器端代码。Thrift传输层为网络I/O提供抽象,并将Thrift与系统的其他部分进行解耦,它可以在任何传输实现上运行,如TCP、HTTP等。
区别
Transport
gRPC 比 Thrift 更超前,提供了对 HTTP/2 一流的支持。 它在 HTTP/2上实现利用该协议的功能来实现高效和流式消息传递模式。
Streaming
gRPC服务定义本身就支持客户端和服务的之间的双向流式传递。
Adoption and community
在采用方面,gRPC 有良好的发展势头,并且已成功围绕 CNCF 项目建立了良好的生态系统。 同样,社区资源,例如良好的文档,外部演示文稿和示例用例,对于 gRPC 来说是很常见的,与 Thrift 相比,采用过程更加流畅。
Performance
在性能方面,由于官方也没有给出gRPC与Thrift比较的明确结果,有一些在线资源对二者性能进行比较显示出更合适Thrift的数据。然而,gRPC在所有版本中都在进行性能测试。因此,在进行二者选择时,性能影响不是决定性的因素。要结合具体的场景和业务架构来选择最合适的协议。此外,目前虽然还有很多的RPC框架提供了类似的功能,但gRPC作为最标准化、可互操作和广泛采用的RPC技术目前处于领先地位。
选择依据
- 选择gRPC而不是Thrift
- 需要良好的文档、示例
- 喜欢、习惯使用HTTP/2、ProtoBuf协议
- 对网络传输带宽敏感
- 选择Thrift而不是gRPC
- 需要在非常多的语言间进行数据交换
- 协议层、传输层有多种控制要求
- 需要稳定的版本
- 不需要良好的文档和示例
- 对CPU敏感
- 选择gRPC而不是Thrift
GraphQL
GraphQL 是另一种技术(由 Facebook 发明并标准化为一种开放技术),在构建进程间通信中变得非常流行。 它是 API 的查询语言,是用于使用现有数据完成这些查询的运行时。 GraphQL 通过允许客户端确定所需的数据,所需的数据以及所需的格式,为传统的客户端-服务器通信提供了根本不同的方法。 相反,gRPC 与支持客户机和服务器之间通信的远程方法具有固定的约定。
GraphQL 更适合直接暴露给消费者的外部服务或 API,在这样的环境中,客户端需要对来自服务器的数据进行更多控制与处理。
在GraphQL和gRPC的大多数实用用例中,GraphQL用倾向于面向外部的服务/api,而支持api的内部服务则使用gRPC实现。
八 总结
现代软件应用程序或服务很少是孤立存在,用来连接服务应用间的进程间通信技术是现代分布式软件应用程序最重要的一方面。gRPC是一个可伸缩的、松耦合的、类型安全的进程间通讯技术,与传统的基于REST/HTTP的通信相比,gRPC更加高效。它允许你就像调用本地程序一样,通过网络传输请求轻松的实现远端服务的连接、调用、操作和调试,很容易去构建分布式应用和服务。
gRPC技术是传统RPC的一种发展,并设法克服了传统RPC的局限性。gRPC由于其进程间通信需求而被各种互联网规模的公司广泛采用,并且最常用于构建内部服务到服务通信。
1 | 参考文献: |