Dubbo原始碼分析十一、服務路由
條件路由規則由兩個條件組成,分別用於對服務消費者和提供者進行匹配。比如有這樣一條規則:
host = 10.20.153.10 => host = 10.20.153.11
該條規則表示 IP 為 10.20.153.10 的服務消費者只可呼叫 IP 為 10.20.153.11 機器上的服務,不可呼叫其他機器上的服務。條件路由規則的格式如下:
[服務消費者匹配條件] => [服務提供者匹配條件]
如果服務消費者匹配條件為空,表示不對服務消費者進行限制。如果服務提供者匹配條件為空,表示對某些服務消費者禁用服務。
初始化
首先看一下路由是如何初始化的。
當發起服務目錄的notify通知時,會根據通知的url列表篩選出有路由規則的,然後新增到路由列表中:
List<URL> routerURLs = categoryUrls.getOrDefault(ROUTERS_CATEGORY, Collections.emptyList());
// 這裡設定routers
toRouters(routerURLs).ifPresent(this::addRouters);
我們看一下url轉Router的方法:
private Optional<List<Router>> toRouters(List<URL> urls) {
if (urls == null || urls.isEmpty()) {
return Optional.empty();
}
List<Router> routers = new ArrayList<>();
for (URL url : urls) {
if (EMPTY_PROTOCOL.equals(url.getProtocol())) {
continue;
}
String routerType = url.getParameter(ROUTER_KEY);
if (routerType != null && routerType.length() > 0) {
url = url.setProtocol(routerType);
}
try {
Router router = ROUTER_FACTORY.getRouter(url);
if (!routers.contains(router)) {
routers.add(router);
}
} catch (Throwable t) {
logger.error("convert router url to router error, url: " + url, t);
}
}
return Optional.of(routers);
}
通過ROUTER_FACTORY建立:
private static final RouterFactory ROUTER_FACTORY = ExtensionLoader.getExtensionLoader(RouterFactory.class)
.getAdaptiveExtension();
這裡提供了幾種路由方式:
file=org.apache.dubbo.rpc.cluster.router.file.FileRouterFactory
script=org.apache.dubbo.rpc.cluster.router.script.ScriptRouterFactory
condition=org.apache.dubbo.rpc.cluster.router.condition.ConditionRouterFactory
service=org.apache.dubbo.rpc.cluster.router.condition.config.ServiceRouterFactory
app=org.apache.dubbo.rpc.cluster.router.condition.config.AppRouterFactory
tag=org.apache.dubbo.rpc.cluster.router.tag.TagRouterFactory
mock=org.apache.dubbo.rpc.cluster.router.mock.MockRouterFactory
我們以條件路由為例:
public class ConditionRouterFactory implements RouterFactory {
public static final String NAME = "condition";
@Override
public Router getRouter(URL url) {
return new ConditionRouter(url);
}
}
根據URL建立了一個ConditionRouter物件,看一下它的初始化方法:
public ConditionRouter(URL url) {
this.url = url;
// 獲取 priority 和 force 配置
this.priority = url.getParameter(PRIORITY_KEY, 0);
this.force = url.getParameter(FORCE_KEY, false);
this.enabled = url.getParameter(ENABLED_KEY, true);
// 獲取路由規則
init(url.getParameterAndDecoded(RULE_KEY));
}
然後是init方法:
public void init(String rule) {
try {
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);
}
}
ConditionRouter 構造方法先是對路由規則做預處理,然後呼叫 parseRule 方法分別對服務提供者和消費者規則進行解析,最後將解析結果賦值給 whenCondition 和 thenCondition 成員變數。
首先先看一下內部類MatchPair的定義:
protected static final class MatchPair {
final Set<String> matches = new HashSet<String>();
final Set<String> mismatches = new HashSet<String>();
private boolean isMatch(String value, URL param) {
...
}
}
isMatch方法後面規則匹配時再分析。這裡我們知道定義了兩個集合,分別用於存放匹配和不匹配的條件。
這裡我們關注parseRule方法:
private static Map<String, MatchPair> parseRule(String rule)
throws ParseException {
// 定義條件對映集合
Map<String, MatchPair> condition = new HashMap<String, MatchPair>();
if (StringUtils.isBlank(rule)) {
return condition;
}
// Key-Value pair, stores both match and mismatch conditions
MatchPair pair = null;
// Multiple values
Set<String> values = null;
// 通過正規表示式匹配路由規則,ROUTE_PATTERN = ([&!=,]*)\s*([^&!=,\s]+)
// 這個表示式看起來不是很好理解,第一個括號內的表示式用於匹配"&", "!", "=" 和 "," 等符號。
// 第二括號內的用於匹配英文字母,數字等字元。舉個例子說明一下:
// host = 2.2.2.2 & host != 1.1.1.1 & method = hello
// 匹配結果如下:
// 括號一 括號二
// 1. null host
// 2. = 2.2.2.2
// 3. & host
// 4. != 1.1.1.1
// 5. & method
// 6. = hello
final Matcher matcher = ROUTE_PATTERN.matcher(rule);
while (matcher.find()) { // Try to match one by one
// 獲取括號一內的匹配結果
String separator = matcher.group(1);
// 獲取括號二內的匹配結果
String content = matcher.group(2);
// Start part of the condition expression.
// 分隔符為空,表示匹配的是表示式的開始部分
if (StringUtils.isEmpty(separator)) {
// 建立 MatchPair 物件
pair = new MatchPair();
// 儲存 <匹配項, MatchPair> 鍵值對,比如 <host, MatchPair>
condition.put(content, pair);
}
// The KV part of the condition expression
// 如果分隔符為 &,表明接下來也是一個條件
else if ("&".equals(separator)) {
// 嘗試從 condition 獲取 MatchPair
if (condition.get(content) == null) {
// 未獲取到 MatchPair,重新建立一個,並放入 condition 中
pair = new MatchPair();
condition.put(content, pair);
} else {
pair = condition.get(content);
}
}
// The Value in the KV part.
// 分隔符為 =
else if ("=".equals(separator)) {
if (pair == null) {
throw new ParseException("Illegal route rule \""
+ rule + "\", The error char '" + separator
+ "' at index " + matcher.start() + " before \""
+ content + "\".", matcher.start());
}
values = pair.matches;
// 將 content 存入到 MatchPair 的 matches 集合中
values.add(content);
}
// The Value in the KV part.
// 分隔符為 !=
else if ("!=".equals(separator)) {
if (pair == null) {
throw new ParseException("Illegal route rule \""
+ rule + "\", The error char '" + separator
+ "' at index " + matcher.start() + " before \""
+ content + "\".", matcher.start());
}
values = pair.mismatches;
// 將 content 存入到 MatchPair 的 mismatches 集合中
values.add(content);
}
// The Value in the KV part, if Value have more than one items.
// 分隔符為 ,
else if (",".equals(separator)) { // Should be separated by ','
if (values == null || values.isEmpty()) {
throw new ParseException("Illegal route rule \""
+ rule + "\", The error char '" + separator
+ "' at index " + matcher.start() + " before \""
+ content + "\".", matcher.start());
}
// 將 content 存入到上一步獲取到的 values 中,可能是 matches,也可能是 mismatches
values.add(content);
} else {
throw new ParseException("Illegal route rule \"" + rule
+ "\", The error char '" + separator + "' at index "
+ matcher.start() + " before \"" + content + "\".", matcher.start());
}
}
return condition;
}
以上就是路由規則的解析邏輯,該邏輯由正規表示式和一個 while 迴圈以及數個條件分支組成。下面通過一個示例對解析邏輯進行演繹。示例為 host = 2.2.2.2 & host != 1.1.1.1 & method = hello
。正則解析結果如下:
括號一 括號二
1. null host
2. = 2.2.2.2
3. & host
4. != 1.1.1.1
5. & method
6. = hello
現線上程進入 while 迴圈:
第一次迴圈:分隔符 separator = null,content = “host”。此時建立 MatchPair 物件,並存入到 condition 中,condition = {“host”: MatchPair@123}
第二次迴圈:分隔符 separator = “=",content = “2.2.2.2”,pair = MatchPair@123。此時將 2.2.2.2 放入到 MatchPair@123 物件的 matches 集合中。
第三次迴圈:分隔符 separator = “&",content = “host”。host 已存在於 condition 中,因此 pair = MatchPair@123。
第四次迴圈:分隔符 separator = “!=",content = “1.1.1.1”,pair = MatchPair@123。此時將 1.1.1.1 放入到 MatchPair@123 物件的 mismatches 集合中。
第五次迴圈:分隔符 separator = “&",content = “method”。condition.get(“method”) = null,因此新建一個 MatchPair 物件,並放入到 condition 中。此時 condition = {“host”: MatchPair@123, “method”: MatchPair@ 456}
第六次迴圈:分隔符 separator = “=",content = “2.2.2.2”,pair = MatchPair@456。此時將 hello 放入到 MatchPair@456 物件的 matches 集合中。
迴圈結束,此時 condition 的內容如下:
{
"host": {
"matches": ["2.2.2.2"],
"mismatches": ["1.1.1.1"]
},
"method": {
"matches": ["hello"],
"mismatches": []
}
}
路由匹配
在服務目錄doList時觸發路由匹配:
invokers = routerChain.route(getConsumerUrl(), invocation);
我們看一下routerChain的route方法:
public List<Invoker<T>> route(URL url, Invocation invocation) {
List<Invoker<T>> finalInvokers = invokers;
for (Router router : routers) {
finalInvokers = router.route(finalInvokers, url, invocation);
}
return finalInvokers;
}
就是對前面notify時新增的路由遍歷執行route方法
我們看一下ConditionRouter的route方法:
@Override
public <T> List<Invoker<T>> route(List<Invoker<T>> invokers, URL url, Invocation invocation)
throws RpcException {
if (!enabled) {
return invokers;
}
if (CollectionUtils.isEmpty(invokers)) {
return invokers;
}
try {
// 先對服務消費者條件進行匹配,如果匹配失敗,表明服務消費者 url 不符合匹配規則,
// 無需進行後續匹配,直接返回 Invoker 列表即可。比如下面的規則:
// host = 10.20.153.10 => host = 10.0.0.10
// 這條路由規則希望 IP 為 10.20.153.10 的服務消費者呼叫 IP 為 10.0.0.10 機器上的服務。
// 當消費者 ip 為 10.20.153.11 時,matchWhen 返回 false,表明當前這條路由規則不適用於
// 當前的服務消費者,此時無需再進行後續匹配,直接返回即可。
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;
}
// 這裡可以簡單的把 Invoker 理解為服務提供者,現在使用服務提供者匹配規則對
// Invoker 列表進行匹配
for (Invoker<T> invoker : invokers) {
// 若匹配成功,表明當前 Invoker 符合服務提供者匹配規則。
// 此時將 Invoker 新增到 result 列表中
if (matchThen(invoker.getUrl(), url)) {
result.add(invoker);
}
}
// 返回匹配結果,如果 result 為空列表,且 force = true,表示強制返回空列表,
// 否則路由結果為空的路由規則將自動失效
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(RULE_KEY));
return result;
}
} catch (Throwable t) {
logger.error("Failed to execute condition router rule: " + getUrl() + ", invokers: " + invokers + ", cause: " + t.getMessage(), t);
}
// 原樣返回,此時 force = false,表示該條路由規則失效
return invokers;
}
route 方法先是呼叫 matchWhen 對服務消費者進行匹配,如果匹配失敗,直接返回 Invoker 列表。如果匹配成功,再對服務提供者進行匹配,匹配邏輯封裝在了 matchThen 方法中。下面來看一下這兩個方法的邏輯:
boolean matchWhen(URL url, Invocation invocation) {
// 服務消費者條件為 null 或空,均返回 true,比如:
// => host != 172.22.3.91
// 表示所有的服務消費者都不得呼叫 IP 為 172.22.3.91 的機器上的服務
return CollectionUtils.isEmptyMap(whenCondition) || matchCondition(whenCondition, url, null, invocation);
}
private boolean matchThen(URL url, URL param) {
// 服務提供者條件為 null 或空,表示禁用服務
return CollectionUtils.isNotEmptyMap(thenCondition) && matchCondition(thenCondition, url, param, null);
}
這兩個方法長的有點像,不過邏輯上還是有差別的,大家注意看。這兩個方法均呼叫了 matchCondition 方法,但它們所傳入的引數是不同的。這個需要特別注意一下,不然後面的邏輯不好弄懂。下面我們對這幾個引數進行溯源。matchWhen 方法向 matchCondition 方法傳入的引數為 [whenCondition, url, null, invocation],第一個引數 whenCondition 為服務消費者匹配條件,這個前面分析過。第二個引數 url 源自 route 方法的引數列表,該引數由外部類呼叫 route 方法時傳入。接下來再來看看 matchThen 向 matchCondition 方法傳入的引數 [thenCondition, url, param, null]。第一個引數不用解釋了。第二個和第三個引數來自 matchThen 方法的引數列表,這兩個引數分別為服務提供者 url 和服務消費者 url。搞清楚這些引數來源後,接下來就可以分析 matchCondition 方法了。
private boolean matchCondition(Map<String, MatchPair> condition, URL url, URL param, Invocation invocation) {
// 將服務提供者或消費者 url 轉成 Map
Map<String, String> sample = url.toMap();
boolean result = false;
// 遍歷 condition 列表
for (Map.Entry<String, MatchPair> matchPair : condition.entrySet()) {
// 獲取匹配項名稱,比如 host、method 等
String key = matchPair.getKey();
String sampleValue;
//get real invoked method name from invocation
// 如果 invocation 不為空,且 key 為 mehtod(s),表示進行方法匹配
if (invocation != null && (METHOD_KEY.equals(key) || METHODS_KEY.equals(key))) {
// 從 invocation 獲取被呼叫方法的名稱
sampleValue = invocation.getMethodName();
} else if (ADDRESS_KEY.equals(key)) {
sampleValue = url.getAddress();
} else if (HOST_KEY.equals(key)) {
sampleValue = url.getHost();
} else {
// 從服務提供者或消費者 url 中獲取指定欄位值,比如 host、application 等
sampleValue = sample.get(key);
if (sampleValue == null) {
// 嘗試通過 default.xxx 獲取相應的值
sampleValue = sample.get(key);
}
}
if (sampleValue != null) {
// 呼叫 MatchPair 的 isMatch 方法進行匹配
if (!matchPair.getValue().isMatch(sampleValue, param)) {
// 只要有一個規則匹配失敗,立即返回 false 結束方法邏輯
return false;
} else {
result = true;
}
} else {
//not pass the condition
// sampleValue 為空,表明服務提供者或消費者 url 中不包含相關欄位。此時如果
// MatchPair 的 matches 不為空,表示匹配失敗,返回 false。比如我們有這樣
// 一條匹配條件 loadbalance = random,假設 url 中並不包含 loadbalance 引數,
// 此時 sampleValue = null。既然路由規則裡限制了 loadbalance 必須為 random,
// 但 sampleValue = null,明顯不符合規則,因此返回 false
if (!matchPair.getValue().matches.isEmpty()) {
return false;
} else {
result = true;
}
}
}
return result;
}
前半部分獲取sampleValue的值,然後呼叫isMatch判斷是否匹配:
private boolean isMatch(String value, URL param) {
// 情況一:matches 非空,mismatches 為空
if (!matches.isEmpty() && mismatches.isEmpty()) {
// 遍歷 matches 集合,檢測入參 value 是否能被 matches 集合元素匹配到。
// 舉個例子,如果 value = 10.20.153.11,matches = [10.20.153.*],
// 此時 isMatchGlobPattern 方法返回 true
for (String match : matches) {
if (UrlUtils.isMatchGlobPattern(match, value, param)) {
return true;
}
}
// 如果所有匹配項都無法匹配到入參,則返回 false
return false;
}
// 情況二:matches 為空,mismatches 非空
if (!mismatches.isEmpty() && matches.isEmpty()) {
// 只要入參被 mismatches 集合中的任意一個元素匹配到,就返回 false
for (String mismatch : mismatches) {
if (UrlUtils.isMatchGlobPattern(mismatch, value, param)) {
return false;
}
}
// mismatches 集合中所有元素都無法匹配到入參,此時返回 true
return true;
}
// 情況三:matches 非空,mismatches 非空
if (!matches.isEmpty() && !mismatches.isEmpty()) {
//when both mismatches and matches contain the same value, then using mismatches first
// matches 和 mismatches 均為非空,此時優先使用 mismatches 集合元素對入參進行匹配。
// 只要 mismatches 集合中任意一個元素與入參匹配成功,就立即返回 false,結束方法邏輯
for (String mismatch : mismatches) {
if (UrlUtils.isMatchGlobPattern(mismatch, value, param)) {
return false;
}
}
// mismatches 集合元素無法匹配到入參,此時再使用 matches 繼續匹配
for (String match : matches) {
// 只要 matches 集合中任意一個元素與入參匹配成功,就立即返回 true
if (UrlUtils.isMatchGlobPattern(match, value, param)) {
return true;
}
}
// 全部失配,則返回 false
return false;
}
// 情況四:matches 和 mismatches 均為空,此時返回 false
return false;
}
isMatch 方法邏輯比較清晰,由三個條件分支組成,用於處理四種情況。這裡對四種情況下的匹配邏輯進行簡單的總結,如下:
條件 | 過程 | |
---|---|---|
情況一 | matches 非空,mismatches 為空 | 遍歷 matches 集合元素,並與入參進行匹配。只要有一個元素成功匹配入參,即可返回 true。若全部失配,則返回 false。 |
情況二 | matches 為空,mismatches 非空 | 遍歷 mismatches 集合元素,並與入參進行匹配。只要有一個元素成功匹配入參,立即 false。若全部失配,則返回 true。 |
情況三 | matches 非空,mismatches 非空 | 優先使用 mismatches 集合元素對入參進行匹配,只要任一元素與入參匹配成功,就立即返回 false,結束方法邏輯。否則再使用 matches 中的集合元素進行匹配,只要有任意一個元素匹配成功,即可返回 true。若全部失配,則返回 false |
情況四 | matches 為空,mismatches 為空 | 直接返回 false |
isMatch 方法是通過 UrlUtils 的 isMatchGlobPattern 方法進行匹配,因此下面我們再來看看 isMatchGlobPattern 方法的邏輯。
public static boolean isMatchGlobPattern(String pattern, String value, URL param) {
if (param != null && pattern.startsWith("$")) {
// 引用服務消費者引數,param 引數為服務消費者 url
pattern = param.getRawParameter(pattern.substring(1));
}
// 呼叫過載方法繼續比較
return isMatchGlobPattern(pattern, value);
}
public static boolean isMatchGlobPattern(String pattern, String value) {
// 對 * 萬用字元提供支援
if ("*".equals(pattern)) {
// 匹配規則為萬用字元 *,直接返回 true 即可
return true;
}
if (StringUtils.isEmpty(pattern) && StringUtils.isEmpty(value)) {
// pattern 和 value 均為空,此時可認為兩者相等,返回 true
return true;
}
if (StringUtils.isEmpty(pattern) || StringUtils.isEmpty(value)) {
// pattern 和 value 其中有一個為空,表明兩者不相等,返回 false
return false;
}
// 定位 * 萬用字元位置
int i = pattern.lastIndexOf('*');
// doesn't find "*"
if (i == -1) {
// 匹配規則中不包含萬用字元,此時直接比較 value 和 pattern 是否相等即可,並返回比較結果
return value.equals(pattern);
}
// 萬用字元 "*" 在匹配規則尾部,比如 10.0.21.*
// "*" is at the end
else if (i == pattern.length() - 1) {
// 檢測 value 是否以“不含萬用字元的匹配規則”開頭,並返回結果。比如:
// pattern = 10.0.21.*,value = 10.0.21.12,此時返回 true
return value.startsWith(pattern.substring(0, i));
}
// "*" is at the beginning
// 萬用字元 "*" 在匹配規則頭部
else if (i == 0) {
// 檢測 value 是否以“不含萬用字元的匹配規則”結尾,並返回結果
return value.endsWith(pattern.substring(i + 1));
}
// "*" is in the middle
// 萬用字元 "*" 在匹配規則中間位置
else {
// 通過萬用字元將 pattern 分成兩半,得到 prefix 和 suffix
String prefix = pattern.substring(0, i);
String suffix = pattern.substring(i + 1);
// 檢測 value 是否以 prefix 開頭,且以 suffix 結尾,並返回結果
return value.startsWith(prefix) && value.endsWith(suffix);
}
}
這兩個方法分別對普通的匹配過程,以及”引用消費者引數“和萬用字元匹配等特性提供了支援。
總結
- 路由物件的初始化
- 路由鏈呼叫
- 單個路由的匹配邏輯
相關文章
- dubbo原始碼分析02:服務引用原始碼
- Dubbo原始碼分析之服務引用原始碼
- Dubbo原始碼分析之服務暴露原始碼
- Dubbo原始碼分析(五)Dubbo呼叫鏈-服務端原始碼服務端
- Dubbo原始碼分析(三)Dubbo的服務引用Refer原始碼
- Dubbo原始碼分析(七)服務目錄原始碼
- Dubbo原始碼之服務引用原始碼
- dubbo服務者原始碼分期原始碼
- Dubbo服務暴露原始碼解析②原始碼
- Dubbo原始碼分析(六)服務引用的具體流程原始碼
- Dubbo原始碼分析(四)服務暴露的具體流程(上)原始碼
- Dubbo原始碼分析(五)服務暴露的具體流程(下)原始碼
- Dubbo原始碼解析之服務叢集原始碼
- Dubbo服務呼叫過程原始碼解析④原始碼
- Dubbo原始碼解析之服務引入過程原始碼
- Dubbo原始碼解析之服務呼叫過程原始碼
- Dubbo原始碼之服務端的釋出原始碼服務端
- Dubbo原始碼學習之-服務匯出原始碼
- Dubbo原理和原始碼解析之服務引用原始碼
- http 服務原始碼分析HTTP原始碼
- http服務原始碼分析HTTP原始碼
- dubbo原始碼分析之服務呼叫方發起呼叫(入口InvokerInvocationHandler.invoke)原始碼
- Dubbo原始碼解析之服務端接收訊息原始碼服務端
- Dubbo原始碼解析之服務匯出過程原始碼
- Dubbo2.7.3版本原始碼學習系列六: Dubbo服務匯出原始碼解析原始碼
- Dubbo之SPI原始碼分析原始碼
- Dubbo原始碼解析之服務釋出與註冊原始碼
- express原始碼分析-路由Express原始碼路由
- Laravel 路由原始碼分析Laravel路由原始碼
- 分散式事務 TCC-Transaction 原始碼分析 —— Dubbo 支援分散式原始碼
- 【spring原始碼】十一、事務Spring原始碼
- Dubbo之限流TpsLimitFilter原始碼分析MITFilter原始碼
- dubbo消費者原始碼分析原始碼
- Dubbo 原始碼分析 - SPI 機制原始碼
- 聊聊Dubbo(九):核心原始碼-服務端啟動流程2原始碼服務端
- 聊聊Dubbo(九):核心原始碼-服務端啟動流程1原始碼服務端
- Dubbo原始碼分析(一)Dubbo與Spring整合例項原始碼Spring
- Laravel 路由管道原始碼分析Laravel路由原始碼