如何設計一個 RPC 系統?

RPC是一種方便的網絡通訊編程模型,因爲和編程語言的高度結合,大大減小了處理網絡數據的複雜度,讓代碼可讀性也有可觀的提升。可是RPC自己的構成卻比較複雜,因爲受到編程語言、網絡模型、使用習慣的約束,有大量的妥協和取捨之處。本文就是經過分析幾種流行的RPC實現案例,提供你們在設計RPC系統時的參考。程序員

因爲RPC底層的網絡開發通常和具體使用環境有關,而編程實現手段也很是多樣化,但不影響使用者,所以本文基本涉及如何實現一個RPC系統。spring

認識 RPC (遠程調用)

咱們在各類操做系統、編程語言生態圈中,多少都會接觸過「遠程調用」的概念。通常來講,他們指的是用簡單的一行代碼,經過網絡調用另一個計算機上的某段程序。好比:編程

  • RMI——Remote Method Invoke:調用遠程的方法。「方法」通常是附屬於某個對象上的,因此一般RMI指對在遠程的計算機上的某個對象,進行其方法函數的調用。
  • RPC——Remote Procedure Call:遠程過程調用。指的是對網絡上另一個計算機上的,某段特定的函數代碼的調用。

遠程調用自己是網絡通訊的一種概念,他的特色是把網絡通訊封裝成一個相似函數的調用。網絡通訊在遠程調用外,通常還有其餘的幾種概念:數據包處理、消息隊列、流過濾、資源拉取等待。下面比較一下他們差別:瀏覽器

方案性能優化

編程方式服務器

信息封裝網絡

傳輸模型數據結構

典型應用架構

遠程調用負載均衡

調用函數,輸入參數,得到返回值。

使用編程語言的變量、類型、函數

發出請求,得到響應

Java RMI

數據包處理

調用Send()/Recv(),使用字節碼數據,編解碼,處理內容

把通訊內容構形成二進制的協議包

發送/接收

UDP編程

消息隊列

調用Put()/Get(),使用「包」對象,處理其包含的內容

消息被封裝成語言可用的對象或結構

對某隊列,存入一個消息;取出一個消息

ActiveMQ

流過濾

讀取一個流,或寫出一個流,對流中的單元包即刻處理

單元長度很小的統一數據結構

鏈接;發送/接收;處理

網絡視頻

資源拉取

輸入一個資源ID,得到資源內容

請求或響應都包含:頭部+正文

請求後等待響應

WWW

針對遠程調用的特色——調用函數。業界在各類語言下都開發過相似的方案,同時也有些方案是試圖作到跨語言的。儘管遠程調用在編程方式上,看起來彷佛是最簡單易用的,可是也有明顯的缺點。因此瞭解清楚遠程調用的優點和缺點,是決定是否要開發、或者使用遠程調用這種模型的關鍵問題。

遠程調用的優點有:

  • 屏蔽了網絡層。所以在傳輸協議和編碼協議上,咱們能夠選擇不一樣的方案。好比WebService方案就是用的HTTP傳輸協議+SOAP編碼協議;而REST的方案每每使用HTTP+JSON協議。Facebook的Thrift甚至能夠定製任何不一樣的傳輸協議和編碼協議,你能夠用TCP+Google Protocol Buffer,也能夠用UDP+JSON……。因爲屏蔽了網絡層,你能夠根據實際須要來獨立的優化網絡部分,而無需涉及業務邏輯的處理代碼,這對於須要在各類網絡環境下運行的程序來講,很是有價值。
  • 函數映射協議。你能夠直接用編程語言來書寫數據結構和函數定義,取代編寫大量的編碼協議格式和分包處理邏輯。對於那些業務邏輯很是複雜的系統,好比網絡遊戲,能夠節省大量定義消息格式的時間。並且函數調用模型很是容易學習,不須要學習通訊協議和流程,讓經驗較淺的程序員也能很容易的開始使用網絡編程。

遠程調用的缺點:

  • 增長了性能消耗。因爲把網絡通訊包裝成「函數」,須要大量額外的處理。好比須要預生產代碼,或者使用反射機制。這些都是額外消耗CPU和內存的操做。並且爲了表達複雜的數據類型,好比變長的類型string/map/list,這些都要數據包中增長更多的描述性信息,則會佔用更多的網絡包長度。
  • 沒必要要的複雜化。若是你僅僅是爲了某些特定的業務需求,好比傳送一個固定的文件,那麼你應該用HTTP/FTP協議模型。若是爲了作監控或者IM軟件,用簡單的消息編碼收發會更快速高效。若是是爲了作代理服務器,用流式的處理會很簡單。另外,若是你要作數據廣播,那麼消息隊列會很容易作到,而遠程調用這幾乎沒法完成。

