文章目錄
Dubbo路由實現原理
Dubbo的路由分爲條件路由、文件路由和腳本路由,對應的dubbo-admin中三種不同的規則配置方式。條件路由是用戶使用Dubbo定義的語法規則去寫的路由規則;文件路由則需要用戶提交一個文件,裏面寫着對應的路由規則,框架基於文件讀取對應的規則;腳本路由則是使用JDK自身的腳本引擎解析路由規則腳本,所有JDK腳本引擎支持的腳本都能解析,默認是JavaScript。
ConditionRouter(條件路由)
參數規則
condition://0.0.0.0/com.foo.BarService?category=routers&dynamic=false&enabled=true&force=true&runtime=false&priority=1&rule=URL.encode("host = 10.20.153.10 => host = 10.20.153.11")
參數名稱 | 含義 |
---|---|
condition:// | (必填)表示路由規則的類型,支持條件路由規則和腳本路由規則,可擴展 |
0.0.0.0 | (必填)表示對所有ip生效,如果只想對某個IP生效,則填入具體IP |
com.foo.BarService | (必填)表示只對指定服務生效 |
category=routers | (必填)表示該數據爲動態配置類型 |
dynamic=false | (必填)表示該數據爲持久數據,當註冊方退出時,數據依然保存在註冊中心 |
enabled=true | (非必填)覆蓋規則是否生效,默認true |
force=true | (非必填) 當路由規則爲空時,是否強制執行,如果不強制執行,則路由結果爲空的的路由規則將自動失效,默認false |
runtime=false | (非必填)是否在每次調用時都執行路由規則,否則只在提供者地址列表變更時預先執行並緩存結果,調用時直接從緩存中獲取路由結果。如果用了參數路由,則必須設置爲true,需要注意設置會影響調用的性能,默認爲false |
priority=1 | (非必填)路由規則優先級,用於排序,優先級越大越靠前,默認爲0 |
rule= + URL.encode(“host = 10.20.153.10 => host = 10.20.153.11”) | (必填)表示路由規則的內容 |
示例:
method = find* => host = 192.168.1.22,192.168.1.23
上面的路由規則表示,調用方法以find開頭的路由到192.168.1.22,192.168.1.23,此時在調用的時候獲取到的原始Invoker(RegisterDirectory或者StaticDirectory中的原始InvokerList)會針對route規則進行過濾。
流程解析
- 校驗:如果規則沒有啓用,則直接返回;如果傳入的Invoker爲空,則直接返回;如果沒有任何whenRule匹配,即沒有匹配規則,則直接發怒會傳入的Invoker列表;如果whenRule有匹配,但是thenRule爲空,即沒有匹配上規則的Invoker,則返回空
- 匹配:遍歷Invoker列表,通過thenRule找出所有符合規則的Invoker介入result集合。
- 返回:如果result不爲空則直接返回result結果集;如果結果集爲空,則查看force(強制執行)是否爲true,如果爲true,則表示強制執行,此時打印warn日誌,並返回空結果集,否則返回所有的Invoker列表。
源碼解析
// ConditionRouter#route
@Override
public <T> List<Invoker<T>> route(List<Invoker<T>> invokers, URL url, Invocation invocation)
throws RpcException {
// invoker列表爲空直接return
if (invokers == null || invokers.isEmpty()) {
return invokers;
}
try {
// 匹配是否符合條件,不匹配直接return所有Invoker,無需過濾
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) {
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;
}
FileRouter(文件路由)
流程解析
文件路由是把規則寫在文件中,文件中寫的是自定義腳本規則,可以是JavaScript、Groovy等,URL中的key值填寫的是文件路徑。文件路由主要做的就是把文件中的路由腳本讀出來,然後調用路由的工廠去匹配對應的 腳本路由 做解析。
源碼解析
// FileRouterFactory#getRouter
// file:///d:/path/to/route.js?router=script ==> script:///d:/path/to/route.js?type=js&rule=<file-content>
@Override
public Router getRouter(URL url) {
try {
// 將文件URL轉換爲腳本路由URL,並加載
// 將原來的協議(可能是“file”)替換爲“script”
String protocol = url.getParameter(Constants.ROUTER_KEY, ScriptRouterFactory.NAME);
// 使用文件後綴配置腳本類型,如js, groovy…
String type = null;
String path = url.getPath();
if (path != null) {
int i = path.lastIndexOf('.');
if (i > 0) {
type = path.substring(i + 1);
}
}
// 讀取文件
String rule = IOUtils.read(new FileReader(new File(url.getAbsolutePath())));
// 讀取是否是運行時的參數
boolean runtime = url.getParameter(Constants.RUNTIME_KEY, false);
// 生成路由工廠可以識別的URL,並把參數添加進去
URL script = url.setProtocol(protocol).addParameter(Constants.TYPE_KEY, type).addParameter(Constants.RUNTIME_KEY, runtime).addParameterAndEncoded(Constants.RULE_KEY, rule);
// 再次調用路由的工廠,由於前面配置了protocol爲script類型,這裏會使用腳本路由進行解析
return routerFactory.getRouter(script);
} catch (IOException e) {
throw new IllegalStateException(e.getMessage(), e);
}
}
ScriptRouter(腳本路由)
流程解析
腳本路由使用JDK自帶的腳本解析器解析腳本並運行,默認使用JavaScript解析器,其邏輯分爲構造方法和route方法兩大部分。
源碼解析
// 以官方文檔中的JavaScript腳本爲例
function route(invokers) {
// 創建一個list
var result = new java.util.ArrayList(invokers.size());
// 遍歷傳入的所有Invoker,過濾所有IP不是10.20.153.10的Invoker
for (i = 0; i < invokers.size(); i ++) {
if ("10.20.153.10".equals(invokers.get(i).getUrl().getHost())) {
result.add(invokers.get(i));
}
}
return result;
} (invokers); // 表示立即執行方法
注意事項:
我們在寫JavaScript腳本的時候需要注意,一個服務只能有一條規則,如果有多條規則,並且規則之間沒有交集,則會把所有的Invoker都過濾。另外,腳本路由中沒有看到沙箱約束,因此會由注入的風險。
- 構造方法主要負責一些初始化工作。
- 初始化參數。獲取規則的腳本類型、路由優先級。如果沒有設置腳本,則默認設置爲JavaScript類型,如果沒有解析到任何規則,則拋出異常。
- 初始化腳本執行引擎。根據腳本的類型,通過Java的ScriptEngineManager創建不同的腳本執行器並緩存起來。
// ScriptRouter#constructor
public ScriptRouter(URL url) {
// 初始化參數。獲取規則的腳本類型、路由優先級
this.url = url;
String type = url.getParameter(Constants.TYPE_KEY);
this.priority = url.getParameter(Constants.PRIORITY_KEY, 0);
String rule = url.getParameterAndDecoded(Constants.RULE_KEY);
// 如果沒有設置腳本,則默認設置爲JavaScript類型
if (type == null || type.length() == 0) {
type = Constants.DEFAULT_SCRIPT_TYPE_KEY;
}
// 如果沒有解析到任何規則,則拋出異常
if (rule == null || rule.length() == 0) {
throw new IllegalStateException(new IllegalStateException("route rule can not be empty. rule:" + rule));
}
// 初始化腳本執行引擎
ScriptEngine engine = engines.get(type);
if (engine == null) {
// 根據腳本的類型,通過Java的ScriptEngineManager創建不同的腳本執行器
engine = new ScriptEngineManager().getEngineByName(type);
if (engine == null) {
throw new IllegalStateException(new IllegalStateException("Unsupported route rule type: " + type + ", rule: " + rule));
}
// 緩存腳本執行器
engines.put(type, engine);
}
this.engine = engine;
this.rule = rule;
}
- route方法則負責具體的過濾邏輯執行。
route方法的核心邏輯就是調用腳本引擎,獲取執行結果並返回。主要是JDK腳本引擎相關知識,不會涉及具體的過濾邏輯,因爲邏輯已經下沉到用戶自定義的腳本里面了。
@Override
@SuppressWarnings("unchecked")
public <T> List<Invoker<T>> route(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException {
try {
List<Invoker<T>> invokersCopy = new ArrayList<Invoker<T>>(invokers);
Compilable compilable = (Compilable) engine;
// 構造要傳入的腳本參數
Bindings bindings = engine.createBindings();
bindings.put("invokers", invokersCopy);
bindings.put("invocation", invocation);
bindings.put("context", RpcContext.getContext());
CompiledScript function = compilable.compile(rule);
// 執行腳本
Object obj = function.eval(bindings);
if (obj instanceof Invoker[]) {
invokersCopy = Arrays.asList((Invoker<T>[]) obj);
} else if (obj instanceof Object[]) {
invokersCopy = new ArrayList<Invoker<T>>();
for (Object inv : (Object[]) obj) {
invokersCopy.add((Invoker<T>) inv);
}
} else {
invokersCopy = (List<Invoker<T>>) obj;
}
return invokersCopy;
} catch (ScriptException e) {
// 如果失敗,則忽略規則。返回invokers列表
logger.error("route error , rule has been ignored. rule: " + rule + ", method:" + invocation.getMethodName() + ", url: " + RpcContext.getContext().getUrl(), e);
return invokers;
}
}
總結
Dubbo的路由規則本質上只有條件路由和腳本路由,文件路由內部其實是讀取的文件中的腳本,最終調用腳本路由的路由方法,相對而言,路由規則比較容易理解。