Dubbo 3.0 !提升不止一点点!

通信框架异步发送请求消息,请求消息发送成功后,返回代表业务结果的 CompletableFuture 给业务线程。之后对于 Future 的处理,根据调用类型会有所区别:

  • 对于同步请求(如上图体现的场景),业务线程会调用 future.get 同步阻塞等待结果,当收到网络层返回的业务结果后,future.get 返回并最终将结果传递给调用发起方。
  • 对于异步请求,业务线程不会调用 future.get,而是将 future 保存在调用上下文或者直接返回给调用者,同时会为 future 注册回调监听器,以便当真正的业务结果从通信层返回时监听器可以对结果做进一步的处理。

接下来具体看一下一次异步 Dubbo RPC 请求的调用流程:

(1)消费方面向 Proxy 代理编程,发出调用请求,请求经过 Filter 链向下传递。

(2)Invoker.invoke() 将请求异步转发给网络层,并收到代表返回结果的 Future。

(3)Future 被包装到 Result,转而由 Result 代表这次远程调用的结果(由于 Result 的异步属性,此时它可能并不包含真正的返回值)。

(4)Result 继续沿着调用链返回,在经过每个 Filter 时,Filter 可选择注册 Listener 监听器,以便在业务结果返回时执行结果预处理。

(5)最终 Proxy 调用 result.recreate() 将结果返回给消费者:

  • 如果方法是 CompletableFuture 签名,则返回 Future;
  • 如果方法是普通同步签名,则返回对象默认值,Future 可通过 RpcContext 拿到;

(6)调用方在拿到代表异步业务结果的 Future 后,可选择注册回调监听器,以监听真正的业务结果返回。

同步调用和异步调用基本上是一致的,并且也是走的回调模式,只是在链路返回之前做了一次阻塞 get 调用,以确保在收到实际结果时再返回。Filter 在注册 Listener 时由于 Future 已处于 complete 状态,因此会同时触发回调 onResponse()/onError()。

关于流程图中提到的 Result,Result 在 Dubbo 的一次 RPC 调用中代表返回结果,在 3.0 中 Result 自身增加了代表状态的接口,类似 Future 现在 Result 可以代表一次未完成的调用。

要让 Result 具备代表异步返回结果的能力,有两中方式来实现:

(1)Result is a Future,在 Java 8 中更合理的方式是继承 CompletionStage 接口。

(2)让 Result 实例持有 Future 实例,与 1 的区别即是设计中选用“继承”还是“组合”。

同时,为了让 Result 更直观的体现其异步结果的特性,也为了方便面向 Result 接口编程,我们可以考虑为Result增加一些异步接口:

Filter SPI

Filter 是 Dubbo 预置的拦截器扩展 SPI,用来做请求的预处理、结果的后处理,框架本身内置了一些拦截器实现,而从用户层面,我相信这个 SPI 也应该是被扩展最多的一个。在 3.0 版本中,Filter 回归单一职责的设计模式,将回调接口单独提取到 Listener 中。

以上是 Filter 的 SPI 定义,Filter 的核心定义中只有一个 invoke() 方法用来传递调用请求。

同时,增加了一个新的回调接口 Listener,每个 Filter 实现可以定义自己的 Listenr 回调器,从而实现对返回结果的异步监听,参考以下是为 MonitorFilter 增加的 Listener 回调实现:

泛化调用异步接口支持

为了更直观的做异步调用,泛化接口新增了

接口:

这样,当我们想做异步调用时,就可以直接这样使用:

更具体用例请参见《泛化调用示例》

https://github.com/apache/incubator-dubbo-samples/tree/3.x/dubbo-samples-generic/dubbo-samples-generic-call

异步与性能

组要注意的是,框架内部的异步实现本身并不能提高单次调用的性能,相反,由于线程切换和回调逻辑的存在,异步反而可能会导致单次调用性能的下降,但是异步带来的优势是能减少对资源的占用,提升整个系统的并发程度和吞吐量,这点对于 RPC 这种需要处理网络延迟的场景非常适用。更多关于异步化设计的好处,请参考其他异步化原理介绍相关文章。

