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文章
1757瀏覽量
57414 -
二進制
+關注
關注
2文章
794瀏覽量
41600 -
服務器
+關注
關注
12文章
9021瀏覽量
85184 -
API
+關注
關注
2文章
1485瀏覽量
61817 -
GraphQL
+關注
關注
0文章
14瀏覽量
563
發布評論請先 登錄
相關推薦
評論