Stream中的Peek操作

germo發表於2021-09-09

前言

大家好,今天開始給大家分享 — Dubbo 專題之 Dubbo 路由規則之條件路由。在前一個章節中我們介紹了 Dubbo 令牌驗證和優雅停機,以及我們也例舉了常見的使用場景並且進行了原始碼解析來分析其實現原理,同時知道 Dubbo 中的令牌驗證核心思想就是透過服務提供端提供的token或者隨機產生的token放入註冊中心進行管理,然後服務消費端獲取token令牌並且在呼叫服務提供端時攜帶 token,服務提供端根據消費端攜帶的token進行驗證。有的小夥伴可能會想:我們多個服務提供者能否透過一定的規則對呼叫的服務提供者進行過濾和限制呢?那接下來我們就圍繞著這個問題一起來學習下 Dubbo 中的路由規則。下面就讓我們快速開始吧!

1. 條件路由簡介

首先我們得了解什麼是路由規則?假設有這樣一個場景如下圖所示:

圖片描述
上圖中我們可以看到有兩個機房分別是機房A、機房B,其中機房A只能訪問到 Service A 和 Service B ,而機房B 只能訪問到 Service C 和 Service D。要實現上面這種場景我們就需要用到所謂的路由規則。路由規則是在發起一次RPC呼叫前過濾目標伺服器地址,而過濾後的地址列表,將作為消費端最終發起RPC呼叫的備選地址。在 Dubbo 中支援兩種路由規則今天我們主要討論條件路由。

  • 條件路由:支援以介面服務或消費者應用為粒度配置路由規則。

2. 使用方式

下面我們簡單的討論下條件路由使用方式:

條件路由

  • 介面服務粒度

    # demo-consumer1 的消費者只能消費所有埠為20880的服務例項
    # demo-consumer2 的消費者只能消費所有埠為20881的服務例項
    ---
    scope: application #應用粒度
    force: true
    runtime: true
    enabled: true
    key: demo-provider 
    conditions:
      - application=demo-consumer1 => address=*:20880
      - application=demo-consumer2 => address=*:20881
    
  • 應用粒度

    # BookFacade 的 queryAll 方法只能消費所有埠為20880的服務例項
    # BookFacade 的 queryByName 方法只能消費所有埠為20881的服務例項
    ---
    scope: service #服務粒度
    force: true
    runtime: true
    enabled: true
    key: com.muke.dubbocourse.common.api.BookFacade
    conditions:
      - method=queryAll => address=*:20880
      - method=queryByName => address=*:20881
    
  • 欄位說明:

    編號 欄位名稱 說明 必填
    1 scope 路由規則的作用粒度,scope的取值會決定key的取值。
    service 服務粒度 application 應用粒度。
    必填
    2 Key 明確規則體作用在哪個介面服務或應用。 scope=service時,
    key取值為[{group}:]{service}[:{version}]的組合 scope=application時,
    key取值為application名稱 。
    必填
    3 enabled enabled=true 當前路由規則是否生效,,預設生效。 可不填
    4 force force=false 當路由結果為空時,是否強制執行,如果不強制執行,
    路由結果為空的路由規則將自動失效,預設為 false
    可不填
    5 runtime runtime=false 是否在每次呼叫時執行路由規則,
    否則只在提供者地址列表變更時預先執行並快取結果,
    呼叫時直接從快取中獲取路由結果。如果用了引數路由,必須設為 true
    需要注意設定會影響呼叫的效能,預設為 false
    可不填
    6 priority priority=1 路由規則的優先順序,用於排序,優先順序越大越靠前執行,預設為 0 可不填
    7 conditions 定義具體的路由規則內容。 必填
  • Conditions規則體

    格式:

    • => 之前的為消費者匹配條件,所有引數和消費者的 URL 進行對比,當消費者滿足匹配條件時,對該消費者執行後面的過濾規則。
    • => 之後為提供者地址列表的過濾條件,所有引數和提供者的 URL 進行對比,消費者最終只拿到過濾後的地址列表。
    • 如果匹配條件為空,表示對所有消費方應用,如:=> host != 192.168.53.11
    • 如果過濾條件為空,表示禁止訪問,如:host = 192.168.53.10 =>

    表示式:

    引數支援:

    • 服務呼叫資訊,如:method, argument 等,暫不支援引數路由
    • URL 本身的欄位,如:protocol, host, port 等
    • 以及 URL 上的所有引數,如:application, organization 等

    條件支援:

    • 等號 = 表示"匹配",如:host = 192.168.53.10
    • 不等號 != 表示"不匹配",如:host != 192.168.53.10

    值支援:

    • 以逗號 , 分隔多個值,如:host != 192.168.53.10,192.168.53.11
    • 以星號 * 結尾,表示通配,如:host != 10.20.*
    • 以美元符 $ 開頭,表示引用消費者引數,如:host = $host