二、响应式编程支持

响应式编程让开发者更方便地编写高性能的异步代码,很可惜,在之前很长一段时间里,dubbo 并不支持响应式编程,简单来说,dubbo 不支持在 rpc 调用时使用 Mono/Flux 这种流对象(reative-stream 里流的概念),给用户使用带来了不便。(关于响应式编程更详细的信息请参见这里:

http://reactivex.io/)。

RSocket 是一个开源的支持 reactive-stream 语义的网络通信协议,他将 reative 语义的复杂逻辑封装起来了,使得上层可以方便实现网络程序。(RSocket详细资料请参见这里:http://rsocket.io/)。

dubbo 在 3.0.0-SNAPSHOT 版本里基于 RSocket 对响应式编程进行了简单的支持,用户可以在请求参数和返回值里使用 Mono 和 Flux 类型的对象。下面我们给出使用范例,(范例源码可以在这里获取:

https://github.com/apache/incubator-dubbo-samples/tree/3.x/dubbo-samples-rsocket)。

首先定义接口如下:

然后实现该 demo 接口:

然后配置并启动服务端,注意协议名字填写 rsocket:

然后配置并启动消费者消费者如下, 注意协议名填写 rsocket:

可以看到配置上除了协议名使用 rsocket 以外其他并没有特殊之处。

实现原理

以前用户并不能在参数或者返回值里使用 Mono/Flux 这种流对象(reative-stream 里的流的概念)。因为流对象自带异步属性,当业务把流对象作为参数或者返回值传递给框架之后,框架并不能将流对象正确的进行序列化。

dubbo 基于 RSocket 实现了 reative 支持。RSocket 将 reative 语义的复杂逻辑封装起来了,给上层提供了简洁的抽象如下:

我们只需要在此基础上添加我们的 rpc 逻辑即可。

  • 从客户端视角看,框架建立连接之后,只需要将请求信息编码到 Payload 里,然后通过 requestStream 方法即可向服务端发起请求。
  • 从服务端视角看,rsocket 收到请求之后,会调用我们实现的 requestStream 方法,我们从 Payload 里解码得到请求信息之后,调用业务方法,然后拿到 Flux 类型的返回值即可。
  • 需要注意的是业务返回值一般是 Flux,而 RSocket 要求的是 Flux,所以我们需要通过 map operator 拦截业务数据,将 BizDO 编码为 Payload 才可以递交给我 RSocket。而 RSocket 会负责数据的传输和 reative 语义的实现。

经过上面的分析,我们知道了 Dubbo 如何基于 RSocket 实现了响应式编程的支持。有了响应式编程支持,业务可以更加方便的实现异步逻辑。

小结

当前 Dubbo 3.0 将提供具备当代特性(如响应性编程)的相关支持,同时汲取阿里内部 HSF 的设计长处来实现两者的融合,当前预览版的很多地方还在探讨中,希望大家能够积极反馈,我们都会虚心学习并参考。

Dubbo 3.0 sample @GitHub:

https://github.com/apache/incubator-dubbo-samples/tree/3.x

本文作者:

覃柳杰(花名:未宇), Github ID: qinliujie,阿里巴巴中间件开发,Dubbo 开源项目 PMC,参与 HSF2.2和 Dubbo3.0 的设计和开发。

吕仁琦(花名:空冥),Github ID: jefflv,阿里巴巴中间件开发,Dubbo 开源项目 commiter,参与了内部 HSF2.0 的设计和开发。

刘军(花名:陆龟),阿里巴巴中间件高级开发工程师,Apache Dubbo (Incubating)PPMC,深度参与 Dubbo 项目开发,主要贡献者之一。

谢育能(花名:思邪),阿里巴巴中间件开发,Dubbo 3.0 开源项目的响应式模块的负责人,参与了内部 HSF2.2 的设计和开发。

9