介紹
上篇文章介紹了 Directory
,再次看一下dubbo調(diào)用的處理流程:
本篇文章介紹調(diào)用的第二步, Router
的實(shí)現(xiàn)。
從圖中可以看到, Router
的實(shí)現(xiàn)有兩種: Script
和 Condition
,官網(wǎng)對其的描述為:
Router
負(fù)責(zé)從多個Invoker
中按路由規(guī)則選出子集,比如讀寫分離,應(yīng)用隔離等
也就是說,第一步通過 Directory
選出當(dāng)前可用的服務(wù)提供者,然后再通過 Router
按規(guī)則過濾出服務(wù)提供者的子集。
使用示例
我們先看一下具體的使用。還是啟動兩個 provider
,端口分別為20880和20881:
啟動 consumer
查看控制臺的輸出:
Hello world, response from provider: 192.168.199.203:20881
Hello world, response from provider: 192.168.199.203:20880
Hello world, response from provider: 192.168.199.203:20881
Hello world, response from provider: 192.168.199.203:20881
Hello world, response from provider: 192.168.199.203:20880
Hello world, response from provider: 192.168.199.203:20880
Hello world, response from provider: 192.168.199.203:20880
Hello world, response from provider: 192.168.199.203:20880
Hello world, response from provider: 192.168.199.203:20881
可以看到 consumer
的調(diào)用會被路由到這兩個服務(wù)提供者。
新建一個路由規(guī)則:
這里會切斷端口為20880的流量,使 consumer
的調(diào)用只能路由到20881上,保存之后在頁面點(diǎn)擊啟用:
這時再看下控制臺的輸出:
Hello world, response from provider: 192.168.199.203:20881
Hello world, response from provider: 192.168.199.203:20881
Hello world, response from provider: 192.168.199.203:20881
Hello world, response from provider: 192.168.199.203:20881
Hello world, response from provider: 192.168.199.203:20881
Hello world, response from provider: 192.168.199.203:20881
Hello world, response from provider: 192.168.199.203:20881
Hello world, response from provider: 192.168.199.203:20881
Hello world, response from provider: 192.168.199.203:20881
可見,路由的規(guī)則起作用了。
代碼分析
看下 Router
的類圖:
它的主要實(shí)現(xiàn)類有兩個,ConditionRouter
和 ScriptRouter
。在剛才的示例中使用了 ConditionRouter
。這里先分析一下 ConditionRouter
的實(shí)現(xiàn)。
分析一下,當(dāng)新建一個路由規(guī)則時,會在zookeeper中新建一個節(jié)點(diǎn):
根據(jù)上篇文章的分析,會調(diào)用 RegistryDirectory
中的 notify
方法進(jìn)行通知:
@Override
public synchronized void notify(List<URL> urls) {
List<URL> invokerUrls = new ArrayList<URL>();
List<URL> routerUrls = new ArrayList<URL>();
List<URL> configuratorUrls = new ArrayList<URL>();
for (URL url : urls) {
String protocol = url.getProtocol();
String category = url.getParameter(Constants.CATEGORY_KEY, Constants.DEFAULT_CATEGORY);
if (Constants.ROUTERS_CATEGORY.equals(category)
|| Constants.ROUTE_PROTOCOL.equals(protocol)) {
routerUrls.add(url);
} else if (Constants.CONFIGURATORS_CATEGORY.equals(category)
|| Constants.OVERRIDE_PROTOCOL.equals(protocol)) {
configuratorUrls.add(url);
} else if (Constants.PROVIDERS_CATEGORY.equals(category)) {
invokerUrls.add(url);
} else {
logger.warn("Unsupported category " + category + " in notified url: " + url + " from registry " + getUrl().getAddress() + " to consumer " + NetUtils.getLocalHost());
}
}
// configurators
if (configuratorUrls != null && !configuratorUrls.isEmpty()) {
this.configurators = toConfigurators(configuratorUrls);
}
// routers
if (routerUrls != null && !routerUrls.isEmpty()) {
List<Router> routers = toRouters(routerUrls);
if (routers != null) { // null - do nothing
setRouters(routers);
}
}
List<Configurator> localConfigurators = this.configurators; // local reference
// merge override parameters
this.overrideDirectoryUrl = directoryUrl;
if (localConfigurators != null && !localConfigurators.isEmpty()) {
for (Configurator configurator : localConfigurators) {
this.overrideDirectoryUrl = configurator.configure(overrideDirectoryUrl);
}
}
// providers
refreshInvoker(invokerUrls);
}
注意這里的 toRouters
方法,該方法通過傳入的url轉(zhuǎn)成 Router
對象:
private List<Router> toRouters(List<URL> urls) {
List<Router> routers = new ArrayList<Router>();
if (urls == null || urls.isEmpty()) {
return routers;
}
if (urls != null && !urls.isEmpty()) {
for (URL url : urls) {
if (Constants.EMPTY_PROTOCOL.equals(url.getProtocol())) {
continue;
}
String routerType = url.getParameter(Constants.ROUTER_KEY);
if (routerType != null && routerType.length() > 0) {
url = url.setProtocol(routerType);
}
try {
Router router = routerFactory.getRouter(url);
if (!routers.contains(router))
routers.add(router);
} catch (Throwable t) {
logger.error("convert router url to router error, url: " + url, t);
}
}
}
return routers;
}
看下url的值:
condition://0.0.0.0/com.alibaba.dubbo.demo.DemoService?category=routers&dynamic=false&enabled=true&force=false&name=切斷provider1的流量&priority=0&router=condition&rule=+%3D%3E+provider.port+%21%3D+20880&runtime=false
官網(wǎng)的介紹如下:
-
condition://
表示路由規(guī)則的類型,支持條件路由規(guī)則和腳本路由規(guī)則,可擴(kuò)展,必填。 -
0.0.0.0
表示對所有 IP 地址生效,如果只想對某個 IP 的生效,請?zhí)钊刖唧w IP,必填。 -
com.foo.BarService
表示只對指定服務(wù)生效,必填。 -
category=routers
表示該數(shù)據(jù)為動態(tài)配置類型,必填。 -
dynamic=false
表示該數(shù)據(jù)為持久數(shù)據(jù),當(dāng)注冊方退出時,數(shù)據(jù)依然保存在注冊中心,必填。 -
enabled=true
覆蓋規(guī)則是否生效,可不填,缺省生效。 -
force=false
當(dāng)路由結(jié)果為空時,是否強(qiáng)制執(zhí)行,如果不強(qiáng)制執(zhí)行,路由結(jié)果為空的路由規(guī)則將自動失效,可不填,缺省為 flase。 -
runtime=false
是否在每次調(diào)用時執(zhí)行路由規(guī)則,否則只在提供者地址列表變更時預(yù)先執(zhí)行并緩存結(jié)果,調(diào)用時直接從緩存中獲取路由結(jié)果。如果用了參數(shù)路由,必須設(shè)為 true,需要注意設(shè)置會影響調(diào)用的性能,可不填,缺省為 flase。 -
priority=1
路由規(guī)則的優(yōu)先級,用于排序,優(yōu)先級越大越靠前執(zhí)行,可不填,缺省為 0。 -
rule=URL.encode("host = 10.20.153.10 => host = 10.20.153.11")
表示路由規(guī)則的內(nèi)容,必填。
通過 routerFactory.getRouter(url);
會創(chuàng)建一個 Router
對象,這里的 routerFactory
就是 ConditionRouterFactory
類型, getRouter
方法會直接返回一個 ConditionRouter
對象:
public class ConditionRouterFactory implements RouterFactory {
public static final String NAME = "condition";
@Override
public Router getRouter(URL url) {
return new ConditionRouter(url);
}
}
查看一下 ConditionRouter
的構(gòu)造方法:
public ConditionRouter(URL url) {
this.url = url;
this.priority = url.getParameter(Constants.PRIORITY_KEY, 0);
this.force = url.getParameter(Constants.FORCE_KEY, false);
try {
String rule = url.getParameterAndDecoded(Constants.RULE_KEY);
if (rule == null || rule.trim().length() == 0) {
throw new IllegalArgumentException("Illegal route rule!");
}
rule = rule.replace("consumer.", "").replace("provider.", "");
int i = rule.indexOf("=>");
String whenRule = i < 0 ? null : rule.substring(0, i).trim();
String thenRule = i < 0 ? rule.trim() : rule.substring(i + 2).trim();
Map<String, MatchPair> when = StringUtils.isBlank(whenRule) || "true".equals(whenRule) ? new HashMap<String, MatchPair>() : parseRule(whenRule);
Map<String, MatchPair> then = StringUtils.isBlank(thenRule) || "false".equals(thenRule) ? null : parseRule(thenRule);
// NOTE: It should be determined on the business level whether the `When condition` can be empty or not.
this.whenCondition = when;
this.thenCondition = then;
} catch (ParseException e) {
throw new IllegalStateException(e.getMessage(), e);
}
}
具體的條件路由規(guī)則請參考官方文檔。構(gòu)造方法中解析了路由規(guī)則,分成兩個部分: whenCondition
和 thenCondition
,在 route
方法中可以看到具體的判斷:
@Override
public <T> List<Invoker<T>> route(List<Invoker<T>> invokers, URL url, Invocation invocation)
throws RpcException {
if (invokers == null || invokers.isEmpty()) {
return invokers;
}
try {
// 當(dāng)不滿足when條件時,返回
if (!matchWhen(url, invocation)) {
return invokers;
}
List<Invoker<T>> result = new ArrayList<Invoker<T>>();
if (thenCondition == null) {
logger.warn("The current consumer in the service blacklist. consumer: " + NetUtils.getLocalHost() + ", service: " + url.getServiceKey());
return result;
}
for (Invoker<T> invoker : invokers) {
// 滿足then條件時,添加到結(jié)果列表
if (matchThen(invoker.getUrl(), url)) {
result.add(invoker);
}
}
if (!result.isEmpty()) {
return result;
} else if (force) {
logger.warn("The route result is empty and force execute. consumer: " + NetUtils.getLocalHost() + ", service: " + url.getServiceKey() + ", router: " + url.getParameterAndDecoded(Constants.RULE_KEY));
return result;
}
} catch (Throwable t) {
logger.error("Failed to execute condition router rule: " + getUrl() + ", invokers: " + invokers + ", cause: " + t.getMessage(), t);
}
return invokers;
}
現(xiàn)在回到 RegistryDirectory
中的 notify
方法,當(dāng)?shù)玫搅寺酚闪斜砗?,會將路由列表設(shè)置到當(dāng)前的 Directory
中,通過調(diào)用 setRouters
方法進(jìn)行設(shè)置:
protected void setRouters(List<Router> routers) {
// copy list
routers = routers == null ? new ArrayList<Router>() : new ArrayList<Router>(routers);
// append url router
String routerkey = url.getParameter(Constants.ROUTER_KEY);
if (routerkey != null && routerkey.length() > 0) {
RouterFactory routerFactory = ExtensionLoader.getExtensionLoader(RouterFactory.class).getExtension(routerkey);
routers.add(routerFactory.getRouter(url));
}
// append mock invoker selector
routers.add(new MockInvokersSelector());
Collections.sort(routers);
this.routers = routers;
}
那么,具體是怎么調(diào)用 route
方法的呢?我們結(jié)合上篇對 Directory
的介紹來分析,回顧一下在 RegisterDirectory
中的 doList
方法執(zhí)行時會通過 methodInvokerMap
變量來獲取 invokers
,具體的調(diào)用如下:
那么,是什么時候設(shè)置 methodInvokerMap
的呢?這個是在注冊中心發(fā)生變更的時候,比如新增服務(wù)提供者或者新增路由規(guī)則,這時會觸發(fā) RegistryDirectory
的 notify
方法:
至此,整個路由服務(wù)的分析就結(jié)束了。結(jié)合上篇文章我們就可以知道, Directory
的功能了:
- 監(jiān)聽注冊中心節(jié)點(diǎn)的變化,如果有節(jié)點(diǎn)新增或者刪除,就會觸發(fā)
notify
方法來更新; - 當(dāng)獲取了當(dāng)前服務(wù)的
Invoker
列表后,如果配置了路由規(guī)則,則使用該路由規(guī)則過濾Invoker
列表。