Dubbo笔记衍生篇⑦:异步场景下的问题

一、前言

本系列为个人Dubbo学习笔记衍生篇,是正文篇之外的衍生内容,内容来源于《深度剖析Apache Dubbo 核心技术内幕》, 过程参考官方源码分析文章。仅用于个人笔记记录。本文分析基于Dubbo2.7.0版本,由于个人理解的局限性,若文中不免出现错误,感谢指正。


本文作为下面两篇文章的衍生篇内容,部分内容参考了《深度剖析Apache Dubbo 核心技术内幕》。仅作为下面两篇文章的补充内容。如需阅读,建议先阅读正文内容。

  1. Dubbo笔记 ⑳ :消费者的异步调用
  2. Dubbo笔记 ㉑ :提供者的异步执行

二、过滤器链

1. 问题描述

以下内容摘自《深度剖析Apache Dubbo 核心技术内幕》:

在2.7.0版本之前,在消费端采用异步调用后,由于异步结果在异步线程(Dubbo框架线程模型线程池中的线程)中单独执行,因此DubboInvoker的invoke()方法在发起远端请求后,会将空的RpcResult对象返回Filter调用链,也就是说,Filter链上的所有Filter获取的远端调用结果都是null,最终null值也直接返回给调用方法。而真正的远端调用结果需要调用方从RpcContext获取future对象来获取,当真正远端结果返回时,已经不会再次走Filter链进行处理了。

在2.7.0版本之前,异步调用时序图如下所示:
在这里插入图片描述
在2.7.0版本之前,服务消费端的调用在发起远程调用DubboInvoker的invoke()方法前进入Filter链,如果是异步调用,则DubboInvoker的invoke()方法会创建一个DefaultFuture对象并设置到RpcContext中,接着返回一个空的RpcResult,然后该RpcResult会一层层返回到Filter链中的Filter,最终返回到业务调用的sayHello()方法,得到结果null。

当调用方要获取真正的响应结果时,则需要首先从RpcContext获取future对象,然后调用其get()方法进行等待。当服务提供方把服务处理结果写回调用方后,调用方I/O处理线程会把结果传递给调用方线程模型中的线程,从而把结果写入future对象,接着调用方线程就会从future的get()方法返回,以获取真正的执行结果。可以看到,当真正结果回来时并没有再次执行Filter链。

这里由于异步,最后一步的过滤器执行会是同步结果,即 null。那么 Filter 即会失效。

为了解决这个问题,在2.7.0版本中为Filter接口增加了回调接口onResponse:

@SPI
public interface Filter {

    Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException;

    default Result onResponse(Result result, Invoker<?> invoker, Invocation invocation) {
        return result;
    }
}

由此可知,所有实现了Filter扩展接口的实现类都会含有这个默认的onResponse()方法,并且默认返回传递过来的result。如果有需要的话,实现类可以重写onResponse()方法,比如FutureFilter。


总结上面的说法:
对于一个 Filter ,我们可以在调用前做操作,这里暂且成为请求过滤,也可以在响应后做操作,这里暂且成为响应过滤。如下:

	public Result invoke(Invoker<?> invoker, Invocation inv) throws RpcException {
        System.out.println("请求过滤操作");
        Result result = invoker.invoke(inv);
        System.out.println("响应过滤操作 result  = " + result );
	}

对于请求过滤, 无论同步还是异步,都可以顺利执行。但是对于异步调用,其真正结果并非立即返回,这里返回的result可能只是一个无意义的结果。则如上代码中的响应过滤操作则不能发挥真正的过滤作用。为了解决该问题,Dubbo为Filter 接口增加了 Filter#onResponse 方法供异步调用或异步执行结束后回调,如下:

public interface Filter {

    Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException;
	// 默认直接返回result
    default Result onResponse(Result result, Invoker<?> invoker, Invocation invocation) {
        return result;
    }

}

2. 实现方案

下面我们来介绍一下过滤器链在异步情况下的处理流程。

2.1 过滤器链的创建

