译者 | 崔皓
审校 | 云昭
gRPC是由Google开发的一个高性能、通用的开源RPC框架,主要面向移动应用开发且基于HTTP/2协议标准而设计,同时支持大多数流行的编程语言。
GraphQL既是一种用于API的查询语言,且GraphQL对API中的数据提供了一套易于理解的完整描述,使得客户端能够准确地获得它需要的数据,而且没有任何冗余,也让API更容易地随着时间推移而演进,还能用于构建强大的开发者工具。
两者看起来并用途不相同,但其实在通信场景中很多开发者面临如何选择的问题。本文笔者带领大家从实用的角度去一一剖析gRPC与GraphQL的取舍之道!
一、开篇
本文主要介绍使用gRPC和GraphQL的时机,先说结论:推荐大家在客户端与服务器之间的通信场景中使用GraphQL,在服务器与服务器之间的通信场景使用gRPC。此外,文末会介绍关于例外情况的处理方式。
二、背景介绍
gRPC是由谷歌在2016年发布的,它是一种高效且对开发者友好的服务器间通信协议。GraphQL是由Meta公司在2015年发布的,是一种高效的、对开发者友好的客户端-服务器通信协议。两者都比REST有明显的优势,并且有很多共同点。
我们将在文章中花大量的篇幅比较它们的特点,然后总结每个协议的优点和缺点。最后,我们会描述每种协议都适合哪些特定的领域,以及什么时候会出现跨领域使用不同协议。
三、比较gRPC和GraphQL的功能
1.界面设计
gRPC和GraphQL都是界面描述语言(IDL),描述了两台计算机如何进行通信。它们适用于不同的编程语言,我们可以使用codegen工具来生成多种语言的类型化接口。IDL抽象出了传输层;GraphQL与传输无关,但通常工作在HTTP之上,而gRPC则工作在HTTP/2之上。我们不需要知道传输层的细节,比如REST中的方法、路径、查询参数和正文格式。我们只需要知道针对一个独立的节点,如何使用高级客户端库与之通信。
2.信息格式
gRPC使用协议缓冲区(又称protobufs),这是一种二进制格式,只包括数值(传递的内容),而GraphQL使用JSON,它是基于文本的,除了数值外还包括字段名(数值的含义)。二进制格式与较少的信息发送相结合,通常导致gRPC消息比GraphQL消息小。(虽然高效的二进制格式在GraphQL中是可行的,但它很少被使用,也不被大多数的库和工具所支持)。
影响消息大小的另一个方面是overfetching(过度获取):我们决定是否只请求特定的字段或总是接收所有的字段(通过避免"overfetching"过滤掉我们不需要的字段,只接受我们需要的字段)。因此,对于GraphQL而言利用了overfetching技术可以在请求中指定需要哪些字段,而在gRPC中,我们可以使用FieldMasks作为请求的可重用的过滤器,该过滤器和overfetching有异曲同工之妙。
gRPC使用二进制格式的另一个好处是,比GraphQL信息的序列化和解析更快。当然,缺点也很明显,它比JSON而言更难查看和调试,毕竟JSON的信息结构更加适合人类阅读。在Temporal项目中(开源微服务平台),我们默认使用protobuf的JSON格式,以利于开发者体验的可见性。(这失去了二进制格式带来的效率,但更看重效率的用户可以切换到二进制)。
3.默认值
gRPC不在消息中包括默认值,而GraphQL可以对参数进行默认值的设置,但不能对请求字段或响应类型进行默认值设置。这导致gRPC消息尺寸较小的另一个因素。它还影响了消费gRPCAPI的DX,在不设置输入字段和将该字段设置为默认值没有什么区别,默认值也是基于字段类型的值。我们不能把`behavior`枚举输入字段默认为`BEHAVIOR_FOO=2`--我们必须把默认值放在第一位(`BEHAVIOR_FOO=0`),这意味着它在总是默认值,或者我们遵循推荐的做法,定义一个`BEHAVIOR_UNSPECIFIED=0`枚举值。
API提供者需要传达UNSPECIFIED的意思(通过记录"unspecified将使用默认行为,目前是FOO"),消费者需要考虑服务器的默认行为在未来是否会改变(如果服务器在消费者正在创建的业务实体中保存了UNSPECIFIED/0值,而服务器后来改变了默认行为,那么该实体将会有所不同),以及这种改变是否是所期望的。如果并不希望如此,客户需要将该值设置为当前的默认值。
这种方式比gRPC版本更简单,这种方式可以知道如果不提供字段会发生什么,而且我们不需要考虑是否要自己传递默认值。
其他类型的默认值也有一些特殊情况。对于数字而言,有时默认的0是一个有效的值,而有时它将意味着一个不同的默认值。对于布尔型,默认的false会导致字段被负数命名。当我们在编码时给布尔变量命名时,我们使用正向命名。例如,我们通常会声明letretryable=true而不是letnnotallow=false。人们通常认为前者更易读,因为后者需要额外的步骤来理解双重否定("notRetryable是假的,所以它是可重试的")。但是如果我们有一个gRPCAPI,我们希望默认状态是可重试,那么我们就必须把这个字段命名为nonRetryable,因为可重试字段的默认值是false,就像gRPC中的所有布尔值一样。
4.请求格式
在gRPC中,我们一次调用一个方法。如果我们需要的数据比一个方法所返回的要多,就需要调用多个方法。如果我们需要第一个方法的响应数据,以便知道下一步调用哪个方法,那么我们就会连续进行多次往返。除非我们和服务器在同一个数据中心,否则会造成很大的延迟。这个问题被称为underfetching,指请求的返回的数据信息不够,这样导致我们需要发多个请求去获取数据。
这也是GraphQL设计能够解决的问题之一。在高延迟的移动连接中,能够在一次请求中获得需要的所有数据,这一点特别重要。在GraphQL中,我们在请求中发送一个字符串,其中包括我们想要调用的所有方法(称为queries和mutations)以及基于第一层结果的嵌套数据。嵌套的数据可能需要从服务器到数据库的后续请求,但它们通常位于同一个数据中心,应该有亚毫秒级别的网络延迟。
GraphQL的请求灵活性让前端和后端团队的耦合度降低。前端开发者不需要等待后端开发者在方法的响应中添加更多的数据(这样客户端就可以在单个请求中接收数据),而是可以在请求中添加更多的查询或嵌套结果字段。当有一个涵盖整个数据的GraphQLAPI时,后端发生变化而导致前端团队时受阻的情况就会明显减少。
GraphQL请求指定了所有需要的数据字段,这意味着客户端可以使用声明式数据获取(declarativedatafetching):生命视图组件接下来所需要的数据,而不是强制性地获取数据(比如调用`grpcClient.callMethod()'),GraphQL客户端库将这些合并成一个请求并提供数据,在数据变化时也可以提供数据。在Web开发中使用React而不是jQuery:声明组件应该是什么样子,并在数据变化时让组件自动更新,而不是必须用jQuery操作DOM。
GraphQL请求格式的另一个效果是增加了可见性:服务器可以看到每个被请求的字段。可以跟踪字段的使用情况,看到客户何时停止使用已废弃的字段,这样就知道何时可以删除它们,而不用支持哪些应该被摒弃的东西。像ApolloGraphOS和Stellate这样的常用工具中都有跟踪功能。
5.向前兼容
gRPC和GraphQL都有很好的向前兼容性;也就是说,很容易以不破坏现有客户端的方式更新服务器。这对已经过时的移动应用程序来说特别重要,但对于加载在用户浏览器标签中的SPA在服务器更新后继续工作来说,也是必要的。
在gRPC中,你可以通过对字段进行数字排序、用新的数字添加字段,以及不改变现有字段的类型/数字来保持向前兼容。在GraphQL中,你可以添加字段,用`@deprecated"`指令废除旧的字段(并让它们发挥作用),并避免将可选参数改为必填。
6.传输
gRPC和GraphQL都支持服务器向客户端传输数据:gRPC有serverstreaming,GraphQL有Subscriptions和指令@defer、@stream和@live。gRPC的HTTP/2也支持客户端和双向流(尽管当一方是浏览器时无法做到双向)。HTTP/2还通过多路复用提高了性能。
gRPC有内置的网络故障重试,该功能在GraphQL中没有直接体现,而是在与之对应的客户端库中支持网络故障重试,比如ApolloClient的RetryLink。
gRPC无法使用大多数API代理,比如ApigeeEdge,它是以HTTP头为操作对象的,而当客户端是浏览器时,我们需要使用gRPC-Web代理或Connect(虽然现代浏览器确实支持HTTP/2,但没有浏览器API允许对请求进行足够的控制)。默认情况下,GraphQL不使用GET缓存:许多HTTP缓存都是在GET请求上工作的,而大多数GraphQL库默认使用POST。GraphQL有许多使用GET的选项,包括将操作放在查询参数中(当操作字符串不太长时是可行的),建立时间持久化查询(通常只用于私有API),以及自动持久化查询。缓存指令可以在字段级提供(整个响应中最短的值被用于Cache-Control头的`max-age')。
7.模式和类型
GraphQL有一个模式,服务器为客户端开发人员发布并用于处理请求。它定义了所有可能的queries和mutations,以及所有的数据类型以及它们之间的关系。这种模式使来自多个服务的数据易于结合。GraphQL有schemastitching(将多个GraphQLAPI组合成一个代理schema的API)和federation(每个下游API声明如何关联共享类型,网关通过向下游API发出请求并结合结果自动解决请求)的概念,用于创建一个超图(所有数据的图,整合了较小的子图/部分模式)。也有一些库将其他协议代理给GraphQL,包括gRPC。
伴随着GraphQL的模式而来的是自检系统:以标准方式查询服务器的能力,以确定其能力是什么。所有的GraphQL服务器库都有自检功能,还有一些基于自省的高级工具,如GraphiQL、请求提示与graphql-eslint和ApolloStudio,其中包括一个带有字段自动完成、提示、自动生成文档和搜索查询的IDE。gRPC有自检功能,但它没有那么广泛,使用它的工具也比较少。
GraphQL模式实现了反应式的规范化客户端缓存:因为每个(嵌套的)对象都有一个类型字段,类型在不同的查询之间是共享的,我们可以告诉客户端哪个字段是类型的ID,客户端可以将数据对象进行规范化存储。这就实现客户端的功能,比如查询结果或更新会触发对视图组件的更新,而这些视图组件依赖于包含相同对象的不同查询。
gRPC和GraphQL类型之间的区别:
- gRPC第3版(截至编写时的最新版本)没有必填字段:相反,每个字段都有一个默认值。在GraphQL中,服务器可以区分一个值是否存在,模式(schema)可以表明一个参数必须存在,或者一个响应字段将始终存在。
- 在gRPC中,没有标准的方法可以知道一个方法是否会变更状态(与GraphQL相比,gRPC将queries和mutations分开)。
- gRPC中支持Map类型,但GraphQL中不支持:如果你有一个数据类型,如`{[key:string]:T}`,需要通过JSON字符串类型实现。
GraphQL虽然具有模式和灵活查询的特点,但是对于公共API来说,速率限制更加复杂(对于私有API,允许列出持久查询)。因为可以在一个请求中包含任意多的查询,而且查询可以任意嵌套的数据,所以不能仅仅限制客户端的请求数量。还需要对整个操作实施成本分析率限制,例如通过使用graphql-cost-analysis-cost-analysis库将各个领域的成本相加,并将其传递给漏斗算法。
四、小结
以下将gRPc和GraphQL的异同以及优缺点通过摘要的方式呈现给大家,方便比较分析。
1.gRPC和GraphQL的相似之处
- 使用codegen的类型化接口
- 抽离网络层
- 可以有JSON响应
- 服务器流媒体
- 良好的前向兼容性
- 可以避免过度获取
2.gRPC和GraphQL的优劣势
(1)gRPC优势
- 二进制格式
更快的网络传输
更快的序列化、解析和验证
然而,比JSON更难查看和调试
- HTTP/2
多重化
客户端和双向流媒体
- 内置重试和最后期限
(2)gRPC劣势
- 需要代理或连接以从浏览器使用
- 无法使用大多数API代理机构
- 没有标准的方法可以知道一个方法是否会突变状态
(3)GraphQL优势
- 客户决定它希望返回哪些数据字段。结果是:没有取不到的数据团队解耦提高辨识度
- 更容易合并来自多种服务的数据
- 进一步发展自检和工具化
- 声明性的数据取用
- 反应式的规范化客户端缓存
(4)GraphQL劣势
- 如果我们已经有了可以公开的gRPC服务,那么增加一个GraphQL服务器就需要更多的后台工作
- HTTPGET缓存默认不工作
- 对于公共API来说,速率限制更为复杂
- MAP类型不被支持
- 低效的文本运输
3.如何取舍?
(1)服务器到服务器(Server-to-Server)
在服务器到服务器的通信中,低延迟往往很重要,而且有时需要更多类型的流,gRPC是明确的标准。然而,在有些情况下,我们可能会发现GraphQL的一些好处更为重要。
- 我们正在使用GraphQLfederartion或模式拼接来创建所有业务数据的超图,并决定由每个服务发布GraphQL子图。我们创建了两个超图端点:一个是由客户调用的外部端点,一个是由服务调用的内部端点。在这种情况下,对于服务来说,暴露一个gRPCAPI可能是不值得的,因为它们都可以通过超图方便地到达。
- 服务的数据字段将发生变化,并让字段级的使用情况可见,这样就可以删除被废弃的字段(而不是永远维护它们)。
还有一个问题是,我们是否应该自己做服务器到服务器的通信。对于数据获取(GraphQL的查询),这是获得响应的最快方式,但对于修改数据(变更),像MartinFowler的"同步调用被认为是有害的",也就导致我们使用异步、事件驱动的架构,在服务之间进行编排或协调。微服务模式建议在大多数情况下使用后者,为了保持DX和开发速度,我们需要一个基于代码的协调器而不是基于DSL的协调器。一旦在像Temporal这样基于代码的协调器中工作时,就不再提出网络请求--平台会可靠处理这一切。在我看来,这就是未来。
(2)客户端-服务器(Client-to-Server)
在客户-服务器通信中,延迟很高。我们希望能够在一次往返中获得所有数据,能够灵活地为不同的视图获取对应的数据,并拥有强大的缓存能力,所以GraphQL是明显的赢家。然而,在有些情况下,我们可以选择使用gRPC来代替。
- 如果已经有可以使用的gRPCAPI了,那么再添加一个GraphQL服务器就不太值得了。
- JSON并不适合这些数据(例如,我们正在发送大量的二进制数据)。
原文链接:https://stackoverflow.blog/2022/11/28/when-to-use-grpc-vs-graphql/
译者介绍
崔皓,51CTO社区编辑,资深架构师,拥有18年的软件开发和架构经验,10年分布式架构经验。曾任惠普技术专家。乐于分享,撰写了很多热门技术文章,阅读量超过60万。《分布式架构原理与实践》作者。