Dubbo——路由機(jī)制(下)

前言

Dubbo——路由機(jī)制(上),介紹了 Router 接口的基本功能以及 RouterChain 加載多個 Router 的實(shí)現(xiàn),之后介紹了 ConditionRouter 這個類對條件路由規(guī)則的處理邏輯以及 ScriptRouter 這個類對腳本路由規(guī)則的處理邏輯。本文繼續(xù)介紹剩余的三個 Router 接口實(shí)現(xiàn)類。

FileRouterFactory

FileRouterFactory 是 ScriptRouterFactory 的裝飾器,其擴(kuò)展名為 file,F(xiàn)ileRouterFactory 在 ScriptRouterFactory 基礎(chǔ)上增加了讀取文件的能力。可以將 ScriptRouter 使用的路由規(guī)則保存到文件中,然后在 URL 中指定文件路徑,F(xiàn)ileRouterFactory 從中解析到該腳本文件的路徑并進(jìn)行讀取,調(diào)用 ScriptRouterFactory 去創(chuàng)建相應(yīng)的 ScriptRouter 對象。

下面來看 FileRouterFactory 對 getRouter() 方法的具體實(shí)現(xiàn),其中完成了 file 協(xié)議的 URL 到 script 協(xié)議 URL 的轉(zhuǎn)換,如下是一個轉(zhuǎn)換示例,首先會將 file:// 協(xié)議轉(zhuǎn)換成 script:// 協(xié)議,然后會添加 type 參數(shù)和 rule 參數(shù),其中 type 參數(shù)值根據(jù)文件后綴名確定,該示例為 js,rule 參數(shù)值為文件內(nèi)容。

可以再結(jié)合接下來這個示例分析 getRouter() 方法的具體實(shí)現(xiàn):

public class FileRouterFactory implements RouterFactory {

    public static final String NAME = "file";

    private RouterFactory routerFactory;

    public void setRouterFactory(RouterFactory routerFactory) {
        this.routerFactory = routerFactory;
    }

    @Override
    public Router getRouter(URL url) {
        try {
            // Transform File URL into Script Route URL, and Load
            // file:///d:/path/to/route.js?router=script ==> script:///d:/path/to/route.js?type=js&rule=<file-content>
            // 默認(rèn)使用script協(xié)議
            String protocol = url.getParameter(ROUTER_KEY, ScriptRouterFactory.NAME); // Replace original protocol (maybe 'file') with 'script'
            String type = null; // Use file suffix to config script type, e.g., js, groovy ...
            String path = url.getPath();
            // 獲取腳本文件的語言類型
            if (path != null) {
                int i = path.lastIndexOf('.');
                if (i > 0) {
                    type = path.substring(i + 1);
                }
            }
            
            // 讀取腳本文件中的內(nèi)容
            String rule = IOUtils.read(new FileReader(new File(url.getAbsolutePath())));

            // FIXME: this code looks useless
            boolean runtime = url.getParameter(RUNTIME_KEY, false);
            // 創(chuàng)建script協(xié)議的URL
            URL script = URLBuilder.from(url)
                    .setProtocol(protocol)
                    .addParameter(TYPE_KEY, type)
                    .addParameter(RUNTIME_KEY, runtime)
                    .addParameterAndEncoded(RULE_KEY, rule)
                    .build();
            // 獲取script對應(yīng)的Router實(shí)現(xiàn)
            return routerFactory.getRouter(script);
        } catch (IOException e) {
            throw new IllegalStateException(e.getMessage(), e);
        }
    }
}

TagRouterFactory & TagRouter

TagRouterFactory 作為 RouterFactory 接口的擴(kuò)展實(shí)現(xiàn),其擴(kuò)展名為 tag。但是需要注意的是,TagRouterFactory 與之前介紹的 ConditionRouterFactory、ScriptRouterFactory 的不同之處在于,它是通過繼承 CacheableRouterFactory 這個抽象類,間接實(shí)現(xiàn)了 RouterFactory 接口。

