我们为什么要切换到 gRPC 呢?
背景
标题:Why We’re Switching to gRPC
发表时间:2019 年 5 月 27 日
原文链接
译者:毕小宝
最近发现翻译网页还是挺有趣的,各种翻译软件虽然能让人看懂文章的大意,但总感觉它们译出来的文字缺少点什么,所以英语学习还是有必要的!
在 NLP 已经很成熟的今天,我对不认识的英语单词还是保持着一种想要录入大脑的心态。不借助任何工具,就能明白一篇英文的含义,这本身会带给我们很多乐趣,就是那种 “10 年 英语没有白学”的成就感!
这是一篇有关 gRPC 的文章,gRPC ,由 google 开发,是一款语言中立、平台中立、开源的远程过程调用(RPC)系统
。我以前只知道 RPC ,还没听说过 gRPC ,它到底为什么会受到作者的青睐呢?
我们来看看原文。
前言
当你使用微服务架构时,你需要做出一个相当基础的决定:服务彼此之间如何通信?尽管大多数人并不会认真对待 REST 原则,然而默认的选择似乎就是所谓的 REST APIs ——即通过 HTTP 协议发送 JSON 数据。最开始我们也是这样选择的,但是,最近我们决定使用 gRPC 作为我们的通信标准了。
gRPC 是由 Google 开发的一个远程过程调用系统,目前是开源状态。尽管它已经存在好几年了,然而我并没有在网上找到多少关于 “人们为什么使用或者不使用 gRPC ” 的原因,所以我决定写一篇文章来解释一下我们使用 gRPC 的理由。
“使用一种高效的二进制编码”是 gRPC 最明显的优势,这使得它比 JSON/HTTP 快很多。更高的速度提升当然是受欢迎的,于我们而言,选择它还有两个重要的原因:明确的接口规范和对于流的支持。
gRPC 的接口规范
当你创建一个新的 gRPC 服务时,第一步操作通常是在一个叫 .proto
的文件中定义接口。接下来将有一段代码展示该文件的样子——它是我们的 API 简化后的一小部分。这个例子定义了一个名叫 "LoopUp"的单个远程过程的和它的输入输出类型。
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 或者协议缓存,也能明白它的含义,因为它的可读性是相当好的。比如,它清楚地描述了一件事情:为了构建一个 Lookup
请求,你应该发送一个 name
参数,它的类型是字符串,并且你会得到一个包含 latitude
和 longitude
的结果Coordinate
。事实上,一旦你像例子中那样,添加一些简单的注释,这个 .proto
文件就是你的服务的 API 文档。
一个真正的服务规范文件可能会更大,当然,它并不会太复杂。仅仅是为方法添加更多的 rpc
语句以及为数据类型添加一些消息语句罢了!
通过 protoc
生成的代码也将保证发送给客户端或者服务器端的数据是与规范一致的。这对调试来说将是巨大的帮助。我记得两次踩坑经历,都是由于我们的服务所生成的 JSON 数据格式错误,而且没有在任何地方进行校验,以至于问题最后只会出现在用户界面上。定位错误的唯一方法就是调试前端代码——对于一个后端开发者、尤其是从来没有使用过 JS 框架的后端开发者而言,这并不是一件容易的事情!
Swagger / OpenAPI
总的来说,使用 Swagger 或者它的后来者 OpenAPI 的话,你也能从 HTTP/JSON APIs 中获得相同的优势。下面的描述跟 gRPC API 是等价的:
ple equivalent to the gRPC API above:
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 的可读性就逊色很多!它表现得更冗余,结构也更复杂(缩进就有 8 级,而不是 gRPC 的一级)。【译者补充:从简洁性来看,缩进层级过多,也就更复杂,可能在编写的过程中就漏掉了某一级的缩进而出错了。】
Streaming
今年的早些时候,我开始为我们的搜索服务(它的作用是什么呢,类似于这个场景:把2019年6月1日从柏林到巴黎的所有线路都给我)设计一个新的 API。在我使用 HTTP 和 JSON 构建了第一个版本的 API 之后,我的一位同事指出:在某些情况下,我们需要使用流结果,这意味着我们一收到第一个结果就应该发送出去。我的 API 仅能返回单个 JSON 数组,因此服务器在收到所有结果之前都不能发送任何数据。
为了使用 API ,我们在前端采用的是客户端轮询的方式。客户端发送一个 POST 请求来建立一个搜索,然后再发送相同的 GET 请求取回结果。这个响应包含一个标识搜索是否完成的字段。这样运行的很好,但是并不优雅,它需要服务端使用一个数据存储组件来存储中间结果,如 Redis 。新的 API 将会由多个更小规模的服务来实现,而且我也不想强制它们都实现这个逻辑。
也就是在这个时候,我们决定尝试 gRPC 。使用 gRPC 发送结果给远程程序,你仅仅需要在 .proto
文件中添加 stream
关键字。这是我们搜索函数的定义:
rpc Search (SearchRequest) returns (stream Trip) {}
通过 protoc
生成的代码包含两个对象:第一个对象有一个 Send
函数,服务器端的代码用它来逐个调用 Trip
对象;另一个对象包含一个 Recv
函数,客户端可以检索它们。从程序员的角度来看,这比实现一个轮询 API 容易多了!
注意事项
这里我想提一下 gRPC 的几个缺点,它们都与工具有关,而不是基于协议本身的。
当你使用 HTTP/JSON 构建 API 时,你可以使用 curl 、httpie 或者 Postman 等任何工具进行简单的人工测试。gRPC 也有类似的工具,叫做 grpcurl ,但是它并不是无缝衔接的:你要么在服务端添加 gRPC 服务器反射扩展应用 ,要么为每个命令指定 .proto
文件。我们发现,在服务器端加入一个小的命令行工具可以让你很便捷地发送简单的请求。通过 protoc
生成的客户端代码实际上让构建 API 变得非常简单。
于我们而言,有一个大问题,就是我们的 HTTP 服务使用了 Kubernetes 负载均衡,这对 gRPC 来说并不能很好地工作。基本上,gRPC 需要应用层的负载均衡,而非 TCP 连接层的。为了解决这个问题,我们参考这篇使用 Kubernetes 实现 gRPC 负载均衡手册安装了 linkerd 。
总结
尽管构建 gRPC API 需要做更多的前期工作,但是我们发现,清晰的 API 规范以及对流的良好支持可以弥补这一点。对于我们来说,gRPC 将是构建任何新内部服务的默认选项。