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 的構造方法,下面一起看一下:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | 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<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);this.whenCondition = when;this.thenCondition = then;} catch (ParseException e) {throw new IllegalStateException(e.getMessage(), e);} } |
如上,ConditionRouter 構造方法先是對路由規則做預處理,然后調用 parseRule 方法分別對服務提供者和消費者規則進行解析,最后將解析結果賦值給 whenCondition 和 thenCondition 成員變量。ConditionRouter 構造方法不是很復雜,這里就不多說了。下面我們把重點放在 parseRule 方法上,在詳細介紹這個方法之前,我們先來看一個內部類。
| 1 2 3 4 | private static final class MatchPair {final Set<String> matches = new HashSet<String>();final Set<String> mismatches = new HashSet<String>(); } |
MatchPair 內部包含了兩個 Set 型的成員變量,分別用于存放匹配和不匹配的條件。這個類兩個成員變量會在 parseRule 方法中被用到,下面來看一下。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 | private static Map<String, MatchPair> parseRule(String rule)throws ParseException {// 定義條件映射集合Map<String, MatchPair> condition = new HashMap<String, MatchPair>();if (StringUtils.isBlank(rule)) {return condition;}MatchPair pair = null;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. = hellofinal 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();// 存儲 <匹配項, MatchPair> 鍵值對,比如 <host, MatchPair>condition.put(content, pair); } // 如果分隔符為 &,表明接下來也是一個條件else if ("&".equals(separator)) {// 嘗試從 condition 獲取 MatchPairif (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,也可能是 mismatchesvalues.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 2 3 4 5 6 7 | 括號一 括號二 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 的內容如下:
| 1 2 3 4 5 6 7 8 9 10 | {"host": {"matches": ["2.2.2.2"],"mismatches": ["1.1.1.1"]},"method": {"matches": ["hello"],"mismatches": []} } |
路由規則的解析過程稍微有點復雜,大家可通過 ConditionRouter 的測試類對該邏輯進行測試。并且找一個表達式,對照上面的代碼走一遍,加深理解。關于路由規則的解析過程就先到這,我們繼續往下看。
?2.2 服務路由
服務路由的入口方法是 ConditionRouter 的 router 方法,該方法定義在 Router 接口中。實現代碼如下:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | public <T> List<Invoker<T>> route(List<Invoker<T>> 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<Invoker<T>> result = new ArrayList<Invoker<T>>();// 服務提供者匹配條件未配置,表明對指定的服務消費者禁用服務,也就是服務消費者在黑名單中if (thenCondition == null) {logger.warn("The current consumer in the service blacklist...");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 ...");return result;}} catch (Throwable t) {logger.error("Failed to execute condition router rule: ...");}// 原樣返回,此時 force = false,表示該條路由規則失效return invokers; } |
router 方法先是調用 matchWhen 對服務消費者進行匹配,如果匹配失敗,直接返回 Invoker 列表。如果匹配成功,再對服務提供者進行匹配,匹配邏輯封裝在了 matchThen 方法中。下面來看一下這兩個方法的邏輯:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 | 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 方法時傳入。有代碼為證,如下:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 | private List<Invoker<T>> route(List<Invoker<T>> invokers, String method) {Invocation invocation = new RpcInvocation(method, new Class<?>[0], new Object[0]);List<Router> 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 了。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | private boolean matchCondition(Map<String, MatchPair> condition, URL url, URL param, Invocation invocation) {// 將服務提供者或消費者 url 轉成 MapMap<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;// 如果 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);}}// --------------------? 分割線 ?-------------------- //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,明顯不符合規則,因此返回 falseif (!matchPair.getValue().matches.isEmpty()) {return false;} else {result = true;}}}return result; } |
如上,matchCondition 方法看起來有點復雜,這里簡單縷縷。分割線以上的代碼實際上主要是用于獲取 sampleValue 的值,分割線以下才是進行條件匹配。條件匹配調用的邏輯封裝在 isMatch 中,代碼如下:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | 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 方法返回 truefor (String match : matches) {if (UrlUtils.isMatchGlobPattern(match, value, param)) {return true;}}// 如果所有匹配項都無法匹配到入參,則返回 falsereturn false;}// 情況二:matches 為空,mismatches 非空if (!mismatches.isEmpty() && matches.isEmpty()) {for (String mismatch : mismatches) {// 只要入參被 mismatches 集合中的任意一個元素匹配到,就返回 falseif (UrlUtils.isMatchGlobPattern(mismatch, value, param)) {return false;}}// mismatches 集合中所有元素都無法匹配到入參,此時返回 truereturn 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 集合中任意一個元素與入參匹配成功,就立即返回 trueif (UrlUtils.isMatchGlobPattern(match, value, param)) {return true;}}return false;}// 情況四:matches 和 mismatches 均為空,此時返回 falsereturn false; } |
isMatch 方法邏輯比較清晰,由三個條件分支組成,用于處理四種情況。這里對四種情況下的匹配邏輯進行簡單的總結,如下:
| 情況一 | matches 非空,mismatches 為空 | 遍歷 matches 集合元素,并與入參進行匹配。只要有一個元素成功匹配入參,即可返回 true。若全部失配,則返回 false。 |
| 情況二 | matches 為空,mismatches 非空 | 遍歷 mismatches 集合元素,并與入參進行匹配。只要有一個元素成功匹配入參,立即 false。若全部失配,則返回 true。 |
| 情況三 | matches 非空,mismatches 非空 | 優先使用 mismatches 集合元素對入參進行匹配,只要任一元素與入參匹配成功,就立即返回 false,結束方法邏輯。否則再使用 matches 中的集合元素進行匹配,只要有任意一個元素匹配成功,即可返回 true。若全部失配,則返回 false |
| 情況四 | matches 為空,mismatches 為空 | 直接返回 false |
isMatch 方法邏輯不是很難理解,大家自己再看看。下面繼續分析 isMatchGlobPattern 方法。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | public static boolean isMatchGlobPattern(String pattern, String value, URL param) {if (param != null && pattern.startsWith("$")) {// 引用服務消費者參數,param 參數為服務消費者 urlpattern = 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 均為空,此時可認為兩者相等,返回 truereturn true;if ((pattern == null || pattern.length() == 0)|| (value == null || value.length() == 0))// pattern 和 value 其中有一個為空,兩者不相等,返回 falsereturn 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,此時返回 truereturn value.startsWith(pattern.substring(0, i));}// 通配符 "*" 在匹配規則頭部else if (i == 0) {// 檢測 value 是否以不含通配符的匹配規則結尾,并返回結果return value.endsWith(pattern.substring(i + 1));}// 通配符 "*" 在匹配規則中間位置else {// 通過通配符將 pattern 分成兩半,得到 prefix 和 suffixString 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 源碼分析系列文章
| 2018-10-01 | Dubbo 源碼分析 - SPI 機制 |
| 2018-10-13 | Dubbo 源碼分析 - 自適應拓展原理 |
| 2018-10-31 | Dubbo 源碼分析 - 服務導出 |
| 2018-11-12 | Dubbo 源碼分析 - 服務引用 |
| 2018-11-17 | Dubbo 源碼分析 - 集群容錯之 Directory |
| 2018-11-20 | Dubbo 源碼分析 - 集群容錯之 Router |
- 本文鏈接:?https://www.tianxiaobo.com/2018/11/20/Dubbo-源碼分析-集群容錯之-Router/
http://www.tianxiaobo.com/2018/11/20/Dubbo-%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90-%E9%9B%86%E7%BE%A4%E5%AE%B9%E9%94%99%E4%B9%8B-Router/?
總結
以上是生活随笔為你收集整理的Dubbo 源码分析 - 集群容错之 Router的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Dubbo 源码分析 - 集群容错之Di
- 下一篇: Dubbo 源码分析 - 集群容错之 C