原文:https://eng.fromatob.com/post/2019/05/why-were-switching-to-grpc/
当您使用微服务风格的体系结构时,您需要做出的一个非常基本的决定是: 您的服务如何相互通信? 默认的选择似乎是使用所谓的 REST APIs 通过 HTTP 发送 JSON,尽管大多数人并不认真对待 REST 原则。 在 fromAtoB,我们就是这样开始的,但是最近我们决定让 gRPC 成为我们的标准。
gRPC 是一个用于远程过程调用的系统,由 Google 开发,现在是开源的。 虽然它已经存在了几年,但是我在网上还没有找到很多关于为什么人们使用它或者不使用它的信息,所以我决定写一篇文章来解释我们使用 gRPC 的原因。
gRPC 的明显优势在于它使用了高效的二进制编码,这可以使它比 json / http 更快。 虽然更快的速度总是受欢迎的,但有两个方面对我们来说更重要: 清晰的接口规范和对流媒体的支持。
gRPC 接口规范
创建新的 gRPC 服务时,第一步始终是在文件.proto中定义接口。 下面的代码展示了它的样子——它是我们自己 API 的一小部分的简化版本。 该示例定义单个远程过程调用”Lookup”,并为其输入和输出定义类型。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
syntax = "proto3";
package fromatob;
// FromAtoB is a simplified version of fromAtoB’s backend API.
service FromAtoB {
rpc Lookup(LookupRequest) returns (Coordinate) {}
}
// A LookupRequest is a request to look up the coordinates for a city by name.
message LookupRequest {
string name = 1;
}
// A Coordinate identifies a location on Earth by latitude and longitude.
message Coordinate {
// Latitude is the degrees latitude of the location, in the range [-90, 90].
double latitude = 1;
// Longitude is the degrees longitude of the location, in the range [-180, 180].
double longitude = 2;
}
使用这个文件,您可以使用 protoc 编译器生成客户端和服务端代码,并且可以开始编写提供或使用 API 的代码。
那么,为什么这是一件好事,而不仅仅是额外的工作? 再看一下上面的代码示例。 即使你从来没有使用过 gRPC 或者 Protocol Buffers,它的可读性也是很好的: 例如,很明显要发出一个查找请求Lookup,你应该发送一个名字name,这是一个字符串,然后你会得到一个坐标Coordinate,这个坐标由 经纬度组成。 事实上,一旦你添加了一些简单的注释,比如在例子中,.proto 文件是服务的 API 文档。
当然,实际服务的规范可以大得多,但也不会复杂得多。 只是会相应增加一些针对方法的rpc声明和针对数据类型的message声明。
由 protoc 生成的代码还将确保客户端或服务器发送的数据符合规范。 这对调试非常有帮助。 我记得在两个实例中,我正在处理的服务以错误的格式生成 JSON 数据,因为这种格式在任何地方都没有验证,所以问题只出现在用户界面中。 找出问题所在的唯一方法是调试 JavaScript 前端代码——如果你是一个从未使用过前端使用的 JavaScript 框架的后端开发人员,那就不那么容易了!
Swagger / OpenAPI
原则上,您可以使用 Swagger 或其继承者 OpenAPI 为 HTTP/JSON APIs 获得同样的优势。 下面是一个与上面的 gRPC API 等价的例子: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
43openapi: 3.0.0
info:
title: A simplified version of fromAtoB’s backend API
version: '1.0'
paths:
/lookup:
get:
description: Look up the coordinates for a city by name.
parameters:
- in: query
name: name
schema:
type: string
description: City name.
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/Coordinate'
'404':
description: Not Found
content:
text/plain:
schema:
type: string
components:
schemas:
Coordinate:
type: object
description: A Coordinate identifies a location on Earth by latitude and longitude.
properties:
latitude:
type: number
description: Latitude is the degrees latitude of the location, in the range [-90, 90].
longitude:
type: number
description: Longitude is the degrees longitude of the location, in the range [-180, 180].
与上面的 gRPC 规范相比较, OpenAPI 一是很难阅读! 它更加冗长,结构更加复杂(有八个级别的缩进而不是一个级别)。
使用 OpenAPI 规范进行验证也比使用 gRPC 更加困难。 至少对于内部服务来说,这意味着 specs 要么不被编写,要么不被更新,随着 API 的发展变得毫无用处。
流媒体
今年早些时候,我开始为我们的搜索设计一个新的 API。 在我用 HTTP 和 JSON 构建了 API 的第一个版本之后,我的一位同事指出,在某些情况下,我们需要流式结果,这意味着我们应该在得到第一个结果后立即开始发送。 我的 API 只返回一个 JSON 数组,因此服务器在收集所有结果之前不能发送任何内容。
在前端使用的 API 中,我们所做的是让客户端对结果进行轮询。 它们发送 POST 请求来设置搜索,然后发送重复的 GET 请求来检索结果。 响应包含一个字段,用于指示搜索是否完成。 这种方法工作得很好,但并不十分优雅,它要求服务器使用类似 Redis 的数据存储来保存中间结果。 新的 API 将由多个较小的服务实现,我不想强迫所有的服务实现这个逻辑。
就在那时,我们决定尝试使用 gRPC。 要使用 gRPC 发送远程过程调用的结果,只需在.proto文件中添加stream。 下面是搜索函数的定义:rpc Search (SearchRequest) returns (stream Trip) {}
由 protoc 编译器生成的代码包括一个具有 Send 函数的对象和一个具有 Recv 函数的对象,前者被服务器代码调用来逐个发送 Trip 对象,后者被客户端代码调用来检索它们。 从程序员的角度来看,这比实现轮询 API 要容易得多。
注意事项
我想提一下 gRPC 的一些缺点。 它们都与工具有关,而与协议本身无关。
在使用 http / json 构建 API 时,可以使用 curl、 httpie 或 Postman 进行简单的手动测试。 有一个类似的 gRPC 工具叫做 grpcurl,但是它并不是无缝的: 你要么在服务器端添加 gRPC 服务器反射扩展,要么针对每个命令指定.proto文件。 我们发现在服务器中包含一个小的命令行实用程序更方便,它允许您进行简单的请求。 由 protoc 生成的客户机代码实际上使这个过程变得非常简单。
对我们来说,一个更大的问题是,我们正在 HTTP 服务中使用的 Kubernetes 负载均衡器不能很好地适用于 gRPC。 基本上,gRCP 需要在应用程序级别而不是 TCP 连接级别进行负载平衡。 为了解决这个问题,我们按照本教程设置了 Linkerd:https://kubernetes.io/blog/2018/11/07/grpc-load-balancing-on-kubernetes-without-tears/
总结
虽然构建 gRPC API 需要更多的前期工作,但我们发现拥有清晰的 API 规范和良好的流媒体支持可以弥补这一缺陷。 对我们来说,gRPC 将是我们构建的任何新内部服务的默认选项。