Dubbo集群容错——Mock

本系列主要参考官网文档、芋道源码的源码解读和《深入理解Apache Dubbo与实战》一书。Dubbo版本为2.6.1。

文章内容顺序:

  1. 什么是Mock

  2. Mock的UML图

  3. Mock的代码逻辑

  • 3.1MockClusterWrapper
  • 3.2MockClusterInvoker
  • 3.3MockClusterInvoker#doMockInvoke
  • 3.4MockClusterInvoker#selectMockInvoker
  1. MockClusterInvoker#selectMockInvoker是怎么通过setAttachment来获得MockInvoker的?
  • 4.1 当Directory创建时,会注入Router,此时就会添加一个MockInvokerSelector,每当调用AbstractDirector#list方法时,就会调用这个MockInvokerSelector
  • 4.2MockInvokersSelector#route
  • 4.3 MockInvokersSelector#getMockedInvokers
  • 4.4 MockInvokersSelector#hasMockProviders
  • 4.5 那如果没有protocol为mock,又要进行本地伪装怎么办呢?
  • 4.6 再介绍下如果真的有配置protocol为mock的方法,会怎么样。
  1. MockInvoker

1. 什么是Mock

本地伪装:通常用于服务降级,比如某验权服务,当服务提供方全部挂掉后,客户端不抛出异常,而是通过 Mock 数据返回授权失败。
服务降级:可以通过服务降级功能,临时屏蔽某个出错的非关键服务,并定义降级后的返回策略。
推荐先阅读官网的这两个概念。
《Dubbo 用户指南 —— 本地伪装》
《Dubbo 用户指南 —— 服务降级》

2. Mock的UML图

image.png

以上类图分成两个部分:不要在意马赛克。
MockClusterWrapper + MockClusterInvoker+ MockClusterSelector
MockProtocol + MockInvoker

3. Mock的代码逻辑

3.1MockClusterWrapper

public class MockClusterWrapper implements Cluster {

    /**
     * 真正的 Cluster 对象
     */
    private Cluster cluster;

    public MockClusterWrapper(Cluster cluster) {
        this.cluster = cluster;
    }

    @Override
    public <T> Invoker<T> join(Directory<T> directory) throws RpcException {
        return new MockClusterInvoker<T>(directory,
                this.cluster.join(directory));
    }

}

注意:cluster 字段才是真正的 Cluster 对象(在Cluster章节介绍的那些)。因为 MockClusterWrapperDubbo SPI Wrapper类,所以对应的 Cluster 对象,都会被它所包装。
这边直接返回了一个MockClusterInvoker,那就进去看看吧。

3.2MockClusterInvoker

MockClusterInvokerDubbo Mock 的核心类,主要功能有三个:

  • 判断是否需要开启 Mock 机制,由invoke 方法完成。
  • 如果需要Mock,由 doMockInvoke方法完成,实际是委托给 Mock Invoker对象来执行。
  • 那么如何拿到这个Mock Invoker对象对象呢?则需要根据 MockInvokersSelector 过滤出对应的 Mock Invoker,由 selectMockInvoker方法完成,相当于交由MockInvokersSelector 完成了路由操作。
    public MockClusterInvoker(Directory<T> directory, Invoker<T> invoker) {
        this.directory = directory;
        this.invoker = invoker;
    }
    public Result invoke(Invocation invocation) throws RpcException {
        Result result;
        // 获得 "mock" 配置项,有多种配置方式
        String value = directory.getUrl().getMethodParameter(invocation.getMethodName(), Constants.MOCK_KEY, Boolean.FALSE.toString()).trim();
        //【第一种】无 mock
        if (value.length() == 0 || value.equalsIgnoreCase("false")) {
            // no mock
            // 调用原 Invoker ,发起 RPC 调用
            result = this.invoker.invoke(invocation);
        //【第二种】强制服务降级 https://dubbo.gitbooks.io/dubbo-user-book/demos/service-downgrade.html
        } else if (value.startsWith("force")) {
            if (logger.isWarnEnabled()) {
                logger.info("force-mock: " + invocation.getMethodName() + " force-mock enabled , url : " + directory.getUrl());
            }
            // force:direct mock
            // 直接调用 Mock Invoker ,执行本地 Mock 逻辑
            result = doMockInvoke(invocation, null);
        // 【第三种】失败服务降级 https://dubbo.gitbooks.io/dubbo-user-book/demos/service-downgrade.html
        } else {
            // fail-mock
            try {
                // 调用原 Invoker ,发起 RPC 调用
                result = this.invoker.invoke(invocation);
            } catch (RpcException e) {
                // 业务性异常,直接抛出
                if (e.isBiz()) {
                    throw e;
                } else {
                    if (logger.isWarnEnabled()) {
                        logger.info("fail-mock: " + invocation.getMethodName() + " fail-mock enabled , url : " + directory.getUrl(), e);
                    }
                    // 失败后,调用 Mock Invoker ,执行本地 Mock 逻辑
                    result = doMockInvoke(invocation, e);
                }
            }
        }
        return result;
    }

