1. 程式人生 > >你應該知道的 RPC 原理

你應該知道的 RPC 原理

在校期間大家都寫過不少程式,比如寫個hello world服務類,然後本地呼叫下,如下所示。這些程式的特點是服務消費方和服務提供方是本地呼叫關係。

而一旦踏入公司尤其是大型網際網路公司就會發現,公司的系統都由成千上萬大大小小的服務組成,各服務部署在不同的機器上,由不同的團隊負責。這時就會遇到兩個問題:1)要搭建一個新服務,免不了需要依賴他人的服務,而現在他人的服務都在遠端,怎麼呼叫?2)其它團隊要使用我們的服務,我們的服務該怎麼釋出以便他人呼叫?下文我們將對這兩個問題展開探討。

Java
123 publicinterfaceHelloWorldService{StringsayHello(Stringmsg);}
Java
12345678 publicclassHelloWorldServiceImplimplementsHelloWorldService{@OverridepublicStringsayHello(Stringmsg){Stringresult="hello world "+msg;System.out.println(result);returnresult;}}
Java
123456 publicclassTest{publicstaticvoidmain(String[]args){HelloWorldService helloWorldService=newHelloWorldServiceImpl();helloWorldService.sayHello("test");}}

1 如何呼叫他人的遠端服務?

由於各服務部署在不同機器,服務間的呼叫免不了網路通訊過程,服務消費方每呼叫一個服務都要寫一坨網路通訊相關的程式碼,不僅複雜而且極易出錯。

如果有一種方式能讓我們像呼叫本地服務一樣呼叫遠端服務,而讓呼叫者對網路通訊這些細節透明,那麼將大大提高生產力,比如服務消費方在執行helloWorldService.sayHello(“test”)時,實質上呼叫的是遠端的服務。這種方式其實就是RPC(Remote Procedure Call Protocol),在各大網際網路公司中被廣泛使用,如阿里巴巴的hsf、dubbo(開源)、Facebook的thrift(開源)、Google grpc(開源)、Twitter的finagle等。

要讓網路通訊細節對使用者透明,我們自然需要對通訊細節進行封裝,我們先看下一個RPC呼叫的流程:

  • 1)服務消費方(client)呼叫以本地呼叫方式呼叫服務;
  • 2)client stub接收到呼叫後負責將方法、引數等組裝成能夠進行網路傳輸的訊息體;
  • 3)client stub找到服務地址,並將訊息傳送到服務端;
  • 4)server stub收到訊息後進行解碼;
  • 5)server stub根據解碼結果呼叫本地的服務;
  • 6)本地服務執行並將結果返回給server stub;
  • 7)server stub將返回結果打包成訊息併發送至消費方;
  • 8)client stub接收到訊息,並進行解碼;
  • 9)服務消費方得到最終結果。

RPC的目標就是要2~8這些步驟都封裝起來,讓使用者對這些細節透明。

1.1 怎麼做到透明化遠端服務呼叫?

怎麼封裝通訊細節才能讓使用者像以本地呼叫方式呼叫遠端服務呢?對java來說就是使用代理!java代理有兩種方式:1) jdk 動態代理;2)位元組碼生成。儘管位元組碼生成方式實現的代理更為強大和高效,但程式碼不易維護,大部分公司實現RPC框架時還是選擇動態代理方式。

下面簡單介紹下動態代理怎麼實現我們的需求。我們需要實現RPCProxyClient代理類,代理類的invoke方法中封裝了與遠端服務通訊的細節,消費方首先從RPCProxyClient獲得服務提供方的介面,當執行helloWorldService.sayHello(“test”)方法時就會呼叫invoke方法。

