Dubbo(原理淺析篇)

dubbo spi擴展

dubbo擴展利用java的spi機制,使高層引用底層,用戶可以通過spi擴展dubbo。
java spi
通過在META-INF目錄創建services文件夾,然後以接口全限定名作爲文件名,多個實現類的權限定名爲文件內容,通過ServiceLoader類的load方法傳入接口class,即可獲取到所有實現類。原理是通過在指定目錄查找該類的文件,從文件中獲取權限定名並創建對象。
dubbo
dubbo沒有直接使用java spi,而是實現一套類似的機制,增加了一些功能。
通過在META-INF目錄dubbo文件夾存放配置文件,文件名命名方式與java spi一樣,不同的是,文件內容以鍵值對的方式,可以對象名爲key,value爲實現類的全限定名。通過ExtensionLoader可以獲取實現類,並且可以通過key直接獲取對應的實現對象。

源碼在ExtensionLoader,主要就是現將指定目錄配置的所有擴展類通過反射實例化,然後放入通過key value形勢存到緩存中。通過指定獲取的擴展類key,獲取到對應實現類。之後爲實現類設置依賴,通過實例中的set方法進行設置,這是dubbo的ioc機制

服務導出

服務通過註冊spring上下文刷新事件,在spring初始化會開始導出服務。
源碼ServiceBean
獲取配置信息,通過解析xml,開始服務導出。

  1. 通過NettyServer啓動一個服務,創建Netty包下的ServerBootstrap啓動服務。底層基於nio的ServerSocketChannel啓動一個服務,監聽端口。
  2. 向註冊中心註冊服務。
    源碼在ZookeeperRegistry,先根據zk配置信息創建zk連接客戶端,再以 分組名/接口全限定名/provider/${url} 目錄下創建一個臨時節點,即創建一個文件代表一條註冊信息。默認分組名爲dubbo

服務引用

Dubbo 服務引用的時機有兩個,

  1. Spring 容器調用 ReferenceBean 的 afterPropertiesSet 方法時引用服務
  2. ReferenceBean 對應的服務被注入到其他類中時引用。默認是第二種,用到才注入。

在類ReferenceBean中,引用bean通過實現spring的FactoryBean的getObject方法,定義生成引用bean的方式,該方法最終調用了ReferenceConfig的init方法,該方法先生成消費者配置類,然後判斷是jvm本地引用還是遠程引用,如果有直接連接服務的url或者url參數是遠程作用域、本地導出的服務沒有包含當前服務等等情況則爲遠程。

如果url只有一個地址,則直接生成invoke對象。invoke調用通過NettyChannel發送消息,通過ChannelFuture獲取調用結果。

如果有多個url則會有多個invoker對象封裝成cluster對象,獲取到一個合併後的inoker對象。最後生成引用類代理對象,如果不是生成代理對象,則客戶端需要直接依賴invoker對象,造成代碼侵入。
dubbo默認生成代理通過javassistProxy,好處時執行代理方法比jdk快,但是生成代理類比jdk慢.
invoke調用服務默認時通過netty通信。

通過配置可以修改代理方式爲jdk
<dubbo:provider proxy=“jdk” />

xml解析,將bean注入spring

源碼: DubboBeanDefinitionParser.parse()
爲bean生成BeanDefinition註冊到spring中,引用bean會實現factoryBean,調用bean時會通過getObject獲取實際的bean,getObject會返回一個代理對象,由代理對象與服務端進行網絡通信獲取處理結果。

集羣容錯

服務目錄

根據服務提供者配置生成的invoke對象集合,而且集合的內容會跟隨註冊中心服務的變化而變化。源碼在RegistryDirectory.

服務路由

條件路由,決定了消費端的調用目標。通過配置路由規則,可以限制某些服務提供者只可以被某些服務消費者調用。相當於服務網關的。源碼在ConditionRouter,對配置進行解析匹配。

集羣

由於服務提供者大部分情況是會部署多臺機器,dubbo通過封裝Cluster接口,將多個invoke合併成一個,消費端只需要通過這個inovke進行調用,不用底層具體調用了哪個api

集羣容錯