可以看到,当执行invoke时,会进行一下逻辑判断
第一种:无 Mock。只调用真正的 invoker 的 #invoke(invocation)方法,发起 RPC 调用,即不进行 Mock 逻辑。
第二种:"mock" 配置项以 "force" 开头,强制服务降级。直接调用 #doMockInvoke(invocation, null) 方法,调用Mock Invoker ,执行本地 Mock 逻辑。
第三种:服务失败服务降级
先调用真正的invoker#invoke(invocation)方法,发起 RPC 调用,当发生RpcException异常且为业务性异常时,直接抛出异常。如果不是业务性异常,调用#doMockInvoke(invocation, null) 方法,调用 Mock Invoker ,执行本地 Mock 逻辑。
Mock需要的配置如下:
<dubbo:reference id="demoService" check="false" interface="com.alibaba.dubbo.demo.DemoService" mock="com.alibaba.dubbo.demo.DemoServiceImplMock"/>

3.3MockClusterInvoker#doMockInvoke

    private Result doMockInvoke(Invocation invocation, RpcException e) {
        Result result;
        // 第一步,获得 Mock Invoker 对象
        Invoker<T> mInvoker;
        // 路由匹配 Mock Invoker 集合
        List<Invoker<T>> mockInvokers = selectMockInvoker(invocation);
        // 如果不存在,创建 MockInvoker 对象
        if (mockInvokers == null || mockInvokers.isEmpty()) {
            mInvoker = (Invoker<T>) new MockInvoker(directory.getUrl());
        // 如果存在,选择第一个
        } else {
            mInvoker = mockInvokers.get(0);
        }
        // 第二步,调用,执行本地 Mock 逻辑
        try {
            result = mInvoker.invoke(invocation);
        } catch (RpcException me) {
            if (me.isBiz()) {
                result = new RpcResult(me.getCause());
            } else {
                throw new RpcException(me.getCode(), getMockExceptionMessage(e, me), me.getCause());
            }
        } catch (Throwable me) {
            throw new RpcException(getMockExceptionMessage(e, me), me.getCause());
        }
        return result;
    }

第一步:先想办法获得 MockInvoker 对象。
调用 #selectMockInvoker(invocation) 方法,路由匹配 Mock Invoker 集合。
存在,创建 MockInvoker 对象。
不能再,选择第一个 Mock Invoker 对象。
第二步:调用 MockInvoker#invoke(invocation) 方法,执行本地 Mock 逻辑。

我们先暂且不考虑MockInvoker的实现到底是怎么样的,他里面是怎么调用invoke的,我们来看看selectMockInvoker是怎么得到mockInvokers的。

3.4MockClusterInvoker#selectMockInvoker

    private List<Invoker<T>> selectMockInvoker(Invocation invocation) {
        List<Invoker<T>> invokers = null;
        // TODO generic invoker?
        if (invocation instanceof RpcInvocation) {
            // 存在隐含契约(虽然在接口声明中增加描述,但扩展性会存在问题.同时放在 attachment 中的做法需要改进
            ((RpcInvocation) invocation).setAttachment(Constants.INVOCATION_NEED_MOCK, Boolean.TRUE.toString());
            // directory 根据 invocation 中 attachment 是否有 Constants.INVOCATION_NEED_MOCK,来判断获取的是 normal invokers or mock invokers
            try {
                invokers = directory.list(invocation);
            } catch (RpcException e) {
                if (logger.isInfoEnabled()) {
                    logger.info("Exception when try to invoke mock. Get mock invokers error for service:" + directory.getUrl().getServiceInterface() + ", method:" + invocation.getMethodName() + ", will contruct a new mock with 'new MockInvoker()'.", e);
                }
            }
        }
        return invokers;
    }

