TLDR:使用 GraphQL 进行客户端-服务器通信,使用 gRPC 进行服务器到服务器通信。有关此规则的例外情况,请参阅“判定”部分。
我已经阅读了很多关于这两种协议的比较,并想写一个全面和公正的协议。(好吧,就像我和我的审稿人所能做到的那样公正
。我的灵感来自connect-web(一个可以在浏览器中使用的TypeScript gRPC客户端)的发布,以及一篇名为GraphQL的流行HN帖子有点糟糕。我个人在第7 层之上构建的通信协议历史:
REST(Rails and Express)
DDP(Meteor's WebSocket Protocol)
GraphQL(我写了一本书)
gRPC(我在Temporal 使用))
背景
gRPC由 Google 于 2016 年发布,是一种高效且开发人员友好的服务器到服务器通信方法。GraphQL由 Meta 于 2015 年发布,作为一种高效且开发人员友好的客户端-服务器通信方法。它们都比 REST 具有显着的优势,并且有很多共同点。我们将用大部分时间比较它们的特征,然后总结每个协议的优点和缺点。最后,我们将知道为什么每个都非常适合其预期域,以及何时可能希望在另一个域中使用一个。
比较 gRPC 和 GraphQL 特性
界面设计
gRPC 和 GraphQL 都是接口描述语言 (IDL),用于描述两台计算机如何相互通信。它们跨不同的编程语言工作,我们可以使用 codegen 工具来生成多种语言的类型化接口。IDL 抽象出传输层;GraphQL 与传输无关,但通常通过 HTTP 使用,而 gRPC 使用 HTTP/2。我们不需要了解传输级的详细信息,例如 REST 中的方法、路径、查询参数和正文格式。我们只需要知道使用更高级别的客户端库与之通信的单个端点。
消息格式
邮件大小很重要,因为较小的邮件通常通过网络发送所需的时间较短。gRPC 使用协议缓冲区(又名 protobufs),这是一种只包含值的二进制格式,而 GraphQL 使用 JSON,它是基于文本的,除了值之外还包括字段名称。二进制格式与较少的信息相结合通常会导致 gRPC 消息小于 GraphQL 消息。(虽然高效的二进制格式在 GraphQL 中是可行的,但它很少使用,并且不受大多数库和工具的支持。
影响消息大小的另一个方面是过度获取:我们是可以只请求特定字段还是始终接收所有字段(我们不需要的“过度提取”字段)。GraphQL 总是在请求中指定需要哪些字段,而在 gRPC 中,我们可以将FieldMask用作请求的可重用过滤器。
gRPC 二进制格式的另一个好处是,与 GraphQL 的文本消息相比,消息的序列化和解析速度更快。缺点是它比人类可读的 JSON 更难查看和调试。我们默认使用 protobuf 的JSON 格式,以获得开发人员体验的可见性优势。(这失去了二进制格式带来的效率,但更重视效率的用户可以切换到二进制。
违约
gRPC 也不在消息中包含默认值,GraphQL 可以对参数执行此操作,但不能对请求字段或响应类型执行此操作。这是 gRPC 消息较小的另一个因素。它还会影响使用 gRPC API 的 DX。将输入字段保留为未设置和将其设置为默认值之间没有区别,默认值基于字段的类型。所有布尔值默认为 false,所有数字和枚举默认为 0。我们不能将“behavior”枚举输入字段默认为“BEHAVIOR_FOO = 2”——我们必须将默认值放在第一位(“BEHAVIOR_FOO = 0”),这意味着它将来将始终是默认值,或者我们遵循推荐的做法,即使用“BEHAVIOR_UNSPECIFIED = 0”枚举值:
enum Behavior { BEHAVIOR_UNSPECIFIED = 0; BEHAVIOR_FOO = 1; BEHAVIOR_BAR = 2; }
API 提供者需要传达 whatmeans(通过记录“未指定将使用默认行为,这是当前”),消费者需要考虑服务器默认行为将来是否会发生变化(如果服务器将 offer/ 0 值保存在消费者正在创建的某个业务实体中,并且服务器稍后更改了默认行为, 实体将开始以不同的方式行事)以及是否需要这样做。如果不需要,客户端需要将值设置为当前默认值。下面是一个示例方案:UNSPECIFIEDFOOUNSPECIFIED
service ExampleGrpcService { rpc CreateEntity (CreateEntityRequest) returns (CreateEntityResponse) {} } message CreateEntityRequest { string name = 1; Behavior behavior = 2; }
如果我们这样做:
const request = new CreateEntityRequest({ name: “my entity” }) service.CreateEntity(request)
我们将发送,根据服务器实现和未来的更改,这可能意味着现在和以后。或者我们可以做:BEHAVIOR_UNSPECIFIEDBEHAVIOR_FOOBEHAVIOR_BAR
const request = new CreateEntityRequest({ name: “my entity”, behavior: Behavior.BEHAVIOR_FOO }) service.CreateEntity(request)
可以肯定的是,该行为被存储为“将保留”。FOOFOO
等效的 GraphQL 模式将是:
type Mutation { createEntity(name: String, behavior: Behavior = FOO): Entity } enum Behavior { FOO BAR }
当我们不在请求中包含时,服务器代码将接收并将 FOO 存储为值,与上述架构中的默认值匹配。behavior= FOO
graphqlClient.request(` mutation { createEntity(name: “my entity”) } `
知道如果未提供字段会发生什么情况,它比 gRPC 版本更简单,我们不需要考虑是否自己传递默认值。
其他类型的默认值还有其他怪癖。对于数字,有时默认值 0 是有效值,有时表示不同的默认值。对于布尔值,默认的 false 会导致负命名字段。当我们在编码时命名布尔变量时,我们使用正名称。例如,我们通常会声明而不是。人们通常发现前者更具可读性,因为后者需要额外的步骤来理解双重否定(“是,所以它是可重试的”)。但是,如果我们有一个 gRPC API,我们希望默认状态是可重试的,那么我们必须命名该字段,因为 anfield 的默认值将是,就像 gRPC 中的所有布尔值一样。let retryable = truelet nonRetryable = falsenotRetryablefalsenonRetryableretryablefalse
请求格式
在 gRPC 中,我们一次调用一个方法。如果我们需要的数据多于单个方法提供的数据,则需要调用多个方法。如果我们需要来自第一个方法的响应数据,以便知道下一步要调用哪个方法,那么我们将连续进行多次往返。除非我们与服务器位于同一数据中心,否则会导致明显的延迟。此问题称为获取不足。
这是 GraphQL 旨在解决的问题之一。在高延迟移动电话连接中,能够在单个请求中获取所需的所有数据尤为重要。在 GraphQL 中,我们发送一个字符串(称为文档)以及我们的请求,其中包含我们要调用的所有方法(称为查询和突变)以及基于第一级结果所需的所有嵌套数据。某些嵌套数据可能需要从服务器到数据库的后续请求,但它们通常位于同一数据中心,该数据中心应具有亚毫秒级的网络延迟。
GraphQL 的请求灵活性让前端和后端团队变得不那么耦合。前端开发人员可以向其请求添加更多查询或嵌套结果字段,而不是前端开发人员等待后端开发人员向方法的响应添加更多数据(以便客户端可以在单个请求中接收数据)。当有一个覆盖组织整个数据图的 GraphQL API 时,前端团队在等待后端更改时被阻止的频率要低得多。
事实上,GraphQL 请求指定了所有需要的数据字段,这意味着客户端可以使用声明性数据获取:而不是命令性获取数据(如调用 'grpcClient.callMethod()“),我们在视图组件旁边声明我们需要的数据,GraphQL 客户端库将这些部分组合成一个请求,并在响应到达时和稍后数据更改时将数据提供给组件。Web 开发中视图库的并行是使用 React 而不是 jQuery:声明我们的组件应该是什么样子,并在数据更改时自动更新它们,而不是使用 jQuery 强制操作 DOM。
GraphQL 的请求格式的另一个影响是提高了可见性:服务器可以看到请求的每个字段。我们可以跟踪字段使用情况并查看客户端何时停止使用已弃用的字段,以便我们知道何时可以删除它们,而不是永远支持我们说要删除的内容。跟踪内置于Apollo GraphOS和Stellate等常用工具中。
向前兼容性
gRPC 和 GraphQL 都有很好的前向兼容性;也就是说,很容易在不破坏现有客户端的情况下更新服务器。这对于可能已过时的移动应用程序尤其重要,但对于在服务器更新后继续工作,用户浏览器选项卡中加载的SPA也是必需的。
在 gRPC 中,可以通过对字段进行数字排序、添加具有新编号的字段以及不更改现有字段的类型/编号来保持向前兼容性。在 GraphQL 中,您可以添加字段,使用 '@deprecated“' 指令弃用旧字段(并让它们正常工作),并避免更改必需的可选参数。
运输
gRPC 和 GraphQL 都支持服务器将数据流式传输到客户端:gRPC 具有服务器流式处理,GraphQL 具有订阅和指令 @defer,@stream 和 @live。gRPC 的 HTTP/2 还支持客户端和双向流式处理(尽管当一侧是浏览器时,我们不能进行双向流式处理)。HTTP/2 还通过多路复用提高了性能。
gRPC 在网络故障时具有内置的重试功能,而在 GraphQL 中,它可能包含在特定的客户端库中,例如 Apollo Client 的RetryLink。gRPC 也有内置的截止日期。
运输也有一些限制。gRPC 无法使用大多数在 HTTP 标头上运行的 API 代理,如Apigee Edge,当客户端是浏览器时,我们需要使用gRPC-Web 代理或Connect(虽然现代浏览器确实支持 HTTP/2,但没有浏览器API允许对请求进行足够的控制)。默认情况下,GraphQL 不适用于 GET 缓存:大部分 HTTP 缓存适用于GET 请求,大多数 GraphQL 库默认使用 POST。 GraphQL 有许多使用 GET 的选项,包括将操作放在查询参数中(当操作字符串不太长时可行)、构建时持久化查询(通常只与私有 API 一起使用), 和自动持久化查询。可以在字段级别提供缓存指令(整个响应中的最短值用于 Cache-Control 标头的“max-age”)。
架构和类型
GraphQL 有一个架构,服务器为客户端开发人员发布并用于处理请求。它定义了所有可能的查询和突变,以及所有数据类型及其相互关系(图形)。通过该架构,可以轻松合并来自多个服务的数据。GraphQL 具有模式拼接(命令性地将多个 GraphQL API 组合成一个代理部分模式的 API)和联合(每个下游 API声明如何关联共享类型,网关通过向下游 API 发出请求并组合结果来自动解析请求)的概念,用于创建超图(我们所有数据的图表,结合了较小的子图/部分模式)。还有一些库将其他协议代理到 GraphQL,包括 gRPC。
随着 GraphQL 的模式而来的是进一步发展的内省:能够以标准方式查询服务器以确定其功能。所有 GraphQL 服务器库都有内省功能,并且有基于内省的高级工具,如GraphiQL、使用graphql-eslint 的请求 linting 和Apollo Studio,其中包括具有字段自动完成、linting、自动生成的文档和搜索功能的查询 IDE。gRPC 具有反射,但它没有那么普遍,并且使用它的工具较少。
GraphQL 模式启用了反应式规范化客户端缓存:因为每个(嵌套)对象都有一个类型字段,所以类型在不同的查询之间共享,我们可以告诉客户端将哪个字段用作每种类型的 ID,客户端可以存储规范化的数据对象。这使高级客户端功能(如查询结果或触发更新的乐观更新)能够查看依赖于包含同一对象的不同查询的组件。
gRPC 和 GraphQL 类型之间存在一些差异:
gRPC 版本 3(截至撰写本文时的最新版本)没有必填字段:相反,每个字段都有一个默认值。在 GraphQL 中,服务器可以区分值存在和不存在 (null),并且架构可以指示参数必须存在或响应字段将始终存在。
在 gRPC 中,没有标准方法可以知道方法是否会改变状态(与 GraphQL,它分离查询和突变)。
映射在 gRPC 中受支持,但在 GraphQL 中不受支持:如果你的数据类型像 '{[key: string] : T}',你需要对整个事情使用 JSON 字符串类型。
GraphQL 的模式和灵活查询的一个缺点是,公共 API 的速率限制更为复杂(对于私有 API,我们可以将持久查询列入允许列表)。由于我们可以在单个请求中包含任意数量的查询,并且这些查询可以请求任意嵌套的数据,因此我们不能只限制来自客户端的请求数量或将成本分配给不同的方法。我们需要对整个操作实现成本分析速率限制,例如通过使用graphql-cost-analysis库对单个字段成本求和并将它们传递给泄漏桶算法。
总结
以下是我们涵盖的主题摘要:
gRPC 和 GraphQL 之间的相似之处
带代码生成的类型化接口
抽象出网络层
可以有 JSON 响应
服务器流式传输
良好的向前兼容性
可以避免过度提取
gRPC
优势
二进制格式:
通过网络更快地传输
更快的序列化、解析和验证
但是,比 JSON 更难查看和调试
HTTP/2:
多路复用
客户端和双向流式处理
内置重试和截止时间
弱点
需要代理或连接才能从浏览器使用
无法使用大多数 API 代理
没有标准方法可以知道方法是否会改变状态
图QL
优势
客户端确定它要返回哪些数据字段。结果:
无欠取
团队解耦
提高可见性
更轻松地合并来自多个服务的数据
进一步发展的内省和工具
声明性数据提取
反应式规范化客户端缓存
Weaknesses
If we already have gRPC services that can be exposed to the public, it takes more backend work to add a GraphQL server.
HTTP GET caching doesn’t work by default.
Rate limiting is more complex for public APIs.
Maps aren’t supported.
Inefficient text-based transport
Verdict
Server-to-server
In server-to-server communication, where low latency is often important, and more types of streaming are sometimes necessary, gRPC is the clear standard. However, there are cases in which we may find some of the benefits of GraphQL more important:
We’re using GraphQL federation or schema stitching to create a supergraph of all our business data and decide to have GraphQL subgraphs published by each service. We create two supergraph endpoints: one external to be called by clients and one internal to be called by services. In this case, it may not be worth it for services to also expose a gRPC API, because they can all be conveniently reached through the supergraph.
We know our services’ data fields are going to be changing and want field-level visibility on usage so that we can remove old deprecated fields (and aren’t stuck with maintaining them forever).
还有一个问题是,我们是否应该自己进行服务器到服务器的通信。对于数据获取(GraphQL 的查询),这是获得响应的最快方式,但对于修改数据(突变),像 Martin Fowler 的“同步调用被认为是有害的”(见此处的侧栏)导致使用异步、事件驱动的架构,在服务之间编排或编排。微服务模式建议在大多数情况下使用后者,为了保持 DX 和开发速度,我们需要一个基于代码的业务流程协调程序,而不是基于 DSL 的业务流程协调程序。一旦我们使用像 Temporal 这样的基于代码的业务流程协调程序,我们就不再自己发出网络请求 — 平台会为我们可靠地处理它。在我看来,这就是未来。
客户端-服务器
在客户端-服务器通信中,延迟很高。我们希望能够在一次往返中获取所需的所有数据,灵活地为不同的视图获取哪些数据,并具有强大的缓存功能,因此 GraphQL 是明显的赢家。但是,在某些情况下,我们可能会选择改用 gRPC:
我们已经有一个可以使用的 gRPC API,在它前面添加一个 GraphQL 服务器的成本不值得。
JSON 不适合数据(例如,我们正在发送大量二进制数据)。
感谢Marc-André Giroux,Uri Goldshtein,Sashko Stubailo,Morgan Kestner,Andrew Ingram,Lenny Burdette,Martin Bonnin,James Watkins-Harvey,Josh Wise,Patrick Rachford和Jay Miller阅读本文的草稿。
-
Google
+关注
关注
5文章
1765浏览量
57537 -
二进制
+关注
关注
2文章
795浏览量
41654 -
服务器
+关注
关注
12文章
9165浏览量
85438 -
API
+关注
关注
2文章
1501浏览量
62035 -
GraphQL
+关注
关注
0文章
14浏览量
577
发布评论请先 登录
相关推荐
评论