Failover Cluster - 失敗自動切換
FailoverClusterInvoker。失敗時,獲取允許重試次數,然後循環重試,每次重試會重新獲取invoke集合,線通過負載均衡選出一個invoke,但是如果選出的invoke不穩定(當前調用失敗過),則會選擇下一個invoke.會判斷是否是粘滯連接,是的話默認會同個消費者會一直調用同一個提供者,但是如果是已經執行失敗過,重試則不會再調用同一個提供者。
Failfast Cluster - 快速失敗
FailfastClusterInvoker。調用一次,失敗就拋異常
Failsafe Cluster - 失敗安全
FailsafeClusterInvoker。調用一次,失敗打印日誌
Failback Cluster - 失敗自動恢復
FailbackClusterInvoker。調用失敗打印日誌,記錄到失敗任務中,通過synchronized開啓同步,創建定時任務,每5秒掃一次失敗任務進行遍歷重試。
Forking Cluster - 並行調用多個服務提供者
ForkingClusterInvoker。遍歷invoke集合,通過線程池每個inovke一個線程異步執行,拿到返回結果就將結果放入阻塞隊列。主線程從阻塞隊列拿任務,只要阻塞隊列有一個執行完成拿到結果就返回(poll自動阻塞等待隊列有結果)。需要注意的是,任一個invoke執行會進行計數,只有失敗量等於invoke數,纔會把異常放入隊列,保證只要有一個成功即返回成功。
Broadcast Cluster
同步遍歷執行,如果有一個異常則暫存異常對象,遍歷執行結束後,判斷存在異常則拋出異常。(廣播處理,可以用於更新提供本地緩存)

整體流程
  • 服務初始化階段

爲服務消費者創建cluster invoker

  • 服務調用階段

整個過程先從服務目錄獲取服務列表,再經過服務路由,篩選符合路由條件的服務(如果沒有配置則無限制),再經過負載均衡策略選擇某一個服務提供者,最後進行調用。

負載均衡

抽象類AbstractLoadBalance,包含了權重計算,如果沒有配置權重則默認時100。權重並不是配置多少就是多少的,獲取權重時會從url中獲取服務提供者啓動時間戳,默認如果啓動時間沒有超過10分鐘,判斷沒有經過預熱進行降權,通過uptime/warmup/weight進行計算,啓動時間越長則權重越高,直到達到配置的權重值。如果超過10分鐘則直接返回配置的權重值。

權重隨機算法的 RandomLoadBalance

根據權重分爲幾個區間,假設權重爲2,4,6,那麼區間則爲0-1,2-5,6-11,通過生成0-11的隨機數,命中哪個區間就調用哪個invoke。獲取到隨機數後,通過遍歷invoke集合,減去每個invoke權重值,當隨機數剪成負數,說明位於這個區間,返回當前遍歷的invoke。缺點是調用次數可能不夠平均。

基於最少活躍調用數算法的 LeastActiveLoadBalance

最小活躍數指的是活躍數低的機器會優先分配,初始每個服務的活躍數都爲0,每收到一個服務請求則活躍數加1,完成一個請求處理則活躍數減去1,即根據當前處理中的請求數決定分配,確保壓力平衡。而且支持權重,如果活躍數相同,則權重高的優先分配,權重也想等則隨機分配。
2.6.4有bug,總的權重沒把降權算上,減去權重時有算降權,可能導致最終沒有一個invoke被選中。還有2.6.4權重值如果設置爲1可能會導致不會被選中,臨界值問題,也是bug。
http://dubbo.apache.org/zh-cn/docs/source_code_guide/loadbalance.html

基於 hash 一致性的 ConsistentHashLoadBalance

基於treemap實現的一致性hash,虛擬節點默認每個invoke爲160個,分佈虛擬節點時.
分配請求命中的節點時,通過參數進行hash,所以相同的參數會被hash到同一個節點,不關心權重。

內部包含ConsistentHashSelector類,基於TreeMap實現一致性hash,一個service一個選擇器。
每個invoke進行40次循環,url+i 進行md5再hash,再4次內循環一個值,將invoke放到對應的位置。每個invoke最終會在環上有160個節點。

  for (Invoker<T> invoker : invokers) {
    for (int i = 0; i < replicaNumber / 4; i++) {
        byte[] digest = md5(invoker.getUrl().toFullString() + i);
        for (int h = 0; h < 4; h++) {
            long m = hash(digest, h);
            virtualInvokers.put(m, invoker);
        }
    }
}
基於加權輪詢算法的 RoundRobinLoadBalance