CacheableRouterFactory 抽象類中維護(hù)了一個 ConcurrentMap 集合(routerMap 字段)用來緩存 Router,其中的 Key 是 ServiceKey。在 CacheableRouterFactory 的 getRouter() 方法中,會優(yōu)先根據(jù) URL 的 ServiceKey 查詢 routerMap 集合,查詢失敗之后會調(diào)用 createRouter() 抽象方法來創(chuàng)建相應(yīng)的 Router 對象。在 TagRouterFactory.createRouter() 方法中,創(chuàng)建的自然就是 TagRouter 對象了。

基于 Tag 的測試環(huán)境隔離方案

通過 TagRouter,可以將某一個或多個 Provider 劃分到同一分組,約束流量只在指定分組中流轉(zhuǎn),這樣就可以輕松達(dá)到流量隔離的目的,從而支持灰度發(fā)布等場景。

目前,Dubbo 提供了動態(tài)和靜態(tài)兩種方式給 Provider 打標(biāo)簽,其中動態(tài)方式就是通過服務(wù)治理平臺動態(tài)下發(fā)標(biāo)簽,靜態(tài)方式就是在 XML 等靜態(tài)配置中打標(biāo)簽。Consumer 端可以在 RpcContext 的 attachment 中添加 request.tag 附加屬性,注意保存在 attachment 中的值將會在一次完整的遠(yuǎn)程調(diào)用中持續(xù)傳遞,我們只需要在起始調(diào)用時進(jìn)行設(shè)置,就可以達(dá)到標(biāo)簽的持續(xù)傳遞。

了解了 Tag 的基本概念和功能之后,再簡單介紹一個 Tag 的使用示例。

在實(shí)際的開發(fā)測試中,一個完整的請求會涉及非常多的 Provider,分屬不同團(tuán)隊進(jìn)行維護(hù),這些團(tuán)隊每天都會處理不同的需求,并在其負(fù)責(zé)的 Provider 服務(wù)中進(jìn)行修改,如果所有團(tuán)隊都使用一套測試環(huán)境,那么測試環(huán)境就會變得很不穩(wěn)定。如下圖所示,4 個 Provider 分屬不同的團(tuán)隊管理,Provider 2 和 Provider 4 在測試環(huán)境測試,部署了有 Bug 的版本,這樣就會導(dǎo)致整個測試環(huán)境無法正常處理請求,在這樣一個不穩(wěn)定的測試環(huán)境中排查 Bug 是非常困難的,因為可能排查到最后,發(fā)現(xiàn)是別人的 Bug。

不同狀態(tài)的 Provider 節(jié)點(diǎn)

為了解決上述問題,我們可以針對每個需求分別獨(dú)立出一套測試環(huán)境,但是這個方案會占用大量機(jī)器,前期的搭建成本以及后續(xù)的維護(hù)成本也都非常高。

下面是一個通過 Tag 方式實(shí)現(xiàn)環(huán)境隔離的架構(gòu)圖,其中,需求 1 對 Provider 2 的請求會全部落到有需求 1 標(biāo)簽的 Provider 上,其他 Provider 使用穩(wěn)定測試環(huán)境中的 Provider;需求 2 對 Provider 4 的請求會全部落到有需求 2 標(biāo)簽的 Provider 4 上,其他 Provider 使用穩(wěn)定測試環(huán)境中的 Provider。

依賴 Tag 實(shí)現(xiàn)的測試環(huán)境隔離方案

在一些特殊場景中,會有 Tag 降級的場景,比如找不到對應(yīng) Tag 的 Provider,會按照一定的規(guī)則進(jìn)行降級。如果在 Provider 集群中不存在與請求 Tag 對應(yīng)的 Provider 節(jié)點(diǎn),則默認(rèn)將降級請求 Tag 為空的 Provider;如果希望在找不到匹配 Tag 的 Provider 節(jié)點(diǎn)時拋出異常的話,我們需設(shè)置 request.tag.force = true。

如果請求中的 request.tag 未設(shè)置,只會匹配 Tag 為空的 Provider,也就是說即使集群中存在可用的服務(wù),若 Tag 不匹配也就無法調(diào)用。一句話總結(jié),攜帶 Tag 的請求可以降級訪問到無 Tag 的 Provider,但不攜帶 Tag 的請求永遠(yuǎn)無法訪問到帶有 Tag 的 Provider。

