Dubbo底層原理分析和分散式實際應用

擊水三千里發表於2019-01-02

1、抓包分析dubbo的協議和呼叫過程 

1.1、支援的協議

1.1.1dubbo協議

 Dubbo預設協議採用單一長連線和NIO非同步通訊,使用基於於netty+hessian(序列化方式)互動。適合於小資料量大併發的服務呼叫,以及服務消費者機器數遠大於服務提供者機器數的情況。Dubbo預設協議不適合傳送大資料量的服務,比如傳檔案,傳視訊等,除非請求量很低。

1.1.2 rmi協議

走java二進位制序列化,多個短連線,適合消費者和提供者數量差不多,適用於檔案的傳輸,一般較少用,在大資料量下的傳輸性,建議使用rmi協議

1.1.3 hessian協議

走hessian序列化協議,多個短連線,適用於提供者數量比消費者數量還多,適用於檔案的傳輸,一般較少用

1.1.4 http協議

走json序列化

1.1.5 webservice

走SOAP文字序列化

 

     

1.2 dubbo的抓包分析(比較複雜)

head固定自由16Bit,bod預設採用Hessian二進位制序列化

 

dubbo的原理草圖
呼叫控制協議 物件序列化協議
header(16B)
頭部資訊
body(預設hessian序列化)
magic(2B)
魔數(2)
flag(1B)
標識(1)
status(1B)
響應狀態(1)
messageId(8B)
訊息ID(8)
bodyLength(4B)
內容長度(4)
dubbo版本 服務介面名 服務版本號 服務方法名 引數描述符 引數值序列化 隱式引數
da bb c5(請求報文)                   16進位制
0a------換行符號------"\n"       請求報文    
        da bb c5 00 00 00 00 00 00 00 00 00 00 00 00 cd
22 32 2e 35 2e 33 22 0d 0a 22 63 6f 6d 2e 74 6f
6e 79 2e 74 65 73 74 2e 64 75 62 62 6f 2e 44 65
6d 6f 53 65 72 76 69 63 65 22 0d 0a 22 30 2e 30
2e 30 22 0d 0a 22 73 61 79 48 65 6c 6c 6f 22 0d
0a 22 4c 6a 61 76 61 5c 2f 6c 61 6e 67 5c 2f 53
74 72 69 6e 67 3b 22 0d 0a 22 74 6f 6e 79 22 0d
0a 7b 22 70 61 74 68 22 3a 22 63 6f 6d 2e 74 6f
6e 79 2e 74 65 73 74 2e 64 75 62 62 6f 2e 44 65
6d 6f 53 65 72 76 69 63 65 22 2c 22 69 6e 74 65
72 66 61 63 65 22 3a 22 63 6f 6d 2e 74 6f 6e 79
2e 74 65 73 74 2e 64 75 62 62 6f 2e 44 65 6d 6f
53 65 72 76 69 63 65 22 2c 22 76 65 72 73 69 6f
6e 22 3a 22 30 2e 30 2e 30 22 7d 0d 0a
魔數 0xda, 0xbb
0d------回車符號------"\r"       標識 0xc5
        響應狀態 0x00
Q:16進位制32為什麼對應字元 2? 訊息ID 00 00 00 00 00 00 00 00 00
A:16進位制32對應    10進位制50
50在ASCII碼錶中,
資料長度 00 00 00 cd  205
           
        "2.5.3"
"com.tony.test.dubbo.DemoService"
"0.0.0"
"sayHello"
"Ljava\/lang\/String;"
"tony"
{"path":"com.tony.test.dubbo.DemoService","interface":"com.tony.test.dubbo.DemoService","version":"0.0.0"}
                       
                       
        響應報文    
        da bb 05 14 00 00 00 00 00 00 00 00 00 00 00 10
31 0d 0a 22 6e 61 6d 65 3a 74 6f 6e 79 22 0d 0a
魔數 da bb
        標識 05
        響應狀態 0x14
        訊息ID 00 00 00 00 00 00 00 00 00
        資料長度 0x10  16
           
        0x31 49
        1