Tips:conditions部分是規則的主體,由1到任意多條規則組成,下面我們就每個規則的配置語法做詳細說明:

3. 使用場景

從上面的簡單介紹我們可以大致瞭解到,當我們消費對訪問服務提供者時我們可以透過一定的規則對服務提供者列表進行過濾。那下面我們列舉下工作中常使用的場景:

  1. 黑名單: 比如我們需要禁止某些服務消費者消費服務
host = 192.168.53.10,192.168.53.11 =>

上面配置表示禁止192.168.53.10192.168.53.11消費者訪問服務提供者。

  1. 服務寄宿在應用上,只暴露一部分的機器,防止整個叢集掛掉
=> host = 192.168.53.1*,192.168.53.2*

上面配置表示只能放192.168.53.1*192.168.53.2 ip 地址開頭的服務提供者。

  1. 讀寫分離:讀取資料和寫入資料操作分開
method = find*,list*,get*,is* => host = 192.168.53.10,192.168.53.11,192.168.53.12
method != find*,list*,get*,is* => host = 192.168.20.97,192.168.53.21

上面配置表示以find*,list*,get*,is*方法命名開始的方法只能訪問192.168.53.10,192.168.53.11,192.168.53.12服務提供者,而不是find*,list*,get*,is*方法命名開始的方法只能訪問192.168.20.97,192.168.53.21服務提供者。

  1. 提供者與消費者部署在同叢集內,本機只訪問本機的服務
=> host = $host

上面配置表示所有消費者只能訪問叢集內的服務。

4. 示例演示

我們以獲取圖書列表為例進行例項演示,其中我們會啟動兩個服務提供者配置兩個埠:2088020881,然後指定路由規則為:呼叫方法queryAll訪問20880、呼叫方法queryByName訪問20881服務。專案結構圖如下:
圖片描述
這裡我們主要看服務提供端dubbo-provider-xml.xml配置內容:


beans xmlns:xsi=""
       xmlns:dubbo=""
       xmlns=""
       xsi:schemaLocation=" /spring-beans-4.3.xsd
        /dubbo.xsd">

    dubbo:protocol port="20880"/>
<!--分別使用 20881和20880配置啟動兩個服務--&gt
<!--    --&gt

    dubbo:application name="demo-provider" metadata-type="remote"/>

    dubbo:registry address="zookeeper://127.0.0.1:2181"/>

    bean id="bookFacade" class="com.muke.dubbocourse.tokenverify.provider.BookFacadeImpl"/>

    <!--暴露服務為Dubbo服務--&gt
    dubbo:service interface="com.muke.dubbocourse.common.api.BookFacade" ref="bookFacade" token="12345"/>

beans>

上面我們指定了服務提供者的埠,這裡請求小夥伴分別以2088020881啟動兩個服務。接下來我們看看在 Dubbo Admin 中配置的路由規則:

enabled: true
runtime: false
force: true
conditions:
 - 'method = queryAll => address=*:20880'
 - 'method = queryByName => address=*:20881'

如下圖所示:

圖片描述

**Tips:**這裡的Service Unique ID 配置規則為:介面許可權定名:版本:分組。

5. 實現原理

根據前面的介紹我們知道在消費端呼叫遠端服務時透過路由規則進行服務的過濾,那麼我們透過原始碼簡單的分析下這個處理過程。這裡我們直接看到路由規則的呼叫核心程式碼org.apache.dubbo.rpc.cluster.RouterChain#route核心方法如下:

    public ListInvokerT>> route(URL url, Invocation invocation) {
        ListInvokerT>> finalInvokers = invokers;
        for (Router router : routers) {
            finalInvokers = router.route(finalInvokers, url, invocation);
        }
        return finalInvokers;
    }

下面展示了我們執行過程中的路由規則:

其中ConditionRouter就是我們的條件路由核心程式碼如下:

    public T> ListInvokerT>> route(ListInvokerT>> invokers, URL url, Invocation invocation)
            throws RpcException {
        if (!enabled) {
            return invokers;
        }

        if (CollectionUtils.isEmpty(invokers)) {
            return invokers;
        }
        try {
            //匹配 method = queryAll => address=*:20880 表示中的 method = queryAll 部分
            if (!matchWhen(url, invocation)) {
                return invokers;
            }
            ListInvokerT>> result = new ArrayListInvokerT>>();
            if (thenCondition == null) {
                //..
                return result;
            }
            for (InvokerT> invoker : invokers) {
                //匹配 method = queryAll => address=*:20880 表示中的 address=*:20880 部分
                if (matchThen(invoker.getUrl(), url)) {
                    result.add(invoker);
                }
            }
            if (!result.isEmpty()) {
                return result;

                //配置force: true 表示如果透過路由規則後沒有服務條件的返回一個空集合,否則路由規則無效返回過濾器的 Invoker 遠端服務代理列表
            } else if (force) {
                //..
                return result;
            }
        } catch (Throwable t) {
            //...
        }
        return invokers;
    }

