-
Notifications
You must be signed in to change notification settings - Fork 794
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
two new blogs about dubbo-async (#299)
- Loading branch information
Showing
8 changed files
with
200 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
--- | ||
title: Dubbo客户端异步接口的实现背景和实践 | ||
keywords: Dubbo, 异步, Reactive | ||
description: Dubbo客户端异步接口的实现背景和实践 | ||
--- | ||
|
||
# Dubbo客户端异步接口的实现背景和实践 | ||
|
||
## 铺垫 | ||
|
||
|
||
|
||
![image | left](../../img/blog/dubboasyn_client/1.png "") | ||
|
||
|
||
先简单介绍下一次完整的Dubbo调用所经历的线程阶段。几个信息这里罗列下 | ||
1. Biz~代表业务线程,即便是业务逻辑处理所处的线程,Biz~线程池可能是业务自己创建维护,大多数的可能是系统框架自身管理的(比如web型的业务系统跑在Tomcat容器下,Biz~线程就是Tomcat维护);IO~代表网络数据处理线程,是IO框架(比如Netty,Grizzly)创建维护,Dubbo Remoting所默认Netty实现是NioEventloopLoopGroup;另外按照Channel与IO线程的绑定关系,也可以直接把IO~看成一个可接受事件消息的Channel。像Biz和IO这样的异步处理阶段在JDK8中有个很精确地抽象描述,叫CompletionStage。 | ||
|
||
2. 大家知道,线程与线程之间做数据通信的方式是共享变量,Biz和IO两个stage之间的数据通信是Queue,具体到Dubbo实现,在客户端一侧的实现(即上图中用1所标注的步骤)中Biz是通过向EventLoop的LinkedBlockingQueue放置一个Task,而EventLoop有对应的Thread会不停的迭代Queue来执行Task中所包含的信息,具体代码可以看SingleThreadEventExecutor(顺便提下,Netty中默认是用无上限的LinkedBlockingQueue,在Biz的速率高于网络速率情况下,似乎好像有Memory Leak的风险)。 | ||
|
||
3. 如上图所示,标准的一次RPC调用经过了图中所示的1,2,3,4的四次消息(事件)传递,分别是客户端业务线程到IO线程的请求发出,服务端IO线程到业务逻辑线程的__请求接受,__服务端处理完成后由业务逻辑线程到IO线程的响应写出,客户端收到结果后从IO线程到业务逻辑的响应处理。除了1与4之间一般需要维护响应和请求的映射对应关系,四次的事件处理都是完全独立的,所以一次RPC调用天然是异步的,而同步是基于异步而来。 | ||
|
||
|
||
## 客户端异步 | ||
|
||
### 实现背景 | ||
在Java语言(其他语言不清楚)下一次本地接口的调用可以透明地通过代理机制转为远程RPC的调用,大多数业务方也比较喜欢这种与本地接口类似的编程方式做远程服务集成,所以虽然RPC内部天然是异步的,但使用Dubbo的用户使用最广泛的还是同步,而异步反而成为小众的使用场景。同步的优点是编程模型更加符合业务方的“传统”习惯,代价是在图中的1代表的请求发出事件后需要阻塞当前的Biz~线程,一直等到4代表的响应处理后才能唤醒。在这个短则微妙级别,长则秒级的1,2,3,4处理过程中都要阻塞Biz~线程,就会消耗线程资源,增加系统资源的开销。 | ||
|
||
所以,客户端异步的出发点是节省线程资源开销,代价是需要了解下异步的使用方式:)。在同步方式下API接口的返回类型是代表着某个业务类,而当异步情况下,响应返回与请求发出是完全独立的两个事件,需要API接口的返回类型变为上述中说的CompletionStage才是最贴合的,这是Dubbo在异步上支持的必然异步。回到最近的Dubbo发布版,是不改变接口的情况下,需要在服务创建时注册一个回调接口来处理响应返回事件。 | ||
|
||
下面以示例来说。 | ||
|
||
### 示例 | ||
|
||
事件通知的示例代码请参考:[https://github.com/dubbo/dubbo-samples/tree/master/dubbo-samples-notify](https://github.com/dubbo/dubbo-samples/tree/master/dubbo-samples-notify) | ||
|
||
事件通知允许 Consumer 端在调用之前、调用正常返回之后或调用出现异常时,触发 `oninvoke`、`onreturn`、`onthrow` 三个事件。 | ||
|
||
可以通过在配置 Consumer 时,指定事件需要通知的方法,如: | ||
|
||
```xml | ||
<bean id="demoCallback" class="com.alibaba.dubbo.samples.notify.impl.NotifyImpl" /> | ||
|
||
<dubbo:reference id="demoService" check="false" interface="com.alibaba.dubbo.samples.notify.api.DemoService" version="1.0.0" group="cn"> | ||
<dubbo:method name="sayHello" onreturn="demoCallback.onreturn" onthrow="demoCallback.onthrow"/> | ||
</dubbo:reference> | ||
``` | ||
|
||
其中,NotifyImpl 的代码如下: | ||
|
||
```java | ||
public class NotifyImpl implements Notify{ | ||
|
||
public Map<Integer, String> ret = new HashMap<Integer, String>(); | ||
|
||
public void onreturn(String name, int id) { | ||
ret.put(id, name); | ||
System.out.println("onreturn: " + name); | ||
} | ||
|
||
public void onthrow(Throwable ex, String name, int id) { | ||
System.out.println("onthrow: " + name); | ||
} | ||
} | ||
``` | ||
|
||
这里要强调一点,自定义 Notify 接口中的三个方法的参数规则如下: | ||
|
||
* `oninvoke` 方法参数与调用方法的参数相同; | ||
* `onreturn`方法第一个参数为调用方法的返回值,其余为调用方法的参数; | ||
* `onthrow`方法第一个参数为调用异常,其余为调用方法的参数。 | ||
|
||
上述配置中,`sayHello`方法为同步调用,因此事件通知方法的执行也是同步执行。可以配置 `async=true`让方法调用为异步,这时事件通知的方法也是异步执行的。特别强调一下,`oninvoke`方法不管是否异步调用,都是同步执行的。 | ||
|
||
### 实践建议 | ||
|
||
* <div data-type="alignment" data-value="justify" style="text-align:justify"> | ||
<div data-type="p">RPC调用后的逻辑非强依赖结果:异步回调是在客户端<strong>非强依赖服务端的结果</strong>情况下,是适用客户端的异步调用。</div> | ||
</div> | ||
|
||
* <div data-type="alignment" data-value="justify" style="text-align:justify"> | ||
<div data-type="p">rx场景:自从了解到reactive的编程模型后,认为只要编程思维能够拥抱reactive,并且业务模型的状态机设计能做适当的调整,任何场景下都比较适用异步来解决,从而得到更好的终端响应体验。 对于Dubbo来说,当下的异步接口模型是需要像reactive的模型接口做改进,才能使得用户更自然地适用异步接口。</div> | ||
</div> | ||
|
||
|
||
### 小结 | ||
|
||
* 客户端异步的出发点就是请求发出和响应处理本身为两个不同的独立事件,响应如何被处理和在哪个线程中处理等都是不需要和请求发出事件的业务逻辑线程做耦合绑定。 | ||
* 响应事件回调的处理逻辑在哪个线程中做处理是需要根据情况来选择。建议,如果回调逻辑比较简单,建议直接在IO线程中;如果包含了远程访问或者DB访问等IO型的__同步__操作,建议在独立的线程池做处理。 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
--- | ||
title: Dubbo客户端异步接口的实现背景和实践 | ||
keywords: Dubbo, 异步, Reactive | ||
description: Dubbo服务端异步接口的实现背景和实践 | ||
--- | ||
|
||
# Dubbo服务端异步接口的实现背景和实践 | ||
|
||
## 铺垫 | ||
建议先对Dubbo的处理过程中涉及的线程阶段先做个了解,具体可参考[Dubbo客户端异步接口的实现背景和使用场景](http://dubbo.apache.org/zh-cn/blog/dubboAsync_client.html)。 | ||
|
||
## 实现背景 | ||
有必要比较详细点的介绍下服务端的线程策略来加深用户在选择服务端异步的判断依据,同时有必要引出协程这一在服务端异步中常常会用到的“秘密武器”。 | ||
|
||
### 服务端的线程策略 | ||
Dubbo是支持多种NIO框架来做Remoting的协议实现,无论是Netty,Mina或者Grizzly,实现都大同小异,都是基于事件驱动的方式来做网络通道建立,数据流读取的,其中Grizzly对于线程策略介绍的为例,通常支持以下四种。Dubbo作为一个RPC框架,默认选择的是第一种策略,原因在于业务服务是CPU密集型还是IO阻塞性,是无法断定的,第一种策略是最保险的策略。当然,对于这几种策略有了了解后,再结合业务场景做针对性的选择是最完美的。 | ||
1. __Worker-thread | ||
策略__ | ||
|
||
最常用最普适的策略,其中IO线程将NIO事件处理委托给工作线程。 | ||
|
||
|
||
|
||
![workerthread-strategy.png | center | 371x244](../../img/blog/dubboasyn_server/1.png "") | ||
|
||
|
||
此策略具有很高的伸缩性。我们可以根据需要更改IO和worker线程池的大小,并且不存在在特定NIO事件处理期间可能发生的某些问题将影响在同一IO线程上注册的其他通道的风险。 | ||
缺点是有线程上下文切换的代价。 | ||
|
||
2. __Same-thread策略__ | ||
|
||
可能是最有效的策略。与第一种不同,同一线程处理当前线程中的NIO事件,避免了昂贵的线程上下文切换。 | ||
|
||
|
||
![samethread-strategy.png | center | 389x264](../../img/blog/dubboasyn_server/2.png "") | ||
|
||
|
||
考虑到这个策略可以调整IO线程池大小,是具备可伸缩性;缺点也很明显,它要求业务处理中一定不要有阻塞处理,因为它可能会阻止在同一个IO线程上发生的其他NIO事件的处理。 | ||
|
||
3. dynamic__策略__ | ||
|
||
如前所述,前两种策略具有明显的优点和缺点。但是,如果策略可以尝试在运行时根据当前条件(负载,收集的统计信息等)巧妙地交换它们,何如? | ||
|
||
|
||
![dynamic-strategy.png | center | 361x387](../../img/blog/dubboasyn_server/3.png "") | ||
|
||
|
||
这种策略可能会带来很多好处,能更好地控制资源,前提是不要使条件评估逻辑过载,防止评估判断的复杂性会使这种策略效率低下。 | ||
多说一句,希望大家对这个策略多留意一下,它可能是Dubbo服务端异步方式的最佳搭配。我也多扯个淡,这几天关注了些adaptive XX或者predictive XX,这里看到dynamic真是亲切,Dubbo作为产品级生产级的微服务解决方案,是必须既要adaptive,又要predictive,还要dynamic,哈哈。 | ||
|
||
4. __Leader-follower __ | ||
策略 | ||
|
||
|
||
|
||
![leaderfollower-strategy.png | center | 443x286](../../img/blog/dubboasyn_server/4.png "") | ||
|
||
此策略类似于第一种,但它不是将NIO事件处理传递给worker线程,而是通过将控制传递给Selector给工作线程,并将实际NIO事件处理当前IO线程中。这种策略其实是把worker和IO线程阶段做了混淆,个人不建议。 | ||
### 协程与线程 | ||
在CPU资源的管理上,OS和JVM的最小调度单位都是线程,业务应用通过扩展实现的协程包是可以具备独立的运行单位,事实上也是基于线程来做的,核心应该是遇到IO阻塞,或者锁等待时,保存上下文,然后切换到另一个协程。至于说的协程开销低,能更高效的使用CPU,这些考虑到协程库的用户态实现和上下文设计是支持的,但也建议大家结合实际业务场景做性能测试。 | ||
|
||
__在默认的Dubbo线程策略中,是有worker线程池来执行业务逻辑,但也常常会发生ThreadPool Full的问题,为了尽快释放worker线程,在业务服务的实现中会另起线程。代价是再次增加线程上下文切换,同时需要考虑链路级别的数据传送(比如tracing信息)和流控的出口控制等等。当然,如果Dubbo能够切换到Same-thread策略,再配合协程库的支持,服务端异步是一种值得推荐的使用方式。__ | ||
|
||
## 示例 | ||
通过示例来体验下Dubbo服务端异步接口。Demo代码请访问github之[https://github.com/dubbo/dubbo-samples/tree/master/dubbo-samples-notify](https://github.com/dubbo/dubbo-samples/tree/master/dubbo-samples-notify)。 | ||
```java | ||
public class AsyncServiceImpl implements AsyncService { | ||
|
||
@Override | ||
public String sayHello(String name) { | ||
System.out.println("Main sayHello() method start."); | ||
final AsyncContext asyncContext = RpcContext.startAsync(); | ||
new Thread(() -> { | ||
asyncContext.signalContextSwitch(); | ||
System.out.println("Attachment from consumer: " + RpcContext.getContext().getAttachment("consumer-key1")); | ||
System.out.println(" -- Async start."); | ||
try { | ||
Thread.sleep(500); | ||
} catch (InterruptedException e) { | ||
e.printStackTrace(); | ||
} | ||
asyncContext.write("Hello " + name + ", response from provider."); | ||
System.out.println(" -- Async end."); | ||
}).start(); | ||
System.out.println("Main sayHello() method end."); | ||
return "hello, " + name; | ||
} | ||
|
||
``` | ||
## 实践建议 | ||
* 不用迷信服务端异步 | ||
* 不要迷信服务端异步 | ||
* 服务端异步在Event-Driven或者Reactive面前基本是伪命题.<span data-type="color" style="color:rgb(36, 41, 46)"><span data-type="background" style="background-color:rgb(255, 255, 255)">补充下原因:服务端异步初衷是说Dubbo的服务端业务线程数(默认是200个)不够,但其实在event-driven模式下, 200个肯定不需要那么多,只需要cpu核数那样就可以,只要业务实现是非阻塞的纯异步方式的非阻塞的业务逻辑处理,用再多的线程数就是浪费资源。</span></span> | ||
* 要用服务端异步,建议服务端的线程策略采用same thread模式+协程包 | ||
|
||
## 小结 | ||
Dubbo在支持业务应用时,会碰到千奇百怪的需求场景,服务端异步为用户提供了一种解决ThreadPool Full的方案。当发生ThreadPool Full的情况下,如果当前系统瓶颈是CPU,不建议用这种方案;如果系统Load不高,调高worker的线程数目,或者采用服务端异步,都是可以考虑的。 |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters