TLDR:使用 GraphQL 進(jìn)行客戶端-服務(wù)器通信,使用 gRPC 進(jìn)行服務(wù)器到服務(wù)器通信。有關(guān)此規(guī)則的例外情況,請參閱“判定”部分。
我已經(jīng)閱讀了很多關(guān)于這兩種協(xié)議的比較,并想寫一個(gè)全面和公正的協(xié)議。(好吧,就像我和我的審稿人所能做到的那樣公正
。我的靈感來自connect-web(一個(gè)可以在瀏覽器中使用的TypeScript gRPC客戶端)的發(fā)布,以及一篇名為GraphQL的流行HN帖子有點(diǎn)糟糕。我個(gè)人在第7 層之上構(gòu)建的通信協(xié)議歷史:
REST(Rails and Express)
DDP(Meteor's WebSocket Protocol)
GraphQL(我寫了一本書)
gRPC(我在Temporal 使用))
背景
gRPC由 Google 于 2016 年發(fā)布,是一種高效且開發(fā)人員友好的服務(wù)器到服務(wù)器通信方法。GraphQL由 Meta 于 2015 年發(fā)布,作為一種高效且開發(fā)人員友好的客戶端-服務(wù)器通信方法。它們都比 REST 具有顯著的優(yōu)勢,并且有很多共同點(diǎn)。我們將用大部分時(shí)間比較它們的特征,然后總結(jié)每個(gè)協(xié)議的優(yōu)點(diǎn)和缺點(diǎn)。最后,我們將知道為什么每個(gè)都非常適合其預(yù)期域,以及何時(shí)可能希望在另一個(gè)域中使用一個(gè)。
比較 gRPC 和 GraphQL 特性
界面設(shè)計(jì)
gRPC 和 GraphQL 都是接口描述語言 (IDL),用于描述兩臺計(jì)算機(jī)如何相互通信。它們跨不同的編程語言工作,我們可以使用 codegen 工具來生成多種語言的類型化接口。IDL 抽象出傳輸層;GraphQL 與傳輸無關(guān),但通常通過 HTTP 使用,而 gRPC 使用 HTTP/2。我們不需要了解傳輸級的詳細(xì)信息,例如 REST 中的方法、路徑、查詢參數(shù)和正文格式。我們只需要知道使用更高級別的客戶端庫與之通信的單個(gè)端點(diǎn)。
消息格式
郵件大小很重要,因?yàn)檩^小的郵件通常通過網(wǎng)絡(luò)發(fā)送所需的時(shí)間較短。gRPC 使用協(xié)議緩沖區(qū)(又名 protobufs),這是一種只包含值的二進(jìn)制格式,而 GraphQL 使用 JSON,它是基于文本的,除了值之外還包括字段名稱。二進(jìn)制格式與較少的信息相結(jié)合通常會(huì)導(dǎo)致 gRPC 消息小于 GraphQL 消息。(雖然高效的二進(jìn)制格式在 GraphQL 中是可行的,但它很少使用,并且不受大多數(shù)庫和工具的支持。
影響消息大小的另一個(gè)方面是過度獲取:我們是可以只請求特定字段還是始終接收所有字段(我們不需要的“過度提取”字段)。GraphQL 總是在請求中指定需要哪些字段,而在 gRPC 中,我們可以將FieldMask用作請求的可重用過濾器。
gRPC 二進(jìn)制格式的另一個(gè)好處是,與 GraphQL 的文本消息相比,消息的序列化和解析速度更快。缺點(diǎn)是它比人類可讀的 JSON 更難查看和調(diào)試。我們默認(rèn)使用 protobuf 的JSON 格式,以獲得開發(fā)人員體驗(yàn)的可見性優(yōu)勢。(這失去了二進(jìn)制格式帶來的效率,但更重視效率的用戶可以切換到二進(jìn)制。
違約
gRPC 也不在消息中包含默認(rèn)值,GraphQL 可以對參數(shù)執(zhí)行此操作,但不能對請求字段或響應(yīng)類型執(zhí)行此操作。這是 gRPC 消息較小的另一個(gè)因素。它還會(huì)影響使用 gRPC API 的 DX。將輸入字段保留為未設(shè)置和將其設(shè)置為默認(rèn)值之間沒有區(qū)別,默認(rèn)值基于字段的類型。所有布爾值默認(rèn)為 false,所有數(shù)字和枚舉默認(rèn)為 0。我們不能將“behavior”枚舉輸入字段默認(rèn)為“BEHAVIOR_FOO = 2”——我們必須將默認(rèn)值放在第一位(“BEHAVIOR_FOO = 0”),這意味著它將來將始終是默認(rèn)值,或者我們遵循推薦的做法,即使用“BEHAVIOR_UNSPECIFIED = 0”枚舉值:
enum Behavior { BEHAVIOR_UNSPECIFIED = 0; BEHAVIOR_FOO = 1; BEHAVIOR_BAR = 2; }
API 提供者需要傳達(dá) whatmeans(通過記錄“未指定將使用默認(rèn)行為,這是當(dāng)前”),消費(fèi)者需要考慮服務(wù)器默認(rèn)行為將來是否會(huì)發(fā)生變化(如果服務(wù)器將 offer/ 0 值保存在消費(fèi)者正在創(chuàng)建的某個(gè)業(yè)務(wù)實(shí)體中,并且服務(wù)器稍后更改了默認(rèn)行為, 實(shí)體將開始以不同的方式行事)以及是否需要這樣做。如果不需要,客戶端需要將值設(shè)置為當(dāng)前默認(rèn)值。下面是一個(gè)示例方案: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)
我們將發(fā)送,根據(jù)服務(wù)器實(shí)現(xiàn)和未來的更改,這可能意味著現(xiàn)在和以后。或者我們可以做: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 }
當(dāng)我們不在請求中包含時(shí),服務(wù)器代碼將接收并將 FOO 存儲為值,與上述架構(gòu)中的默認(rèn)值匹配。behavior= FOO
graphqlClient.request(` mutation { createEntity(name: “my entity”) } `
知道如果未提供字段會(huì)發(fā)生什么情況,它比 gRPC 版本更簡單,我們不需要考慮是否自己傳遞默認(rèn)值。
其他類型的默認(rèn)值還有其他怪癖。對于數(shù)字,有時(shí)默認(rèn)值 0 是有效值,有時(shí)表示不同的默認(rèn)值。對于布爾值,默認(rèn)的 false 會(huì)導(dǎo)致負(fù)命名字段。當(dāng)我們在編碼時(shí)命名布爾變量時(shí),我們使用正名稱。例如,我們通常會(huì)聲明而不是。人們通常發(fā)現(xiàn)前者更具可讀性,因?yàn)楹笳咝枰~外的步驟來理解雙重否定(“是,所以它是可重試的”)。但是,如果我們有一個(gè) gRPC API,我們希望默認(rèn)狀態(tài)是可重試的,那么我們必須命名該字段,因?yàn)?anfield 的默認(rèn)值將是,就像 gRPC 中的所有布爾值一樣。let retryable = truelet nonRetryable = falsenotRetryablefalsenonRetryableretryablefalse
請求格式
在 gRPC 中,我們一次調(diào)用一個(gè)方法。如果我們需要的數(shù)據(jù)多于單個(gè)方法提供的數(shù)據(jù),則需要調(diào)用多個(gè)方法。如果我們需要來自第一個(gè)方法的響應(yīng)數(shù)據(jù),以便知道下一步要調(diào)用哪個(gè)方法,那么我們將連續(xù)進(jìn)行多次往返。除非我們與服務(wù)器位于同一數(shù)據(jù)中心,否則會(huì)導(dǎo)致明顯的延遲。此問題稱為獲取不足。
這是 GraphQL 旨在解決的問題之一。在高延遲移動(dòng)電話連接中,能夠在單個(gè)請求中獲取所需的所有數(shù)據(jù)尤為重要。在 GraphQL 中,我們發(fā)送一個(gè)字符串(稱為文檔)以及我們的請求,其中包含我們要調(diào)用的所有方法(稱為查詢和突變)以及基于第一級結(jié)果所需的所有嵌套數(shù)據(jù)。某些嵌套數(shù)據(jù)可能需要從服務(wù)器到數(shù)據(jù)庫的后續(xù)請求,但它們通常位于同一數(shù)據(jù)中心,該數(shù)據(jù)中心應(yīng)具有亞毫秒級的網(wǎng)絡(luò)延遲。
GraphQL 的請求靈活性讓前端和后端團(tuán)隊(duì)變得不那么耦合。前端開發(fā)人員可以向其請求添加更多查詢或嵌套結(jié)果字段,而不是前端開發(fā)人員等待后端開發(fā)人員向方法的響應(yīng)添加更多數(shù)據(jù)(以便客戶端可以在單個(gè)請求中接收數(shù)據(jù))。當(dāng)有一個(gè)覆蓋組織整個(gè)數(shù)據(jù)圖的 GraphQL API 時(shí),前端團(tuán)隊(duì)在等待后端更改時(shí)被阻止的頻率要低得多。
事實(shí)上,GraphQL 請求指定了所有需要的數(shù)據(jù)字段,這意味著客戶端可以使用聲明性數(shù)據(jù)獲取:而不是命令性獲取數(shù)據(jù)(如調(diào)用 'grpcClient.callMethod()“),我們在視圖組件旁邊聲明我們需要的數(shù)據(jù),GraphQL 客戶端庫將這些部分組合成一個(gè)請求,并在響應(yīng)到達(dá)時(shí)和稍后數(shù)據(jù)更改時(shí)將數(shù)據(jù)提供給組件。Web 開發(fā)中視圖庫的并行是使用 React 而不是 jQuery:聲明我們的組件應(yīng)該是什么樣子,并在數(shù)據(jù)更改時(shí)自動(dòng)更新它們,而不是使用 jQuery 強(qiáng)制操作 DOM。
GraphQL 的請求格式的另一個(gè)影響是提高了可見性:服務(wù)器可以看到請求的每個(gè)字段。我們可以跟蹤字段使用情況并查看客戶端何時(shí)停止使用已棄用的字段,以便我們知道何時(shí)可以刪除它們,而不是永遠(yuǎn)支持我們說要?jiǎng)h除的內(nèi)容。跟蹤內(nèi)置于Apollo GraphOS和Stellate等常用工具中。
向前兼容性
gRPC 和 GraphQL 都有很好的前向兼容性;也就是說,很容易在不破壞現(xiàn)有客戶端的情況下更新服務(wù)器。這對于可能已過時(shí)的移動(dòng)應(yīng)用程序尤其重要,但對于在服務(wù)器更新后繼續(xù)工作,用戶瀏覽器選項(xiàng)卡中加載的SPA也是必需的。
在 gRPC 中,可以通過對字段進(jìn)行數(shù)字排序、添加具有新編號的字段以及不更改現(xiàn)有字段的類型/編號來保持向前兼容性。在 GraphQL 中,您可以添加字段,使用 '@deprecated“' 指令棄用舊字段(并讓它們正常工作),并避免更改必需的可選參數(shù)。
運(yùn)輸
gRPC 和 GraphQL 都支持服務(wù)器將數(shù)據(jù)流式傳輸?shù)?/strong>客戶端:gRPC 具有服務(wù)器流式處理,GraphQL 具有訂閱和指令 @defer,@stream 和 @live。gRPC 的 HTTP/2 還支持客戶端和雙向流式處理(盡管當(dāng)一側(cè)是瀏覽器時(shí),我們不能進(jìn)行雙向流式處理)。HTTP/2 還通過多路復(fù)用提高了性能。
gRPC 在網(wǎng)絡(luò)故障時(shí)具有內(nèi)置的重試功能,而在 GraphQL 中,它可能包含在特定的客戶端庫中,例如 Apollo Client 的RetryLink。gRPC 也有內(nèi)置的截止日期。
運(yùn)輸也有一些限制。gRPC 無法使用大多數(shù)在 HTTP 標(biāo)頭上運(yùn)行的 API 代理,如Apigee Edge,當(dāng)客戶端是瀏覽器時(shí),我們需要使用gRPC-Web 代理或Connect(雖然現(xiàn)代瀏覽器確實(shí)支持 HTTP/2,但沒有瀏覽器API允許對請求進(jìn)行足夠的控制)。默認(rèn)情況下,GraphQL 不適用于 GET 緩存:大部分 HTTP 緩存適用于GET 請求,大多數(shù) GraphQL 庫默認(rèn)使用 POST。 GraphQL 有許多使用 GET 的選項(xiàng),包括將操作放在查詢參數(shù)中(當(dāng)操作字符串不太長時(shí)可行)、構(gòu)建時(shí)持久化查詢(通常只與私有 API 一起使用), 和自動(dòng)持久化查詢。可以在字段級別提供緩存指令(整個(gè)響應(yīng)中的最短值用于 Cache-Control 標(biāo)頭的“max-age”)。
架構(gòu)和類型
GraphQL 有一個(gè)架構(gòu),服務(wù)器為客戶端開發(fā)人員發(fā)布并用于處理請求。它定義了所有可能的查詢和突變,以及所有數(shù)據(jù)類型及其相互關(guān)系(圖形)。通過該架構(gòu),可以輕松合并來自多個(gè)服務(wù)的數(shù)據(jù)。GraphQL 具有模式拼接(命令性地將多個(gè) GraphQL API 組合成一個(gè)代理部分模式的 API)和聯(lián)合(每個(gè)下游 API聲明如何關(guān)聯(lián)共享類型,網(wǎng)關(guān)通過向下游 API 發(fā)出請求并組合結(jié)果來自動(dòng)解析請求)的概念,用于創(chuàng)建超圖(我們所有數(shù)據(jù)的圖表,結(jié)合了較小的子圖/部分模式)。還有一些庫將其他協(xié)議代理到 GraphQL,包括 gRPC。
隨著 GraphQL 的模式而來的是進(jìn)一步發(fā)展的內(nèi)省:能夠以標(biāo)準(zhǔn)方式查詢服務(wù)器以確定其功能。所有 GraphQL 服務(wù)器庫都有內(nèi)省功能,并且有基于內(nèi)省的高級工具,如GraphiQL、使用graphql-eslint 的請求 linting 和Apollo Studio,其中包括具有字段自動(dòng)完成、linting、自動(dòng)生成的文檔和搜索功能的查詢 IDE。gRPC 具有反射,但它沒有那么普遍,并且使用它的工具較少。
GraphQL 模式啟用了反應(yīng)式規(guī)范化客戶端緩存:因?yàn)槊總€(gè)(嵌套)對象都有一個(gè)類型字段,所以類型在不同的查詢之間共享,我們可以告訴客戶端將哪個(gè)字段用作每種類型的 ID,客戶端可以存儲規(guī)范化的數(shù)據(jù)對象。這使高級客戶端功能(如查詢結(jié)果或觸發(fā)更新的樂觀更新)能夠查看依賴于包含同一對象的不同查詢的組件。
gRPC 和 GraphQL 類型之間存在一些差異:
gRPC 版本 3(截至撰寫本文時(shí)的最新版本)沒有必填字段:相反,每個(gè)字段都有一個(gè)默認(rèn)值。在 GraphQL 中,服務(wù)器可以區(qū)分值存在和不存在 (null),并且架構(gòu)可以指示參數(shù)必須存在或響應(yīng)字段將始終存在。
在 gRPC 中,沒有標(biāo)準(zhǔn)方法可以知道方法是否會(huì)改變狀態(tài)(與 GraphQL,它分離查詢和突變)。
映射在 gRPC 中受支持,但在 GraphQL 中不受支持:如果你的數(shù)據(jù)類型像 '{[key: string] : T}',你需要對整個(gè)事情使用 JSON 字符串類型。
GraphQL 的模式和靈活查詢的一個(gè)缺點(diǎn)是,公共 API 的速率限制更為復(fù)雜(對于私有 API,我們可以將持久查詢列入允許列表)。由于我們可以在單個(gè)請求中包含任意數(shù)量的查詢,并且這些查詢可以請求任意嵌套的數(shù)據(jù),因此我們不能只限制來自客戶端的請求數(shù)量或?qū)⒊杀痉峙浣o不同的方法。我們需要對整個(gè)操作實(shí)現(xiàn)成本分析速率限制,例如通過使用graphql-cost-analysis庫對單個(gè)字段成本求和并將它們傳遞給泄漏桶算法。
總結(jié)
以下是我們涵蓋的主題摘要:
gRPC 和 GraphQL 之間的相似之處
帶代碼生成的類型化接口
抽象出網(wǎng)絡(luò)層
可以有 JSON 響應(yīng)
服務(wù)器流式傳輸
良好的向前兼容性
可以避免過度提取
gRPC
優(yōu)勢
二進(jìn)制格式:
通過網(wǎng)絡(luò)更快地傳輸
更快的序列化、解析和驗(yàn)證
但是,比 JSON 更難查看和調(diào)試
HTTP/2:
多路復(fù)用
客戶端和雙向流式處理
內(nèi)置重試和截止時(shí)間
弱點(diǎn)
需要代理或連接才能從瀏覽器使用
無法使用大多數(shù) API 代理
沒有標(biāo)準(zhǔn)方法可以知道方法是否會(huì)改變狀態(tài)
圖QL
優(yōu)勢
客戶端確定它要返回哪些數(shù)據(jù)字段。結(jié)果:
無欠取
團(tuán)隊(duì)解耦
提高可見性
更輕松地合并來自多個(gè)服務(wù)的數(shù)據(jù)
進(jìn)一步發(fā)展的內(nèi)省和工具
聲明性數(shù)據(jù)提取
反應(yīng)式規(guī)范化客戶端緩存
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).
還有一個(gè)問題是,我們是否應(yīng)該自己進(jìn)行服務(wù)器到服務(wù)器的通信。對于數(shù)據(jù)獲取(GraphQL 的查詢),這是獲得響應(yīng)的最快方式,但對于修改數(shù)據(jù)(突變),像 Martin Fowler 的“同步調(diào)用被認(rèn)為是有害的”(見此處的側(cè)欄)導(dǎo)致使用異步、事件驅(qū)動(dòng)的架構(gòu),在服務(wù)之間編排或編排。微服務(wù)模式建議在大多數(shù)情況下使用后者,為了保持 DX 和開發(fā)速度,我們需要一個(gè)基于代碼的業(yè)務(wù)流程協(xié)調(diào)程序,而不是基于 DSL 的業(yè)務(wù)流程協(xié)調(diào)程序。一旦我們使用像 Temporal 這樣的基于代碼的業(yè)務(wù)流程協(xié)調(diào)程序,我們就不再自己發(fā)出網(wǎng)絡(luò)請求 — 平臺會(huì)為我們可靠地處理它。在我看來,這就是未來。
客戶端-服務(wù)器
在客戶端-服務(wù)器通信中,延遲很高。我們希望能夠在一次往返中獲取所需的所有數(shù)據(jù),靈活地為不同的視圖獲取哪些數(shù)據(jù),并具有強(qiáng)大的緩存功能,因此 GraphQL 是明顯的贏家。但是,在某些情況下,我們可能會(huì)選擇改用 gRPC:
我們已經(jīng)有一個(gè)可以使用的 gRPC API,在它前面添加一個(gè) GraphQL 服務(wù)器的成本不值得。
JSON 不適合數(shù)據(jù)(例如,我們正在發(fā)送大量二進(jìn)制數(shù)據(jù))。
感謝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
+關(guān)注
關(guān)注
5文章
1765瀏覽量
57530 -
二進(jìn)制
+關(guān)注
關(guān)注
2文章
795瀏覽量
41652 -
服務(wù)器
+關(guān)注
關(guān)注
12文章
9160瀏覽量
85421 -
API
+關(guān)注
關(guān)注
2文章
1501瀏覽量
62017 -
GraphQL
+關(guān)注
關(guān)注
0文章
14瀏覽量
577
發(fā)布評論請先 登錄
相關(guān)推薦
評論