"name:tony"
       
       
       
       
       
       

 

1.3 dubbo工作原理和流程

1.3.1、dubbo的專案結構

第一層:service層,介面層,給服務提供者和消費者來實現的

第二層:config層,配置層,主要是對dubbo進行各種配置的

第三層:proxy層,服務代理層,透明生成客戶端的stub和服務單的skeleton

第四層:registry層,服務註冊層,負責服務的註冊與發現

第五層:cluster層,叢集層,封裝多個服務提供者的路由以及負載均衡,將多個例項組合成一個服務

第六層:monitor層,監控層,對rpc介面的呼叫次數和呼叫時間進行監控

第七層:protocol層,遠端呼叫層,封裝rpc呼叫

第八層:exchange層,資訊交換層,封裝請求響應模式,同步轉非同步

第九層:transport層,網路傳輸層,抽象mina和netty為統一介面

第十層:serialize層,資料序列化層

1.3.2、dubbo的工作流程:

1)第一步,provider向註冊中心去註冊

2)第二步,consumer從註冊中心訂閱服務,註冊中心會通知consumer註冊好的服務

3)第三步,consumer呼叫provider

4)第四步,consumer和provider都非同步的通知監控中心

1.3.3、註冊中心掛了可以繼續通訊嗎?

可以,因為剛開始初始化的時候,消費者會將提供者的地址等資訊拉取到本地快取,所以註冊中心掛了可以繼續通訊

 

 2、負載均衡策略

dubbo原始碼包裡的負載均衡策略

 

1)random loadbalance 隨機

預設情況下,dubbo是random load balance隨機呼叫實現負載均衡,可以對provider不同例項設定不同的權重,會按照權重來負載均衡,權重越大分配流量越高,一般就用這個預設的就可以了。

 

2)roundrobin loadbalance 輪詢

還有roundrobin loadbalance,這個的話預設就是均勻地將流量打到各個機器上去,但是如果各個機器的效能不一樣,容易導致效能差的機器負載過高。所以此時需要調整權重,讓效能差的機器承載權重小一些,流量少一些。

 

3)leastactive loadbalance 最少活躍數

相同活躍數的隨機,活躍數指呼叫前後計數差。使慢的提供者收到更少請求,因為越慢的提供者的呼叫前後計數差會越大。

 

4)consistanthash loadbalance 一致性hash

一致性Hash演算法,相同引數的請求一定分發到一個provider上去,provider掛掉的時候,會基於虛擬節點均勻分配剩餘的流量,抖動不會太大。如果你需要的不是隨機負載均衡,是要一類請求都到一個節點,那就走這個一致性hash策略。

 

更加詳細的解釋可以看官網: 

http://dubbo.apache.org/zh-cn/docs/source_code_guide/loadbalance.html 

 

3、dubbo叢集容錯策略

1)failover cluster模式

失敗自動切換,自動重試其他機器,預設就是這個,常見於讀操作

可通過retries="2"來設定重試次數(不含第一次,預設就是2)

2)failfast cluster模式

快速失敗,一次呼叫失敗就立即失敗,常見於寫操作

3)failsafe cluster模式

失敗安全,出現異常時忽略掉,常用於不重要的介面呼叫,比如記錄日誌

4)failback cluster模式

失敗自動恢復,失敗了後臺自動記錄請求,然後定時重發,比較適合於寫訊息佇列這種

5)forking cluster

並行呼叫多個provider,只要一個成功就立即返回

6)broadcacst cluster

逐個呼叫所有的provider

 

4、dubbo的SPI原理

4.1、什麼是spi

spi,簡單來說,就是service provider interface,說白了是什麼意思呢,比如你有個介面,現在這個介面有3個實現類,那麼在系統執行的時候對這個介面到底選擇哪個實現類呢?這就需要spi了,需要根據指定的配置或者是預設的配置,去找到對應的實現類載入進來,然後用這個實現類的例項物件

介面A -> 實現A1,實現A2,實現A3

配置一下,介面A = 實現A2