我们这里首先看一下Dubbo的 过滤器链创建过程:Filter链是在ProtocolFilterWrapper的buildInvokerChain()方法中建立并使用Invoker串联的,其实现如下。无论是消费者还是提供者都会经由此方法创建过滤器链并使用。

    private static <T> Invoker<T> buildInvokerChain(final Invoker<T> invoker, String key, String group) {
        Invoker<T> last = invoker;
        // 获取适用的过滤器
        List<Filter> filters = ExtensionLoader.getExtensionLoader(Filter.class).getActivateExtension(invoker.getUrl(), key, group);
        
        if (!filters.isEmpty()) {
        	// 生成过滤器链
            for (int i = filters.size() - 1; i >= 0; i--) {
                final Filter filter = filters.get(i);
                final Invoker<T> next = last;
                last = new Invoker<T>() {

                    @Override
                    public Class<T> getInterface() {
                        return invoker.getInterface();
                    }

                    @Override
                    public URL getUrl() {
                        return invoker.getUrl();
                    }

                    @Override
                    public boolean isAvailable() {
                        return invoker.isAvailable();
                    }

                    @Override
                    public Result invoke(Invocation invocation) throws RpcException {
                    	// 1. 执行过滤器
                        Result result = filter.invoke(next, invocation);
                        // 2. 如果返回结果是 AsyncRpcResult 则表明是异步调用。
                        if (result instanceof AsyncRpcResult) {
                            AsyncRpcResult asyncResult = (AsyncRpcResult) result;
                            // 3. 通过 AsyncRpcResult#thenApplyWithContext 方法等待调用结束后做进一步操作
                            asyncResult.thenApplyWithContext(r -> filter.onResponse(r, invoker, invocation));
                            return asyncResult;
                        } else {
                        	// 4. 同步调用直接调用 onResponse
                            return filter.onResponse(result, invoker, invocation);
                        }
                    }

                    @Override
                    public void destroy() {
                        invoker.destroy();
                    }

                    @Override
                    public String toString() {
                        return invoker.toString();
                    }
                };
            }
        }
        return last;
    }

这里我们按照代码注释顺序来分析:

  1. 代码1 执行执行过滤器链,需要注意,每个过滤器执行的结果将作为下一个过滤器的入参执行。

  2. 根据返回结果类型判断是同步请求还是异步请求。返回类型是 AsyncRpcResult 则为异步调用,否则为同步调用。

  3. 如果是异步请求则通过 asyncResult.thenApplyWithContext(r -> filter.onResponse(r, invoker, invocation)); 来对 AsyncRpcResult#resultFuture 添加 thenApply操作。
    这里 asyncResult.thenApplyWithContext 实现如下:

    	// 把发起远程调用时的上下文恢复,并保存当前上下文
    	private Function<Result, Result> beforeContext = (result) -> {
            tmpContext = RpcContext.getContext();
            tmpServerContext = RpcContext.getServerContext();
            RpcContext.restoreContext(storedContext);
            RpcContext.restoreServerContext(storedServerContext);
            return result;
        };
    	// 恢复保存的响应线程的上下文
        private Function<Result, Result> afterContext = (result) -> {
            RpcContext.restoreContext(tmpContext);
            RpcContext.restoreServerContext(tmpServerContext);
            return result;
        };
        
        public void thenApplyWithContext(Function<Result, Result> fn) {
        	// 1. resultFuture.thenApply : 给 resultFuture 添加 thenApply 操作
        	// 2. compose(beforeContext) : 在执行 fn 前将上下文切换为 发起调用时的上下文
        	// 3. andThen(afterContext) : 在执行后将 上下文恢复为响应结果时的上下文
            this.resultFuture = resultFuture.thenApply(fn.compose(beforeContext).andThen(afterContext));
        }
    

    解释一下:由于是异步调用,所以发起调用和响应调用结果并非同一时间,可能会造成发起调用时的上下文可能和响应调用结果时的上下文不同,所以 thenApplyWithContext() 的作用是等得到响应结果后首先执行beforeContext函数,以恢复发起远程调用时线程对应的上下文对象,并保存响应线程对应的上下文对象,随后执行 fn 逻辑,执行完成后再通过 beforeContext 恢复响应上下文。其中storedContext 和 storedServerContext 是 AsyncResult 在创建时保存的上下文内容。
    所以这里的逻辑是:消费者端接收到异步调用的响应,首先恢复异步调用发起时的上下文,随后执行 filter.onResponse 方法,执行结束后将上下文切换回响应时的上下文。

  4. 同步调用,直接执行 filter.onResponse 方法即可。