Java
12345678910111213141516171819202122232425 publicclassRPCProxyClient implementsjava.lang.reflect.InvocationHandler{privateObjectobj;publicRPCProxyClient(Objectobj){this.obj=obj;}/**     * 得到被代理物件;     */publicstaticObjectgetProxy(Objectobj){returnjava.lang.reflect.Proxy.newProxyInstance(obj.getClass().getClassLoader(),obj.getClass().getInterfaces(),newRPCProxyClient(obj));}/**     * 呼叫此方法執行     */publicObjectinvoke(Objectproxy,Method method,Object[]args)throwsThrowable{//結果引數;Objectresult=newObject();// ...執行通訊相關邏輯// ...returnresult;}}
Java
123456 publicclassTest{publicstaticvoidmain(String[]args){HelloWorldService helloWorldService=(HelloWorldService)RPCProxyClient.getProxy(HelloWorldService.class);helloWorldService.sayHello("test");}}

1.2  怎麼對訊息進行編碼和解碼?

1.2.1 確定訊息資料結構

上節講了invoke裡需要封裝通訊細節,而通訊的第一步就是要確定客戶端和服務端相互通訊的訊息結構。客戶端的請求訊息結構一般需要包括以下內容:

1)介面名稱

在我們的例子裡介面名是“HelloWorldService”,如果不傳,服務端就不知道呼叫哪個介面了;

2)方法名

一個介面內可能有很多方法,如果不傳方法名服務端也就不知道呼叫哪個方法;

3)引數型別&引數值

引數型別有很多,比如有bool、int、long、double、string、map、list,甚至如struct(class);

以及相應的引數值;

4)超時時間

5)requestID,標識唯一請求id,在下面一節會詳細描述requestID的用處。

同理服務端返回的訊息結構一般包括以下內容。

1)返回值

2)狀態code

3)requestID

1.2.2 序列化

一旦確定了訊息的資料結構後,下一步就是要考慮序列化與反序列化了。

什麼是序列化?序列化就是將資料結構或物件轉換成二進位制串的過程,也就是編碼的過程。

什麼是反序列化?將在序列化過程中所生成的二進位制串轉換成資料結構或者物件的過程。

為什麼需要序列化?轉換為二進位制串後才好進行網路傳輸嘛!為什麼需要反序列化?將二進位制轉換為物件才好進行後續處理!

現如今序列化的方案越來越多,每種序列化方案都有優點和缺點,它們在設計之初有自己獨特的應用場景,那到底選擇哪種呢?從RPC的角度上看,主要看三點:1)通用性,比如是否能支援Map等複雜的資料結構;2)效能,包括時間複雜度和空間複雜度,由於RPC框架將會被公司幾乎所有服務使用,如果序列化上能節約一點時間,對整個公司的收益都將非常可觀,同理如果序列化上能節約一點記憶體,網路頻寬也能省下不少;3)可擴充套件性,對網際網路公司而言,業務變化快,如果序列化協議具有良好的可擴充套件性,支援自動增加新的業務欄位,刪除老的欄位,而不影響老的服務,這將大大提供系統的健壯性。

目前國內各大網際網路公司廣泛使用hessian、protobuf、thrift、avro等成熟的序列化解決方案來搭建RPC框架,這些都是久經考驗的解決方案。

1.3  通訊

訊息資料結構被序列化為二進位制串後,下一步就要進行網路通訊了。目前有兩種IO通訊模型:1)BIO;2)NIO。一般RPC框架需要支援這兩種IO模型,原理可參考:《一個故事講清楚 NIO》

如何實現RPC的IO通訊框架?1)使用java nio方式自研,這種方式較為複雜,而且很有可能出現隱藏bug,見過一些網際網路公司使用這種方式;2)基於mina,mina在早幾年比較火熱,不過這些年版本更新緩慢;3)基於netty,現在很多RPC框架都直接基於netty這一IO通訊框架,比如阿里巴巴的HSF、dubbo,Twitter的finagle等。

1.4  訊息裡為什麼要帶有requestID?

如果使用netty的話,一般會用channel.writeAndFlush()方法來發送訊息二進位制串,這個方法呼叫後對於整個遠端呼叫(從發出請求到接收到結果)來說是一個非同步的,即對於當前執行緒來說,將請求傳送出來後,執行緒就可以往後執行了,至於服務端的結果,是服務端處理完成後,再以訊息的形式傳送給客戶端的。於是這裡出現以下兩個問題:

1)怎麼讓當前執行緒“暫停”,等結果回來後,再向後執行?

