译者:无明
服务间的通信方式是在采用微服务架构时需要做出一个最基本的决策。默认的选项是通过 HTTP 发送 JSON,也就是所谓的 REST API。我们也是从 REST 开始的,但最近我们决定改用 gRPC。
gRPC是谷歌开发的一个远程调用框架,现在已开源。尽管它已经出现了多年,但网上关于人们为什么要用它或者为什么不用它的信息并不多。于是,我决定写这篇文章分享一下我们为什么要使用 gRPC。
gPRC 的一个很明显的优势是它使用了二进制编码,所以它比 JSON/HTTP 更快。虽然说速度越快越好,但我们也要考虑另外两个因素:清晰的接口规范和对流式传输的支持。
gRPC 的接口规范
创建 gRPC 服务的第一步是在.proto 文件中定义好接口。下面的代码是一个接口的定义,它定义了一个简单的远程过程调用”Lookup“以及相应的输入和输出类型。
复制代码
代码语言:javascript复制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 或提供 API 服务的代码。
那么,为什么说这个接口定义其实不算是额外的工作量反而是件好事?看看上面的代码,即使你之前从来没有使用过 gRPC 或者 Protocol Buffer,也能轻松读懂它。比如,如果要发送一个 Lookup 请求,你需要发送 name 字符串,然后会接收到由 latitude 和 longitude 组成的 Coordinate 对象。实际上,因为你已经在.proto 文件中加入了一些简单的注释,所以它也可以作为服务的 API 文档来使用。
当然,真正的服务定义规范比这个要长得多,但也不会太复杂,只是会多一些用于定义方法的 rpc 语句和一些用于定义数据类型的 message 语句。
通过 protoc 编译器生成的代码可以确保客户端发送或服务器端接收到的数据是遵循规范的,这样非常有助于调试。我记得有两次我开发的服务因为格式没有经过验证而生成了错误的 JSON 数据,这些问题只会在用户界面上表现出来。要想找出问题的根源,我们只能调试前端 JavaScript 代码,而这对于一个不太熟悉 JavaScript 框架的后端开发人员来说这并不容易!
Swagger/OpenAPI
当然,如果使用的是 JSON/HTTP,Swagger或者OpenAPI也提供了类似的东西。下面的例子与上述的 gRPC API 相当。
复制代码
代码语言:javascript复制openapi: 3.0.0
info:title: A simplified version of fromAtoB’s backend APIversion: '1.0'
paths:/lookup:get:description: Look up the coordinates for a city by name.parameters:- in: queryname: nameschema:type: stringdescription: City name.responses:'200':description: OKcontent:application/json:schema:$ref: '#/components/schemas/Coordinate''404':description: Not Foundcontent:text/plain:schema:type: string
components:schemas:Coordinate:type: objectdescription: A Coordinate identifies a location on Earth by latitude and longitude.properties:latitude:type: numberdescription: Latitude is the degrees latitude of the location, in the range [-90, 90].longitude:type: numberdescription: Longitude is the degrees longitude of the location, in the range [-180, 180].
相比 gRPC,OpenAPI 的定义更难懂,也更啰嗦,结构也更复杂(8 层的缩进)。
OpenAPI 的规范验证比 gRPC 要困难一些,至少对于内部服务来说。随着 API 的不断演化,如果不去更新规范,它就会变得毫无用处。
流式传输
今年早些时候,我开始为我们的搜索服务设计一个新的 API。在我使用 JSON/HTTP 设计了第一版 API 之后,我的一个同事告诉我说,在某些情况下,我们需要流式传输搜索结果,也就是在有第一批结果时就开始传输。而我之前设计的 API 只返回一个单独的 JSON 数组,在服务器端收集到所有结果之前是不会向客户端发送任何数据的。
我们的 API 要求客户端轮询搜索结果,先是发送一个 POST 请求发起搜索,然后再不断发送 GET 请求获取搜索结果。响应消息中包含了一个用于表示搜索是否已完成的字段。这种方式虽然没有什么问题,但还不够优雅,而且要求服务器端将中间结果保存在数据存储(如 Redis)中。
这个时候,我们决定试一试 gRPC。要通过 gRPC 发送结果,只需要在.proto 文件中加入 stream 关键字。下面是我们的 Search 函数定义:
复制代码
代码语言:javascript复制rpc Search (SearchRequest) returns (stream Trip) {}
使用 protoc 编译器生成的代码中包含了一个对象,这个对象有一个 Send 函数,我们的服务器端代码将调用这个函数将 Trip 对象一个接一个地发送出去。代码中还包含了一个 Recv 函数,客户端代码通过调用这个函数来接收 Trip 对象。从开发者的角度来看,这比实现轮询 API 要简单得多。
注意事项
gRPC 也有一些不足之处,不过它们都与相关的开发工具有关,并不是 gRPC 本身的问题。
如果我们使用 JSON/HTTP 开发 API,就可以使用 curl、httpie 或者 Postman 进行简单的手动测试。gRPC 也有一个类似的工具叫作grpcurl,不过它使用起来并不是很方便,你要么需要在服务器端添加gRPC 服务器反射插件,要么需要在每个命令后面附上.proto 文件。
另一个是 Kubernetes 负载均衡器问题,负载均衡器可以支持 HTTP,但对 gPRC 支持得并不好。gPRC 要求应用层的负载均衡,而不是在 TCP 连接层。为了解决这个问题,我们参考了这篇文章搭建了 Linkerd。
结论
尽管开发 gRPC API 在前期需要做更多的工作,但拥有清晰的 API 定义和对流式传输的支持对我们来说更重要。在构建新的内部服务时,gRPC 将会是我们的首选。