TagRouter

下面再來看 TagRouter 的具體實(shí)現(xiàn)。在 TagRouter 中持有一個 TagRouterRule 對象的引用,在 TagRouterRule 中維護(hù)了一個 Tag 集合,而在每個 Tag 對象中又都維護(hù)了一個 Tag 的名稱,以及 Tag 綁定的網(wǎng)絡(luò)地址集合,如下圖所示:

TagRouter、TagRouterRule、Tag 與 address 映射關(guān)系圖

另外,在 TagRouterRule 中還維護(hù)了 addressToTagnames、tagnameToAddresses 兩個集合(都是 Map<String, List<String>> 類型),分別記錄了 Tag 名稱到各個 address 的映射以及 address 到 Tag 名稱的映射。在 TagRouterRule 的 init() 方法中,會根據(jù) tags 集合初始化這兩個集合。

了解了 TagRouterRule 的基本構(gòu)造之后,我們繼續(xù)來看 TagRouter 構(gòu)造 TagRouterRule 的過程。TagRouter 除了實(shí)現(xiàn)了 Router 接口之外,還實(shí)現(xiàn)了 ConfigurationListener 接口,如下圖所示:


TagRouter 繼承關(guān)系圖

ConfigurationListener 用于監(jiān)聽配置的變化,其中就包括 TagRouterRule 配置的變更。當(dāng)我們通過動態(tài)更新 TagRouterRule 配置的時候,就會觸發(fā) ConfigurationListener 接口的 process() 方法,TagRouter 對 process() 方法的實(shí)現(xiàn)如下:

public class TagRouter extends AbstractRouter implements ConfigurationListener {

    @Override
    public synchronized void process(ConfigChangedEvent event) {
        if (logger.isDebugEnabled()) {
            logger.debug("Notification of tag rule, change type is: " + event.getChangeType() + ", raw rule is:\n " +
                    event.getContent());
        }
        
        try {
            if (event.getChangeType().equals(ConfigChangeType.DELETED)) {
                // DELETED事件會直接清空tagRouterRule
                this.tagRouterRule = null;
            } else {
                // 其他事件會解析最新的路由規(guī)則,并記錄到tagRouterRule字段中
                this.tagRouterRule = TagRuleParser.parse(event.getContent());
            }
        } catch (Exception e) {
            logger.error("Failed to parse the raw tag router rule and it will not take effect, please check if the " +
                    "rule matches with the template, the raw rule is:\n ", e);
        }
    }
}

我們可以看到,如果是刪除配置的操作,則直接將 tagRouterRule 設(shè)置為 null,如果是修改或新增配置,則通過 TagRuleParser 解析傳入的配置,得到對應(yīng)的 TagRouterRule 對象。TagRuleParser 可以解析 yaml 格式的 TagRouterRule 配置,下面是一個配置示例:

force: false
runtime: true
enabled: false
priority: 1
key: demo-provider
tags:
  - name: tag1
    addresses: null
  - name: tag2
    addresses: ["30.5.120.37:20880"]
  - name: tag3
    addresses: []

經(jīng)過 TagRuleParser 解析得到的 TagRouterRule 結(jié)構(gòu),如下所示:


TagRouterRule 結(jié)構(gòu)圖

除了上圖展示的幾個集合字段,TagRouterRule 還從 AbstractRouterRule 抽象類繼承了一些控制字段,后面介紹的 ConditionRouterRule 也繼承了 AbstractRouterRule。

AbstractRouterRule繼承關(guān)系圖

