免責聲明:盡管標題有爭議,但本文并不是試圖證明RPC比REST更好,或者GraphQL比RPC更好。相反,本文的目的是向你介紹這些方法的大致情況以及它們的優缺點。最終的選擇將會是一個權衡。
盡管 HTTP 是一個應用層(例如,L7)協議,但在 API 開發方面,HTTP 實際上扮演著一個較低層次的傳輸機制的角色。
在 HTTP 上如何實現 API 有許多方法,它們在概念上有所不同:
- REST
- RPC
- GraphQL
...但是一個普通的開發人員需要知道的實際清單并不局限于這三個家伙。在這個領域中還有 JSON、gRPC、protobuf 等其它術語。讓我們試著一次性了解所有這些術語吧!
代表性狀態轉移(REST)
首先,REST只是一種軟件架構風格。它是一組設計約束,而不是具體的協議。REST 依賴于資源的概念。例如,一個 REST API 是由一組資源(名詞)和與這些資源交互的有限數量的動作(動詞,查詢 fetch、創建 create、更新 update、刪除 delete 等)組成。它在思想上非常接近最初的 HTTP 設計,主要基于資源(URLs)和方法(GET、POST、PUT、DELETE)。因此,從實現的角度來看,REST 模型到 HTTP 協議的映射相對簡單:
# Create new bookPOST http://myapi.com/books/ (author="R. Feynman", year=1975) -> book_id
# Get book with ID = 1GET http://myapi.com/books/1 () -> (id=1, author="R. Feynman", year=1975)
# Update book with ID = 1PUT http://myapi.com/books/1 (id=1, author="Richard Feynman", year=1975) -> (...)
# Delete book with ID = 1DELETE http://myapi.com/books/1 () -> nu
可能每一個現代 web 框架都提供了構建一個 REST 風格的 Web 服務所需的所有現成工具。從客戶端的角度來看,調用一個 REST API 非常簡單——它只需要將指定的 HTTP 方法發送到一組預定義的 URLs。
然而,從 API 設計者的角度來看,如何用資源術語(即名詞)表示現實世界的領域并保持有限數量的動作(即動詞)是并不明顯的。令人驚訝的是,盡管 REST 一次在當今被廣泛使用,但被認為是真正的 REST 風格的現代 API 并不多。在設計 REST 風格的 Web 服務時堅持純粹,或導致 API 設計非常笨重。
遠程過程調用(RPC)
同樣,RPC也是一種 API 設計技術。RPC 聚焦于動作(動詞)概念,這通常使得涉及到的資源(名詞)非常原始和特別。對于業務模型中的每個過程或事務,API 設計者只需要添加一個 RPC 端點:
# Create new bookcreateBook(author, year) -> book_id
# Get book by IDgetBook(book_id) -> book
# Change book's authorsetBookAuthor(book_id, author) -> null
# Delete book by IDdeleteBook(book_id) -> nu
在幕后,應該有另外一層將這樣的過程調用映射到 HTTP 請求。例如,setBookAuthor(1, "Richard Feynman")會被這樣映射:
<span role="presentation" style="box-sizing: border-box; padding-right: 0.1px;">POST http://myapi.com/set-book-author/ (book_id=1, author="Richard Feynman") -> null</span role="presentation" style="box-sizing: border-box; padding-right: 0.1px;">
現在,讓我們比較一下 REST 和 RPC 中改變作者姓名的任務。雖然在 RPC 中的實現看起來比較簡單,但在 REST 風格的實現中有許多有爭議性的問題需要回答。如果作者的名字是 book 的一個屬性,那么我們是否應該在發送PUT /books/1 時,提供一個包含修改過的作者字段的完整的 book 對象?如果客戶端沒有完全的 book 對象怎么辦?我們應該先獲取 book 對象,還是只傳送 book ID 和新的作者名字?但是服務端的其它屬性怎么辦?如果一個幾乎空白的PUT 請求到達,它們是否應該歸零(這將是災難性的,但是與 REST 原則的決定一致)?或者我們應該放棄 HTTPPUT 方法,開始使用PATCH 方法(你聽說過這個方法嗎)?
幸運的是,如果我們遵循 RPC 方案,我們可以簡單忽略上面這些問題。但是,當然也有一個缺點——一個典型的 RPC API 通常包含大量的自定義過程。顯然,這使得 RPC 模型到 HTTP 層的映射是一個不平凡的任務。好在 API 設計者很少需要考慮這一部分。有很多庫在 HTTP 上實現了 RPC 層,其中一些庫非常杰出(是的,我說的是谷歌的 gRPC 和臉書的 Thrift)。但是 RPC 方案還有一個更復雜的問題...
一打豐富的 REST 資源結合 3-5 個 HTTP 方法通常可以覆蓋一百個用例。REST API 故意為領域模型引入了一個常規結構,使其增長和演化更加可控。相反,RPC APIs 是自然增長的。引入一個新的用例通常需要在已經膨脹的列表中再增加幾個 API 端點。由于 API 設計沒有強制面向實體的結構,因此在相對短的時間內,RPC 調用的數量可能會超過一個團隊可以處理的最大復雜度。
GraphQL
現在,,我們已經知道 RPC 和 REST 方案都不理想。REST 存在過度和欠缺查詢問題,可能會導致在設計階段的比較耗神。但是如果我們嘗試用一百個特別的 RPC 端點來取代面向實體的設計,隨著時間的推移,維護過度增長的 RPC API 會是一個噩夢。
GraphQL試圖解決這兩種技術的弱點。假設面向實體的領域模型有助于開發人員長期安心,GraphQL 方案從定義模板開始,例如資源集(即名詞)和它們的關聯關系。聽起來很像一張圖,不是嗎?有了正式的模板定義,GraphQL 在其上構建了一個相當復雜的服務器和客戶端。厚客戶端允許查詢(QL 代表 query language,即查詢語言)自定義的和組合的資源。厚服務器知道如何根據客戶端的查詢和領域模板來填充響應。
因此,GraphQL API 基本上由一個端點組成。即,沒有臃腫的 API 了。同時,它超級靈活和可以定制的查詢有助于避免從服務器查詢不必要的數據。此外,開發人員仍然可以從正式的面向實體的領域模板中受益。但是凡事總是有代價的。這里的代價是 GraphQL 客戶端和服務器端的極度復雜性。所以,是的,一切都關乎取舍...
protobuf、JSON 等怎么樣?
活動部分的適當分類能夠有助于你將注意力集中在 API 開發領域。現在,我們已經知道 REST 和 RPC 僅僅只是架構風格。而 GraphQL,我傾向于認為它也是一種風格,即使從技術上來講它是一種由運行時支持的正式語言。
但是,如果 GraphQL 是由一個特定實現支持的,那么我們可能期望 RPC 和 REST 也有相同的實現支持。事實上,確實有很多著名的 RPC 框架——gRPC、Apache Thrift和Apache Avro等等。令人驚訝的是,REST 框架中似乎沒有明顯的領導者。可能是因為 REST 在技術上非常接近 HTTP,而且幾乎每個 Web 框架都已經很好地支持了它。
嗯,但是protobuf怎么樣?顯然,它只是數據在通過網絡被發送或存儲到某個地方之前對其進行序列化的方法之一。gRPC 是一個特殊的 RPC 框架,完全依賴 protobuf。而且由于這兩個技術通常是一起使用的,而且被一起發布,因此很多人都將 gRPC 和 protobuf 交互使用。然而,在你的REST風格的API中使用protobuf也非常不錯,protobuf 可以用作一種編碼格式,我們目前在工作中對我們的一些服務就是這么做的。因此,protobuf 不應該與 REST 或 RPC 框架一起比較,而是可以與 XML 或 JSON 一起比較。
更讓你困惑的是,Apache Thrift 有它自己的序列化格式,稱作 Thrift!即,gRPC 使用 protobuf 編碼格式,而 Thrift 使用 Thrift 編碼格式
總之,設計一個 HTTP API 有許多不同的架構風格。其中最流行的三種是 REST、RPC 和 GraphQL。每一種風格都可以用很多種方法實現,而且有很多著名的框架,例如 gRPC 和 Apache Thrift。在底層,它們依賴更低級別的機制,如數據序列化(protobuf、JSON、XML)或協議(JSON-RPC、 XML-RPC)。而且較低級別的代碼有時會在不同的風格之間復用,這使得整個 API 開發領域乍一看都非常復雜。
作者介紹
Ivan Velichko 是一名涉及很多領域且具有 10 年實踐經驗的軟件工程師;熱衷于可靠性和分布式系統。軟件開發不僅是專業還是愛好。最有意義的活動是增加對復雜系統的理解。
原文鏈接
API Developers Never REST