所以,遠程調用最適合的場景是:業務需求多變,網絡環境多變。

RPC方案的核心問題

因爲遠程調用的使用接口是「函數」,因此要如何構建這個「函數」,就產生了三個方面須要決策的問題:

1 . 如何表示「遠程」的信息

所謂遠程,就是指網絡上另一個位置,那麼網絡地址就是必需要輸入的部分。在TCP/IP網絡下,IP地址和端口號表明了運行中程序的一個入口。因此指定IP地址和端口是發起遠程調用所必需的。

然而,一個程序可能會運行不少個功能,能夠接收多個不一樣含義的遠程調用。這樣如何去讓用戶指定這些不一樣含義的遠程調用入口,就成爲了另一個問題。固然最簡單的是每一個端口一種調用,可是一個IP最多支持65535個端口,並且別的網絡功能也可能須要端口,因此這種方案可能會不夠用,同時一個數字表明一個功能也不太好理解,必需要查表才能明白。

因此咱們必須想別的方法。在面向對象的思想下,有些方案提出了:以不一樣的對象來概括不一樣的功能組合,先指定對象,再指定方法。這個想法很是符合程序員的理解方式,EJB就是這種方案的。一旦你肯定了用對象這種模型來定義遠程調用的地址,那麼你就須要有一種指定遠程對象的方法,爲了指定對象,你必需要能把對象的一些信息,從被調用方(服務器端)傳輸給調用方(客戶端)。

最簡單的方案就是客戶端輸入一串字符串做爲對象的「名字」,發給服務器端,查找註冊了這個「名字」的對象,若是找到了,服務器端就會用某種技術「傳輸」這個對象給客戶端,而後客戶端就能夠調用他的方法了。固然這種傳輸不多是把整個服務器上的對象數據拷貝給客戶端,而是用一些符號或者標誌的方法,來表明這個服務器上的對象,而後發給客戶端。

若是你不是使用面向對象的模型,那麼遠程的一個函數,也是必需要定位和傳輸的,由於你調用的函數必須先能找到,而後成爲客戶端側的一個接口,才能調用。針對「遠程對象」(這裏說的對象包括面向對象的對象或者僅僅是 函數)如何表達才能在網絡上定位;以及定位成功以後以什麼形式供客戶端調用,都是「遠程調用」設計方案中第一個重要的問題。

2 . 函數的接口形式應該如何表示

遠程調用因爲受到網絡通訊的約束,因此每每不能徹底的支持編程語言的全部特性。好比C語言函數中的指針類型參數,就沒法經過網絡傳遞出去。所以遠程調用的函數定義,能用語言中的什麼特性,不能用什麼特性,是須要在設計方案是規定下來的。

這種規定若是太嚴格,會影響使用者的易用性;若是太寬泛,則可能致使遠程調用的性能低下。如何去設計一種方式,把編程語言中的函數,描述成一個遠程調用的函數,也是須要考慮的問題。不少方案採用了配置文件這種通用的方式,而另一些方案能夠直接在源代碼中裏面加特殊的註釋。

通常來講,編譯型語言如C/C++只能採用源代碼根據配置文件生成的方案,虛擬機型語言如C#/JAVA能夠採用反射機制結合配置文件(設置是在源代碼中用特殊註釋來代替配置文件)的方案,若是是腳本語言就更簡單,有時候連配置文件都不須要,由於腳本本身就能夠充當。總之遠程調用的接口要知足怎樣的約束,也是一個須要仔細考慮的問題。

3. 用什麼方法來實現網絡通訊

遠程調用最重要的實現細節,就是關於網絡通訊。用何種通訊方式來承載遠程調用的問題,細化下來就是兩個子問題:用什麼樣的服務程序提供網絡功能?用什麼樣的通訊協議?

遠程調用系統能夠本身直接對TCP/IP編程來實現通訊,也能夠委託一些其餘軟件,好比Web服務器、消息隊列服務器等等……也可使用不一樣的網絡通訊框架,如Netty/Mina這些開源框架。通訊協議則通常有兩層:一個是傳輸協議,好比TCP/UDP或者高層一點的HTTP,或者本身定義的傳輸協議;另一個是編碼協議,就是如何把一個編程語言中的對象,序列化和反序列化成爲二進制字節流的方案,流行的方案有JSON、Google Protocol Buffer等等,不少開發語言也有本身的序列化方案,如JAVA/C#都自帶。以上這些技術細節,應該選擇使用哪些,直接關係到遠程調用系統的性能和環境兼容性。

