1. 程式人生 > >dubbo框架自己理解

dubbo框架自己理解

RPC是遠端呼叫過程的簡寫,是一個協議,處於網路通訊協議的第五層:會話層,其下就是TCP/IP協議,在建立在其基礎上的通訊會話協議。RPC定義了互動的模式,而應用程式使用這些模式,來訪問其他伺服器的方法,並不需要關係具體的網路上的細節。     

 

     一、RPC基礎知識   

 

     1.RPC模式

 

     RPC採用C/S模式,客戶端傳送請求,服務端響應。

     基於底層的協議,比如TCP/IP模式。

 

     2.設計目的

 

     ①通過固定的協議,呼叫非本機的方法

     ②實現不同程式語言之間的通訊

     ③不需要了解底層協議,像本地方法一樣調。它完全封裝了網路傳輸,以及其他細節。

 

     二、RPC過程詳解

 

          

                                                圖一   RPC呼叫過程

 

     從RPC的角度看,應該有服務的提供方,即生產者;還有服務的呼叫方,即消費者。

 

     對消費者來時,在RPC呼叫過程中,使用第1步、第2步、第3步、第4步是透明的,其他的都是使用RPC框架去封裝這些事情。當應用開始呼叫PRC的方式時,就會去容器中去取Bean物件,所以我們應該首先註冊Bean物件到容器中,我們通過Java的動態代理,將代理過程封裝到代理物件中,代理物件實現介面,建立例項到容器中。相應的,在呼叫遠端物件的物件方法時,就會呼叫動態代理中的方法,這就是代理層的作用。

 

     代理物件在獲取到請求方法、介面和引數時,就會用序列化層,將這些資訊封裝成一個請求報文,再讓通訊層向服務端傳送報文的內容,然後就到了生產者這塊。

 

     相應的服務必須有個監聽器,來監聽來自其他服務的請求,一般都會用容器做訊息的監聽,就會呼叫對應的Bean物件的方法,去處理響應的請求。當然,RPC框架不會讓容器中的每一個框架都會被呼叫,所以只有註冊了的Bean才會被RPC的請求呼叫到。然後,通過請求中的類、方法、引數,反射呼叫對應的Bean,拿到結果之後,通過序列化層,封裝好結果報文,服務端的通訊層將報文反饋給呼叫方,呼叫方解析到返回值,動態代理類返回結果,呼叫結束。

 

     這樣,一個完整的RPC呼叫反饋鏈條就完成了。

 

     1.消費者設計

 

               

                                               圖二  消費者設計

 

     ①代理層:

     消費者將對應的介面,通過RPC框架的代理來生成一個物件到Spring容器中。代理層將代理介面生成該介面的物件,該物件處理呼叫時傳過來的物件、方法、引數,通過序列化層封裝好,呼叫網路層。

 

     ②序列化層:

     將請求的引數序列化成報文;將返回的報文反序列化成物件;

 

     ③網路層:

     將報文與服務端通訊;接收返回結果。

 

     2.生產者設計

 

            

                                              圖三  生產者設計 

 

     ①代理層:

     一個應用提供服務,必須由一個網路監聽的模組,這個模組大多有開源的容器來處理網路上的監聽;服務需要註冊,只有註冊了的服務才可以被呼叫;註冊的服務需要被我們發射呼叫到,來進行相應的處理。

 

     ②序列化層:

     就是相應的做請求的反序列化和結果的序列化。

 

     ③網路層:

     接收客戶端報文;將序列化的結果返回給客戶端。

 

     三、RPC模式總結

 

                     

                                                 圖三  RPC模式總結

 

     1.Proxy代理層

     用於物件的代理;物件的反射呼叫;RPC流程的控制。

 

     2.Serialize序列化層

     將請求序列化和結果反序列化。

 

     3.Invoke網路模組

     主要用於網路通訊的相關處理。

 

     4.Container容器元件

     這層主要用於代理層監聽網路請求。

 

 

(1)什麼是RPC

RPC(Remote Procedure Call Protocol)遠端過程呼叫協議。一個通俗的描述是:客戶端在不知道呼叫細節的情況下,呼叫存在於遠端計算機上的某個物件,就像呼叫本地應用程式中的物件一樣。比較正式的描述是:一種通過網路從遠端計算機程式上請求服務,而不需要了解底層網路技術的協議。那麼我們至少從這樣的描述中挖掘出幾個要點:

  • RPC是協議:既然是協議就只是一套規範,那麼就需要有人遵循這套規範來進行實現。目前典型的RPC實現包括:Dubbo、Thrift、GRPC、Hetty等。這裡要說明一下,目前技術的發展趨勢來看,實現了RPC協議的應用工具往往都會附加其他重要功能,例如Dubbo還包括了服務管理、訪問許可權管理等功能。

  • 網路協議和網路IO模型對其透明:既然RPC的客戶端認為自己是在呼叫本地物件。那麼傳輸層使用的是TCP/UDP還是HTTP協議,又或者是一些其他的網路協議它就不需要關心了。既然網路協議對其透明,那麼呼叫過程中,使用的是哪一種網路IO模型呼叫者也不需要關心。

  • 資訊格式對其透明:我們知道在本地應用程式中,對於某個物件的呼叫需要傳遞一些引數,並且會返回一個呼叫結果。至於被呼叫的物件內部是如何使用這些引數,並計算出處理結果的,呼叫方是不需要關心的。那麼對於遠端呼叫來說,這些引數會以某種資訊格式傳遞給網路上的另外一臺計算機,這個資訊格式是怎樣構成的,呼叫方是不需要關心的。

  • 應該有跨語言能力:為什麼這樣說呢?因為呼叫方實際上也不清楚遠端伺服器的應用程式是使用什麼語言執行的。那麼對於呼叫方來說,無論伺服器方使用的是什麼語言,本次呼叫都應該成功,並且返回值也應該按照呼叫方程式語言所能理解的形式進行描述。