在系統實際執行的時候,會載入你的配置,用實現A2例項化一個物件來提供服務

比如說你要通過jar包的方式給某個介面提供實現,然後你就在自己jar包的META-INF/services/目錄下放一個跟介面同名的檔案,裡面指定介面的實現裡是自己這個jar包裡的某個類。ok了,別人用了一個介面,然後用了你的jar包,就會在執行的時候通過你的jar包的那個檔案找到這個介面該用哪個實現類。

這是jdk提供的一個功能。

但是dubbo也用了spi思想,不過沒有用jdk的spi機制,是自己實現的一套spi機制。

Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();

這行程式碼就是dubbo裡大量使用的,就是對很多元件,都是保留一個介面和多個實現,然後在系統執行的時候動態根據配置去找到對應的實現類。如果你沒配置,那就走預設的實現好了,沒問題。

@SPI("dubbo")  

public interface Protocol {  


    int getDefaultPort();  
  

    @Adaptive  

    <T> Exporter<T> export(Invoker<T> invoker) throws RpcException;  

  

    @Adaptive  

    <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException;  



    void destroy();  

}  

在dubbo自己的jar裡,在/META_INF/dubbo/internal/com.alibaba.dubbo.rpc.Protocol檔案中:

dubbo=com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol

http=com.alibaba.dubbo.rpc.protocol.http.HttpProtocol

hessian=com.alibaba.dubbo.rpc.protocol.hessian.HessianProtocol

 

4.2、如何擴充套件dubbo中的元件

自己寫個工程,要是那種可以打成jar包的,裡面的src/main/resources目錄下,搞一個META-INF/services,裡面放個檔案叫:com.alibaba.dubbo.rpc.Protocol,檔案裡搞一個my=com.zhss.MyProtocol。自己把jar弄到nexus私服裡去。

 

然後自己搞一個dubbo provider工程,在這個工程裡面依賴你自己搞的那個jar,然後在spring配置檔案裡給個配置:

<dubbo:protocol name=”my” port=”20000” />

這個時候provider啟動的時候,就會載入到我們jar包裡的my=com.zhss.MyProtocol這行配置裡,接著會根據你的配置使用你定義好的MyProtocol了,這個就是簡單說明一下,你通過上述方式,可以替換掉大量的dubbo內部的元件,就是扔個你自己的jar包,然後配置一下即可。

dubbo裡面提供了大量的類似上面的擴充套件點,就是說,你如果要擴充套件一個東西,只要自己寫個jar,讓你的consumer或者是provider工程,依賴你的那個jar,在你的jar裡指定目錄下配置好介面名稱對應的檔案,裡面通過key=實現類。

然後對對應的元件,用類似<dubbo:protocol>用你的哪個key對應的實現類來實現某個介面,你可以自己去擴充套件dubbo的各種功能,提供你自己的實現。

 

下面的demo展示瞭如何應用SPI

https://gitee.com/lzhcode/maven-parent/blob/master/lzh-springboot/lzh-springboot-spi/src/main/java/com/lzhsite/spring/Application.java

 

4.3、SPI 在實際專案中的應用

4.3.1、在mysql-connector-java-xxx.jar中發現了META-INF\services\java.sql.Driver檔案,裡面只有兩行記錄:

com.mysql.jdbc.Driver

com.mysql.fabric.jdbc.FabricMySQLDriver

我們可以分析出,java.sql.Driver是一個規範介面,com.mysql.jdbc.Driver

com.mysql.fabric.jdbc.FabricMySQLDriver則是mysql-connector-java-xxx.jar對這個規範的實現介面。

 

4.3.2、在jcl-over-slf4j-xxxx.jar中發現了META-INF\services\org.apache.commons.logging.LogFactory檔案,裡面只有一行記錄:

org.apache.commons.logging.impl.SLF4JLogFactory

相信不用我贅述,大家都能理解這是什麼含義了

 

5、dubbo如何生成動態代理