2.2 消费者异步调用

提供者对于过滤器链的处理依赖于FutureAdapter 中 DefaultFuture 的回调
消费者在发起异步调用时,会创建一个 FutureAdapter保存到上下文中,并且会返回一个 AsyncRpcResult 或 SimpleAsyncRpcResult。这里我们无需关注AsyncRpcResult 和SimpleAsyncRpcResult 的区别,我们关注的是其构造方式,对于消费者端,其构造在 DubboInvoker#doInvoke 中,如下:

     if (isAsyncFuture) {
            // register resultCallback, sometimes we need the async result being processed by the filter chain.
            result = new AsyncRpcResult(futureAdapter, futureAdapter.getResultFuture(), false);
        } else {
            result = new SimpleAsyncRpcResult(futureAdapter, futureAdapter.getResultFuture(), false);
        }

AsyncRpcResult 构造函数具体实现如下:

 	public AsyncRpcResult(CompletableFuture<Object> future, final CompletableFuture<Result> rFuture, boolean registerCallback) {
        if (rFuture == null) {
            throw new IllegalArgumentException();
        }
        resultFuture = rFuture;
        // 消费者端传入false,则不会注册回调方法,提供者端 为true
        if (registerCallback) {
            future.whenComplete((v, t) -> {
                RpcResult rpcResult;
                if (t != null) {
                    if (t instanceof CompletionException) {
                        rpcResult = new RpcResult(t.getCause());
                    } else {
                        rpcResult = new RpcResult(t);
                    }
                } else {
                    rpcResult = new RpcResult(v);
                }
                // instead of resultFuture we must use rFuture here, resultFuture may being changed before complete when building filter chain, but rFuture was guaranteed never changed by closure.
                rFuture.complete(rpcResult);
            });
        }
        this.valueFuture = future;
        // employ copy of context avoid the other call may modify the context content
        // 保存发起调用时的上下文
        this.storedContext = RpcContext.getContext().copyOf();
        this.storedServerContext = RpcContext.getServerContext().copyOf();
    }

我们这里需要关注的是对于消费者来说:

  • AsyncRpcResult#resultFuture 被赋值为 futureAdapter.getResultFuture(),即 AsyncRpcResult#resultFuture 等同于 FutureAdapter#resultFuture
  • AsyncRpcResult#valueFuture 被赋值为 futureAdapter,即 AsyncRpcResult#valueFuture 等同于 FutureAdapter 本身。

那么我们再看一下 FutureAdapter 的构造函数,我们这里知道,当异步调用结果返回时,会触发future 的callback 方法,也就是会执行ResponseCallback#done 方法。

    public FutureAdapter(ResponseFuture future) {
        this.future = future;
        this.resultFuture = new CompletableFuture<>();
        future.setCallback(new ResponseCallback() {
            @Override
            public void done(Object response) {
                Result result = (Result) response;
                // 代码1 :触发过滤器链
                FutureAdapter.this.resultFuture.complete(result);
                V value = null;
                try {
                    value = (V) result.recreate();
                } catch (Throwable t) {
                    FutureAdapter.this.completeExceptionally(t);
                }
                // 代码2 :触发对于value 的操作,如主线程中等待获取结果的回调
                FutureAdapter.this.complete(value);
            }

            @Override
            public void caught(Throwable exception) {
                FutureAdapter.this.completeExceptionally(exception);
            }
        });
    }

