前言

在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。

為了解決上述問題,我們可以針對每個需求分別獨(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 降級的場景,比如找不到對應(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ò)地址集合,如下圖所示:

另外,在 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 接口,如下圖所示:

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 還從 AbstractRouterRule 抽象類繼承了一些控制字段,后面介紹的 ConditionRouterRule 也繼承了 AbstractRouterRule。

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)系如下圖所示:

ServiceRouterFactory 創(chuàng)建的 Router 實(shí)現(xiàn)是 ServiceRouter,與 ServiceRouter 類似的是 AppRouter,兩者都繼承了 ListenableRouter 抽象類(雖然 ListenableRouter 是個抽象類,但是沒有抽象方法留給子類實(shí)現(xiàn)),繼承關(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í)例級別的配置。