Dubbo技術知識總結之四——Dubbo集羣容錯

接上篇《Dubbo技術知識總結之三——Dubbo 啓動與服務暴露、引用》

四. 集羣容錯

在客戶端已經從註冊中心拉取和訂閱服務列表完畢的前提下,Dubbo 完成一次完整的 RPC 調用,流程如下:

  1. 服務列表聚合
  2. 路由
  3. 負載均衡
  4. 選擇一臺機器進行 RPC 調用;
  5. 請求交給底層 I/O 線程池處理;
  6. 讀寫、序列化、反序列化;
  7. 方法調用;

將上面的步驟進行細化,在一次 RPC 調用過程中,Cluster 層的流程如下:

  1. 根據不同的容錯機制,生成 Invoker 對象,調用 AbstractClusterInvoker 的 Invoker 方法;
  2. 獲得可調用的服務列表;
  3. 使用 Router 接口處理服務列表,根據路由規則過濾一部分服務;
  4. 負載均衡
  5. RPC 調用

其中步驟 1, 2, 3 是模板方法,使用通用的校驗、參數準備等準備工作。最終,不同的容錯機制的子類實現不同的 doInvoke 方法,每個子類方法都有各自的路由、負載均衡實現策略。

本章節主要總結 RPC 在 Cluster 層的工作,涉及步驟 1, 2, 3, 4,其中容錯機制見[5.1](##5.1 容錯機制),容錯過程中獲取 Invoker 列表需要用到 Directory,見[5.2](##5.2 Directory);Directory 過程中需要用到路由,見[5.3](##5.3 路由);負載均衡見[5.4](##5.4 負載均衡)。剩餘步驟 5, 6, 7 是具體的 RPC 調用,見[第六章](#6. 遠程調用)。

4.1 容錯機制

容錯過程是在各容錯機制實現子類的 doInvoke 方法重寫實現的。容錯過程對上層用戶是完全透明的,上層用戶不用關心容錯過程是怎麼實現的,同時用戶也可以通過不同的配置項來選擇不同的容錯機制。支持的容錯機制如下:

注:
大部分容錯機制的核心步驟都是:

  1. 校驗;
  2. 獲取配置參數;
  3. 實現各自容錯機制的調用;

在上述步驟 3 容錯機制的調用中,主要步驟都是:

  1. 校驗;
  2. 負載均衡;
  3. RPC 調用;

如果有不同,在各自條目中進行說明

  1. Failover:重試失敗,默認策略
    • 調用失敗,嘗試調用其他服務器;
    • 根據配置的重試次數,進行重試;如果有成功,則返回;全部重試失敗之後,拋出異常;
  2. Failfast:快速失敗
    • RPC 調用失敗後,將異常封裝爲 RpcException,拋出並返回,不做任何重試;
  3. Failsafe:安全失敗
    • 出現異常時忽略;
  4. Failback:定時重試失敗
    • 調用失敗後,將該失敗的 invocation 緩存到 ConcurrentHashMap 中,並返回空結果集;同時設置定時線程池,定時時間到了就將失敗的任務投入線程池,重新請求;
    • 如果重新請求成功,則從緩存中移除,請求失敗則判斷失敗次數;如果失敗次數少於設定的閾值,則重新投入定時線程池;如果多於設定的閾值,打印錯誤並放棄該請求;
    • 定時重試失敗的實現思路,可以用於 Kafka 的重試隊列
  5. Forking:並行
    • 根據設定的並行數量,循環執行負載均衡,篩選出可調用的 Invoker 列表;
    • 循環使用線程池,同時調用多個相同的服務;多個服務中,只要其中一個返回,就立即返回結果;所有線程調用失敗,則拋出異常;
      • 該部分的實現是通過阻塞隊列 BlockingQueue 實現的;將多個調用任務投入線程池後,任務執行結果投入 BlockingQueue
      • 如果任務執行結果是異常類型,投入 BlockingQueue 拋出異常;此時記錄異常次數,只有到記錄異常次數等於服務數量時,說明所有服務都拋出異常,此時再將異常信息投入 BlockingQueue
      • 調用任務投入線程池之後,就立即調用 BlockingQueue # poll(int) 方法拉取結果,拉取到第一個結果就返回。如果返回值正常,就是其中一個服務的返回結果;如果返回值爲 Exception 類型,說明所有服務都出現異常;
  6. Broadcast:廣播
    • 廣播調用所有可用服務,循環遍歷所有 Invoker,每個 Invoker 分別做 RPC 調用;
    • 如果有任意一個節點報錯,等待廣播最後完成之後拋出;如果多個節點異常,最後一個節點拋出的異常會覆蓋前面拋出的異常;
  7. Available:可用
    • 最簡單的方式,請求不會做負載均衡,遍歷所有服務列表,找到第一個可用節點,直接請求並返回結果;
  8. Mock:仿真
    • 調用失敗時返回僞造的響應結果,或者直接強行返回僞造結果;
  9. Mergeable:合併:將多個節點請求的結果合併;

4.2 Directory

容錯過程中需要獲取 Invoker 列表,用於後續的路由和負載均衡。這個過程需要用到 Directory # list 方法執行。Directory 接口有一個抽象類 AbstractDirectory,以及兩個主要實現類:動態列表 RegistryDirectory,以及靜態列表 StaticDirectory。主要總結的是動態列表 RegistryDirectory,以及封裝了基礎方法的抽象類 AbstractDirectory
RegistryDirectory 主要實現了兩個功能:

  1. 與註冊中心的訂閱,動態更新本地的 Invoker 列表;
  2. 實現父類的 doList 方法;

4.2.1 訂閱與動態更新

註冊中心訂閱的部分主要在 ZookeeperRegistry # doSubscribe() 方法中實現,見[第二章註冊中心](#二. 註冊中心)部分。
在監聽到註冊中心對應 URL 變化後,觸發 RegistryDirectory 對各種本地配置的動態更新。更新的配置包括:

  1. 路由信息:通過路由工廠 RouterFactory 將 URL 包裝成路由規則(見[5.3](#5.3 路由)),更新本地路由信息;
    • 更新路由規則,是通過 override 協議實現的;
  2. 服務提供者配置 Configurator:管理員可以在 dubbo-admin 下動態修改生產者的參數,這些參數會保存在配置中心的 configurators 類目錄下;
  3. Invoker 修改:如果監聽到的 Invoker 類型 URL 不爲空,則將新的 URL 與本地舊 URL 合併,同時銷燬舊 Invoker;

4.2.2 doList

doList 方法主要作用,就是調用路由方法。

4.3 路由

注:路由的整體思路與筆者設計的動態彙總統計業務不謀而合,通過表達式的方式實現數據的處理。

路由會根據用戶配置的不同路由策略,對 Invoker 列表進行過濾。主要分爲條件路由文本路由腳本路由。路由工廠 RouterFactory 是一個 SPI 接口,用戶可以自行通過實現 Router 接口擴展 Router 類;在調用的時候,在 URL 的 protocol 參數中可以設置 file / script / condition,分別尋找對應的實現類。

4.3.1 條件路由 (ConditionRouter)

條件路由使用的是 condition://協議,URL 形式是:“condition://0.0.0.0/com.foo.DemoService?category=routers&dynamic=false&rule=” + URL.encode(“host = 10.20.153.10 => host = 10.20.153.11”);每個參數都是有含義的:

參數名 含義
condition:// 路由類型爲條件路由(可擴展)
0.0.0.0 對全部 IP 生效,填入具體 IP,則只對該 IP 生效
com.foo.DemoService 對指定服務生效,必填
category=routers 當前設置指該數據爲動態配置類型,必填
dynamic=false 當前設置表示該數據爲持久數據,必填
enable=true 覆蓋規則生效,默認生效
force=false 路由結果爲空時,是否強制執行,默認爲 false,路由爲空時將自動失效
rule=… 路由規則內容,必填

條件路由最關鍵的部分在於 rule 的路由規則。以下面的路由規則爲例:

method = find* => host = 192.168.1.22
  1. 該路由規則的意義:所有調用 find 開頭的方法,都會被路由到 192.168.1.22 的服務節點上;
  2. => 之前部分是服務消費者匹配條件
    • 如果匹配條件爲空,則表示應用於所有消費者;
  3. => 之後部分是服務提供者列表的過濾條件
    • 如果過濾條件爲空,則表示禁止訪問;
  4. 表示規則的表達式支持 $protocol佔位符方式,也支持 =, != 等條件,也支持通配符 *

條件路由的具體實現類是 ConditionRouter,整體的思想是通過正則表達式,按照 =>進行分割,然後對符號前後的內容進行正則表達式的匹配,匹配結果存入對象 MatchPair 中。對於上述的佔位符、通配符等,MatchPair 會進行匹配解析。

注:條件路由的整體思路,類似於筆者設計的動態彙總統計業務。

4.3.2 文件路由 (FileRouter)

文件路由通常和腳本路由搭配使用。文件路由將規則寫到文件中,文件中寫的是自定義的腳本規則,腳本可以是 Javascript, Groovy 等,文件路由 FileRouter 找到對應文件,將文件中的腳本內容按照類型匹配腳本路由,執行解析。

4.3.3 腳本路由 (ScriptRouter)

腳本路由使用 JDK 自帶的腳本解析器,對腳本解析並運行,默認使用 Javascript 解析器。在構造腳本路由時初始化腳本執行引擎,根據腳本不同的類型,通過 JDK 提供的 ScriptEngineManager 創建不同的腳本執行器。接收到腳本內容後,執行 route 方法。具體的過濾邏輯需要用戶自行定義。

注:在筆者設計的動態彙總統計業務中,筆者使用了 Aviator 表達式引擎,它與腳本路由中的腳本執行器 ScriptEngineManager 類似。

4.4 負載均衡

很多容錯策略在路由選擇出所有可用 Invoker 列表中實行最後一步篩選,負載均衡。
負載均衡的核心是 LoadBalance 接口及其子類具體實現的,但並不是直接使用 LoadBalance 方法。在容錯策略中的負載均衡先使用了抽象父類 AbstractClusterInvoker 中定義的 Invoker select 方法,它在 LoadBalance 基礎上又封裝了一些特性:

  1. 粘滯連接:儘可能讓客戶端總是向同一提供者發起調用。
    • 類似的策略,也在 Kafka 再均衡策略 StickyAssignor 中用過;
  2. 可用檢測
  3. 避免重複調用

select 方法也使用了模板模式,在 select 方法中處理通用邏輯,最後提供 doSelect 抽象方法供各子類具體實現。Dubbo 內置了四種負載均衡算法,此外由於 LoadBalance 接口帶有 @SPI 註解,所以用戶也可以自行擴展負載均衡算法。在調用方法時我們可以在 URL 中通過 loadbalance=xxx 動態指定 select 方法的負載均衡算法。

4.4.1 Random

根據權重,設置隨機概率做負載均衡。

4.4.2 RoundRobin

《Nginx》篇 6.2.2

4.4.3 LeastActive

LeastActive 就是最少活躍調用負載均衡,Dubbo 在運行過程中會統計每一次 Invoker 的調用,每次從活躍數最少的 Invoker 中選一個節點。

4.4.4 一致性 Hash

一致性 Hash 的原理見《數據結構與算法》篇第五章

Dubbo 的一致性 Hash 負載均衡,將接口名 + 方法名作爲 Key 值,類型爲 ConsistentHashSelector 實例對象作爲 Value 存入一個 ConcurrentHashMap 中。每次請求進入,解析請求獲取到方法,將該方法轉爲 Key 值,找到對應的 ConsistentHashSelector 進行負載均衡。所以 ConsistentHashSelector 是 Dubbo 中一致性 Hash 實現的核心。
ConsistentHashSelector 的環形散列是用 TreeMap 實現的,所有真實節點、虛擬節點都放在 TreeMap 中。將節點的 IP + 遞增數字,然後作 MD5 計算,最後進行 Hash 計算,作爲 TreeMap 的 Key 值。TreeMap 的 Value 值爲對應的某個可以調用的節點。關鍵代碼如下:

    // 遍歷所有節點
    for (Invoker<T> invoker : invokers) {
        // 得到每個節點的 IP
        String address = invoker.getUrl().getAddress();
        // replicaNumber 是生成的虛擬節點數量,默認 160 個
        for (int i = 0; i < replicaNumber / 4; i++) {
            // 對 IP + 遞增數字作 MD5 計算,作爲節點標識
            byte[] digest = md5(address + i);
            for (int h = 0; h < 4; h++) {
                // 對標識作 Hash 計算,作爲 TreeMap 的 Key 值
                long m = hash(digest, h);
                // 當前 Invoker 爲 Value
                virtualInvokers.put(m, invoker);
            }
        }
    }

每次請求進來後,進行上述的 Key 值運算,每次請求的參數都不同,但是由於 TreeMap 是有序的樹形結構,所以可以調用 TreeMap#ceilingEntry 方法,找到最近一個大於或等於給定 Key 值的節點 Entry。這樣的操作相當於一致性 Hash 算法的順時針向前查找的效果。

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