directory根据 invocation中 是否有 Constants.INVOCATION_NEED_MOCK ,来判断获取的是一个 normal invoker 还是一个 mock invoker
如果 directory#list返回多个Mock invoker ,只使用第一个 invoker .

在此之前,我想通过介绍通过调用invocation#setAttachment,怎么得到MockInvoker的。

4. MockClusterInvoker#selectMockInvoker是怎么通过setAttachment来获得MockInvoker的?

4.1 当Directory创建时,会注入Router,此时就会添加一个MockInvokerSelector,每当调用AbstractDirector#list方法时,就会调用这个MockInvokerSelector

下面的图是AbstractDirector#setRouters()调用的时机,可以看到当创建一个Directory的时候,就会进入到setRouters()方法中,这个方法每次都会添加一个MockInvokersSelector,而当我们调用AbstractDirector#list想获取Invokers时,就会轮询这个routers集合,必定会调用到MockInvokersSelector#route方法。知道了这点之后,就需要看看MockInvokersSelector#route方法的实现了。

image.png

image.png

image.png

4.2MockInvokersSelector#route

    public <T> List<Invoker<T>> route(final List<Invoker<T>> invokers, URL url, final Invocation invocation) throws RpcException {
        // 获得普通 Invoker 集合
        if (invocation.getAttachments() == null) {
            return getNormalInvokers(invokers);
        } else {
            // 获得 "invocation.need.mock" 配置项
            String value = invocation.getAttachments().get(Constants.INVOCATION_NEED_MOCK);
            // 获得普通 Invoker 集合
            if (value == null) {
                return getNormalInvokers(invokers);
            // 获得 MockInvoker 集合
            } else if (Boolean.TRUE.toString().equalsIgnoreCase(value)) {
                return getMockedInvokers(invokers);
            }
        }
        // 其它,不匹配,直接返回 `invokers` 集合
        return invokers;
    }

在这个方法里,MockClusterInvoker#selectMockInvoker设置的invocation.need.mock终于用到了,
这个方法attachmentsnullinvocation.need.mocknull,则返回 普通 Invoker 集合。(会过滤掉MockInvoker集合)
invocation.need.mock=true 则返回 MockInvoker 集合。
所以当我们没有设置Mock的时候,设置的这个MockInvokersSelector就相当于不存在。

4.3 MockInvokersSelector#getMockedInvokers

这里简单介绍下MockInvokersSelector#getMockedInvokers

   private <T> List<Invoker<T>> getMockedInvokers(final List<Invoker<T>> invokers) {
        // 不包含 MockInvoker 的情况下,直接返回 null
        if (!hasMockProviders(invokers)) {
            return null;
        }
        // 过滤掉普通 kInvoker ,创建 MockInvoker 集合
        List<Invoker<T>> sInvokers = new ArrayList<Invoker<T>>(1); // 一般情况就一个,所以设置了默认数组大小为 1 。
        for (Invoker<T> invoker : invokers) {
            if (invoker.getUrl().getProtocol().equals(Constants.MOCK_PROTOCOL)) {
                sInvokers.add(invoker);
            }
        }
        return sInvokers;
    }

4.4 MockInvokersSelector#hasMockProviders

再看一下MockInvokersSelector#hasMockProviders(invokers)的判别逻辑

    private <T> boolean hasMockProviders(final List<Invoker<T>> invokers) {
        boolean hasMockProvider = false;
        for (Invoker<T> invoker : invokers) {
            if (invoker.getUrl().getProtocol().equals(Constants.MOCK_PROTOCOL)) { // 协议为 "mock"
                hasMockProvider = true;
                break;
            }
        }
        return hasMockProvider;
    }

注意,上面方法传参中的invkers是这个接口方法不同机器的所有的invokers
上面的判别方式也很简单,判断这些invokersprotocol是否为mock

4.5 那如果没有protocolmock,又要进行本地伪装怎么办呢?

也很简单,在getMockedInvokers中会直接返回一个null,当doMockInvoke收到由selectMockInvoker(invocation)传达的mockInvoker,发现他为空,通过配置解析出来的url就直接创建一个MockInvoker对象,在里面进行本地伪装。

4.6 再介绍下如果真的有配置protocol为mock的方法,会怎么样。

final public class MockProtocol extends AbstractProtocol {

    public int getDefaultPort() {
        return 0;
    }

    public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
        throw new UnsupportedOperationException();
    }

    public <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException {
        return new MockInvoker<T>(url);
    }
}