2)如果有多個執行緒同時進行遠端方法呼叫,這時建立在client server之間的socket連線上會有很多雙方傳送的訊息傳遞,前後順序也可能是隨機的,server處理完結果後,將結果訊息傳送給client,client收到很多訊息,怎麼知道哪個訊息結果是原先哪個執行緒呼叫的?

如下圖所示,執行緒A和執行緒B同時向client socket傳送請求requestA和requestB,socket先後將requestB和requestA傳送至server,而server可能將responseA先返回,儘管requestA請求到達時間更晚。我們需要一種機制保證responseA丟給ThreadA,responseB丟給ThreadB。

怎麼解決呢?

1)client執行緒每次通過socket呼叫一次遠端介面前,生成一個唯一的ID,即requestID(requestID必需保證在一個Socket連線裡面是唯一的),一般常常使用AtomicLong從0開始累計數字生成唯一ID;

2)將處理結果的回撥物件callback,存放到全域性ConcurrentHashMap裡面put(requestID, callback);

3)當執行緒呼叫channel.writeAndFlush()傳送訊息後,緊接著執行callback的get()方法試圖獲取遠端返回的結果。在get()內部,則使用synchronized獲取回撥物件callback的鎖,再先檢測是否已經獲取到結果,如果沒有,然後呼叫callback的wait()方法,釋放callback上的鎖,讓當前執行緒處於等待狀態。

4)服務端接收到請求並處理後,將response結果(此結果中包含了前面的requestID)傳送給客戶端,客戶端socket連線上專門監聽訊息的執行緒收到訊息,分析結果,取到requestID,再從前面的ConcurrentHashMap裡面get(requestID),從而找到callback物件,再用synchronized獲取callback上的鎖,將方法呼叫結果設定到callback物件裡,再呼叫callback.notifyAll()喚醒前面處於等待狀態的執行緒。

