RPC與REST
- 訪問遠程服務
- 1遠程服務調用(Remote Procedure Call,RPC):RPC 解決什么問題?如何解決的?為什么要那樣解決?
- 1.1 先解決兩個進程間如何交換數據的問題,也就是進程間通信(Inter-Process Communication,IPC)。可以考慮的辦法有以下幾種:
- 1.2 通信的成本
- 1.3 三個基本問題
- 1.4 統一的 RPC 與 分裂的 RPC
- 2 REST 設計風格
- 2.1 理解 REST
- 2.2 RESTful 的系統
訪問遠程服務
遠程服務將計算機的工作范圍從單機擴展到網絡,從本地延伸至遠程,是構建分布式系統的首要基礎。
而遠程服務又不僅僅是為了分布式系統服務的,在網絡時代,瀏覽器、移動設備、桌面應用和服務端的程序,普遍都有與其他設備交互的需求,所以今天很難找到沒有開發過遠程服務的人,但是沒有正確理解遠程服務的coder比比皆是。
1遠程服務調用(Remote Procedure Call,RPC):RPC 解決什么問題?如何解決的?為什么要那樣解決?
進程間通信:盡管今天的大多數RPC 已經不再追求這個目標了,但無可否認,在最初的時候,這是先要解決的:讓調用遠程方法和本地方法一樣。
1.1 先解決兩個進程間如何交換數據的問題,也就是進程間通信(Inter-Process Communication,IPC)。可以考慮的辦法有以下幾種:
- 管道或具名管道:管道類似于兩個進程間的橋梁,可通過管道在進程間傳遞 少量的字符流或字節流。普通管道只用于有親緣關系的進程(由一個進程啟動的另外一個進程)間通信。具名管道拜托了普通管道沒有名字的限制,除具有普通管道的所有功能外,它還允許無親緣關系進程間的通信。管道典型的應用就是命令行中的 | 操作符,如:
ps -ef | grep java
ps 與 grep 都有獨立的進程,以上命令就是將ps 的標準輸出鏈接到grep 的標準輸入上。 - 信號(signal):用于通知目標進程有某事發生,除了用于進程間通信外,進程還可以發送信號給進程自身。信號的典型命令就是 kill 命令:
kill -9 pid
- 信號量(Semaphore):信號量用于兩個進程間同步協作手段,它相當于操作系統提供的一個特殊變量,程序可以在上面進行 wait() notify() 操作。
- 消息隊列:以上三種方式只適合傳遞少量信息,消息隊列用于進程間數據量較多的通信。進程可以向隊列添加消息,被賦予讀權限的進程可以消費消息,MQ 克服了信號承載量小,管道只能用于無格式字節流以及緩沖區大小受限的缺點,但實時性相對受限。
- 共享內存(Shared Memory) :允許多個進程訪問同一塊公共的內存空間,這是效率最高的進程間通信方式。當一塊進程被多進程共享時,各個進程往往會與其他通信機制,如信號量結合使用。
- 套接字接口(Socket):消息隊列和共享內存只適合于單機多進程間的通信,套接字接口是更為普適的進程間通信機制,可用于不同機器之間的進程通信。套接字(socket)當僅限于本機進程間通信時,套接字接口是優化過的,不會經過網絡協議棧,不需要打包拆包、計算校驗和等,只是簡單的將應用層數據從一個進程拷貝到另一個進程。這種進程間通信叫 IPC Socket。
1.2 通信的成本
IPC Socket 不僅適用于本地相同機器間不同進程間的通信,由于 Socket 網絡棧的統一接口,它也理所應當的能支持基于網絡的跨機器進程間通信。這么做有一個看起來無比誘人的好處,由于 Socket 是各個操作系統都有提供的標準接口,完全有可能把遠程方法調用的通信細節隱藏在操作系統底層,從應用層面商看來可以做到遠程調用與本地的進程間通信在編碼上完全一致。事實上,在原始分布式時代的確是這么用的,但這種透明的調用形式卻反問濫用以至于顯著降低了分布式系統的性能。
所以,對 RPC 提出一系列的疑問:
- 兩個進程間通信,誰作為客戶端,誰作為服務端?
- 怎樣進行異常處理,如何讓調用者獲知?
- 服務端出現多線程競爭后改怎么辦?
- 如何提高網絡利用的效率?比如連接是否可以被多個請求復用以減少開銷,是否支持多播?
- 參數、返回值如何表示?應該有怎樣的字節序?
- 如何保證網絡的可靠性?
- 發送的請求服務端收不到回復怎么辦?
…
這里的中心觀點是:本地調用與遠程調用當做一樣處理,這是犯了方向性的錯誤。
Sun 公司的一眾大佬們總結了通過網絡記性分布式運算的八宗罪:
- 網路是可靠的,安全的,同質化的;(這是三點)
- 延遲是不存在的;
- 帶寬是無限的;
- 拓撲結構是一成不變的;
- 總會有一個管理員;
- 不必考慮傳輸成本。
以上八大問題潛臺詞就是說如果遠程服務調用要弄透明化的話,就必須為這些罪過埋單(就是RPC不能透明化)。這算是給 RPC 是否能等同于 IPC 正式定下了一個具有公信力的結論。至此,RPC 應該是一種高層次的或者說語言層次的特征,而不是像 IPC 那樣,是低層次的貨系統層次的特征成為工業界、學術界的主流觀點。
RPC 的定義:遠程服務調用是指位于互不重合的內存地址空間中的兩個程序,在語言層面上,以同步的方式使用有限帶寬的信道來傳輸程序控制信息。
1.3 三個基本問題
RPC 協議無外乎變著花樣使用各種手段來解決以下三個基本問題:
- 如何表示數據:這里的數據包含了傳遞給方法的參數,以及方法執行后的返回值。不同進程間數據讀取問題。且 RPC 完全可能綿連交互雙方各自使用不同程序語言的情況:即使是用了相同語言的 RPC 協議,在不同硬件指令集、不同OS下,同樣的數據類型也完全可能有不一樣的表現細節,如數據寬度、字節差異等。有效的做法是:將交互雙方所涉及的數據轉換為某種事先約定好的中立數據流格式來進行傳輸,將數據流轉換回不同語言中對應的數據類型來進行使用,就是序列化與反序列化。每種 RPC 協議都要有對應的序列化協議,如 gRPC Protocol Buffers,其他眾多輕量級 RPC 的 JSON Serialization。
- 如何傳輸數據:指如何通過網路,在兩個服務的 Endpoint 間相互操作、交換數據。這里的“交換數據”通常指的是應用層協議,實際傳輸一般是基于標準的 TCP、UDP 等標準的傳輸層協議來完成的。兩個服務交互不只是扔個序列化數據就行,還需有異常、超時、安全、認證、授權、事務等,這些都可能是雙方需交換的。這類數據叫:Wire Protocol。常見的有:Java RMI 的 JRMP,Web Service 的 SOAP,如果要求足夠簡單,雙方都是 HTTP Endpoint,就可直接使用 HTTP 協議(如 JSON-RPC)。
- 如何確定方法:這在本地方法調用中不是太大問題,編譯器會根據語言規范,將調用的方法簽名轉換為進程空間中子過程入口位置的指針。不過一旦要考慮不同語言,事情又麻煩起來,每門語言的方法前面可能不同,所以“如何表示同一個方法”,“找到對應的方法”還是得弄個跨語言的統一標準。這個標準非常簡單,如直接給程序的每個方法都規定一個唯一的、在任何機器上都絕不重復的編號,直接找對應方法。這是當初DCE 的解決方案,就是 UUID,雖然此后DCE 還是弄了一套語言無關的接口描述語言(Interface Description Language,IDL),但 UUID 卻廣為流傳。用于表示方法的協議:Web Service 的 WSDL,JSON-RPC 的JSON-WSP。
1.4 統一的 RPC 與 分裂的 RPC
剛開始的時候,大家總是嘗試 RPC 可以解決所有分布式問題,如剛開始的時候既要支持面向對象,又要支持多種語言,功能還齊全。但是沒有一家成功的,簡單、普適、高性能這三點,似乎真的難以同時滿足。所以現在的 RPC 仍處于百家爭鳴的時代,大家都不再去追求大而全的“完美”,而是有自己的針對性作為主要的發展方向。如:
- 面向對象發展的 RMI等;
- 朝著性能發展的,如 gRPC 和 Thrift,決定 RPC 性能的主要兩個因素:序列化效率和信息密度。序列化效率指輸出結果容量小,速度快,效率自然高;信息密度則取決于協議中有效負載(Payload)所占總傳輸數據的比例大小,使用傳輸協議的層次越高,信息密度越低,SOAP 使用XML拙劣的性能表現就是前車之鑒。gRPC 和 Thrift 都有自己優秀的專有序列化器,而傳輸協議方面,gRPC 是基于 HTTP/2 的,支持多路復用和 Header 壓縮,Thrift 則直接基于傳輸層的 TCP 協議來實現,省去了額外應用層協議的開銷。
- 朝著簡化發展:代表為 JSON-RPC,要說功能最強、速度最快的 RPC 可能會很有爭議,但選功能弱的、速度慢的,JSON-RPC 肯定會在候選人之一。犧牲了功能和效率,換來的是協議的簡單輕便,接口與格式都更為通用,尤其適合用于 Web 瀏覽器這類一般不會有額外協議支持、額外客戶端支持的應用場合。
到了最近幾年,RPC 框架有明顯的朝著更高層次(不僅負責調用遠程服務,還要管理遠程服務)與插件化方向發展的趨勢,不再追求獨立地解決 RPC 的全部三個問題(表示數據、傳遞數據、表示方法),而是將一部分功能設計成擴展點,讓用戶自己去選擇。框架聚焦于提供核心的、更高層次的能力,如提供負載均衡、服務注冊、可觀察性等方面的支持。這一類框架的代表有 Facebook 的 Thrift 和阿里的 Dubbo。尤其是斷更多年后重啟的 Dubbo 表現得更為明顯,它默認有自己的傳輸協議(Dubbo協議),同時也支持其他協議;默認采用 Hessian2 作為序列化器,如果你有 JSON 需求,可以替換為 Fastjson,如你對性能有更高的追求,可以替換為 Kryo、Protocol Buffers 等效率更好的序列化器,如果你不想依賴其他組件庫,直接使用 JDK 自帶的序列化器也是可以的。
開發一個分布式系統,是不是一定要用 RPC 呢?
2 REST 設計風格
很多人會拿 REST 與 RPC 互相比較,其實,REST 無論是在思想上、概念上,還是適用范圍上,與 RPC 都不盡相同,充其量只能算有些相似,但本質上并不是同一類型的東西。
2.1 理解 REST
個人會有好惡偏愛,但計算機科學是務實的,有了RPC還會提出REST,有了面向過程編程還能產生面向資源編程,后者總會有些前者沒有的閃光點,我們先理解 REST,在談論評價它。
比較容易理解 REST 思想的途徑是先理解什么是 HTTP,再配合一些實際例子來進行類比,你會發現REST(Representational State Transfer)實際是 HTT(Hypertext Transfer)的進一步抽象,兩者就如同接口與實現類的關系一般。
Hypertext 超文本一詞已被普遍接受,它指的是能夠進行分支判斷和差異響應的文本,相應地,超媒體指的是能夠進行分支判斷和差異響應的圖形、電影和聲音(也包括文本)的復合體。就是指文字可以點擊、可以出發腳本等。
下面從“超文本”或“超媒體”的含義來理解 REST 中的相關概念:
-
資源(Resource)比如你在閱讀的一篇文章,其內容本身就是資源;
-
表征(Representation)當你通過電腦瀏覽閱讀此文章時,瀏覽器向服務端發出請求“我需要這個資源的HTML 格式”,服務端向瀏覽器返回的這個 HTML 就是表征,或者是文本的 PDF、Markdown等都是,即表現形式。
-
狀態(State)當你讀完了這篇文章,想看后面是什么內容時,你想服務器發送“下一篇文章”的請求,但服務端不知道下一篇是哪一篇,就得根據你現在的上下文信息來判斷,以來的就是狀態;
-
轉移(Transfer)無論狀態是由服務端還是客戶端來提供,“取下一篇文章”這個行為必然只能由服務端來提供,服務端通過某種形式,把當前文章轉變為下一篇,這就叫“表征狀態轉移”。
-
統一接口(Uniform Interface)前面提到的下一篇的點擊跳轉,這是一種讓表征狀態發生轉移的方式,但是 URI 的含義是統一資源標識符,是一個名詞,如何表達出“轉移”的含義的呢?答案是 HTTP 協議中已經提前約定好了一套“統一接口”,它包括:GET、HEAD POST PUT DELETE TRACE OPTIONS七種基本操作,任何一個支持 HTTP協議的服務器都會遵守這套規定,對特定的 URI 采取這些操作,服務器就會觸發相應的表征狀態轉移的動作。
-
超文本驅動(Hypertext Driven)任何網站的導航(狀態轉移)行為都不可能是預置于瀏覽器代碼之中,而是由服務器發出的請求響應信息(超文本)來驅動的。
-
自描述信息 消息的類型以及應如何處理這條消息。如 Content-Type:application/json;charset=utf-8
2.2 RESTful 的系統
理解了上面的概念,我們就可以開始討論面向資源的呢編程思想與幾個具體的軟件架構設計原則了。一套完整的、完全滿足 REST 風格的系統應滿足以下六大原則:
- 服務端與客戶端分離(Client-Server)將用戶界面所關注的邏輯和數據存儲所關注的邏輯分離開來,有助于提高用戶界面的跨平臺的可移植性。
- 無狀態(Stateless)無狀態是 REST 的一條核心原則,部分開發者在做接口規劃時,覺得 REST 風格的服務怎么設計都感覺別怒,很有可能的一種原因是在服務端持有著比較重的狀態。應由客戶端承擔狀態維護職責,但目前大多數系統都達不到這個要求。
- 可緩存(Cacheability)無狀態服務雖然提升了系統的可見性、可靠性和可伸縮性,但降低了系統的網絡性。“降低網絡性”是指某個功能如果使用有狀態的設計只需一次(或少量)請求即可完成,而無狀態的得多次。為了緩解這個矛盾,REST 希望軟件系統能入萬維網一樣,允許客戶端和中間的通訊傳遞者(如代理)將部分服務端的應答緩存起來。
- 分層系統(Layered System)這里的分層并不是表示層、服務層、持久層這種,而是客戶端一般不用知道是否直接連接到了最終的服務器,或中間服務器。中間服務器可以通過負載均衡和共享緩存的機制提高系統的可擴展性,這樣也便于緩存、伸縮和安全策略的部署。該原則的典型應用是內容分發網絡。如我們瀏覽某個網站,并不是直接訪問源服務器,而是訪問了某個 CDN 服務器。
- 統一接口(Uniform Interface)這是 REST 的另一條核心原則,REST 希望開發者面向資源編程,希望軟件系統設計的重點放在抽象系統該有哪些資源上,而不是抽象系統該有哪些行為上。這條原則可以類比計算機中對文件管理的操作來理解,管理文件的操作是可數的,而且對所有文件都是固定的,統一的。如果面向資源來設計系統,同樣會有類似的操作特征,所有這些操作都借用了HTTP協議中固有的操作命令來完成。因為面向資源編程的抽象程度更高。想要在架構設計中合理恰當的利用統一接口,系統應能做到每次請求中都包含資源的ID,所有操作均通過資源ID 來進行;建議每個資源都是自描述的消息;建議通過超文本來驅動應用狀態的轉移。
- 按需代碼(Code-On-Demand)可選原則。
本節開篇提出的 REST 與 RPC 在思想上的差異?
REST 的基本思想是面向資源來抽象問題,它與此前流行的面向過程的編程在抽象主體上有本質區別。在 REST 提出以前,人們設計分布式系統的唯一方案就只有 RPC,開發者是圍繞著“遠程方法”去設計兩個系統交互的,這樣做的壞處不僅是“如何在不同系統間表示一個方法”、“如何獲得接口能夠提供的方法清單”都成了需要專門協議去解決的問題,更在于服務的每個方法都是獨立的,服務者必須熟悉每個方法才能使用,而REST抽象為了幾類