AbstractRouterRule 中核心字段的具體含義大致可總結(jié)為如下:

  • key(string 類型)、scope(string 類型):key 明確規(guī)則體作用在哪個服務(wù)或應(yīng)用。scope 為 service 時,key 由 [{group}:]{service}[:{version}] 構(gòu)成;scope 為 application 時,key 為 application 的名稱。

  • rawRule(string 類型):記錄了路由規(guī)則解析前的原始字符串配置。

  • runtime(boolean 類型):表示是否在每次調(diào)用時執(zhí)行該路由規(guī)則。如果設(shè)置為 false,則會在 Provider 列表變更時預(yù)先執(zhí)行并緩存結(jié)果,調(diào)用時直接從緩存中獲取路由結(jié)果。

  • force(boolean 類型):當(dāng)路由結(jié)果為空時,是否強(qiáng)制執(zhí)行,如果不強(qiáng)制執(zhí)行,路由結(jié)果為空的路由規(guī)則將自動失效。該字段默認(rèn)值為 false。

  • valid(boolean 類型):用于標(biāo)識解析生成當(dāng)前 RouterRule 對象的配置是否合法。

  • enabled(boolean 類型):標(biāo)識當(dāng)前路由規(guī)則是否生效。

  • priority(int 類型):用于表示當(dāng)前 RouterRule 的優(yōu)先級。

  • dynamic(boolean 類型):表示該路由規(guī)則是否為持久數(shù)據(jù),當(dāng)注冊方退出時,路由規(guī)則是否依然存在。

我們可以看到,AbstractRouterRule 中的核心字段與前面的示例配置是一一對應(yīng)的。

我們知道,Router 最終目的是要過濾符合條件的 Invoker 對象,下面我們一起來看 TagRouter 是如何使用 TagRouterRule 路由邏輯進(jìn)行 Invoker 過濾的,大致步驟如下:

  • 1、如果 invokers 為空,直接返回空集合。

  • 2、檢查關(guān)聯(lián)的 tagRouterRule 對象是否可用,如果不可用,則會直接調(diào)用 filterUsingStaticTag() 方法進(jìn)行過濾,并返回過濾結(jié)果。在 filterUsingStaticTag() 方法中,會比較請求攜帶的 tag 值與 Provider URL 中的 tag 參數(shù)值。

  • 3、獲取此次調(diào)用的 tag 信息,這里會嘗試從 Invocation 以及 URL 的參數(shù)中獲取。

  • 4、如果此次請求指定了 tag 信息,則首先會獲取 tag 關(guān)聯(lián)的 address 集合。

    • 如果 address 集合不為空,則根據(jù)該 address 集合中的地址,匹配出符合條件的 Invoker 集合。如果存在符合條件的 Invoker,則直接將過濾得到的 Invoker 集合返回;如果不存在,就會根據(jù) force 配置決定是否返回空 Invoker 集合。

    • 如果 address 集合為空,則會將請求攜帶的 tag 值與 Provider URL 中的 tag 參數(shù)值進(jìn)行比較,匹配出符合條件的 Invoker 集合。如果存在符合條件的 Invoker,則直接將過濾得到的 Invoker 集合返回;如果不存在,就會根據(jù) force 配置決定是否返回空 Invoker 集合。

    • 如果 force 配置為 false,且符合條件的 Invoker 集合為空,則返回所有不包含任何 tag 的 Provider 列表。

  • 5、如果此次請求未攜帶 tag 信息,則會先獲取 TagRouterRule 規(guī)則中全部 tag 關(guān)聯(lián)的 address 集合。如果 address 集合不為空,則過濾出不在 address 集合中的 Invoker 并添加到結(jié)果集合中,最后,將 Provider URL 中的 tag 值與 TagRouterRule 中的 tag 名稱進(jìn)行比較,得到最終的 Invoker 集合。

上述流程的具體實(shí)現(xiàn)是在 TagRouter.route() 方法中,如下所示:

public class TagRouter extends AbstractRouter implements ConfigurationListener {