Java
1234567 publicObjectget(){synchronized(this){// 旋鎖while(!isDone){// 是否有結果了wait();//沒結果是釋放鎖,讓當前執行緒處於等待狀態}}}
Java
1234567 privatevoidsetDone(Response res){this.res=res;isDone=true;synchronized(this){//獲取鎖,因為前面wait()已經釋放了callback的鎖了notifyAll();// 喚醒處於等待的執行緒}}

2 如何釋出自己的服務?

如何讓別人使用我們的服務呢?有同學說很簡單嘛,告訴使用者服務的IP以及埠就可以了啊。確實是這樣,這裡問題的關鍵在於是自動告知還是人肉告知。

人肉告知的方式:如果你發現你的服務一臺機器不夠,要再新增一臺,這個時候就要告訴呼叫者我現在有兩個ip了,你們要輪詢呼叫來實現負載均衡;呼叫者咬咬牙改了,結果某天一臺機器掛了,呼叫者發現服務有一半不可用,他又只能手動修改程式碼來刪除掛掉那臺機器的ip。現實生產環境當然不會使用人肉方式。

有沒有一種方法能實現自動告知,即機器的增添、剔除對呼叫方透明,呼叫者不再需要寫死服務提供方地址?當然可以,現如今zookeeper被廣泛用於實現服務自動註冊與發現功能!

簡單來講,zookeeper可以充當一個服務登錄檔(Service Registry),讓多個服務提供者形成一個叢集,讓服務消費者通過服務登錄檔獲取具體的服務訪問地址(ip+埠)去訪問具體的服務提供者。如下圖所示:

具體來說,zookeeper就是個分散式檔案系統,每當一個服務提供者部署後都要將自己的服務註冊到zookeeper的某一路徑上: /{service}/{version}/{ip:port}, 比如我們的HelloWorldService部署到兩臺機器,那麼zookeeper上就會建立兩條目錄:分別為/HelloWorldService/1.0.0/100.19.20.01:16888  /HelloWorldService/1.0.0/100.19.20.02:16888。

zookeeper提供了“心跳檢測”功能,它會定時向各個服務提供者傳送一個請求(實際上建立的是一個 socket 長連線),如果長期沒有響應,服務中心就認為該服務提供者已經“掛了”,並將其剔除,比如100.19.20.02這臺機器如果宕機了,那麼zookeeper上的路徑就會只剩/HelloWorldService/1.0.0/100.19.20.01:16888。

服務消費者會去監聽相應路徑(/HelloWorldService/1.0.0),一旦路徑上的資料有任務變化(增加或減少),zookeeper都會通知服務消費方服務提供者地址列表已經發生改變,從而進行更新。

更為重要的是zookeeper 與生俱來的容錯容災能力(比如leader選舉),可以確保服務登錄檔的高可用性。

3 小結

RPC幾乎是每一個從學校進入網際網路公司的同學都要首先學習的框架,之前面試過一個在大型網際網路公司工作過兩年的同學,對RPC還是停留在使用層面,這是不應該的。本文也僅是對RPC的一個比較粗糙的描述,希望對大家有所幫助,錯誤之處也請指出修正。

4 一些開源的RPC框架

打賞支援我寫出更多好文章,謝謝!

打賞作者

打賞支援我寫出更多好文章,謝謝!

相關推薦

應該知道RPC 原理(好文,忍不住轉到自己空間。)

在校期間大家都寫過不少程式,比如寫個hello world服務類,然後本地呼叫下,如下所示。這些程式的特點是服務消費方和服務提供方是本地呼叫關係。 而一旦踏入公司尤其是大型網際網路公司就會發現,公司的系統都由成千上萬大大小小的服務組成,各服務部署在不同的機器

應該知道RPC 原理

在校期間大家都寫過不少程式,比如寫個hello world服務類,然後本地呼叫下,如下所示。這些程式的特點是服務消費方和服務提供方是本地呼叫關係。 而一旦踏入公司尤其是大型網際網路公司就會發現,公司的系統都由成千上萬大大小小的服務組成,各服務部署在不同的機器上,由不

【Big Data 每日一題20181103】應該知道RPC原理

你應該知道的RPC原理 在學校期間大家都寫過不少程式,比如寫個hello world服務類,然後本地呼叫下,如下所示。這些程式的特點是服務消費方和服務提供方是本地呼叫關係。   而一旦踏入公司尤其是大型網際網路公司就會發現,公司的系統都由成千上萬大大小小的服務組成,各

色彩力量!21款應該知道的優秀品牌設計

data- 一個 蘋果 歐洲 alt img 一次 rpi width 色彩理論是設計的核心,也是常常被忽略的領域。色彩本身就與情感相連。一般而言,紅色、橙色和黃色這種暖色充滿活力。令人振奮。而藍色、綠色這種冷色調則顯得更加沈穩內斂。這一點非常重要。所以,當我們談及

應該知道的 5 個 Docker 工具

.so 使用 開發環境 /var/ 聲明 use 中大 mount host 你可以在網上找到大量炫酷的Docker 工具,並且大部分是開源的,可以通過Github訪問。在過去的兩年裏,我開始在開發項目中大量使用Docker。當你開始使用Docker,你會發現它比你想象的還

第一次使用Android Studio時應該知道的一切配置

出現 jpg rcu true 導入 職位 文章 加載 什麽 【聲明】 歡迎轉載,但請保留文章原始出處→_→ 生命壹號:http://www.cnblogs.com/smyhvae/ 文章來源:http://www.cnblogs.com/smyhvae/p/43909

第一次使用Android Studio時應該知道的一切配置(二):新建一個屬於自己的工程並安裝Genymotion模擬器

人性 pro net 參考 json irb 一個地方 vid 調試 【聲明】 歡迎轉載,但請保留文章原始出處→_→ 生命壹號:http://www.cnblogs.com/smyhvae/ 文章來源:http://www.cnblogs.com/smyhvae/p/439

應該知道的網頁設計中的規則和禁忌

以下內容由Mockplus團隊翻譯整理,僅供學習交流,Mockplus是更快更簡單的原型設計工具。 網頁設計是一個棘手的話題。當你創建網站時你需要考慮很多事情。為了簡化這個任務,我這裏準備了一個列表,每個網頁設計師在設計網頁時都應該考慮這些註意事項。好消息是,這都是一些簡單的設計原則

Android中關於View滑動的實現應該知道

nan ida gif 當前位置 距離 保存 改變 post 控件 滑動作為Android中最基礎的特效之一,使用場景非常廣泛。實現的方式也有多種,理解各種滑動的實現方式。清楚在開發中根據自己的實際需求,選擇合理的實現方案。這篇文章從:scrollTo()/scrollBy

關於 Token,應該知道的十件事

敏感信息 you load 冒充 tro hex 服務器 xhr cors 轉自:http://ju.outofmemory.cn/entry/134189 原文是一篇很好的講述 Token 在 Web 應用中使用的文章,而這是我和 Special 合作翻譯的譯文。 1.

做網站SEO優化,這些網絡引流方法,應該知道

尋求 可能 垃圾郵件 百度搜 如果 什麽 網站鏈接 很快 建立 對於網站SEO優化來說,網站流量的重要性不言而喻!國內的站長平臺工具通過用網站流量來衡量一個網站的權重,當你的網站流量很高的時候,同時會影響你網站的權重,進而影響你網站SEO優化排名。所以說流量對於一個網站的意

華為認證那些應該知道的事情

含金量 network 技術人才 設計工程師 技術 soc 入職 信息 網絡架構 華為認證主要分為三個等級:HCNA、HCNP以及HCIE:一、 華為認證數據通信工程師-HCDA(Huawei Certified Network Associate) 認證前提:無適

第一次使用Android Studio時應該知道的一切配置(三):gradle項目構建

gen 官方 配置文件 conf 什麽 學習 package ack 處的 ?【聲明】 歡迎轉載,但請保留文章原始出處→_→ 生命壹號:http://www.cnblogs.com/smyhvae/ 文章來源:http://www.cnblogs.com/smyhvae

應該知道的JAVA面試題

適配器模式 初始 現在 訂票 http 一致性 用戶 策略模式 參數 經常面試一些候選人,整理了下我面試使用的題目,陸陸續續整理出來的題目很多,所以每次會抽一部分來問。答案會在後面的文章中逐漸發布出來。 基礎題目 Java線程的狀態 進程和線程的區別,進程間如何通訊,

使用“數據驅動測試”之前應該知道的(二)

clas back args ase 沒有 告訴 ... last 數據文件 我們繼續上期的話題,單純讀取數據文件來做自動化是有諸多問題的。那麽我們借助單元測試框架來做自動化就爽多了,因為它解決了測試中的幾問題。 如何定義一條測試用例,我們知道編程的世界裏並沒“用例”的概

Select 使用不當引發的core,應該知道

retcode 代碼 async fetch sse com 基礎 -a cnblogs 排查一個死機問題,搞了好幾天時間,最終確定原因;最終確定問題原因,在此分享一下; 第一步:常規根據core文件查看棧信息,gdb –c core xxxx 如下rip不正確,指令地址錯

sping IOC和DI 應該知道事情1

spa oschina rabl logs abstract pri ati lap 好的 springIOC和spring DI作為spring core的核心思想,有必要學習下才能更好的使用spring =================================

Python:應該知道這些

服務 數學 語言特點 這樣的 數學庫 郵件 說明 商業 圖形顯示 1. Python的出生 1989年  Guido van Rossum開始編寫Python語言編輯器(龜叔為了打發無聊的聖誕節) 1991年  第一個Python編譯器誕生(正式誕生) 1994年  Pyt

應該知道的react router 4(三)

req static mat 測試 lin -name trie span sta   上一篇我們說到了路由組件的嵌套。想必你已經運用自如了。那麽,這一次我們來聊一聊4.X中Router的變更。 在3.X中我們若使用路由的模式,可通過在Router上配置history的值即

應該知道的react router 4(五)

parent 常用 training api 應該 urn 包裝 外部 prop   或許,你覺得我麻煩,明明一篇文章可以搞定的內容,非要寫幾篇。是不是正在吐槽我?沒關系,我的目的達到了。手動傲嬌( ̄? ̄) 然後,我們就要來聊一聊withRouter了。 我們都知道,當我在