以上三個問題,就是遠程調用系統必須考慮的核心選型。根據每一個方案所面對的約束不一樣,他們都會在這三個問題上作出取捨,從而適應其約束。可是如今並不存在一個「萬能」或者「通用」的方案,其緣由就是:在如此複雜的一個系統中,若是要照顧的特性越多,須要付出的成本(易用性代價、性能開銷)也會越多。

下面,咱們能夠研究下業界現存的各類遠程調用方案,看他們是如何在這三個方面作平衡和選擇的。

業界方案舉例

1. CORBA

CORBA是一個「古老」的,雄心勃勃的方案,他試圖在完成遠程調用的同時,還完成跨語言的通訊的任務,所以其複雜程度是最高的,可是它的設計思想,也被後來更多的其餘方案所學習。在通訊對象的定位上,它使用URL來定義一個遠程對象,這是在互聯網時代很是容易接受的。其對象的內容則限定在C語言類型上,而且只能傳遞值,這也是很是容易理解的。爲了能讓不一樣語言的程序通訊,因此就必需要在各類編程語言以外獨立設計一種僅僅用於描述遠程接口的語言,這就是所謂的IDL:Interface Description Language 接口描述語言。

用這個方法,你就能夠先用一種超然於全部語言以外的語言來定義接口,而後使用工具自動生成各類編程語言的代碼。這種方案對於編譯型語言幾乎是惟一選擇。CORBA並無對通訊問題有任何約定,而是留給具體語言的實現者去處理,這也許是他沒有普遍流行的緣由之一。

實際上CORBA有一個很是著名的繼承者,他就是Facebook公司的Thrift框架。Thrift也是使用一種IDL編譯生成多種語言的遠程調用方案,而且用C++/JAVA等多種語言完整的實現了通訊承載,因此在開源框架中是特別有號召力的一個。Thrfit的通訊承載還有個特色,就是能組合使用各類不一樣的傳輸協議和編碼協議,好比TCP/UDP/HTTP配合JSON/BIN/PB……這讓它幾乎能夠選擇任何的網絡環境。

Thrift的模型相似下圖,這裏有的stub表示「樁代碼」,就是客戶端直接使用的函數形式程序;skeleton表示「骨架代碼」,是須要程序員編寫具體提供遠程服務功能的模板代碼,通常對模版作填空或者繼承(擴展)便可。這個stub-skeleton模型幾乎是全部遠程調用方案的標配。

2. JAVA RMI

JAVA RMI是JAVA虛擬機自帶的一個遠程調用方案。它也是可使用URL來定位遠程對象,使用JAVA自帶的序列化編碼協議傳遞參數值。在接口描述上,因爲這是一個僅限於JAVA環境下的方案,因此直接用JAVA語言的Interface類型做爲定義語言。用戶經過實現這個接口類型來提供遠程服務,同時JAVA會根據這個接口文件自動生成客戶端的調用代碼供調用者使用。他的底層通訊實現,仍是用TCP協議實現的。在這裏,Interface文件就是JAVA語言的IDL,同時也是skeleton模板,供開發者來填寫遠程服務內容。而stub代碼則因爲JAVA的反射功能,由虛擬機直接包辦了。

這個方案因爲JAVA虛擬機的支持,使用起來很是簡單,徹底按照標誌的JAVA編程方法就能夠輕鬆解決問題,可是這也僅僅能在JAVA環境下運行,限制了其適用的範圍。魚與熊掌不可兼得,易用性和適用性每每是互相沖突的。這和CORBA/Thrift追求最大範圍的適用性有很大的差異,也致使了二者在易用性上的不一樣。

3. Windows RPC

Windows中對RPC支持是比較早和比較完善的。首先它經過GUID來查詢對象,而後使用C語言類型做爲參數值的傳遞。因爲Windows的API主要是C語言的,因此對於RPC功能來講,仍是要用一種IDL來描述接口,最後生成.h和.c文件來生產RPC的stub和skeleton代碼。而通訊機制,因爲是操做系統自帶的,因此使用內核LPC機制承載,這一點仍是對使用者來講比較方便的。可是也限制了只能用於Windows程序之間作調用。

4. WebService & REST

