作者:Levin Fritz
当你使用微服务风格的体系结构时,你需要做的一个非常基本的决定是:你的服务如何相互通信?默认的选择似乎是通过HTTP发送JSON — 使用所谓的REST API,尽管大多数人不太重视REST原则。我们在fromAtoB就是这样开始的,但最近我们决定将gRPC作为我们的标准。
gRPC是一个用于远程过程调用的系统,由谷歌开发,现在是开源的。虽然它已经存在好几年了,但是我还没有在网上找到很多关于人们为什么使用或不使用它的信息,所以我决定写一篇文章来解释我们使用gRPC的原因。
gRPC的明显优势是它使用了一种高效的二进制编码,这使得它比JSON/HTTP更快。虽然速度更快总是受欢迎的,但是有两个方面对我们来说更重要:清晰的接口规范和对流的支持。
gRPC接口规范
当你创建一个新的gRPC服务时,第一步总是在.proto文件中定义接口。下面的代码展示了它的样子 — 它是我们自己的API的一小部分的简化版本。该示例定义了单个远程过程调用“Lookup”及其输入和输出类型。
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,它是一个字string,你会得到一个Coordinate,它包含latitude和longitude。实际上,一旦你添加了一些简单的注释,例如在本例中,.proto文件就是你的服务的API文档。
当然,实际服务的规范可以更大,但不会更复杂。它只是更多的用于方法的rpc语句和用于数据类型的message语句。
由protoc生成的代码还将确保客户机或服务器发送的数据符合规范。这对调试有很大的帮助。我记得有两个实例,其中我正在处理的服务生成的JSON数据格式错误,而且由于该格式没有在任何地方进行验证,因此问题只出现在用户界面中。找出问题的唯一方法是调试JavaScript前端代码 — 如果你是一个从未使用过前端使用的JavaScript框架的后端开发者,那么调试JavaScript前端代码就不那么容易了!
Swagger/OpenAPI
原则上,使用Swagger或它的后续OpenAPI,你可以为HTTP/JSON API获得相同的好处。下面是一个与上面的gRPC API相同的例子:
openapi: 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更加困难。至少对于内部服务,这意味着要么没有编写规范,要么没有更新规范,随着API的发展,这些规范将变得毫无用处。
流处理
今年早些时候,我开始为我们的搜索设计一个新的API(想想“2019年6月1日给我从柏林到巴黎的所有连接”)。在我用HTTP和JSON构建了API的第一个版本之后,我的一个同事指出,在某些情况下,我们需要对结果进行流处理,这意味着我们应该在收到第一个结果时就开始发送它们。我的API只返回了一个JSON数组,所以服务器在收集所有结果之前不能发送任何东西。
我们在前端使用的API中所做的是让客户端轮询结果。它们发送POST请求来设置搜索,然后发送重复的GET请求来检索结果。响应包含一个字段,该字段指示搜索是否完成。这可以很好地工作,但不优雅,并且需要服务器使用诸如Redis之类的数据存储来保存中间结果。新的API将由多个较小的服务实现,我不想强迫它们都实现这个逻辑。
那时我们决定试用gRPC。要用gRPC发送远程过程调用的结果,只需在.proto文件中添加stream关键字。这是我们的Search函数的定义:
rpc Search (SearchRequest) returns (stream Trip) {}
由protoc编译器生成的代码包括一个带有Send函数的对象,服务器代码调用该函数来逐个发送Trip对象,和一个带有Recv函数的对象,客户机代码调用该函数来检索它们。从程序员的角度来看,这比实现轮询API要容易得多。
注意事项
我想提一下gRPC的几个缺点。它们都与工具有关,而不是协议本身。
使用HTTP/JSON构建API时,可以使用curl、httpie或Postman进行简单的手工测试。gRPC也有一个类似的工具,名为grpcurl,但它并不是无缝的:你必须在服务器端添加gRPC服务器反射扩展名,或者在每个命令上指定.proto文件。我们发现在服务器中包含一个小的命令行实用程序更方便,它允许你进行简单的请求。由protoc生成的客户机代码实际上使这变得非常简单。
对我们来说,一个更大的问题是Kubernetes负载平衡器(用于HTTP服务)在gRPC上不能很好地工作。基本上,gRCP需要应用程序级的负载平衡,而不是TCP连接级的负载平衡。为了解决这个问题,我们按照本教程的指导建立了Linkerd:Kubernetes无痛作gRPC负载平衡。
https://kubernetes.io/blog/2018/11/07/grpc-load-balancing-on-kubernetes-without-tears/
结论
尽管构建gRPC API需要更多的前期工作,但是我们发现,拥有清晰的API规范和对流的良好支持可以弥补这一点。对于我们来说,gRPC将是我们构建的任何新的内部服务的默认选项。