這裡有兩個最為重要的方法分別是:org.apache.dubbo.rpc.cluster.router.condition.ConditionRouter#matchWhenorg.apache.dubbo.rpc.cluster.router.condition.ConditionRouter#matchThenmatchWhen方法主要負責匹配前置條件例如:method = queryAll => address=*:20880 表示中的 method = queryAll 部分,matchThen方法主要負責匹配後置條件method = queryAll => address=*:20880 表示中的 address=*:20880 部分。matchWhen核心程式碼如下:

   boolean matchWhen(URL url, Invocation invocation) {
        //判斷whenCondition條件是否為空,並且執行matchCondition匹配表示式
        return CollectionUtils.isEmptyMap(whenCondition) || matchCondition(whenCondition, url, null, invocation);
    }

方法org.apache.dubbo.rpc.cluster.router.condition.ConditionRouter#matchCondition核心程式碼如下:

/**
     *
     * 匹配條件
     *
     * @author liyong
     * @date 11:36 PM 2020/11/28
     * @param condition
     * @param url
     * @param param
     * @param invocation
     * @exception
     * @return boolean
     **/
    private boolean matchCondition(MapString, MatchPair> condition, URL url, URL param, Invocation invocation) {
        MapString, String> sample = url.toMap();
        boolean result = false;
        //method = queryAll => address=*:20880
        for (Map.EntryString, MatchPair> matchPair : condition.entrySet()) {
            String key = matchPair.getKey();
            String sampleValue;
            //從invocation中獲取呼叫的真實方法名稱
            if (invocation != null && (METHOD_KEY.equals(key) || METHODS_KEY.equals(key))) {
                sampleValue = invocation.getMethodName();
                //判斷是否配置 address
            } else if (ADDRESS_KEY.equals(key)) {
                sampleValue = url.getAddress();
                //判斷是否配置 host
            } else if (HOST_KEY.equals(key)) {
                sampleValue = url.getHost();
            } else {
                //從URL轉換的map中獲取對應key的值
                sampleValue = sample.get(key);
                if (sampleValue == null) {
                    sampleValue = sample.get(key);
                }
            }
            if (sampleValue != null) {
                //匹配條件配置和真實呼叫引數值是否匹配
                if (!matchPair.getValue().isMatch(sampleValue, param)) {
                    return false;
                } else {
                    result = true;
                }
            } else {
                //沒有匹配的條件
                if (!matchPair.getValue().matches.isEmpty()) {
                    return false;
                } else {
                    result = true;
                }
            }
        }
        return result;
    }

上面的程式碼非常簡單小夥伴可以根據註釋去學習,其中Map condition結構如下:

從資料結構中我們可以看出我們配置的method = queryByName,我們這裡沒有配置host預設為*。接下來我們繼續看看matchThen方法核心程式碼如下:

    private boolean matchThen(URL url, URL param) {
         //判斷thenCondition條件是否為空,並且執行matchCondition匹配表示式
        return CollectionUtils.isNotEmptyMap(thenCondition) && matchCondition(thenCondition, url, param, null);
    }

這裡和上面的matchWhen方法都呼叫matchCondition,那我們看看thenCondition的資料結構:

圖片描述
是我們配置的規則後半部分address=*:20881。由此我們可以總結:假設我們的條件路由規則是method = queryByName => address=*:20881那麼先對服務呼叫方匹配method = queryByName前部分,如果滿足前面部分則繼續去匹配規則的後面部分address=*:20881,如果都匹配則 Invoker 代理物件將作為呼叫代理候選者。

6. 小結

在本小節中我們主要學習了 Dubbo 中路由規則之條件路由以及使用方式。同時也分析了條件路由實現的原理,其本質上是透過過濾器對服務提供者列表進行規則的匹配,如果匹配不上則過濾掉服務提供者。

本節課程的重點如下:

  1. 理解 Dubbo 路由規則和條件路由

  2. 瞭解了條件路由使用方式

  3. 瞭解條件路由實現原理

  4. 瞭解條件路由使用場景

作者

個人從事金融行業,就職過易極付、思建科技、某網約車平臺等重慶一流技術團隊,目前就職於某銀行負責統一支付系統建設。自身對金融行業有強烈的愛好。同時也實踐大資料、資料儲存、自動化整合和部署、分散式微服務、響應式程式設計、人工智慧等領域。同時也熱衷於技術分享創立公眾號和部落格站點對知識體系進行分享。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/1868/viewspace-2797603/,如需轉載,請註明出處,否則將追究法律責任。

相關文章