APP下载

我们为什么从 REST 转向 gRPC?

消息来源:baojiabao.com 作者: 发布时间:2026-05-14

报价宝综合消息我们为什么从 REST 转向 gRPC?

服务间的通讯方式是在采用微服务架构时需要做出一个最基本的决策。预设的选项是通过 HTTP 传送 JSON,也就是所谓的 REST API。我们也是从 REST 开始的,但最近我们决定改用 gRPC。gRPC是Google开发的一个远端呼叫框架,现在已开源。尽管它已经出现了多年,但网上关于人们为什么要用它或者为什么不用它的资讯并不多。于是,我决定写这篇文章分享一下我们为什么要使用 gRPC。

gPRC 的一个很明显的优势是它使用了二进位制编码,所以它比 JSON/HTTP 更快。虽然说速度越快越好,但我们也要考虑另外两个因素:清晰的界面规范和对流式传输的支援。

gRPC 的界面规范

建立 gRPC 服务的第一步是在.proto 档案中定义好界面。下面的程式码是一个界面的定义,它定义了一个简单的远端过程呼叫”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 或提供 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 相当。

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 层的缩排)。

OpenAPI 的规范验证比 gRPC 要困难一些,至少对于内部服务来说。随着 API 的不断演化,如果不去更新规范,它就会变得毫无用处。

流式传输

今年早些时候,我开始为我们的搜寻服务设计一个新的 API。在我使用 JSON/HTTP 设计了第一版 API 之后,我的一个同事告诉我说,在某些情况下,我们需要流式传输搜寻结果,也就是在有第一批结果时就开始传输。而我之前设计的 API 只返回一个单独的 JSON 阵列,在服务器端收集到所有结果之前是不会向客户端传送任何资料的。

我们的 API 要求客户端轮询搜寻结果,先是传送一个 POST 请求发起搜寻,然后再不断发送 GET 请求获取搜寻结果。响应讯息中包含了一个用于表示搜寻是否已完成的字段。这种方式虽然没有什么问题,但还不够优雅,而且要求服务器端将中间结果储存在资料储存(如 Redis)中。

这个时候,我们决定试一试 gRPC。要通过 gRPC 传送结果,只需要在.proto 档案中加入 stream 关键字。下面是我们的 Search 函式定义:

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 将会是我们的首选。

2020-01-22 10:54:00

相关文章