乐趣区

分布式服务框架gRPC

什么是 gRPC

gRPC 是 Google 开发的高性能、通用的开源 RPC 框架,其由 Google 主要面向移动应用开发并基于 HTTP/ 2 协议标准而设计,基于 Protobuf(Protocol Buffers)序列化协议开发,且支持众多开发语言。在 gRPC 中一个客户端可以像使用本地对象那样直接调用位于不同机器上的服务端应用的方法 (methods)。这让你能够更容易的构建分布式的应用和服务。和其他RPC 系统类似,gRPC也是基于定义一个服务,指定服务可以被远程调用的方法以及他们的参数和返回类型。在服务端,实现服务的接口然后运行一个 gRPC 服务来处理可出端的请求。在客户端,客户端拥有一个存根(stub 在某些语言中仅称为客户端),提供与服务器相同的方法。

·gRPC客户端和服务器可以在各种环境中运行并相互通信,并且可以使用 gRPC 支持的任何语言编写。因此,例如,您可以使用 Go,Python 或 Ruby 的客户端轻松地用 Java 创建 gRPC 服务器。此外,最新的 Google API 的接口将拥有 gRPC 版本,可让您轻松地在应用程序中内置 Google 功能。

使用 protocol buffer

默认情况下,gRPC 使用 protocol buffer,用于序列化结构化数据(尽管它可以与其他数据格式(例如 JSON)一起使用)。使用协议缓冲区的第一步是在 proto 文件中为要序列化的数据定义结构:proto 文件扩展名为.proto 的普通文本文件。protocol buffer 数据被构造为消息,其中每个消息都是信息的逻辑记录,其中包含一系列称为字段的名称 / 值对。这是一个简单的示例:

message Person {
  string name = 1;
  int32 id = 2;
  bool has_ponycopter = 3;
}

定义了数据结构后,就可以使用 protocol buffer 编译器 protoc 生成你所选语言的数据访问类。访问类为每个字段提供了简单的访问器(例如 name()) 和set_name()),以及将整个结构序列化为原始字节或从原始字节中解析出整个结构的方法 - 例如,如果您选择的语言是 C ++,则在上面的示例将生成一个名为 Person 的类。然后,您可以在应用程序中使用此类来填充,序列化和检索 Person 的 protocol buffer 消息。

除此之外你还要在 .proto 件中定义 gRPC 服务,并将 RPC 方法参数和返回类型指定为 protocol buffer 消息:

// The greeter service definition.
service Greeter {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply) {}}

// The request message containing the user's name.
message HelloRequest {string name = 1;}

// The response message containing the greetings
message HelloReply {string message = 1;}

gRPC 使用也是使用编译器 protoc 从 proto 文件生成代码,不过编译器要首先安装一个 gRPC 插件。使用 gRPC 插件,你可以获得生成的 gRPC 客户端和服务器代码,以及用于填充,序列化和检索消息类型的常规 protocol buffer 访问类代码。

下面会更详细地介绍 gRPC 里的一些关键的概念。

服务定义

与许多 RPC 系统一样,gRPC 围绕定义服务的思想,指定可通过其参数和返回类型远程调用的方法。默认情况下,gRPC 使用 protocol buffer 作为接口定义语言(IDL)来描述服务接口和有效负载消息的结构。如果需要,可以使用其他替代方法。

service HelloService {rpc SayHello (HelloRequest) returns (HelloResponse);
}

message HelloRequest {string greeting = 1;}

message HelloResponse {string reply = 1;}

gRPC 允许定义四种服务方法:

  • 一元 RPC,客户端向服务器发送单个请求并获得单个响应,就像普通函数调用一样。
rpc SayHello(HelloRequest) returns (HelloResponse){}
  • 服务器流式 RPC,客户端向服务器发送请求,并获取流以读取回一系列消息。客户端从返回的流中读取,直到没有更多消息为止。gRPC 保证单个 RPC 调用中的消息顺序。
rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse){}
  • 客户端流式 RPC,客户端使用提供的流写入消息序列然后将它们发送到服务器。客户端写完消息后,它将等待服务器读取消息并返回响应。gRPC 保证了在单个 RPC 调用中的消息顺序。
rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse) {}
  • 双向流式 RPC,双方都使用读写流发送一系列消息。这两个流是独立运行的,因此客户端和服务器可以按照自己喜欢的顺序进行读写:例如,服务器可以在写响应之前等待接收完所有客户端消息,或者可以先读取一条消息再写入一条消息,或其他一些读写组合。每个流中的消息顺序都会保留。
rpc BidiHello(stream HelloRequest) returns (stream HelloResponse){}

在下面的 RPC 生命周期章节我们会更详细的比较这几种不同的 RPC。

使用 API 界面

