最近在阅读Dubbo的一些源码,发现一些关于线程池的改造挺不错的,并阅读了Why神和其他大佬的技术文章,决定对Dubbo的消费端线程模型的模型进行简单总结。
参考文章
Dubbo版本
当前在github上,Dubbo的重要版本如下所示,我主要分析2.6.x和master(2.7.x)两个版本。
Dubbo2.6线程模型
客户端调用某一服务时,调用 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 进行了分析及提了建议,我直接搬运过来了。
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 中 对这个改动进行了描述
在 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 移除掉。