在Dubbo中,沒有使用CGLib進行代理,而是使用JDK和Javassist來進行動態代理!我們知道,動態代理是無法用反射做的,只能靠動態生成位元組碼,這就需要使用位元組碼工具包,比如asm和Javassist等,在Spring3.2.2之前版本的原始碼中,我們可以看到是有單獨spring-asm的模組的,但在Spring3.2.2版本開始,就沒有spring-asm模組了,不是不使用了,而是spring-asm已經整合到spring-core中了,可見asm在Spring中的地位(CGLib使用的就是asm),至於Dubbo為什麼不使用CGLib?

雖然ASM稍快,但並沒有快一個數量級, 
而JAVAASSIST的位元組碼生成方式比ASM方便, 
JAVAASSIST只需用字串拼接出Java原始碼,便可生成相應位元組碼, 
而ASM需要手工寫位元組碼。

 

6、分散式服務介面如何保證介面的冪等性

所謂冪等性,就是說一個介面,多次發起同一個請求,你這個介面得保證結果是準確的,比如不能多扣款,不能多插入一條資料,不能將統計值多加了1。這就是冪等性。

其實保證冪等性主要是三點:

(1)對於每個請求必須有一個唯一的標識,舉個例子:訂單支付請求,肯定得包含訂單id,一個訂單id最多支付一次,對吧

(2)每次處理完請求之後,必須有一個記錄標識這個請求處理過了,比如說常見的方案是在mysql中記錄個狀態啥的,比如支付之前記錄一條這個訂單的支付流水,而且支付流水採

(3)每次接收請求需要進行判斷之前是否處理過的邏輯處理,比如說,如果有一個訂單已經支付了,就已經有了一條支付流水,那麼如果重複傳送這個請求,則此時先插入支付流水,orderId已經存在了,唯一鍵約束生效,報錯插入不進去的。然後你就不用再扣款了。

7、分散式服務介面請求的順序性如何保證

首先,一般來說,我個人給你的建議是,你們從業務邏輯上最好設計的這個系統不需要這種順序性的保證,因為一旦引入順序性保障,會導致系統複雜度上升,而且會帶來效率低下,熱點資料壓力過大,等問題。

下面我給個我們用過的方案吧,簡單來說,首先你得用dubbo的一致性hash負載均衡策略,將比如某一個訂單id對應的請求都給分發到某個機器上去,接著就是在那個機器上因為可能還是多執行緒併發執行的,你可能得立即將某個訂單id對應的請求扔一個記憶體佇列裡去,強制排隊,這樣來確保他們的順序性(dubbo的一致性hash負載均衡策略能保證99.99%有序,除非服務端識別到的客戶端發起請求和客戶端真正發起請求的順序是不同的,如果需要的話可以考慮引入分散式鎖,但是會影響效能)

是這樣引發的後續問題就很多,比如說要是某個訂單對應的請求特別多,造成某臺機器成熱點怎麼辦?解決這些問題又要開啟後續一連串的複雜技術方案。。。曾經這類問題弄的我們頭疼不已,所以,還是建議什麼呢?

最好是比如說剛才那種,一個訂單的插入和刪除操作,能不能合併成一個操作,就是一個刪除,或者是什麼,避免這種問題的產生。

 

8、Provider和Consumer都配置timeout超時時間的情況

dubbo的provider和consumer的配置檔案中,如果都配置了timeout的超時時間,dubbo預設以consumer中配置的時間為

在Provider上儘量多配置Consumer端屬性
原因如下:作服務的提供者,比服務使用方更清楚服務效能引數,如呼叫的超時時間,合理的重試次數,等等
在Provider配置後,Consumer不配置則會使用Provider的配置值,即Provider配置可以作為Consumer的預設值。否則,Consumer會使用Consumer端的全域性設定,這對於Provider不可控的,並且往往是不合理的
PS: 配置的覆蓋規則:

1) 方法級配置別優於介面級別,即小Scope優先

2) Consumer端配置 優於 Provider配置 優於 全域性配置,最後是Dubbo Hard Code的配置值(見配置文件)
 

相關文章