export(Invoker<T> invoker)实现方法,不允许调用,直接抛出 UnsupportedOperationException 异常。
refer(Class<T> type, Url) 实现方法,引用创建MockInvoker对象。一般情况下,我们可以通过 dubbo-admin 运维平台或者直接向 Zookeeper写入静态 URL
顺带一提,这个protocol是在com.alibaba.dubbo.rpc.support,继承了AbstractProtocolMockProtocol,在Dubbo还有一个MockProtocol与此同名但是不在同一个包中。
从这个protocl的实现我们也可以看出来Mock是不允许provider来实现的,而且当Consumer调用refer实现时,与MockClusterInvoker#doMockInvoke一样,也是直接返回了一个MockInvoker

5. MockInvoker

最后我们终于可以来看一下这个MockInvoker的逻辑了

    public Result invoke(Invocation invocation) throws RpcException {
        if (invocation instanceof RpcInvocation) {
            ((RpcInvocation) invocation).setInvoker(this);
        }
        // 获得 `"mock"` 配置项,方法级 > 类级
        String mock = getUrl().getParameter(invocation.getMethodName() + "." + Constants.MOCK_KEY);
        if (StringUtils.isBlank(mock)) {
            mock = getUrl().getParameter(Constants.MOCK_KEY);
        }
        if (StringUtils.isBlank(mock)) { // 不允许为空
            throw new RpcException(new IllegalAccessException("mock can not be null. url :" + url));
        }
        // 标准化 `"mock"` 配置项
        mock = normalizedMock(URL.decode(mock));
        // 等于 "return " ,返回值为空的 RpcResult 对象
        if (Constants.RETURN_PREFIX.trim().equalsIgnoreCase(mock.trim())) {
            RpcResult result = new RpcResult();
            result.setValue(null);
            return result;
            // 以 "return " 开头,返回对应值的 RpcResult 对象
        } else if (mock.startsWith(Constants.RETURN_PREFIX)) {
            mock = mock.substring(Constants.RETURN_PREFIX.length()).trim();
            mock = mock.replace('`', '"');
            try {
                // 解析返回类型
                Type[] returnTypes = RpcUtils.getReturnTypes(invocation);
                // 解析返回值
                Object value = parseMockValue(mock, returnTypes);
                // 创建对应值的 RpcResult 对象,并返回
                return new RpcResult(value);
            } catch (Exception ew) {
                throw new RpcException("mock return invoke error. method :" + invocation.getMethodName() + ", mock:" + mock + ", url: " + url, ew);
            }
            // 以 "throw" 开头,抛出 RpcException 异常
        } else if (mock.startsWith(Constants.THROW_PREFIX)) {
            mock = mock.substring(Constants.THROW_PREFIX.length()).trim();
            mock = mock.replace('`', '"');
            if (StringUtils.isBlank(mock)) { // 禁止为空
                throw new RpcException(" mocked exception for Service degradation. ");
            } else { // user customized class
                // 创建自定义异常
                Throwable t = getThrowable(mock);
                // 抛出业务类型的 RpcException 异常
                throw new RpcException(RpcException.BIZ_EXCEPTION, t);
            }
            // 自定义 Mock 类,执行自定义逻辑
        } else {
            try {
                // 创建 Invoker 对象
                Invoker<T> invoker = getInvoker(mock);
                // 执行 Invoker 对象的调用逻辑
                return invoker.invoke(invocation);
            } catch (Throwable t) {
                throw new RpcException("Failed to create mock implemention class " + mock, t);
            }
        }
    }

invoke 执行服务降级,首先获取 mock 参数,并对 mock 参数进行处理,如去除 force:fail:前缀。Dubbo 服务降级有三种处理情况:

  • return:直接返回,可以是 emptynulltruefalsejson格式,由方式 parseMockValue 进行解析。
  • throw:直接抛出异常。如果没有指定异常,抛出 RpcException,否则抛出指定的 Exception。
  • xxxServiceMock:执行 xxxServiceMock 方法。如果 mock=true 或 mock=defalut则查找 xxxServiceMock 方法后执行,如果 mock=com.dubbo.testxxxService 则执行指定的方法。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,558评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,002评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,024评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,144评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,255评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,295评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,068评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,478评论 1 305
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,789评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,965评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,649评论 4 336
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,267评论 3 318
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,982评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,223评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,800评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,847评论 2 351