Dubbo 源碼分析-集群容錯之 Router

1. 簡介

上一篇文章分析了集群容錯的第一部分 – 服務目錄 Directory。服務目錄在刷新 Invoker 列表的過程中,會通過 Router 進行服務路由。上一篇文章關於服務路由相關邏輯沒有細緻分析,一筆帶過了,本篇文章將對此進行詳細的分析。首先,先來介紹一下服務目錄是什麼。服務路由包含一條路由規則,路由規則決定了服務消費者的調用目標,即規定了服務消費者可調用哪些服務提供者。Dubbo 目前提供了三種服務路由實現,分別為條件路由 ConditionRouter、腳本路由 ScriptRouter 和標籤路由 TagRouter。其中條件路由是我們最常使用的,標籤路由暫未在我所分析的 2.6.4 版本中提供,該實現會在 2.7.0 版本中提供。本篇文章將分析條件路由相關源碼,腳本路由和標籤路由這裡就不分析了。下面進入正題。

2. 源碼分析

條件路由規則有兩個條件組成,分別用於對服務消費者和提供者進行匹配。比如有這樣一條規則:

host = 10.20.153.10 => host = 10.20.153.11

該條規則表示 IP 為 10.20.153.10 的服務消費者 只可 調用 IP 為 10.20.153.11 機器上的服務,不可調用其他機器上的服務。條件路由規則的格式如下:

[服務消費者匹配條件] => [服務提供者匹配條件]

如果服務消費者匹配條件為空,表示不對服務消費者進行限制。如果服務提供者匹配條件為空,表示對某些服務消費者禁用服務。Dubbo 官方文檔對條件路由進行了比較詳細的介紹,大家可以參考下,這裡就不過多說明了。

條件路由實現類 ConditionRouter 需要對用戶配置的路由規則進行解析,得到一系列的條件。然後再根據這些條件對服務進行路由。本章將分兩節進行說明,2.1節介紹表達式解析過程。2.2 節介紹服務路由的過程。接下來,我們先從表達式解析過程看起。

2.1 表達式解析

條件路由規則是一條字符串,對於 Dubbo 來說,它並不能直接理解字符串的意思,需要將其解析成內部格式才行。條件表達式的解析過程始於 ConditionRouter 的構造方法,下面一起看一下:

public ConditionRouter(URL url) {
 this.url = url;
 // 獲取 priority 和 force 配置
 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 when = 
 StringUtils.isBlank(whenRule) || "true".equals(whenRule) 
 ? new HashMap() : parseRule(whenRule);
 // 解析服務提供者匹配規則
 Map then = 
 StringUtils.isBlank(thenRule) || "false".equals(thenRule) 
 ? null : parseRule(thenRule);
 this.whenCondition = when;
 this.thenCondition = then;
 } catch (ParseException e) {
 throw new IllegalStateException(e.getMessage(), e);
 }
}
 

如上,ConditionRouter 構造方法先是對路由規則做預處理,然後調用 parseRule 方法分別對服務提供者和消費者規則進行解析,最後將解析結果賦值給 whenCondition 和 thenCondition 成員變量。ConditionRouter 構造方法不是很複雜,這裡就不多說了。下面我們把重點放在 parseRule 方法上,在詳細介紹這個方法之前,我們先來看一個內部類。

private static final class MatchPair {
 final Set matches = new HashSet();
 final Set mismatches = new HashSet();
}

MatchPair 內部包含了兩個 Set 型的成員變量,分別用於存放匹配和不匹配的條件。這個類兩個成員變量會在 parseRule 方法中被用到,下面來看一下。

private static Map parseRule(String rule)
 throws ParseException {
 // 定義條件映射集合
 Map condition = new HashMap();
 if (StringUtils.isBlank(rule)) {
 return condition;
 }
 MatchPair pair = null;
 Set 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()) {
 	// 獲取括號一內的匹配結果
 String separator = matcher.group(1);
 // 獲取括號二內的匹配結果
 String content = matcher.group(2);
 // 分隔符為空,表示匹配的是表達式的開始部分
 if (separator == null || separator.length() == 0) {
 // 創建 MatchPair 對象
 pair = new MatchPair();
 // 存儲  鍵值對,比如 
 condition.put(content, pair); 
 } 
 
 // 如果分隔符為 &,表明接下來也是一個條件
 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);
 }
 } 
 
 // 分隔符為 =
 else if ("=".equals(separator)) {
 if (pair == null)
 throw new ParseException("Illegal route rule ...");
 values = pair.matches;
 // 將 content 存入到 MatchPair 的 matches 集合中
 values.add(content);
 } 
 
 // 分隔符為 != 
 else if ("!=".equals(separator)) {
 if (pair == null)
 throw new ParseException("Illegal route rule ...");
 values = pair.mismatches;
 // 將 content 存入到 MatchPair 的 mismatches 集合中
 values.add(content);
 }
 
 // 分隔符為 ,
 else if (",".equals(separator)) {
 if (values == null || values.isEmpty())
 throw new ParseException("Illegal route rule ...");
 // 將 content 存入到上一步獲取到的 values 中,可能是 matches,也可能是 mismatches
 values.add(content);
 } else {
 throw new ParseException("Illegal route rule ...");
 }
 }
 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": []
 }
}

