?譯者 | 崔皓
審校 | 云昭
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萬。《分布式架構原理與實踐》作者。?