輪詢算法指的是每個機器輪流處理,但是會導致性能好的機器和性能差的機器得到相同的請求量。所以允許通過加權來控制。2.6.4以前輪流處理的同時,通過一個mod值記錄當前處理到第幾次,然後循環mod次機器集合去將權重-1,如果某個機器的權重值爲0,則輪到這個機器時會跳過,最後處於哪個機器就返回哪個機器。

這個算法有一個問題是時間複雜度是0(n),每次都要減1,那麼如果權重很大,mod值很大的時候,要經過很多次遍歷才能拿到最終處理的機器。

2.6.4之後每個服務器對應兩個權重,分別爲 weight 和 currentWeight。其中 weight 是固定的,currentWeight 會動態調整,初始值爲0。當有新的請求進來時,遍歷服務器列表,讓它的 currentWeight 加上自身權重。遍歷完成後,找到最大的 currentWeight,並將其減去權重總和,然後返回相應的服務器即可。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-sRilbFCV-1585712715352)(evernotecid://EA56B0F8-13B5-4B11-8191-1E827D9FC2A6/appyinxiangcom/22897236/ENResource/p20)]

服務調用過程源碼

消費者通過代理對象調用遠程服務,將數據進行編碼,然後通過netty客戶端發送給服務端,服務端解碼後將請求派發給指定的線程池,線程池調用具體的服務,然後相應請求,再將結果返回。

  1. MockClusterInvoker。服務調用時,會判斷是否包含mock配置,有的話會根據mock做降級調用,比如直接返回不調用,或者調用加個try catch,在異常時代調用mock的代碼。
  2. AbstractInvoker。調用時判斷是否爲異步調用,異步調用直接返回,需要返回值則將Future設置到rpcContent,同步調用則直接get等待返回值,此時會用戶線程進入超時await狀態,在服務端響應後會喚醒,在超時後仍沒收到響應會報錯超時異常。具體返回值獲取DefaultFuture類處理,DefaultFuture創建時會要求傳入requeset對象,包含一個調用編號DefaultFuture類有一個FUTURES靜態map,以調用編號爲key,future對象爲value,以便響應結果時可以通過調用編號找到對應的future處理
  3. 最終發送請求,由netty的NettyClient的send方法發送,send方法通過NettyChannel的send發送,最後通過channel write寫數據出去。在發送數據後,會先判斷sent參數是否爲true,爲true則等待write結果,如果發送失敗就報錯。如果sent爲false則直接返回。
  4. ExchangeCodec,消息編碼。將消息封裝成dubbo規定的消息結構,包含魔數(用於消息校驗,標識是dubbo發出的消息,接收方進行校驗)、消息長度記錄、調用編號到消息頭等,將發送的dubbo版本號、路徑、方法名、參數進行序列化存儲到結構中。
  5. 接受接受放。DecodeableRpcInvocation::decode,解碼:魔數校驗,取出消息頭消息長度,長度校驗,對消息進行解碼(in.readUTF()獲取版本號、請求路徑、方法名、參數值、調用編號),通過返序列化獲取參數值。最後獲取到一個request對象
  6. NettyHandler處理request,之後由nettyServer處理,最後交給線程派發模型處理。
  7. 線程派發模型:dubbo把處理請求的線程稱爲io線程,如果業務處理只是內存操作可以在io線程執行,如果是耗時操作,不可以在io線程上執行,會影響請求接受,應該把處理派發到線程池中處理。線程派發器Dispatcher,它的職責是創建具有線程派發能力的ChannelHandler,dubbo支持5種派發模型,默認是all類型。由AllChannelHandler處理,建立連接、斷開鏈接、處理請求和響應都封裝成ChannelEventRunnable給線程池處理。
    [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-sSFkSlTZ-1585712715355)(evernotecid://EA56B0F8-13B5-4B11-8191-1E827D9FC2A6/appyinxiangcom/22897236/ENResource/p21)]
  8. ChannelEventRunnable把消息再交給各handler處理,比如消息解碼,最後是HeaderExchangeHandler,判斷是雙向通信會調用具體的請求方法後,通過channel將響應結果封裝到Response send給客戶端,出現異常也同樣將異常信息封裝後返回。同樣需要將響應數據進行編碼,需要在消息頭設置魔數、響應狀態、消息長度、序列化請求結果,調用編號.
  9. 服務消費方接受響應結果。消息解碼,根據調用編號在DefaultFuture的靜態map參數FUTURES找到對應的DefaultFuture對象,然後將respose設置進去,通過Condition::signal喚醒線程。

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-YvpnCGvp-1585712715355)(evernotecid://EA56B0F8-13B5-4B11-8191-1E827D9FC2A6/appyinxiangcom/22897236/ENResource/p48)]

tcp拆包、粘包解決

在TCP網絡傳輸工程中,由於TCP包的緩存大小限制,每次請求數據有可能不在一個TCP包裏面,或者也可能多個請求的數據在一個TCP包裏面。那麼如果合理的decode接受的TCP數據很重要,需要考慮TCP拆包和粘包的問題

dubbo具有高度自定義的協議,消息被分爲消息頭和消息體。因爲高度自定義不能直接用netty進行消息編碼和解碼。消息頭規定爲16字節。源碼在ExchangeCodec

  1. 服務端先創建消息頭大小的buff,從buffer(用戶緩衝區)中讀取,即先把消息頭讀出來。當讀取的消息可讀數據還不夠16字節時,即還不夠消息頭的大小,則從channel中讀取到buffer,再等待下次tcp包過來繼續讀取。且會通過消息頭的0號位置魔數校驗。(讀取到的消息會暫存到成員變量NettyCodecAdapter::InternalDecoder::buff,等到處理完一個完整的dubbo數據包後再置空,如果處理完後,buff還有剩餘數據,則會重複流程去讀取更多數據)
  2. 讀到完整的消息頭後,取出消息頭標誌的消息長度,根據消息長度讀取消息。同樣不夠讀取會重新去讀。
  3. 當buffer的數據包含一個完整的dubbo包數據後,開始解碼。

發生拆包時,當前tcp沒有包含完整數據,會去繼續讀取放到buffer。直到讀取到完整數據包
發生黏包時,讀取完一個完整數據包後,會發現buffer還可讀,繼續下次循環去讀取,接下來可能又出現消息不夠,繼續等待下次tcp包數據讀取的情況。

如此看來tcp包的有序性就很重要了,如果tcp沒有保證有序性,那麼數據就是亂的了。
https://blog.csdn.net/zhengyangzkr/article/details/71486851

服務訂閱 推和拉

dubbo訂閱服務基於zk的是用watch機制,建立長鏈接,要有心跳檢測機制。長鏈接會佔用長期系統資源。
拉就是主動定時拉取服務列表。

dubbo異常處理

ExceptionFilter,服務端將在異常時將異常封裝返回,客戶端獲取結果判斷時異常拋出異常。
自定義異常參考應用篇拋出。

總結

  • 服務提供端在項目啓動時向註冊中心註冊服務,在/分組名/服務權限定名/providers目錄下,創建臨時節點,包含ip地址等,同個服務多個機器註冊則構成集羣,然後以服務地址爲key,Exporter爲value,Exporter包含服務調用的invoke。消費端請求過來後會通過key找到對應的invoke進行調用處理,通過反射調用方法。
  • 服務消費端有一個服務目錄,會根據註冊中心的服務情況動態變成invoke集合,消費端發起調用時會從服務目錄獲取到invoke集合,然後經過條件路由過濾掉一些invoke之後,如果有多個invoke,則構造一個cluster對象,再封裝返回一個invoke,發起調用時,再通過cluster的類型做不同的調用方式。如果是一次只調用一個機器,會通過負載均衡策略選擇一個機器的invoker進行調用,而且基於粘滯策略,如果不出現問題,同個消費者會一直調用同一個機器的invoke,避免重複創建連接。
  • 獲取到一個invoker之後,消費端將消息進行編碼,發起調用時會根據服務降級策略和異步同步策略進行調用,調用時會封裝DefaultFuture對象,生成調用編號,通過nettyClient與服務端建立連接,將調用編號請求信息等通過nettyChannel發送給服務端,然後調用線程進入await有限等待。服務端接收到消息後,對消息進行解碼,將消息交給對應的方法處理後將處理結果封裝成respose後,將調用編號和處理結果通過nettyChannel發送給消費端,消費端通過調用編號找到對應的DefaultFuture喚醒線程得到請求結果。

服務端和消費端都會通過nettyChannel去啓動一個server,創建invoke,客戶端請求會把請求地址、端口、請求id也帶在消息體中,dubbo處理完消息後,再通過請求地址發送消費端,消費端根據id找到請求線程,將請求結果設置到id對應的線程後,喚醒請求線程。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章