路由規則的解析過程稍微有點複雜,大家可通過 ConditionRouter 的測試類對該邏輯進行測試。並且找一個表達式,對照上面的代碼走一遍,加深理解。關於路由規則的解析過程就先到這,我們繼續往下看。

2.2 服務路由

服務路由的入口方法是 ConditionRouter 的 router 方法,該方法定義在 Router 接口中。實現代碼如下:

public  List> route(List> invokers, URL url, Invocation invocation)
 throws RpcException {
 if (invokers == null || invokers.isEmpty()) {
 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> result = new ArrayList>();
 // 服務提供者匹配條件未配置,表明對指定的服務消費者禁用服務,也就是服務消費者在黑名單中
 if (thenCondition == null) {
 logger.warn("The current consumer in the service blacklist...");
 return result;
 }
 // 這裡可以簡單的把 Invoker 理解為服務提供者,現在使用服務消費者匹配規則對 
 // Invoker 列表進行匹配
 for (Invoker 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 ...");
 return result;
 }
 } catch (Throwable t) {
 logger.error("Failed to execute condition router rule: ...");
 }
 
 // 原樣返回,此時 force = false,表示該條路由規則失效
 return invokers;
}

router 方法先是調用 matchWhen 對服務消費者進行匹配,如果匹配失敗,直接返回 Invoker 列表。如果匹配成功,再對服務提供者進行匹配,匹配邏輯封裝在了 matchThen 方法中。下面來看一下這兩個方法的邏輯:

boolean matchWhen(URL url, Invocation invocation) {
 // 服務消費者條件為 null 或空,均返回 true,比如:
 // => host != 172.22.3.91
 // 表示所有的服務消費者都不得調用 IP 為 172.22.3.91 的機器上的服務
 return whenCondition == null || whenCondition.isEmpty() 
 || matchCondition(whenCondition, url, null, invocation); // 進行條件匹配
}
private boolean matchThen(URL url, URL param) {
 // 服務提供者條件為 null 或空,表示禁用服務
 return !(thenCondition == null || thenCondition.isEmpty()) 
 && matchCondition(thenCondition, url, param, null); // 進行條件匹配
}

這兩個方法長的有點像,不過邏輯上還是有差別的,大家注意看。這兩個方法均調用了 matchCondition 方法,不過它們所傳入的參數是不同的,這個需要特別注意。不然後面的邏輯不好弄懂。下面我們對這幾個參數進行溯源。matchWhen 方法向 matchCondition 方法傳入的參數為 [whenCondition, url, null, invocation],第一個參數 whenCondition 為服務消費者匹配條件,這個前面分析過。第二個參數 url 源自 route 方法的參數列表,該參數由外部類調用 route 方法時傳入。有代碼為證,如下:

private List> route(List> invokers, String method) {
 Invocation invocation = new RpcInvocation(method, new Class>[0], new Object[0]);
 List routers = getRouters();
 if (routers != null) {
 for (Router router : routers) {
 if (router.getUrl() != null) {
 // 注意第二個參數
 invokers = router.route(invokers, getConsumerUrl(), invocation);
 }
 }
 }
 return invokers;
}

上面這段代碼來自 RegistryDirectory,第二個參數表示的是服務消費者 url。matchCondition 的 invocation 參數也是從這裡傳入的。

接下來再來看看 matchThen 向 matchCondition 方法傳入的參數 [thenCondition, url, param, null]。第一個參數不用解釋了。第二個和第三個參數來自 matchThen 方法的參數列表,這兩個參數分別為服務提供者 url 和服務消費者 url。搞清楚這些參數來源後,接下倆就可以分析 matchCondition 了。

