1.Apache Dubbo
官方網址
http://dubbo.apache.org/zh-cn/docs/user/preface/background.html (使用者文檔)
http://dubbo.apache.org/zh-cn/blog/download.html (源碼)
https://github.com/apache/dubbo (源碼)
2. 概述
2.1框架設計
來自官網
http://dubbo.apache.org/zh-cn/docs/dev/design.html
#圖例說明:
>> 圖中左邊淡藍背景的為服務消費方使用的接口,
右邊淡綠色背景的為服務提供方使用的接口,
位于中軸線上的為雙方都用到的接口。
>> 圖中從下至上分為十層,各層均為單向依賴,
右邊的黑色箭頭代表層之間的依賴關系,每一層都可以剝離上層被復用,
其中,Service 和 Config 層為 API,其它各層均為 SPI。
>> 圖中綠色小塊的為擴展接口,藍色小塊為實現類,圖中只顯示用于關聯各層的實現類。
>> 圖中藍色虛線為初始化過程,即啟動時組裝鏈,
紅色實線為方法調用過程,即運行時調時鏈,
紫色三角箭頭為繼承,可以把子類看作父類的同一個節點,線上的文字為調用的方法。
各層說明
>> service層:
接口層,給服務提供者和消費者來實現的
>> config 配置層:
對外配置接口,以 ServiceConfig, ReferenceConfig 為中心,
可以直接初始化配置類,也可以通過 spring 解析配置生成配置類
>> proxy 服務代理層:
服務接口透明代理,生成服務的客戶端 Stub 和服務器端 Skeleton, 以 ServiceProxy 為中心,
擴展接口為 ProxyFactory
>> registry 注冊中心層:
封裝服務地址的注冊與發現,以服務 URL 為中心,
擴展接口為 RegistryFactory, Registry, RegistryService
>> cluster 路由層:
封裝多個提供者的路由及負載均衡,并橋接注冊中心,以 Invoker 為中心,
擴展接口為 Cluster, Directory, Router, LoadBalance
>> monitor 監控層:
RPC 調用次數和調用時間監控,以 Statistics 為中心,
擴展接口為 MonitorFactory, Monitor, MonitorService
>> protocol 遠程調用層:
封裝 RPC 調用,以 Invocation, Result 為中心,
擴展接口為 Protocol, Invoker, Exporter
>> exchange 信息交換層:
封裝請求響應模式,同步轉異步,以 Request, Response 為中心,
擴展接口為 Exchanger, ExchangeChannel, ExchangeClient, ExchangeServer
>> transport 網絡傳輸層:
抽象 mina 和 netty 為統一接口,以 Message 為中心,
擴展接口為 Channel, Transporter, Client, Server, Codec
>> serialize 數據序列化層:
可復用的一些工具,擴展接口為 Serialization, ObjectInput, ObjectOutput, ThreadPool
#關系說明
>> 在 RPC 中,Protocol 是核心層,也就是只要有 Protocol + Invoker + Exporter,
就可以完成非透明的 RPC 調用,然后在 Invoker 的主過程上 Filter 攔截點。
>> 圖中的 Consumer 和 Provider 是抽象概念,
只是想讓看圖者更直觀的了解哪些類分屬于客戶端與服務器端,
不用 Client 和 Server 的原因是 Dubbo 在很多場景下都使用
Provider, Consumer, Registry, Monitor 劃分邏輯拓普節點,保持統一概念。
>> Cluster 是外圍概念,所以 Cluster 的目的是將多個 Invoker 偽裝成一個 Invoker,
這樣其它人只要關注 Protocol 層 Invoker 即可,
加上 Cluster 或者去掉 Cluster 對其它層都不會造成影響,
因為只有一個提供者時,是不需要 Cluster 的。
>> Proxy 層封裝了所有接口的透明化代理,而在其它層都以 Invoker 為中心,
只有到了暴露給用戶使用時,才用 Proxy 將 Invoker 轉成接口,或將接口實現轉成 Invoker,
也就是去掉 Proxy 層 RPC 是可以 Run 的,只是不那么透明,不那么看起來像調本地服務一樣調遠程服務。
>> Remoting 實現是 Dubbo 協議的實現,如果你選擇 RMI 協議,
整個 Remoting 都不會用上,Remoting 內部再劃為 Transport 傳輸層和 Exchange 信息交換層,
Transport 層只負責單向消息傳輸,是對 Mina, Netty, Grizzly 的抽象,
它也可以擴展 UDP 傳輸,而 Exchange 層是在傳輸層之上封裝了 Request-Response 語義。
>> Registry 和 Monitor 實際上不算一層,而是一個獨立的節點,
只是為了全局概覽,用層的方式畫在一起。
模塊說明
>> dubbo-common 公共邏輯模塊:
包括 Util 類和通用模型。
>> dubbo-remoting 遠程通訊模塊:
相當于 Dubbo 協議的實現,如果 RPC 用 RMI協議則不需要使用此包。
>> dubbo-rpc 遠程調用模塊:
抽象各種協議,以及動態代理,只包含一對一的調用,不關心集群的管理。
>> dubbo-cluster 集群模塊:
將多個服務提供方偽裝為一個提供方,包括:負載均衡, 容錯,路由等,
集群的地址列表可以是靜態配置的,也可以是由注冊中心下發。
>> dubbo-registry 注冊中心模塊:
基于注冊中心下發地址的集群方式,以及對各種注冊中心的抽象。
>> dubbo-monitor 監控模塊:
統計服務調用次數,調用時間的,調用鏈跟蹤的服務。
>> dubbo-config 配置模塊:是 Dubbo 對外的 API,用戶通過 Config 使用Dubbo,隱藏 Dubbo 所有細節。
>> dubbo-container 容器模塊:
是一個 Standlone 的容器,以簡單的 Main 加載 Spring 啟動,
因為服務通常不需要 Tomcat/JBoss 等 Web 容器的特性,沒必要用 Web 容器去加載服務。
>> dubbo-plugin 插件模塊
qos是dubbo的在線運維命令,dubbo 2.5.8新版本重構了telnet模塊,提供了新的telnet命令支持,
新版本的telnet端口與dubbo協議的端口是不同的端口,默認為22222,可以通過配置文件dubbo.properties修改。
[
http://dubbo.apache.org/zh-cn/docs/user/references/qos.html
http://dubbo.apache.org/zh-cn/docs/user/references/telnet.html
]
#整體上按照分層結構進行分包,與分層的不同點在于:
>> container 為服務容器,用于部署運行服務,沒有在層中畫出。
>> protocol 層和 proxy 層都放在 rpc 模塊中,這兩層是 rpc 的核心,
在不需要集群也就是只有一個提供者時,可以只使用這兩層完成 rpc 調用。
>> transport 層和 exchange 層都放在 remoting 模塊中,為 rpc 調用的通訊基礎。
>> serialize 層放在 common 模塊中,以便更大程度復用。
依賴關系
>> 圖中小方塊 Protocol, Cluster, Proxy, Service, Container, Registry, Monitor 代表層或模塊,
藍色的表示與業務有交互,綠色的表示只對 Dubbo 內部交互。
>> 圖中背景方塊 Consumer, Provider, Registry, Monitor 代表部署邏輯拓撲節點。
>> 圖中藍色虛線為初始化時調用,紅色虛線為運行時異步調用,紅色實線為運行時同步調用。
>> 圖中只包含 RPC 的層,不包含 Remoting 的層,Remoting 整體都隱含在 Protocol 中。
領域模型
#在 Dubbo 的核心領域模型中:
#1.Protocol
是服務域,它是 Invoker 暴露和引用的主功能入口,
它負責 Invoker 的生命周期管理。
#2.Invoker
是實體域,它是 Dubbo 的核心模型,
其它模型都向它靠擾,或轉換成它,它代表一個可執行體,可向它發起 invoke 調用,
它有可能是一個本地的實現,也可能是一個遠程的實現,也可能一個集群實現。
#3.Invocation
是會話域,它持有調用過程中的變量,比如方法名,參數等。
#其他模型 or class
#1. xxxConfig
>> 服務提供方被抽象為: ServiceBean
>> 服務消費方被抽象為: ReferenceBean
#2.URL (公共契約)
>> URL 作為配置信息的統一格式,所有擴展點都通過傳遞 URL 攜帶配置信息。
>> 所有擴展點參數都包含 URL 參數,URL 作為上下文信息貫穿整個擴展點設計體系。
>> URL 采用標準格式:protocol://username:password@host:port/path?key=value&key=value
例如:
>> 服務導出時, URL示例:
dubbo://192.168.0.199:20880/com.zy.dubbo.IDubboService?anyhost=true&application=dubbo-demo-provider&bean.name=ServiceBean:com.zy.dubbo.IDubboService:1.0.0&bind.ip=172.29.165.17&bind.port=20880&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=com.zy.dubbo.IDubboService&methods=dubbo&pid=20208&qos.enable=false®ister=true&release=2.7.3&revision=1.0.0&side=provider×tamp=1577948892519&token=true&version=1.0.0
>> 服務引用時, URL示例:
registry://127.0.0.1:2181/org.apache.dubbo.registry.RegistryService?application=dubbo-demo-consumer&backup=127.0.0.1:2182,127.0.0.1:2183&check=true&dubbo=2.0.2&pid=17684&qos.enable=true®istry=zookeeper&release=2.7.3×tamp=1577948729805
基本設計原則
>> 采用 Microkernel + Plugin 模式,
Microkernel 只負責組裝 Plugin,Dubbo 自身的功能也是通過擴展點實現的,
也就是 Dubbo 的所有功能點都可被用戶自定義擴展所替換。
>> 采用 URL 作為配置信息的統一格式,所有擴展點都通過傳遞 URL 攜帶配置信息。
2.2網絡通信 & 序列化
協議 | 連接個數 | 連接方式 | 傳輸協議 | 傳輸方式 | 序列化 | 適用范圍 | 適用場景 | 約束 |
---|---|---|---|---|---|---|---|---|
dubbo:// | 單連接 | 長連接 | TCP | NIO 異步傳輸 | Hessian | 傳入傳出參數數據包較小(建議小于100K),消費者比提供者個數多,單一消費者無法壓滿提供者,盡量不要用 dubbo 協議傳輸大文件或超大字符串。 | 常規遠程服務方法調用 | 參數及返回值需實現 Serializable 接口/參數及返回值不能自定義實現 List, Map, Number, Date, Calendar 等接口,只能用 JDK 自帶的實現,因為 hessian 會做特殊處理,自定義實現類中的屬性值都會丟失。/Hessian 序列化,只傳成員屬性值和值的類型,不傳方法或靜態變量 |
rmi:// | 多連接 | 阻塞式短連接 | TCP | 同步傳輸 | jdk | 傳入傳出參數數據包大小混合,消費者與提供者個數差不多,可傳文件。 | 常規遠程服務方法調用,與原生RMI服務互操作 | 參數及返回值需實現 Serializable 接口/dubbo 配置中的超時時間對 RMI 無效,需使用 java 啟動參數設置:-Dsun.rmi.transport.tcp.responseTimeout=3000 |
hessian:// | 多連接 | 短連接 | HTTP | 同步傳輸 | Hessian | 傳入傳出參數數據包較大,提供者比消費者個數多,提供者壓力較大,可傳文件。 | 頁面傳輸,文件傳輸,或與原生hessian服務互操作 | 同dubbo:// |
http:// | 多連接 | 短連接 | HTTP | 同步傳輸 | 表單序列化 | 傳入傳出參數數據包大小混合,提供者比消費者個數多,可用瀏覽器查看,可用表單或URL傳入參數,暫不支持傳文件。 | 需同時給應用程序和瀏覽器 JS 使用的服務。 | 參數及返回值需符合 Bean 規范 |
webservice:// | 多連接 | 短連接 | HTTP | 同步傳輸 | SOAP 序列化 | - | 系統集成,跨語言調用 | 參數及返回值需實現 Serializable 接口/參數盡量使用基本類型和 POJO |
thrift:// | - | - | - | - | - | - | - | - |
memcached:// | - | - | - | - | - | - | - | - |
redis:// | - | - | - | - | - | - | - | - |
rest:// | - | - | - | - | - | - | - | - |
2.3SPI機制
http://www.lxweimin.com/p/43f529637f12
2.4集群負載均衡策略
http://dubbo.apache.org/zh-cn/docs/source_code_guide/loadbalance.html
#1)random loadbalance
默認情況下,dubbo是random load balance隨機調用實現負載均衡,
可以對provider不同實例設置不同的權重,會按照權重來負載均衡,
權重越大分配流量越高,一般就用這個默認的就可以了。
#2)roundrobin loadbalance
這個的話默認就是均勻地將流量打到各個機器上去,
但是如果各個機器的性能不一樣,容易導致性能差的機器負載過高。
所以此時需要調整權重,讓性能差的機器承載權重小一些,流量少一些。
#3)leastactive loadbalance
這個就是自動感知一下,如果某個機器性能越差,那么接收的請求越少,
越不活躍,此時就會給不活躍的性能差的機器更少的請求
#4)consistanthash loadbalance
一致性Hash算法,相同參數的請求一定分發到一個provider上去,
provider掛掉的時候,會基于虛擬節點均勻分配剩余的流量,抖動不會太大。
如果你需要的不是隨機負載均衡,是要一類請求都到一個節點,那就走這個一致性hash策略。
2.5集群容錯策略
http://dubbo.apache.org/zh-cn/docs/source_code_guide/cluster.html
為了避免單點故障,現在的應用通常至少會部署在兩臺服務器上。
對于一些負載比較高的服務,會部署更多的服務器。
這樣,在同一環境下的服務提供者數量會大于1。
對于服務消費者來說,同一環境下出現了多個服務提供者。
這時會出現一個問題,服務消費者需要決定選擇哪個服務提供者進行調用。
另外服務調用失敗時的處理措施也是需要考慮的,是重試呢,還是拋出異常,亦或是只打印異常等。
為了處理這些問題,Dubbo 定義了集群接口 Cluster 以及 Cluster Invoker。
集群 Cluster 用途是將多個服務提供者合并為一個 Cluster Invoker,并將這個 Invoker 暴露給服務消費者。
這樣一來,服務消費者只需通過這個 Invoker 進行遠程調用即可,
至于具體調用哪個服務提供者,以及調用失敗后如何處理等問題,現在都交給集群模塊去處理。
集群模塊是服務提供者和服務消費者的中間層,為服務消費者屏蔽了服務提供者的情況,
這樣服務消費者就可以專心處理遠程調用相關事宜。
比如發請求,接受服務提供者返回的數據等。
這就是集群的作用。
集群容錯的組件
包含 Cluster、Cluster Invoker、Directory、Router 和 LoadBalance 等。
集群工作過程可分為兩個階段:
第一個階段是在服務消費者初始化期間,集群 Cluster 實現類為服務消費者創建 Cluster Invoker 實例,即上圖中的 merge 操作。
第二個階段是在服務消費者進行遠程調用時。
以 FailoverClusterInvoker 為例,該類型 Cluster Invoker 首先會調用
Directory 的 list 方法列舉 Invoker 列表(可將 Invoker 簡單理解為服務提供者)。
Directory 的用途是保存 Invoker,可簡單類比為 List<Invoker>。
其實現類 RegistryDirectory 是一個動態服務目錄,可感知注冊中心配置的變化,
它所持有的 Invoker 列表會隨著注冊中心內容的變化而變化。
每次變化后,RegistryDirectory 會動態增刪 Invoker,
并調用 Router 的 route 方法進行路由,過濾掉不符合路由規則的 Invoker。
當 FailoverClusterInvoker 拿到 Directory 返回的 Invoker 列表后,
它會通過 LoadBalance 從 Invoker 列表中選擇一個 Invoker。
最后 FailoverClusterInvoker 會將參數傳給 LoadBalance 選擇出的 Invoker 實例的 invoker 方法,
進行真正的遠程調用。
Dubbo 主要提供了這樣幾種容錯方式
#1)failover cluster模式
失敗自動切換,自動重試其他機器,默認就是這個,常見于讀操作
#2)failfast cluster模式
一次調用失敗就立即失敗,常見于寫操作
#3)failsafe cluster模式
出現異常時忽略掉,常用于不重要的接口調用,比如記錄日志
#4)failbackc cluster模式
失敗了后臺自動記錄請求,然后定時重發,比較適合于寫消息隊列這種
#5)forking cluster
并行調用多個provider,只要一個成功就立即返回
#6)broadcacst cluster
逐個調用所有的provider
2.6服務治理, 降級, 重試機制, 冪等性
研究下降級, 重試, 冪等代碼實現機制, 不僅僅是dubbo的
http://www.lxweimin.com/writer#/notebooks/16290018/notes/40247546/preview (冪等性)
3.代碼實現 (小 demo)
https://github.com/zhangxin1932/dubbo-spring-cloud.git
4.源碼解析
4.1 服務導出過程export
項目啟動時, provider向 zk 注冊所有@Service(dubbo的注解)的過程
// 這里的 refresh 過程, 可以參考 http://www.lxweimin.com/p/5d5890645165
--> AbsctApplicationContext#refresh
// refresh --> step5
--> ApplicationContext#invokeBeanFactoryPostProcessors
--> @EnableDubbo
--> @DubboComponentScan
// 關于 @Import 注解的作用, 上述網址也已描述
--> @Import(DubboComponentScanRegistrar.class)
--> DubboComponentScanRegistrar#registerBeanDefinitions
--> ServiceAnnotationBeanPostProcessor#registerServiceBeans
// 這一步, 根據所有加了 @org.apache.dubbo.config.annotation.Service注解的實現類的接口,
// 構造多個 ServiceBean 對應的 BeanDefinition, 并將構造的不同的 ServiceBean 的 BeanDefinition, 注冊進 Spring 中
--> ServiceAnnotationBeanPostProcessor#buildServiceBeanDefinition
.......
// refresh --> step12
--> ApplicationContext#finishRefresh
--> AbstractApplicationContext#publishEvent(ApplicationEvent)
// 由于 ServiceBean 實現了 ApplicationListener 接口, 其實例化后, 會調用其方法ServiceBean#onApplicationEvent
--> ServiceBean#onApplicationEvent
--> ServiceBean#export
// 接下來的過程, 可以參考其官網, 解釋的較為詳細
http://dubbo.apache.org/zh-cn/docs/source_code_guide/export-service.html
provider在zk上注冊的數據
4.2 服務引用過程refer --> 引用!!!不是服務調用!!!
項目啟動時, consumer從 zk 注冊所有@Reference注解
// 這里的 refresh 過程, 可以參考 http://www.lxweimin.com/p/5d5890645165
--> AbsctApplicationContext#refresh
// refresh --> step5
--> ApplicationContext#invokeBeanFactoryPostProcessors
--> @EnableDubbo
--> @DubboComponentScan
// 關于 @Import 注解的作用, 上述網址也已描述
--> @Import(DubboComponentScanRegistrar.class)
--> DubboComponentScanRegistrar#registerBeanDefinitions
--> DubboComponentScanRegistrar#registerReferenceAnnotationBeanPostProcessor
// 這一步, 注冊 ReferenceAnnotationBeanPostProcessor 對應的 BeanDefinition
--> BeanRegistrar#registerInfrastructureBean
...
// refresh --> step11
--> ApplicationContext#finishBeanFactoryInitialization
--> ReferenceAnnotationBeanPostProcessor
// 這里最終走進了下述方法, 該類實現了 Spring 的 InstantiationAwareBeanPostProcessorAdapter 接口, 會在 bean 實例化時調用
--> AnnotationInjectedBeanPostProcessor#postProcessPropertyValues
// 后續 Reference bean 創建流程及 NettyClient 啟動流程參考下圖中的調用鏈路即可
...
--> ServiceBean#onApplicationEvent
--> ServiceBean#export
// 接下來的過程, 可以參考其官網, 解釋的較為詳細
http://dubbo.apache.org/zh-cn/docs/source_code_guide/refer-service.html
producer & consumer都會向zk注冊自身信息
dubbo中如果采用了dubbo協議進行通信:
provider向注冊中心注冊信息時, 會啟動 NettyServer
org.apache.dubbo.remoting.transport.netty4.NettyServer#doOpen
consumer從注冊中心獲取信息時, 會啟動 NettyClient
org.apache.dubbo.remoting.transport.netty4.NettyClient#doOpen
4.3 Dubbo通信的編解碼機制及解決粘包拆包問題 & 序列化
http://dubbo.apache.org/zh-cn/docs/source_code_guide/service-invoking-process.html (官網)
http://www.lxweimin.com/p/249eebe64a91 (RPC調用過程)
Dubbo的網絡通信框架Netty是基于TCP協議的,TCP協議的網絡通信會存在粘包和拆包的問題, 原因為:
>> 當要發送的數據大于TCP發送緩沖區剩余空間大小,將會發生拆包
>> 待發送數據大于MSS(最大報文長度),TCP在傳輸前將進行拆包
>> 要發送的數據小于TCP發送緩沖區的大小,TCP將多次寫入緩沖區的數據一次發送出去,將會發生粘包
>> 接收數據端的應用層沒有及時讀取接收緩沖區中的數據,將發生粘包
業界的解決方法一般有以下幾種:
>> 將每個數據包分為消息頭和消息體,消息頭中應該至少包含數據包的長度,這樣接收端在接收到數據后,就知道每一個數據包的實際長度了(Dubbo就是這種方案)
>> 消息定長,每個數據包的封裝為固定長度,不夠補0
>> 在數據包的尾部設置特殊字符,比如FTP協議
當然Netty中提供了這些解決方案的 Handler, 如 DelimiterBasedFrameDecoder, LengthFieldBasedFrameDecoder等.
4.3.1 Dubbo消息協議頭規范
此處以 Netty4為例分析.
偏移量(Bit) | 字段 | 取值 |
---|---|---|
0 ~ 7 | 魔數高位 | 0xda00 |
8 ~ 15 | 魔數低位 | 0xbb |
16 | 數據包類型 | 0 - Response, 1 - Request |
17 | 調用方式 | 僅在第16位被設為1的情況下有效,0 - 單向調用,1 - 雙向調用 |
18 | 事件標識 | 0 - 當前數據包是請求或響應包,1 - 當前數據包是心跳包 |
19 ~ 23 | 序列化器編號 | 2 - Hessian2Serialization 3 - JavaSerialization 4 - CompactedJavaSerialization 6 - FastJsonSerialization 7 - NativeJavaSerialization 8 - KryoSerialization 9 - FstSerialization |
24 ~ 31 | 狀態 | 20 - OK 30 - CLIENT_TIMEOUT 31 - SERVER_TIMEOUT 40 - BAD_REQUEST 50 - BAD_RESPONSE ...... |
32 ~ 95 | 請求編號 | 共8字節,運行時生成 |
96 ~ 127 | 消息體長度 | 運行時計算 |
dubbo的消息頭是一個定長的 16個字節的數據包:
>> magic High(第0-7位) & Magic Low(第8-15位):
共2byte, 類似java字節碼文件里的魔數,用來判斷是不是dubbo協議的數據包,就是一個固定的數字
>> Serialization id(序列id):
第16-20位, 共1byte
>> event: 第21位
>> two way: 第22位, 一個標志位,是單向的還是雙向的
>> Req/res: 第23位, 請求或響應標識
>> status: 第24-31位, 共1byte.狀態位,設置請求響應狀態,request為空,response才有值.
>> Id(long):第32-95位, 共8byte.每一個請求的唯一識別id
由于采用異步通訊的方式,用來把請求request和返回的response對應上.
>> data length: 第96-127位, 共4byte, 消息體長度,int 類型
Dubbo采用消息頭和消息體的方式來解決粘包拆包,
并在消息頭中放入了一個唯一Id來解決異步通信關聯request和response的問題.
4.3.2 請求(編碼/解碼)----響應(編碼-解碼)
4.3.2.1 初始化Codec2(編解碼器)
org.apache.dubbo.remoting.transport.netty4.NettyCodecAdapter
// 關于 Dubbo中NettyServer, NettyClient 啟動詳見上文.
// NettyServer#doOpen, 或 NettyClient#decode 啟動定義 Handler 時, 走到這里, 根據 URL 獲取 Codec, 默認為 DubboCountCodec, Netty4.
--> AbstractEndpoint#getChannelCodec
// 初始化 Codec, 默認為 DubboCountCodec
--> AbstractEndpoint#AbstractEndpoint
// NettyServer#doOpen 及 NettyClient#doOpen 中, 初始化了 Codec --> 即為 DubboCountCodec
// pipeline.addLast("decoder", adapter.getDecoder()) --> org.apache.dubbo.remoting.transport.netty4.NettyCodecAdapter.InternalDecoder
// pipeline.addLast("encoder", adapter.getEncoder()) --> org.apache.dubbo.remoting.transport.netty4.NettyCodecAdapter.InternalEncoder
// 當請求或響應時, 會進入這兩個 Internalxxx 中, 調用:
// codec.encode --> 對應 DubboCountCodec.encode, 或 codec.decode --> 對應 DubboCountCodec.decode
--> NettyCodecAdapter#NettyCodecAdapter
4.3.2.2 請求 & 響應編解碼大致流程
// 請求 編碼過程
--> NettyCodecAdapter.InternalEncoder#encode
--> DubboCountCodec#encode
--> ExchangeCodec#encode
--> ExchangeCodec#encodeRequest
--> DubboCodec#encodeRequestData(Channel, ObjectOutput, Object, String)
// 響應 編碼過程
--> NettyCodecAdapter.InternalEncoder#encode
--> DubboCountCodec#encode
--> ExchangeCodec#encode
--> ExchangeCodec#encodeResponse
--> DubboCodec#encodeResponseData(Channel, ObjectOutput, Object, String)
// 請求 & 響應 解碼過程
--> NettyCodecAdapter.InternalDecoder#decode
--> DubboCountCodec#decode
--> ExchangeCodec#decode(Channel, ChannelBuffer)
--> ExchangeCodec#decode(Channel, ChannelBuffer, int, byte[])
--> ExchangeCodec#decodeBody
4.3.2.3 源碼解析
NettyCodecAdapter --> Netty編解碼器適配器類
package org.apache.dubbo.remoting.transport.netty4;
import org.apache.dubbo.common.URL;
import org.apache.dubbo.remoting.Codec2;
import org.apache.dubbo.remoting.buffer.ChannelBuffer;
import io.netty.buffer.ByteBuf;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import io.netty.handler.codec.MessageToByteEncoder;
import java.io.IOException;
import java.util.List;
/**
* Netty 編解碼器適配器, 最終是通過 ExtensionLoader, 即 SPI 來動態添加 Codec2 的, 可修改默認配置
*/
final public class NettyCodecAdapter {
private final ChannelHandler encoder = new InternalEncoder();
private final ChannelHandler decoder = new InternalDecoder();
private final Codec2 codec;
private final URL url;
private final org.apache.dubbo.remoting.ChannelHandler handler;
public NettyCodecAdapter(Codec2 codec, URL url, org.apache.dubbo.remoting.ChannelHandler handler) {
this.codec = codec;
this.url = url;
this.handler = handler;
}
public ChannelHandler getEncoder() {
return encoder;
}
public ChannelHandler getDecoder() {
return decoder;
}
// 編碼器
private class InternalEncoder extends MessageToByteEncoder {
@Override
protected void encode(ChannelHandlerContext ctx, Object msg, ByteBuf out) throws Exception {
// 獲取 buffer
org.apache.dubbo.remoting.buffer.ChannelBuffer buffer = new NettyBackedChannelBuffer(out);
// 獲取 channel
Channel ch = ctx.channel();
NettyChannel channel = NettyChannel.getOrAddChannel(ch, url, handler);
try {
// 對 msg 進行編碼
codec.encode(channel, buffer, msg);
} finally {
NettyChannel.removeChannelIfDisconnected(ch);
}
}
}
// 解碼器
private class InternalDecoder extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf input, List<Object> out) throws Exception {
// 獲取 buffer
ChannelBuffer message = new NettyBackedChannelBuffer(input);
// 獲取 channel
NettyChannel channel = NettyChannel.getOrAddChannel(ctx.channel(), url, handler);
try {
// 解碼
do {
int saveReaderIndex = message.readerIndex();
Object msg = codec.decode(channel, message);
if (msg == Codec2.DecodeResult.NEED_MORE_INPUT) {
message.readerIndex(saveReaderIndex);
break;
} else {
//is it possible to go here ?
if (saveReaderIndex == message.readerIndex()) {
throw new IOException("Decode without read data.");
}
if (msg != null) {
out.add(msg);
}
}
} while (message.readable());
} finally {
NettyChannel.removeChannelIfDisconnected(ctx.channel());
}
}
}
}
DubboCountCodec --> 默認的編解碼器
package org.apache.dubbo.rpc.protocol.dubbo;
import org.apache.dubbo.remoting.Channel;
import org.apache.dubbo.remoting.Codec2;
import org.apache.dubbo.remoting.buffer.ChannelBuffer;
import org.apache.dubbo.remoting.exchange.Request;
import org.apache.dubbo.remoting.exchange.Response;
import org.apache.dubbo.remoting.exchange.support.MultiMessage;
import org.apache.dubbo.rpc.AppResponse;
import org.apache.dubbo.rpc.RpcInvocation;
import java.io.IOException;
import static org.apache.dubbo.rpc.Constants.INPUT_KEY;
import static org.apache.dubbo.rpc.Constants.OUTPUT_KEY;
public final class DubboCountCodec implements Codec2 {
// 實際的編解碼器為 DubboCodec (其 extends ExchangeCodec)
private DubboCodec codec = new DubboCodec();
@Override
public void encode(Channel channel, ChannelBuffer buffer, Object msg) throws IOException {
// 編碼, 透傳至 ExchangeCodec#encode
codec.encode(channel, buffer, msg);
}
// 解碼
@Override
public Object decode(Channel channel, ChannelBuffer buffer) throws IOException {
int save = buffer.readerIndex();
MultiMessage result = MultiMessage.create();
do {
// 這里走進了 ExchangeCodec.decode
Object obj = codec.decode(channel, buffer);
if (Codec2.DecodeResult.NEED_MORE_INPUT == obj) {
buffer.readerIndex(save);
break;
} else {
result.addMessage(obj);
logMessageLength(obj, buffer.readerIndex() - save);
save = buffer.readerIndex();
}
} while (true);
if (result.isEmpty()) {
return Codec2.DecodeResult.NEED_MORE_INPUT;
}
if (result.size() == 1) {
return result.get(0);
}
return result;
}
private void logMessageLength(Object result, int bytes) {
if (bytes <= 0) {
return;
}
if (result instanceof Request) {
try {
((RpcInvocation) ((Request) result).getData()).setAttachment(INPUT_KEY, String.valueOf(bytes));
} catch (Throwable e) {
/* ignore */
}
} else if (result instanceof Response) {
try {
((AppResponse) ((Response) result).getResult()).setAttachment(OUTPUT_KEY, String.valueOf(bytes));
} catch (Throwable e) {
/* ignore */
}
}
}
}
ExchangeCodec#encodeRequest --> 請求編碼(響應是另一相似方法)
// 請求編碼
protected void encodeRequest(Channel channel, ChannelBuffer buffer, Request req) throws IOException {
// 獲取序列化方式,默認是Hessian序列化, 這里也是通過 ExtensionLoader 來獲取的序列化方式
Serialization serialization = getSerialization(channel);
// new 了一個 16 字節(即 128 位)的 byte 數組,就是 request 的消息頭
byte[] header = new byte[HEADER_LENGTH];
// 往消息頭中 set magic number,前 2 個 byte 已經填充
Bytes.short2bytes(MAGIC, header);
// 往消息頭中 set request and serialization flag, 第三個byte已經填充
header[2] = (byte) (FLAG_REQUEST | serialization.getContentTypeId());
// 如果請求是 twoWay, 則將 第三個 byte 與 (byte) 0x40 進行 |= 計算
if (req.isTwoWay()) {
header[2] |= FLAG_TWOWAY;
}
// 如果請求是 event, 則將 第三個 byte 與 (byte) 0x20 進行 |= 計算
if (req.isEvent()) {
header[2] |= FLAG_EVENT;
}
// set request id.這個時候是 0
Bytes.long2bytes(req.getId(), header, 4);
// 編碼 request data.
int savedWriteIndex = buffer.writerIndex();
buffer.writerIndex(savedWriteIndex + HEADER_LENGTH);
ChannelBufferOutputStream bos = new ChannelBufferOutputStream(buffer);
// 序列化數據
ObjectOutput out = serialization.serialize(channel.getUrl(), bos);
if (req.isEvent()) {
// 編碼事件數據
encodeEventData(channel, out, req.getData());
} else {
// 編碼消息體數據
encodeRequestData(channel, out, req.getData(), req.getVersion());
}
out.flushBuffer();
if (out instanceof Cleanable) {
((Cleanable) out).cleanup();
}
bos.flush();
bos.close();
int len = bos.writtenBytes();
// 校驗 payload 是否超過指定長度, 默認為 8 * 1024 * 1024, 可在 URL 中設置 payload, 改變其大小
checkPayload(channel, len);
// 在消息頭中設置消息體長度
Bytes.int2bytes(len, header, 12);
// write
buffer.writerIndex(savedWriteIndex);
buffer.writeBytes(header); // write header.
buffer.writerIndex(savedWriteIndex + HEADER_LENGTH + len);
}
4.4 消費方發起調用過程
http://dubbo.apache.org/zh-cn/docs/source_code_guide/service-invoking-process.html (官網)
// 在此之前, 通過上述4.2 中生成的 proxy0 找到對應地址的 InvokerInvocationHandler
// 這一步, 將請求封裝為 RpcInvocation, 調用 recreate 方法封裝異步請求結果
// return invoker.invoke(new RpcInvocation(method, args)).recreate();
--> InvokerInvocationHandler#invoke
// 這里進入集群 (cluster): 判斷是否有 mock 請求
--> MockClusterInvoker#invoke
// 由于此處無 mock, 請求進入下述流程
--> AbstractClusterInvoker#invoke
// 獲取所有的 invokers
--> AbstractClusterInvoker#list
// 這里進入目錄 (Directory) 查找 invokers
--> AbstractDirectory#list
// invokers = routerChain.route(getConsumerUrl(), invocation);
--> RegistryDirectory#doList
// 這里進入路由 (route), 將傳入的invokers和設置的路由規則匹配,獲得符合條件的invokers返回
--> RouterChain#route
--> MockInvokersSelector#route
--> MockInvokersSelector#getNormalInvokers
// 這里進入負載均衡 (loadbalance), 通過 ExtensionLoader 加載 SPI 中的 loadbalance
--> AbstractClusterInvoker#initLoadBalance
// 發起調用
--> FailoverClusterInvoker#doInvoke
// 通過上一步負載均衡 (loadbalance) 及獲取的 invokers, 選取其中一個 invoker, 進行調用
// 若只有一個 invoker, 則直接返回
--> AbstractClusterInvoker#select
--> AbstractClusterInvoker#doSelect
// 這一步真正通過 上述 SPI 配置的 loadbalance 算法, 進行 loadbalance, 返回一個 invoker
--> AbstractLoadBalance#select
--> InvokerWrapper#invoke
... 這里進行各種 filterWrapper, filter 的調用
--> ListenerInvokerWrapper#invoke
--> AsyncToSyncInvoker#invoke
--> AbstractInvoker#invoke
--> DubboInvoker#doInvoke
--> ReferenceCountExchangeClient#request(Object, int)
--> HeaderExchangeClient#request(Object, int)
--> HeaderExchangeChannel#request(Object, int)
--> AbstractPeer#send
--> AbstractClient#send
--> NettyChannel#send
... 這里進行 netty4 的 channel.writeAndFlush
調用鏈
4.4 生產方接收請求及處理過程
http://dubbo.apache.org/zh-cn/docs/source_code_guide/service-invoking-process.html (官網)
調用邏輯
—> NettyServerHandler#channelRead
—> AbstractPeer#received(Channel, Object)
—> MultiMessageHandler#received(Channel, Object)
—> HeartbeatHandler#received(Channel, Object)
—> AllChannelHandler#received(Channel, Object)
—> ExecutorService#execute(Runnable) // 由線程池執行后續的
—> ChannelEventRunnable#run()
—> DecodeHandler#received(Channel, Object)
—> HeaderExchangeHandler#received(Channel, Object)
—> HeaderExchangeHandler#handleRequest(ExchangeChannel, Request)
—> DubboProtocol.requestHandler#reply(ExchangeChannel, Object)
—> Filter#invoke(Invoker, Invocation)
—> AbstractProxyInvoker#invoke(Invocation)
—> JavassistProxyFactory#getInvoker
—> Wrapper0#invokeMethod(Object, String, Class[], Object[])
—> DemoServiceImpl#sayHello(String)
4.4.1 線程派發模型
#概述
Dubbo 將底層通信框架中接收請求的線程稱為 IO 線程。
如果一些事件處理邏輯可以很快執行完,比如只在內存打一個標記,
此時直接在 IO 線程上執行該段邏輯即可。
但如果事件的處理邏輯比較耗時,比如該段邏輯會發起數據庫查詢或者 HTTP 請求。
此時我們就不應該讓事件處理邏輯在 IO 線程上執行,而是應該派發到線程池中去執行。
原因也很簡單,IO 線程主要用于接收請求,如果 IO 線程被占滿,將導致它不能接收新的請求。
#Dispatcher
Dispatcher 真實的職責創建具有線程派發能力的 ChannelHandler,比如 AllChannelHandler、MessageOnlyChannelHandler 和 ExecutionChannelHandler 等,其本身并不具備線程派發能力。Dubbo 支持 5 種不同的線程派發策略
策略 | 用途 |
---|---|
all | 所有消息都派發到線程池,包括請求,響應,連接事件,斷開事件等 |
direct | 所有消息都不派發到線程池,全部在 IO 線程上直接執行 |
message | 只有請求和響應消息派發到線程池,其它消息均在 IO 線程上執行 |
execution | 只有請求消息派發到線程池,不含響應。其它消息均在 IO 線程上執行 |
connection | 在 IO 線程上,將連接斷開事件放入隊列,有序逐個執行,其它消息派發到線程池 |
默認配置下,Dubbo 使用 all 派發策略,即將所有的消息都派發到線程池中。
請求對象會被封裝 ChannelEventRunnable 中,ChannelEventRunnable 將會是服務調用過程的新起點。
ChannelEventRunnable 僅是一個中轉站,它的 run 方法中并不包含具體的調用邏輯,
僅用于將參數傳給其他 ChannelHandler 對象進行處理,該對象類型為 DecodeHandler。
DecodeHandler 主要是包含了一些解碼邏輯。
解碼完畢后,完全解碼后的 Request 對象會繼續向后傳遞,下一站是 HeaderExchangeHandler。
4.6 DefaultFuture 中 FUTURES 保存調用結果 及同步異步問題
http://dubbo.apache.org/zh-cn/docs/source_code_guide/service-invoking-process.html (官網)
#同步異步概述
Dubbo 實現同步和異步調用比較關鍵的一點就在于由誰調用 ResponseFuture 的 get 方法。
同步調用模式下,由框架自身調用 ResponseFuture 的 get 方法。
異步調用模式下,則由用戶調用該方法。
ResponseFuture 是一個接口,它的默認實現類是 DefaultFuture。
當服務消費者還未接收到調用結果時,用戶線程調用 get 方法會被阻塞住。
同步調用模式下,框架獲得 DefaultFuture 對象后,會立即調用 get 方法進行等待。
而異步模式下則是將該對象封裝到 FutureAdapter 實例中,
并將 FutureAdapter 實例設置到 RpcContext 中,供用戶使用。
FutureAdapter 是一個適配器,用于將 Dubbo 中的 ResponseFuture 與 JDK 中的 Future 進行適配。
這樣當用戶線程調用 Future 的 get 方法時,經過 FutureAdapter 適配,
最終會調用 ResponseFuture 實現類對象的 get 方法,也就是 DefaultFuture 的 get 方法。
#異步模式下
一般情況下,服務消費方會并發調用多個服務,每個用戶線程發送請求后,
會調用不同 DefaultFuture 對象的 get 方法進行等待。
一段時間后,服務消費方的線程池會收到多個響應對象。
這個時候要考慮一個問題,如何將每個響應對象傳遞給相應的 DefaultFuture 對象,且不出錯。
答案是通過調用編號。
DefaultFuture 被創建時,會要求傳入一個 Request 對象。
此時 DefaultFuture 可從 Request 對象中獲取調用編號,
并將 <調用編號, DefaultFuture 對象> 映射關系存入到靜態 Map 中,即 FUTURES。
線程池中的線程在收到 Response 對象后,會根據 Response 對象中的調用編號,
到 FUTURES 集合中取出相應的 DefaultFuture 對象,
然后再將 Response 對象設置到 DefaultFuture 對象中。
最后再喚醒用戶線程,這樣用戶線程即可從 DefaultFuture 對象中獲取調用結果了。
5.擴展點分析
5.1 SPI 機制及 @Activate & @Adaptive 應用原理
http://www.lxweimin.com/p/43f529637f12
參考資源
https://www.cnblogs.com/simoncook/p/9570535.html (服務查找過程)
https://yq.aliyun.com/articles/69793 (多注冊中心配置)
https://www.cnblogs.com/sinxsoft/p/4984321.html (多注冊中心配置)
https://blog.csdn.net/qq_27529917/article/details/80632078 (dubbo-zk)