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的路由规则本质上只有条件路由和脚本路由,文件路由内部其实是读取的文件中的脚本,最终调用脚本路由的路由方法,相对而言,路由规则比较容易理解。

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