那麼上面的描述情況可以用下圖表示:

這裡寫圖片描述

 

(2)RPC要素

當然,上圖是作為RPC的呼叫者所觀察到的現象(而實際情況是客戶端或多或少的還是需要知道一些呼叫RPC的細節)。但是我們是要講解RPC的基本概念,所以RPC協議內部是怎麼回事就要說清楚:

這裡寫圖片描述

  • Client:RPC協議的呼叫方。就像上文所描述的那樣,最理想的情況是RPC Client在完全不知道有RPC框架存在的情況下發起對遠端服務的呼叫。但實際情況來說Client或多或少的都需要指定RPC框架的一些細節。

  • Server:在RPC規範中,這個Server並不是提供RPC伺服器IP、埠監聽的模組。而是遠端服務方法的具體實現(在JAVA中就是RPC服務介面的具體實現)。其中的程式碼是最普通的和業務相關的程式碼,甚至其介面實現類本身都不知道將被某一個RPC遠端客戶端呼叫。

  • Stub/Proxy:RPC代理存在於客戶端,因為要實現客戶端對RPC框架“透明”呼叫,那麼客戶端不可能自行去管理訊息格式、不可能自己去管理網路傳輸協議,也不可能自己去判斷呼叫過程是否有異常。這一切工作在客戶端都是交給RPC框架中的“代理”層來處理的。

  • Message Protocol:在上文我們已經說到,一次完整的client-server的互動肯定是攜帶某種兩端都能識別的,共同約定的訊息格式。RPC的訊息管理層專門對網路傳輸所承載的訊息資訊進行編號和解碼操作。目前流行的技術趨勢是不同的RPC實現,為了加強自身框架的效率都有一套(或者幾套)私有的訊息格式。例如前文所講到的RMI框架使用的訊息協議為JRMP;後文我們將詳細講解的RPC框架Thrift也有私有的訊息協議,“- Transfer/Network Protocol”(當然它還支援一些通用的訊息格式,如JSON)。

  • Transfer/Network Protocol:傳輸協議層負責管理RPC框架所使用的網路協議、網路IO模型。例如Hessian的傳輸協議基於HTTP(應用層協議);而Thrift的傳輸協議基於TCP(傳輸層協議)。傳輸層還需要統一RPC客戶端和RPC服務端所使用的IO模型;常用的IO模型在之前已經詳細講解過了(可參見我之前的博文《架構設計:系統間通訊(3)——IO通訊模型和JAVA實踐 上篇》)

  • Selector/Processor:存在於RPC服務端,由於伺服器端某一個RPC介面的實現的特性(它並不知道自己是一個將要被RPC提供給第三方系統呼叫的服務)。所以在RPC框架中應該有一種“負責執行RPC介面實現”的角色。它負責了包括:管理RPC介面的註冊、判斷客戶端的請求許可權、控制介面實現類的執行在內的各種工作。

  • IDL:實際上IDL(介面定義語言)並不是RPC實現中所必須的。但是需要跨語言的RPC框架一定會有IDL部分的存在。這是因為要找到一個各種語言能夠理解的訊息結構、介面定義的描述形式。如果您的RPC實現沒有考慮跨語言性,那麼IDL部分就不需要包括,例如JAVA RMI因為就是為了在JAVA語言間進行使用,所以JAVA RMI就沒有相應的IDL。

  • 一定要說明一點,不同的RPC框架實現都有一定設計差異。例如生成Stub的方式不一樣,IDL描述語言不一樣、服務註冊的管理方式不一樣、執行服務實現的方式不一樣、採用的訊息格式封裝不一樣、採用的網路協議不一樣。但是基本的思路都是一樣的,上圖中的所列出的要素也都是具有的。

2、RPC框架的效能依據

這裡寫圖片描述

