Davids原理探究:Dubbo路由實現原理

Dubbo路由實現原理

Dubbo的路由分爲條件路由、文件路由和腳本路由,對應的dubbo-admin中三種不同的規則配置方式。條件路由是用戶使用Dubbo定義的語法規則去寫的路由規則;文件路由則需要用戶提交一個文件,裏面寫着對應的路由規則,框架基於文件讀取對應的規則;腳本路由則是使用JDK自身的腳本引擎解析路由規則腳本,所有JDK腳本引擎支持的腳本都能解析,默認是JavaScript。

Dubbo路由總體結構

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規則進行過濾。

流程解析

  1. 校驗:如果規則沒有啓用,則直接返回;如果傳入的Invoker爲空,則直接返回;如果沒有任何whenRule匹配,即沒有匹配規則,則直接發怒會傳入的Invoker列表;如果whenRule有匹配,但是thenRule爲空,即沒有匹配上規則的Invoker,則返回空
  2. 匹配:遍歷Invoker列表,通過thenRule找出所有符合規則的Invoker介入result集合。
  3. 返回:如果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都過濾。另外,腳本路由中沒有看到沙箱約束,因此會由注入的風險。

  1. 構造方法主要負責一些初始化工作。
    1. 初始化參數。獲取規則的腳本類型、路由優先級。如果沒有設置腳本,則默認設置爲JavaScript類型,如果沒有解析到任何規則,則拋出異常。
    2. 初始化腳本執行引擎。根據腳本的類型,通過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;
}
  1. 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的路由規則本質上只有條件路由和腳本路由,文件路由內部其實是讀取的文件中的腳本,最終調用腳本路由的路由方法,相對而言,路由規則比較容易理解。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章