.proto 文件中的服务定义开始,gRPC 提供了 protocol buffer 编译器插件,插件可生成客户端和服务器端代码。gRPC 用户通常在客户端调用这些 API,并在服务器端实现相应的 API。

  • 在服务侧,服务器实现服务中声明的方法并运行一个 gRPC 服务器来处理客户端的调用。gRPC 的基础设施解码传入的请求,执行服务的方法,编码服务的响应。
  • 在客户端,客户端拥有一个名为 stub(存根)的本地对象(在有些语言中更倾向于把 stub 叫做客户端)该对象同样实现了服务中方法。客户端可以只在本地对象上调用这些方法,将调用参数包装在适当的 protocol buffer 消息类型中,gRPC 会负责将请求发送给服务器并且返回服务端的 protocol buffer 响应。

同步 vs 异步

同步 RPC 调用会阻塞当前线程直到服务器收到响应为止,这是最接近 RPC 所追求的过程调用抽象的近似方法。另一方面,网络本质上是异步的,并且在许多情况下能够启动 RPC 而不阻塞当前线程很有用。

大多数语言中的 gRPC 编程界面都有同步和异步两种形式。可以在每种语言的教程和参考文档中找到更多信息。

RPC 生命周期

现在让我们具体看一下当一个 gRPC 客户端调用了一个 gRPC 服务器的方法后都发生了什么。我们不会查看具体实现细节,留到后面的编程语言教程中再看实现细节。

一元 RPC

首先来看一个最简单的 RPC 类型,客户端发送一个请求然后接受一个响应。

  • 一旦客户端调用了存根 / 客户端对象上的方法,服务器会被通知 RPC 已经被调用了,同样会接收到调用时客户端的元数据、调用的方法名称以及制定的截止时间(如果适用的话)。
  • 然后,服务器可以立即发送自己的初始元数据(必须在发送任何响应之前发送),也可以等待客户端的请求消息 - 哪个先发生应用程序指定的。
  • 服务器收到客户的请求消息后,它将完成创建和填充其响应所需的必要工作。然后将响应(如果成功)连同状态详细信息(状态代码和可选状态消息)以及可选尾随元数据一起返回。
  • 如果状态是 OK,客户端将获得响应,从而在客户端完成并终结整个调用过程。

服务器流式 RPC

一个服务器流式 RPC 与简单的一元 RPC 类似,不同的是服务器在接收到客户端的请求消息后会发回一个响应流。在发送回所有的响应后,服务器的状态详情(状态码和可选的状态信息)和可选的尾随元数据会被发回以完成服务端的工作。客户端在接收到所有的服务器响应后即完成操作。

客户端流式 RPC

客户端流式 RPC 也类似于一元 PRC,不同之处在于客户端向服务器发送请求流而不是单个请求。服务器通常在收到客户端的所有请求后(但不一定)发送单个响应,以及其状态详细信息和可选的尾随元数据。

双向流式 RPC

在双向流式 RPC 中,调用再次由客户端调用方法发起,服务器接收客户端元数据,方法名称和期限。同样,服务器可以选择发回其初始元数据,或等待客户端开始发送请求。

接下来发生的情况取决于应用程序,因为客户端和服务器可以按任何顺序进行读取和写入 - 流操作完全是独立地运行。因此,例如,服务器可以等到收到所有客户端的消息后再写响应,或者服务器和客户端可以玩“乒乓”:服务器收到请求,然后发回响应,然后客户端发送基于响应的另一个请求,依此类推。

截止时间 / 超时时间

gRPC 允许客户端指定在 RPC 被 DEADLINE_EXCEEDED 错误终结前愿意等待多长时间来让 RPC 完成工作。在服务器端,服务器可以查看一个特定的 RPC 是否超时或者还有多长时间剩余来完成 RPC。

如何指定期限或超时的方式因语言而异 - 例如,并非所有语言都有默认期限,某些语言 API 按照期限(固定的时间点)工作,而某些语言 API 根据超时来工作(持续时间)。

RPC 终止

在 gRPC 中,客户端和服务端对调用是否成功做出独立的基于本地的决定,而且两端的结论有可能不匹配。这意味着,比如说,你可能会有一个在服务端成功完成(“我已经发送完所有响应了”)但是在客户端失败(“响应是在我指定的 deadline 之后到达的”)的 RPC。服务器也有可能在客户端发送所有请求之前决定 RPC 完成了。

取消 RPC

客户端或服务器都可以随时取消 RPC。取消操作将立即终止 RPC,因此不再进行任何工作。这不是“撤消”:取消之前所做的更改不会回滚。

元数据

元数据是以键值对列表形式提供的关于特定 RPC 调用的信息(比如说身份验证详情),其中键是字符串,值通常来说是字符串(但是也可以是二进制数据)。元数据对 gRPC 本身是不透明的 - 它允许客户端向服务器提供与调用相关的信息,反之亦然。

对元数据的访问取决于语言。

通道

一个 gRPC 通道提供了一个到指定主机和端口号的 gRPC 服务器的连接,它在创建客户端存根(或者对某些语言来说就是“客户端”)时被使用。客户端可以指定通道参数来更改 gRPC 的默认行为,比如说打开 / 关闭消息压缩。每个通道都有状态,状态包括 connectedidle(闲置)

gRPC 怎么处理关掉的通道是语言相关的,有些语言还允许查询通道的状态。

退出移动版