private boolean matchCondition(Map condition, URL url, URL param, Invocation invocation) {
 // 將服務提供者或消費者 url 轉成 Map
 Map sample = url.toMap();
 boolean result = false;
 // 遍歷 condition 列表
 for (Map.Entry matchPair : condition.entrySet()) {
 // 獲取匹配項名稱,比如 host、method 等
 String key = matchPair.getKey();
 String sampleValue;
 // 如果 invocation 不為空,且 key 為 mehtod(s),表示進行方法匹配
 if (invocation != null && (Constants.METHOD_KEY.equals(key) || Constants.METHODS_KEY.equals(key))) {
 // 從 invocation 獲取調用方法名稱
 sampleValue = invocation.getMethodName();
 } else {
 // 從服務提供者或消費者 url 中獲取指定字段值,比如 host、application 等
 sampleValue = sample.get(key);
 if (sampleValue == null) {
 // 嘗試通過 default.xxx 獲取相應的值
 sampleValue = sample.get(Constants.DEFAULT_KEY_PREFIX + key);
 }
 }
 
 // --------------------:sparkles: 分割線 :sparkles:-------------------- //
 
 if (sampleValue != null) {
 // 調用 MatchPair 的 isMatch 方法進行匹配
 if (!matchPair.getValue().isMatch(sampleValue, param)) {
 // 只要有一個規則匹配失敗,立即返回 false 結束方法邏輯
 return false;
 } else {
 result = true;
 }
 } else {
 // 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;
}
 

如上,matchCondition 方法看起來有點複雜,這裡簡單縷縷。分割線以上的代碼實際上主要是用於獲取 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()) {
 for (String mismatch : mismatches) {
 // 只要入參被 mismatches 集合中的任意一個元素匹配到,就返回 false
 if (UrlUtils.isMatchGlobPattern(mismatch, value, param)) {
 return false;
 }
 }
 // mismatches 集合中所有元素都無法匹配到入參,此時返回 true
 return true;
 }
 // 情況三:matches 非空,mismatches 非空
 if (!matches.isEmpty() && !mismatches.isEmpty()) {
 // 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;
 }
 }
 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 方法邏輯不是很難理解,大家自己再看看。下面繼續分析 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 ((pattern == null || pattern.length() == 0)
 && (value == null || value.length() == 0))
 // pattern 和 value 均為空,此時可認為兩者相等,返回 true
 return true;
 if ((pattern == null || pattern.length() == 0)
 || (value == null || value.length() == 0))
 // pattern 和 value 其中有一個為空,兩者不相等,返回 false
 return false;
 // 查找 * 通配符位置
 int i = pattern.lastIndexOf('*');
 if (i == -1) {
 // 匹配規則中不包含通配符,此時直接比較 value 和 pattern 是否相等即可,並返回比較結果
 return value.equals(pattern);
 }
 // 通配符 "*" 在匹配規則尾部,比如 10.0.21.*
 else if (i == pattern.length() - 1) {
 // 檢測 value 是否以不含通配符的匹配規則開頭,並返回結果。比如:
 // pattern = 10.0.21.*,value = 10.0.21.12,此時返回 true
 return value.startsWith(pattern.substring(0, i));
 }
 // 通配符 "*" 在匹配規則頭部
 else if (i == 0) {
 // 檢測 value 是否以不含通配符的匹配規則結尾,並返回結果
 return value.endsWith(pattern.substring(i + 1));
 }
 // 通配符 "*" 在匹配規則中間位置
 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);
 }
}

以上就是 isMatchGlobPattern 兩個重載方法的全部邏輯,這兩個方法分別對普通的匹配,以及”引用消費者參數“和通配符匹配做了支持。這兩個方法的邏輯並不是很複雜,而且我也在代碼上進行了比較詳細的註釋,大家自己看看吧,就不多說了。

3. 總結

本篇文章對條件路由的表達式解析和服務路由過程進行了較為細緻的分析。總的來說,條件路由的代碼還是有一些複雜的,需要耐下心來看。在閱讀條件路由代碼的過程中,要多調試。一般的框架都會有單元測試,Dubbo 也不例外,因此大家可以直接通過 ConditionRouterTest 對條件路由進行調試,無需自己手寫測試用例。

好了,關於條件路由就先分析到這,謝謝閱讀。

Dubbo 源碼分析-集群容錯之 Router


分享到:


相關文章: