Dubbo消费端线程模型改造

1,074 阅读4分钟

最近在阅读Dubbo的一些源码,发现一些关于线程池的改造挺不错的,并阅读了Why神和其他大佬的技术文章,决定对Dubbo的消费端线程模型的模型进行简单总结。

参考文章

Dubbo 2.7.5在线程模型上的优化

Dubbo 发布里程碑版本,性能提升30%

Dubbo Consumer线程模型

Dubbo版本

当前在github上,Dubbo的重要版本如下所示,我主要分析2.6.x和master(2.7.x)两个版本。

Dubbo2.6线程模型

图片地址:github.com/apache/dubb…

客户端调用某一服务时,调用 DubboInvoker.doInvoke 发起远程调用。

其中会选择某一 ExchangeClient ,来进行远程发送处理逻辑。

默认是共享连接。

在 HeaderExchangeChannel 中创建一个 DefaultFuture 对象并返回

最终使用某一具体的网络传输框架进行发送。这里是 NettyClient, 传输的channel为 NioSocketChannel 。

发送的这段逻辑并没有用到Dubbo的线程池,最终由IO线程池处理。

服务端接收处理完后回复给客户端。

NettyClientHandler.channelRead 方法监听处理服务端的回调信息。

最终在 AllChannelHandler.received 方法中获取 ExecutorService,将任务封装成一个 ChannelEventRunable 对象,交给线程池去处理。

ExecuroeService 在 客户端服务引用初始化客户端连接对象时创建,默认是 CachedThreadPool,最终会存放在一个 ConcurrentHashMap 中,key 为 远程服务端口 port, Value 为 线程池。

Dubbo2.6线程模型的缺点

在《Dubbo 发布里程碑版本,性能提升30%》一文中,是这样描述的:

对 2.7.5 版本之前的 Dubbo 应用,尤其是一些消费端应用,当面临需要消费大量服务且并发数比较大的大流量场景时(典型如网关类场景),经常会出现消费端线程数分配过多的问题

一些大佬在 github 进行了分析及提了建议,我直接搬运过来了。

引用地址:github.com/apache/dubb…

引用地址:github.com/apache/dubb…

2.7.5 版本的 Threadless executor 线程机制

1、业务线程发出请求,拿到一个 Future 实例。

2、在调用 future.get() 之前,先调用 ThreadlessExecutor.wait(),wait 会使业务线程在一个阻塞队列上等待,直到队列中被加入元素。

3、当业务数据返回后,生成一个 Runnable Task 并放入 ThreadlessExecutor 队列。

4、业务线程将 Task 取出并在本线程中执行:反序列化业务数据并 set 到 Future。

5、业务线程拿到结果直接返回。

这样,相比于老的线程池模型,由业务线程自己负责监测并解析返回结果,免去了额外的消费端线程池开销。

以前的版本在获取 ExecutorService 时,是在 WrappedChannelHandler 中创建的,现在有所不同,是在发送时 DubboInvoer 会获取ThreadlessExecutor,并赋值给 DefaultFuture,并它有点特殊,实际不是线程池。

在 AllChannelHander 中处理接收信息时,以前的版本是放到 获取的线程池去中去处理,现在改为 交由 ThreadlessExecutor 去处理。

ThreadlessExecutor 在 DubboInvoker.doInvoke 中获取。

ThreadlessExecutor 中有一个 sharedExecutor,这个是真正的线程池。

sharedExecurtor的获取交由通过 SPI 机制获得的 DefaultExecutorRepository。

sharedExecutor的创建是服务引用初始化客户端连接时创建的。

ThreadlessExecutor 在同步方法下才会被创建

刚开始的 ThreadlessExecutor 机制中的 sharedExecutor 还是以 port 维度创建的,在21年取消了以 port 维度,直接全局共享。

在 github 中 对这个改动进行了描述

地址:github.com/apache/dubb…

在 HeaderExchangeChannel 中,会创建 DefaultFuture,ThreadlessExecutor 被赋值其中。

那 ThreadlessExecutor 在什么地方具体调用呢?

在 AsyncToSyncInvoker 中会进行异步向同步的转换,也是默认行为。

同步调用,即当结果未返回时会阻塞等待,直到结果返回。

之前的方式是直接调用 CompletableFuture.get() 方法进行阻塞等待。

而引入 ThreadlessExecutor 之后,阻塞等待方式发生了变化,具体在 waitAndDrain 方法里面。

在 waitAndDrain 中 会调用 LinkedBlockingQueue.take 方法进行获取任务,如果没有,会阻塞等待。

当获取到时,会使用 synchronized 锁住,并执行任务。

一个 RPC 调用绑定一个 ThreadlessExecutor,当任务完成后,会更新 finished 状态为 ture,之后就不会再执行了。

什么时候会将任务放到该队列呢?

当消费端监听到服务端返回的消息时,会在 AllChannelHandler.received 方法中分发任务。

现在会获取到 ThreadlessExecutor,并调用 execute 方法进行分发任务。

将 Runable 添加到 上述的阻塞队列中。

此时,上述 take 阻塞的线程重新唤醒,继续处理后续逻辑。

更新 waiting 状态为 false,并调用 runable.run 方法执行任务。

最后更新 finished 状态为 ture。

那 ThreadlessExecutor 中的 sharedExecutor 什么时候能用到呢?

在 WrapperChannelHandler 中有这么一段话

1.使用ThreadlessExceutor,aka.,将回调直接委托给发起调用的线程。 2.使用 sharedExecutor 执行回调。

正常情况下的RPC服务端回调由 ThreadlessExecutor 处理,每一次 RPC Call 都会创建一个新的 ThredlessExecutor。

当后续处理一些回调时,会使用 ThreadlessExecutor 中的 sharedExecutor。

在另一个地方也会用到 sharedExecutor,在 WrappedChannelHandler 获取 ExecutorService 时,

一个典型的场景就是 等待超时了,之后服务端把结果传送至客户端时会使用 sharedExecutor,因为客户端有定时任务去检测是否超时,超时后会把对用的 DefaultFuture 移除掉。