我们这里关注一下代码1,当结果返回时,触发 FutureAdapter#resultFuture 的操作,入参为异步调用结果值。而 我们上面提到 FutureAdapter#resultFuture 等同于AsyncRpcResult#resultFuture。那么也就是说这里会触发AsyncRpcResult#resultFuture 的操作。而在上面过滤器链的创建过程中,我们提到,如果是异步调用则会通过 asyncResult.thenApplyWithContext 为AsyncRpcResult#resultFuture 添加 thenApply 操作,所以这里会调用 AsyncRpcResult#resultFuture 的 thenApply 操作,即实现了当异步调用结果返回时会执行了 Filter#onResponse 方法来完成响应过滤。

我们这里总结一下:

  1. 当消费者发起异步调用时,返回类型是 AsyncRpcResult。所以当构建过滤器链时,Filter 判断当前返回类型是 AsyncRpcResult,并不会直接执行 Filter#onResponse( 同步调用会直接执行 Filter#onResponse ),而是向AsyncRpcResult#resultFuture 添加 thenApply 操作,而操作的具体内容就是 执行 Filter#onResponse 方法。
  2. 当异步调用结果返回时,触发 FutureAdapter 中 DefaultFuture 的回调,在其回调中,会触发AsyncRpcResult#resultFuture 的操作,从而触发了 Filter 设置的 thenApply 操作,执行了 Filter#onResponse 方法。

2.3 提供者异步执行

提供者对于过滤器链的处理依赖于AsyncRpcResult 中的回调

首先我们来看一下提供者异步执行时 AsyncRpcResult 的创建场景, 如下:

	//  AbstractProxyInvoker#invoke
    @Override
    public Result invoke(Invocation invocation) throws RpcException {
        RpcContext rpcContext = RpcContext.getContext();
        try {
        	// 1. 调用代理类执行指定方法。这里 会调用JavassistProxyFactory#getInvoker 中创建的匿名类调用的 doInvoke  方法
            Object obj = doInvoke(proxy, invocation.getMethodName(), invocation.getParameterTypes(), invocation.getArguments());
            
            if (RpcUtils.isReturnTypeFuture(invocation)) {
            // 2. 如果 future_returntype 属性为 true。则认为方法返回类型是 CompletableFuture。返回 AsyncRpcResult
                return new AsyncRpcResult((CompletableFuture<Object>) obj);
            } else if (rpcContext.isAsyncStarted()) { // ignore obj in case of RpcContext.startAsync()? always rely on user to write back.
            	// 3. 这里判断是否启用了 AsyncContext 上下文, 如果启用了也会返回 AsyncRpcResult
                return new AsyncRpcResult(((AsyncContextImpl)(rpcContext.getAsyncContext())).getInternalFuture());
            } else {
            	// 4. 返回同步执行结果
                return new RpcResult(obj);
            }
        } catch (InvocationTargetException e) {
            // TODO async throw exception before async thread write back, should stop asyncContext
            if (rpcContext.isAsyncStarted() && !rpcContext.stopAsync()) {
                logger.error("Provider async started, but got an exception from the original method, cannot write the exception back to consumer because an async result may have returned the new thread.", e);
            }
            return new RpcResult(e.getTargetException());
        } catch (Throwable e) {
            throw new RpcException("Failed to invoke remote proxy method " + invocation.getMethodName() + " to " + getUrl() + ", cause: " + e.getMessage(), e);
        }
    }

这里需要注意:

  1. AsyncRpcResult 构造时的入参都是两种异步执行时产生的 CompletableFuture。当异步执行结束后,会直接更新 CompletableFuture#result 的值。

  2. 在上面的代码中,我们看到AsyncRpcResult 的创建时通过如下构造函数。这里需要注意,registerCallback = true,也就是会注册 AsyncRpcResult 中的回调。

      	public AsyncRpcResult(CompletableFuture<Object> future) {
            this(future, true);
        }
      	public AsyncRpcResult(CompletableFuture<Object> future, boolean registerCallback) {
            this(future, new CompletableFuture<>(), registerCallback);
        }
    

综上,当提供者发起异步执行后,会创建 AsyncRpcResult 实例并且将 可以感知最终结果的 CompletableFuture作为入参传入,并且注册了回调。回调如下:

