定義&應(yīng)用場(chǎng)景
標(biāo)簽路由通過(guò)將某一個(gè)或多個(gè)服務(wù)的提供者劃分到同一個(gè)分組恒序,約束流量只在指定分組中流轉(zhuǎn),從而實(shí)現(xiàn)流量隔離的目的,可以作為藍(lán)綠發(fā)布、灰度發(fā)布等場(chǎng)景的能力基礎(chǔ)伺糠。
下圖是標(biāo)簽路由的一個(gè)典型場(chǎng)景蓄喇,應(yīng)用A和應(yīng)用B都要訪問(wèn)同一組dubbo服務(wù)骗污,應(yīng)用A請(qǐng)求dubbo接口時(shí)傳遞TagA肌括,請(qǐng)求就會(huì)被路由到服務(wù)中紅色框的providerA、providerB泛范,因?yàn)閜roviderA让虐、providerB被打上了TagA;同理罢荡,應(yīng)用B因?yàn)閿y帶了TagB赡突,請(qǐng)求會(huì)被路由到providerC、providerD区赵,因?yàn)楹笳弑淮蛏狭薚agB惭缰。
降級(jí)約定:
1、consumer攜帶request.tag=tag1 時(shí)優(yōu)先選擇 標(biāo)記了tag=tag1 的 provider笼才。若集群中不存在與請(qǐng)求標(biāo)記對(duì)應(yīng)的服務(wù)漱受,默認(rèn)將降級(jí)請(qǐng)求 tag為空的provider;如果要改變這種默認(rèn)行為患整,即找不到匹配tag1的provider返回異常拜效,需設(shè)置request.tag.force=true。
2各谚、comsumer側(cè)request.tag未設(shè)置時(shí),只會(huì)匹配tag為空的provider到千。即使集群中存在可用的服務(wù)昌渤,若tag不匹配也就無(wú)法調(diào)用,這與約定1不同憔四,攜帶標(biāo)簽的請(qǐng)求可以降級(jí)訪問(wèn)到無(wú)標(biāo)簽的服務(wù)膀息,但不攜帶標(biāo)簽/攜帶其他種類標(biāo)簽的請(qǐng)求永遠(yuǎn)無(wú)法訪問(wèn)到其他標(biāo)簽的服務(wù)。
標(biāo)簽路由使用方式
到處我們基本清楚了什么是標(biāo)簽路由了赵,標(biāo)簽路由的作用是什么潜支。那下面我們來(lái)看下怎么使用標(biāo)簽路由。
provider端標(biāo)簽設(shè)置
靜態(tài)標(biāo)簽
靜態(tài)標(biāo)簽配置也有三種方式 :
1柿汛、dubbo xml配置文件中添加如下配置指定標(biāo)簽
<dubbo:provider tag="tag1"/>
或者
<dubbo:service tag="tag1"/>
2冗酿、通過(guò)注解指定標(biāo)簽
@org.apache.dubbo.config.annotation.Service(tag = "tag1")
3、通過(guò)jvm參數(shù)或os環(huán)境變量指定。如下:
java -jar xxx-provider.jar -Ddubbo.provider.tag=tag1
動(dòng)態(tài)標(biāo)簽
在系統(tǒng)后臺(tái)下發(fā)如下的yaml配置:dubbo-tag-route-demo應(yīng)用增加了兩個(gè)標(biāo)簽組裁替,tag1包含一個(gè)實(shí)例127.0.0.1:20880项玛,tag2包含一個(gè)實(shí)例127.0.0.1:20881
force: false
runtime: true
enabled: true
key: dubbo-tag-route-demo
tags:
- name: tag1
addresses: ["127.0.0.1:20880"]
- name: tag2
addresses: ["127.0.0.1:20881"]
consumer傳遞標(biāo)簽
靜態(tài)標(biāo)簽
consumer也可以通過(guò)dubbo xml配置指定標(biāo)簽:
<dubbo:reference id="xxxxxxApi" interface="com.XXXXXApi" tag="tag1"/>
動(dòng)態(tài)標(biāo)簽
在發(fā)起調(diào)用dubbo前,通過(guò)RpcContext.getContext().setAttachment(Constants.TAG_KEY,"TAG_A")攜帶標(biāo)簽弱判。請(qǐng)求標(biāo)簽的作用域?yàn)槊恳淮?invocation襟沮,使用 attachment 來(lái)傳遞請(qǐng)求標(biāo)簽,注意保存在 attachment 中的值將會(huì)在一次完整的遠(yuǎn)程調(diào)用中持續(xù)傳遞昌腰,得益于這樣的特性开伏,我們只需要在起始調(diào)用時(shí),通過(guò)一行代碼的設(shè)置遭商,達(dá)到標(biāo)簽的持續(xù)傳遞固灵。
關(guān)于標(biāo)簽傳遞:因?yàn)镃onsumerContextFilter在每次請(qǐng)求之后會(huì)清空Attachments,所以dubbo調(diào)用A攜帶的tagA株婴,要傳遞到下次dubbo調(diào)用B怎虫,需要應(yīng)用自己來(lái)做。具體的解法可以將標(biāo)簽放到theadlocal中困介,這樣dubbo調(diào)用B可以從threadlocal中獲取到tagA大审,然后傳遞下去。
還有一個(gè)場(chǎng)景也是需要注意的 座哩,如果dubbo調(diào)用是在一個(gè)單獨(dú)的線程或線程池中發(fā)起的 徒扶,標(biāo)簽的傳遞也要解決跨線程傳遞的問(wèn)題,無(wú)侵入跨線程傳遞參數(shù)的解決方案有阿里開源的transmittable-thread-local根穷。
標(biāo)簽路由原理
上面已經(jīng)清楚的標(biāo)簽路由的使用方式和應(yīng)用場(chǎng)景姜骡,那dubbo的標(biāo)簽路由具體是怎么實(shí)現(xiàn)的呢?
我們先來(lái)看看整體流程屿良。從下圖知圈澈,在發(fā)起dubbo調(diào)用之后,最終會(huì)走到RouterChain來(lái)做服務(wù)調(diào)用路由尘惧。如果TagRouter在RouterChain里康栈,則會(huì)由TagRouter來(lái)做標(biāo)簽路由的邏輯。
從圖中可以看到具體的路由邏輯在TagRouter喷橙,而TagRouter使用的路由規(guī)則來(lái)自配置中心的啥么。在動(dòng)態(tài)標(biāo)簽的場(chǎng)景,其他系統(tǒng)將標(biāo)簽路由規(guī)則寫到配置中心贰逾,配置中心通過(guò)event將路由規(guī)則通知給TagRouter悬荣。
下面結(jié)合代碼來(lái)看下,TagRouter是如何做路由的疙剑。
從下面代碼看到氯迂,在沒(méi)有動(dòng)態(tài)標(biāo)簽路由規(guī)則時(shí)践叠,會(huì)根據(jù)靜態(tài)標(biāo)簽路由規(guī)則來(lái)路由,路由的邏輯就是按照上面提到的降級(jí)約定來(lái)做的囚戚。
存在動(dòng)態(tài)標(biāo)簽路由規(guī)則時(shí)酵熙,會(huì)先按照動(dòng)態(tài)標(biāo)簽路由規(guī)則來(lái)路由,如果動(dòng)態(tài)路由規(guī)則路由到的adress為空驰坊,則會(huì)嘗試使用靜態(tài)標(biāo)簽路由規(guī)則來(lái)路由一次匾二。
如果請(qǐng)求沒(méi)有攜帶標(biāo)簽,則只能路由到?jīng)]有打標(biāo)簽的adress拳芙,不能降級(jí)到打了標(biāo)簽的adress察藐,同降級(jí)約定一致。
public <T> List<Invoker<T>> route(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException {
if (CollectionUtils.isEmpty(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);
}
List<Invoker<T>> result = invokers;
String tag = StringUtils.isEmpty(invocation.getAttachment(Constants.TAG_KEY)) ? url.getParameter(Constants.TAG_KEY) :
invocation.getAttachment(Constants.TAG_KEY);
// if we are requesting for a Provider with a specific tag
if (StringUtils.isNotEmpty(tag)) {
List<String> addresses = tagRouterRuleCopy.getTagnameToAddresses().get(tag);
// filter by dynamic tag group first
if (CollectionUtils.isNotEmpty(addresses)) {
result = filterInvoker(invokers, invoker -> addressMatches(invoker.getUrl(), addresses));
// if result is not null OR it's null but force=true, return result directly
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
result = filterInvoker(invokers, invoker -> tag.equals(invoker.getUrl().getParameter(Constants.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)) {
return result;
}
// FAILOVER: return all Providers without any tags.
else {
List<Invoker<T>> tmp = filterInvoker(invokers, invoker -> addressNotMatches(invoker.getUrl(),
tagRouterRuleCopy.getAddresses()));
return filterInvoker(tmp, invoker -> StringUtils.isEmpty(invoker.getUrl().getParameter(Constants.TAG_KEY)));
}
} else {
// List<String> addresses = tagRouterRule.filter(providerApp);
// return all addresses in dynamic tag group.
List<String> addresses = tagRouterRuleCopy.getAddresses();
if (CollectionUtils.isNotEmpty(addresses)) {
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.
}
return filterInvoker(result, invoker -> {
String localTag = invoker.getUrl().getParameter(Constants.TAG_KEY);
return StringUtils.isEmpty(localTag) || !tagRouterRuleCopy.getTagNames().contains(localTag);
});
}