    @Override
    public <T> List<Invoker<T>> route(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException {
        if (CollectionUtils.isEmpty(invokers)) {
            // 如果invokers為空,直接返回空集合
            return invokers;
        }

        // since the rule can be changed by config center, we should copy one to use.
        final TagRouterRule tagRouterRuleCopy = tagRouterRule;
        if (tagRouterRuleCopy == null || !tagRouterRuleCopy.isValid() || !tagRouterRuleCopy.isEnabled()) {
            return filterUsingStaticTag(invokers, url, invocation);
        }
        // 檢查關(guān)聯(lián)的tagRouterRule對象是否可用,如果不可用,則會直接調(diào)用filterUsingStaticTag() 方法進(jìn)行過濾
        List<Invoker<T>> result = invokers;
        // 獲取此次調(diào)用的tag信息,嘗試從Invocation以及URL中獲取
        String tag = StringUtils.isEmpty(invocation.getAttachment(TAG_KEY)) ? url.getParameter(TAG_KEY) :
                invocation.getAttachment(TAG_KEY);

        // if we are requesting for a Provider with a specific tag
        if (StringUtils.isNotEmpty(tag)) {// 此次請求一個特殊的tag
            // 獲取tag關(guān)聯(lián)的address集合
            List<String> addresses = tagRouterRuleCopy.getTagnameToAddresses().get(tag);
            // filter by dynamic tag group first
            if (CollectionUtils.isNotEmpty(addresses)) {
                // 根據(jù)上面的address集合匹配符合條件的Invoker
                result = filterInvoker(invokers, invoker -> addressMatches(invoker.getUrl(), addresses));
                // if result is not null OR it's null but force=true, return result directly
                
                // 如果存在符合條件的Invoker,則直接將過濾得到的Invoker集合返回
                // 如果不存在符合條件的Invoker,根據(jù)force配置決定是否返回空Invoker集合
                if (CollectionUtils.isNotEmpty(result) || tagRouterRuleCopy.isForce()) {
                    return result;
                }
            } else {
                // dynamic tag group doesn't have any item about the requested app OR it's null after filtered by
                // dynamic tag group but force=false. check static tag
                
                // 如果 address 集合為空,則會將請求攜帶的 tag 與 Provider URL 中的 tag 參數(shù)值進(jìn)行比較,匹配出符合條件的 Invoker 集合。
                result = filterInvoker(invokers, invoker -> tag.equals(invoker.getUrl().getParameter(TAG_KEY)));
            }
            // If there's no tagged providers that can match the current tagged request. force.tag is set by default
            // to false, which means it will invoke any providers without a tag unless it's explicitly disallowed.
            if (CollectionUtils.isNotEmpty(result) || isForceUseTag(invocation)) {
                // 存在符合條件的Invoker或是force配置為true
                return result;
            }
            // FAILOVER: return all Providers without any tags.
            else {
            // 如果 force 配置為 false,且符合條件的 Invoker 集合為空,則返回所有不包含任何 tag 的 Provider 列表。
                List<Invoker<T>> tmp = filterInvoker(invokers, invoker -> addressNotMatches(invoker.getUrl(),
                        tagRouterRuleCopy.getAddresses()));
                return filterInvoker(tmp, invoker -> StringUtils.isEmpty(invoker.getUrl().getParameter(TAG_KEY)));
            }
        } else {
            // List<String> addresses = tagRouterRule.filter(providerApp);
            // return all addresses in dynamic tag group.
            // 如果此次請求未攜帶 tag 信息,則會先獲取 TagRouterRule 規(guī)則中全部 tag 關(guān)聯(lián)的 address 集合。
            List<String> addresses = tagRouterRuleCopy.getAddresses();
            if (CollectionUtils.isNotEmpty(addresses)) {
                // 如果 address 集合不為空,則過濾出不在 address 集合中的 Invoker 并添加到結(jié)果集合中。
                result = filterInvoker(invokers, invoker -> addressNotMatches(invoker.getUrl(), addresses));
                // 1. all addresses are in dynamic tag group, return empty list.
                if (CollectionUtils.isEmpty(result)) {
                    return result;
                }
                // 2. if there are some addresses that are not in any dynamic tag group, continue to filter using the
                // static tag group.
            }
            // 如果不存在符合條件的 Invoker 或是 address 集合為空,則會將請求攜帶的 tag 與 Provider URL 中的 tag 參數(shù)值進(jìn)行比較,得到最終的 Invoker 集合。
            return filterInvoker(result, invoker -> {
                String localTag = invoker.getUrl().getParameter(TAG_KEY);
                return StringUtils.isEmpty(localTag) || !tagRouterRuleCopy.getTagNames().contains(localTag);
            });
        }
    }
}

ServiceRouter & AppRouter

除了之前介紹的 TagRouterFactory 繼承了 CacheableRouterFactory 之外,ServiceRouterFactory 也繼承 CachabelRouterFactory,具有了緩存的能力,具體繼承關(guān)系如下圖所示:


CacheableRouterFactory 繼承關(guān)系圖

ServiceRouterFactory 創(chuàng)建的 Router 實(shí)現(xiàn)是 ServiceRouter,與 ServiceRouter 類似的是 AppRouter,兩者都繼承了 ListenableRouter 抽象類(雖然 ListenableRouter 是個抽象類,但是沒有抽象方法留給子類實(shí)現(xiàn)),繼承關(guān)系如下圖所示:


ListenableRouter 繼承關(guān)系圖

ListenableRouter 在 ConditionRouter 基礎(chǔ)上添加了動態(tài)配置的能力,ListenableRouter 的 process() 方法與 TagRouter 中的 process() 方法類似,對于 ConfigChangedEvent.DELETE 事件,直接清空 ListenableRouter 中維護(hù)的 ConditionRouterRule 和 ConditionRouter 集合的引用;對于 ADDED、UPDATED 事件,則通過 ConditionRuleParser 解析事件內(nèi)容,得到相應(yīng)的 ConditionRouterRule 對象和 ConditionRouter 集合。這里的 ConditionRuleParser 同樣是以 yaml 文件的格式解析 ConditionRouterRule 的相關(guān)配置。ConditionRouterRule 中維護(hù)了一個 conditions 集合(List<String> 類型),記錄了多個 Condition 路由規(guī)則,對應(yīng)生成多個 ConditionRouter 對象。

整個解析 ConditionRouterRule 的過程,與前文介紹的解析 TagRouterRule 的流程類似。

在 ListenableRouter 的 route() 方法中,會遍歷全部 ConditionRouter 過濾出符合全部路由條件的 Invoker 集合,具體實(shí)現(xiàn)如下:

public abstract class ListenableRouter extends AbstractRouter implements ConfigurationListener {