在物理伺服器效能相同的情況下,以下幾個因素會對一款RPC框架的效能產生直接影響:

  • 所支援的網路IO模型:您的RPC伺服器可以只支援傳統的阻塞式同步IO,也可以做一些改進讓您的RPC伺服器支援非阻塞式同步IO,或者在您的伺服器上實現對多路IO模型的支援。這樣的RPC伺服器的效能在高併發狀態下,會有很大的差別。特別是單位處理效能下對記憶體、CPU資源的使用率。

  • 基於的網路協議:一般來說您可以選擇讓您的RPC使用應用層協議,例如HTTP或者之前我們提到的HTTP/2協議,或者使用TCP協議,讓您的RPC框架工作在傳輸層。工作在哪一層網路上會對RPC框架的工作效能產生一定的影響,但是對RPC最終的效能影響並不大。但是至少從各種主流的RPC實現來看,沒有采用UDP協議做為主要的傳輸協議的。

  • 選擇的訊息封裝格式:選擇或者定義一種訊息格式的封裝,要考慮的問題包括:訊息的易讀性、描述單位內容時的訊息體大小、編碼難度、解碼難度、解決半包/粘包問題的難易度。當然如果您只是想定義一種RPC專用的訊息格式,那麼訊息的易讀性可能不是最需要考慮的。訊息封裝格式的設計是目前各種RPC框架效能差異的最重要原因,這就是為什麼幾乎所有主流的RPC框架都會設計私有的訊息封裝格式的原因。

  • 實現的服務處理管理方式:在高併發請求下,如何管理註冊的服務也是一個性能影響點。您可以讓RPC的Selector/Processor使用單個執行緒執行服務的具體實現(這意味著上一個客戶端的請求沒有處理完,下一個客戶端的請求就需要等待)、您也可以為每一個RPC具體服務的實現開啟一個獨立的執行緒執行(可以一次處理多個請求,但是作業系統對於“可執行的最大執行緒數”是有限制的)、您也可以執行緒池來執行RPC具體的服務實現(目前看來,在單個服務節點的情況下,這種方式是比較好的)、您還可以通過註冊代理的方式讓多個服務節點來執行具體的RPC服務實現。

在上文中的傳輸管理層分析原始碼,基本原理如下:
  1. client一個執行緒呼叫遠端介面,生成一個唯一的ID(比如一段隨機字串,UUID等),Dubbo是使用AtomicLong從0開始累計數字的
  2. 將打包的方法呼叫資訊(如呼叫的介面名稱,方法名稱,引數值列表等),和處理結果的回撥物件callback,全部封裝在一起,組成一個物件object
  3. 向專門存放呼叫資訊的全域性ConcurrentHashMap裡面put(ID, object)
  4. 將ID和打包的方法呼叫資訊封裝成一物件connRequest,使用IoSession.write(connRequest)非同步傳送出去
  5. 當前執行緒再使用callback的get()方法試圖獲取遠端返回的結果,在get()內部,則使用synchronized獲取回撥物件callback的鎖, 再先檢測是否已經獲取到結果,如果沒有,然後呼叫callback的wait()方法,釋放callback上的鎖,讓當前執行緒處於等待狀態。
  6. 服務端接收到請求並處理後,將結果(此結果中包含了前面的ID,即回傳)傳送給客戶端,客戶端socket連線上專門監聽訊息的執行緒收到訊息,分析結果,取到ID,再從前面的ConcurrentHashMap裡面get(ID),從而找到callback,將方法呼叫結果設定到callback物件裡。
  7. 監聽執行緒接著使用synchronized獲取回撥物件callback的鎖(因為前面呼叫過wait(),那個執行緒已釋放callback的鎖了),再notifyAll(),喚醒前面處於等待狀態的執行緒繼續執行(callback的get()方法繼續執行就能拿到呼叫結果了),至此,整個過程結束。

使用NIO設計RPC呼叫分析

前面提到,由於NIO的SocketChannel是非阻塞的,所以不再需要連線池,使用一個連線就夠了。

NIO單一長連線RPC執行緒模型

但是如果真的使用NIO來進行RPC呼叫的話,會有資料和呼叫方對應不上的問題,如下圖:

NIO非同步順序問題

如上圖所示,如果多個執行緒共用一個連線,那麼每個執行緒呼叫之後返回的順序是不可控的,所以有可能先發出資料的反而後得到返回值,這就使得資料對應不上了。個人覺得因為這一點,NIO及其適合聊天室型別的設計,因為每個聊天方都是一個單獨的SocketChannel連線,而此時並沒有順序問題。

但是對RPC呼叫來說,每次呼叫的返回值必須與呼叫方對應上,為此,Dubbo的設計是給每個請求設計一個請求id,在傳送請求與傳送返回值時都帶上這個id。詳細思路如下圖:

NIO單一長連線的RPC設計

業務執行緒在發出請求之前,需要儲存一個請求物件,同時掛起相應的業務執行緒(掛起不會被任務排程,所以不存線上程切換消耗),這個請求物件包含了此次請求的id,然後在獲取服務端返回的資料的時候,解析出這個id,通過這個id取出請求物件,並喚醒對應的執行緒。



作者:峽客
連結:https://www.jianshu.com/p/13bef2795c44
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯絡作者獲得授權並註明出處。