在互聯網時代,程序須要經過互聯網來互相調用。而互聯網上最流行的協議是HTTP協議和WWW服務,所以使用HTTP協議的Web Service就瓜熟蒂落的成爲跨系統調用的最流行方案。因爲可使用大多數互聯網的基礎設施,因此Web Service的開發和實現幾乎是毫無難度的。通常來講,它都會使用URL來定位遠程對象,而參數則經過一系列預約義的類型(主要是C語言基礎類型),以及對象序列化方式來傳遞。接口生成方面,你能夠本身直接對HTTP作解析,也可使用諸如WSDL或者SOAP這樣的規範。在REST的方案中,則限定了只有PUT/GET/DELETE/POST四種操做函數,其餘都是參數。

總結一下上面的這些RPC方案,咱們發現,針對遠程調用的三個核心問題,通常業界有如下幾個選擇:

  • 遠程對象定位:使用URL;或者使用名字服務來查找
  • 遠程調用參數傳遞:使用C的基本類型定義;或者使用某種預訂的序列化(反序列化)方案
  • 接口定義:使用某種特定格式的技術,直接按預先約定一種接口定義文件;或者使用某種描述協議IDL來生成這些接口文件
  • 通訊承載:有使用特定TCP/UDP之類的服務器,也有可讓用戶本身開發定製的通訊模型;還有使用HTTP或者消息隊列這一類更加高級的傳輸協議

方案選型

在咱們肯定了遠程調用系統方案几個可行選擇後,天然就要明確一下各個方案的優缺點,這樣才能選擇真正合適需求的設計:

1. 對於遠程對象的描述:使用URL是互聯網通行的標準,比較方便用戶理解,也容易添加往後須要擴展到內容,由於URL自己是一個由多個部分組合的字符串;而名字服務則老式一些,可是依然有他的好處,就是名字服務能夠附帶負載均衡、容災擴容、自定義路由等一系列特性,對於需求複雜的定位比較容易實現。

2. 遠程調用的接口描述:若是隻限制於某個語言、操做系統、平臺上,直接利用「隱喻」方式的接口描述,或者以「註解」類型註釋手段來標註源代碼,實現遠程調用接口的定義,是最方便不過的。可是,若是須要兼容編譯型語言,如C/C++,就必定要用某種IDL來生成這些編譯語言的源代碼了。

3.通訊承載:給用戶本身定製通訊模塊,能提供最好的適用性,可是也讓用戶增長了使用的複雜程度。而HTTP/消息隊列這種承載方式,在系統的部署、運維、編程上都會比較簡單,缺點就是對於性能、傳輸特性的定製空間就比較小。

分析完核心問題,咱們還須要考慮一些適用性場景:

1. 面向對象仍是面向過程:若是咱們只是考慮作面向過程的遠程調用,只須要定位到「函數」便可。而若是是面向對象的,則須要定位到「對象」。因爲函數是無狀態的,因此其定位過程能夠簡單到一個名字便可,而對象則須要動態的查找到其ID或句柄。

2.跨語言仍是單一語言:單一語言的方案中,頭文件或接口定義徹底用一種語言處理便可,若是是跨語言的,就少難免要IDL

3. 混合式通訊承載仍是使用HTTP服務器承載:混合式承載可能能夠用到TCP/UDP/共享內存等底層技術,能夠提供最優的性能,可是使用起來必然很是麻煩。使用HTTP服務器的話,則很是簡單,由於WWW服務的開源軟件、庫衆多,並且客戶端使用瀏覽器或者一些JS頁面便可調試,缺點是其性能較低。

假設咱們如今要爲某種業務邏輯很是多變的領域,如企業業務應用領域,或遊戲服務器端領域,去設計一個遠程調用系統,咱們可能應該以下選擇:

1. 使用名字服務定位遠程對象:因爲企業服務是須要高可用性的,使用名字服務能在查詢名字時識別和選擇可用性服務對象。J2EE方案中的EJB(企業JavaBean)就是用名字服務的。

2. 使用IDL來生成接口定義:因爲企業服務或遊戲服務,其開發語言可能不是統一的,又或者須要高性能的編程語言如C/C++,因此只能使用IDL。

3.使用混合式通訊承載:雖然企業服務看起來無需在很複雜的網絡下運行,可是不一樣的企業的網絡環境又多是千差萬別的,因此要作一個通用的系統,最好仍是不怕麻煩提供混合式的通訊承載,這樣能夠在TCP/UDP等各類協議中選擇。

 

注:關注做者公衆號,瞭解更多分佈式架構、微服務、netty、MySQL、spring、性能優化、

等知識點。公衆號:《JAVA架構進階之路》

相關文章
相關標籤/搜索