    @Override
    public <T> List<Invoker<T>> route(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException {
        if (CollectionUtils.isEmpty(invokers) || conditionRouters.size() == 0) {
            // 檢查邊界條件,直接返回invokers集合
            return invokers;
        }

        // We will check enabled status inside each router.
        // 路由規(guī)則進(jìn)行過濾
        for (Router router : conditionRouters) {
            invokers = router.route(invokers, url, invocation);
        }

        return invokers;
    }
}

ServiceRouter 和 AppRouter 都是簡單地繼承了 ListenableRouter 抽象類,且沒有覆蓋 ListenableRouter 的任何方法,兩者只有以下兩點(diǎn)區(qū)別。

  • 一個是 priority 字段值不同。ServiceRouter 為 140,AppRouter 為 150,也就是說 ServiceRouter 要先于 AppRouter 執(zhí)行。

  • 另一個是獲取 ConditionRouterRule 配置的 Key 不同。ServiceRouter 使用的 RuleKey 是由 {interface}:[version]:[group] 三部分構(gòu)成,獲取的是一個服務(wù)對應(yīng)的 ConditionRouterRule。AppRouter 使用的 RuleKey 是 URL 中的 application 參數(shù)值,獲取的是一個服務(wù)實(shí)例對應(yīng)的 ConditionRouterRule。

總結(jié)

本文我們是緊接Dubbo——路由機(jī)制(上)的內(nèi)容,繼續(xù)介紹了剩余 Router 接口實(shí)現(xiàn)的內(nèi)容。

我們介紹了基于文件的 FileRouter 實(shí)現(xiàn),其底層會依賴之前介紹的 ScriptRouter;接下來又講解了基于 Tag 的測試環(huán)境隔離方案,以及如何基于 TagRouter 實(shí)現(xiàn)該方案,同時深入分析了 TagRouter 的核心實(shí)現(xiàn);最后我們還介紹了 ListenableRouter 抽象類以及 ServerRouter 和 AppRouter 兩個實(shí)現(xiàn),它們是在條件路由的基礎(chǔ)上添加了動態(tài)變更路由規(guī)則的能力,同時區(qū)分了服務(wù)級別和服務(wù)實(shí)例級別的配置。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容