public AsyncRpcResult(CompletableFuture<Object> future, final CompletableFuture<Result> rFuture, boolean registerCallback) {
        if (rFuture == null) {
            throw new IllegalArgumentException();
        }
        // 1. resultFuture  = rFuture
        resultFuture = rFuture;
        if (registerCallback) {
            future.whenComplete((v, t) -> {
                RpcResult rpcResult;
                if (t != null) {
                    if (t instanceof CompletionException) {
                        rpcResult = new RpcResult(t.getCause());
                    } else {
                        rpcResult = new RpcResult(t);
                    }
                } else {
                    rpcResult = new RpcResult(v);
                }
                // 2. 触发过滤器链的操作
                rFuture.complete(rpcResult);
            });
        }
        // 2. valueFuture  = future
        this.valueFuture = future;
        // employ copy of context avoid the other call may modify the context content
        this.storedContext = RpcContext.getContext().copyOf();
        this.storedServerContext = RpcContext.getServerContext().copyOf();
    }

我们上面提到过 入参的 CompletableFuture 可以直接感知到异步执行是否结束。当异步执行结束后,会回调 CompletableFuture#whenComplete 方法,即回调中的 future.whenComplete 方法。而在 future.whenComplete 方法中会将 执行结果封装为 RpcResult。再通过 代码2 来触发 rFuture.complete(rpcResult); 的后续操作。

需要注意:在代码1 中 resultFuture 被赋值为 rFuture,即此时 resultFuture = rFuture。而在上面我们提到过,过滤器链在创建时,如果返回值是 AsyncRpcResult,则并不会直接执行 Filter#onResponse( 同步调用会直接执行 Filter#onResponse ),而是向AsyncRpcResult#resultFuture 添加 thenApply 操作,而操作的具体内容就是 执行 Filter#onResponse 方法。所以在生成过滤器链时也会将 resultFuture(即rFuture ) 添加 thenApply 操作。即代码2这里会触发过滤器链中的添加的 then 操作,从而调用 Filter#onResponse 方法,完成异步执行的过滤器链的响应调用。

三、上下文对象传递

在服务提供端使用异步执行之前,业务可以直接在服务提供方服务代码内使用RpcContext.getContext获取上下文对象,进而可以访问其中保存的内容:

    @SneakyThrows
    @Override
    public String sayHello(String msg) {
    	// 输出上下文中的 context 数据
        System.out.println("context = " + RpcContext.getContext().getAttachment("context"));
        return "sayHello : " + msg;
    }

但在服务提供端使用异步执行后,由于真正执行服务处理的是另外的线程,而RpcContext内部的ThreadLocal变量是不能跨线程访问的,因此在启动异步执行后,需要业务先保存上下文对象,然后在业务线程里再访问,如下:

    @Override
    public CompletableFuture<String> sayHello2(String msg) {
        final RpcContext rpcContext = RpcContext.getContext();
        return CompletableFuture.supplyAsync(new Supplier<String>() {
            @SneakyThrows
            @Override
            public String get() {
                System.out.println("context = " + rpcContext.getAttachment("context"));
                return "sauHello2 " + msg;
            }
        }, POOL);
    }

如果使用了AsyncContext方式的异步执行,则可以方便地使用signalContextSwitch()方法来实现Context的切换,如下:

    @SneakyThrows
    @Override
    public String sayHello(String msg) {
        // 开启异步
        AsyncContext asyncContext = RpcContext.startAsync();
        // 这里由于处理业务的线程是 POOL 提供,所以Dubbo 线程不会被阻塞
        POOL.execute(new Runnable() {
            @Override
            public void run() {
                // 如果要使用上下文,则必须要放在第一句执行
                asyncContext.signalContextSwitch();
                System.out.println("context = " + RpcContext.getContext().getAttachment("context"));
                asyncContext.write("sayHello : " + msg);
            }
        });
        return null;
    }

以上:内容部分参考
《深度剖析Apache Dubbo 核心技术内幕》
https://dubbo.apache.org/zh/docs/v2.7/dev/source/
如有侵扰,联系删除。 内容仅用于自我记录学习使用。如有错误,欢迎指正

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

猫吻鱼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值