Dubbo笔记衍生篇⑨:Dubbo 线程模型

一、前言

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


本篇本来是 Dubbo笔记 ⑥ : 服务发布流程 - NettyServer 中 的内容,为了方便日后查找和修改,还是单独成文。

二、 Netty 的通信模型

Netty线程模型的时候,一般首先会想到的是经典的 Reactor线程模型,尽管不同的NIO框架对于 Reactor模式的实现存在差异,但本质上还是遵循了 Reactor的基础线程模型。

1. Reactor 线程模型

在目前的线程模型中一种是传统阻塞的I/O模型,一种是Reactor线程模型。Reactor模型根据Reactor的数量和处理资源线程池的数量不同又分为 Reactor单线程模型、 Reactor多线程模型、主从Reactor多线程模型。

Reactor模型的核心是:Reactor+Handles。Reactor在一个单独的线程中运行,负责监听和分发事件,将接收到的io事件交给不同的Handle来处理响应。Handles是处理程序执行I/O事件的实际操作,Reactor通过调度适当的Handles来处理io事件。


这里以个人理解的形式介绍一下三种模型:

  1. Reactor单线程模型 :乡镇级小诊所,只有一个医生(Thread)。当有新病人来到时(与新连接需要建立),患者需要先到医生这挂号(Acceptor 处理客户端的新连接),随后医生对病人进行问诊(Handler 处理任务), 由于有的病人可能在打点滴或者测量体温,在这个时间医生可以处理其他病人(NIO多路复用,一个医生线程可以处理多个病人连接)。
  2. Reactor多线程模型 :上面的模型很容易发现问题,当病人有很多时,医生忙碌不过来,新来的病人甚至都没有办法挂号(新连接一直被阻塞无法连接)。所以医院扩大规模(Reactor多线程模型),此时医生扩展,新招聘了很多医生(Worker 线程池出现),同时出现一个前台挂号医生(Acceptor) 来将病人挂号并分诊到不同的医生那里去。此时则变成,当一个患者出现,首先找到分诊医生进行挂号(新连接建立),分诊医生给其指定一个医生,让患者去指定的医生那去问诊。
  3. 主从 Reactor多线程模型 :上面的模型看似很好,但是我们实际上会发现医院里挂号的窗口往往不止一个,因为人数继续增多,一个挂号医生可能忙不过来(单 Acceptor 线程无法处理过来太多的连接),那么此时则需要扩展挂号医生,则会变成多个挂号医生(Boss 线程池) 来帮助患者挂号,随后将患者分诊到不同的医生上去(建立新连接,分发到不同的Work 上)。此时的 挂号医生们 为 Boss 线程池,执行 Acceptor 的工作 ,问诊医生们为 Work 线程池,执行 Handler 的工作。

如有需要,详参: Reactor线程模型

三、Dubbo线程模型

1. 简单介绍

Dubbo 默认的底层网络通信使用的是Netty,服务提供方NettyServer 使用两级线程池,其中boss线程池主要用来接受客户端的链接请求,并把完成Tcp三次握手的连接分发给 worker 来处理,我们把 boss 和worker线程组称为 I/O 线程。

在实际调用时,如果服务提供方的逻辑处理能够迅速完成,并且不会发起新的IO 请求,那么直接在 IO 线程上处理会更快,因为这样减少了线程池调度与上下文切换的开销。

但是如果处理逻辑较慢,或者需要发起新的IO请求(比如需要查询数据库),则IO线程必须派发请求到新的线程池进行处理,否则 IO 线程被阻塞,导致不能接收其他请求。


我们这里举个例子来帮助理解:

我们这里把 Boss 线程池理解为医院挂号护士, Workder线程池理解为医院问诊医生。传统的 Netty 就是靠这两个线程池完成工作,由 护士分发患者找不同的医生问诊 (boss 线程池分发新连接给 worker 线程池处理)。而 Dubbo 的线程模型相当于给 问诊医生 添加了很多助理医生(业务线程池)。这时候问诊医生在处理一些耗时操作时可以交由助理医生来帮助完成,问诊医生可以继续接诊下一个病人,防止后面的病人等不及(worker 线程池接收到请求后可以交由 业务线程池处理,自身便可继续接受下一个请求,防止请求堆积阻塞)。而我们通过 Dubbo 线程模型 来规定助理医生可以帮助处理哪些事情。如 : AllDispatcher 则表示所有的事件都会交由助理医生 (业务线程池)处理;DirectDispatcher 表示所有的事情都不交给助理医生(业务线程池)处理 等。


2. 线程模型分类

在Dubbo 中,线程模型的扩展接口为 org.apache.dubbo.remoting.Dispatcher,all为默认的线程模型,即由AllChannelHandler 来处理Netty 事件。根据请求的消息是否被 IO线程处理还是被业务线程处理,Dubbo提供了下面几种线程模型:

all=org.apache.dubbo.remoting.transport.dispatcher.all.AllDispatcher
direct=org.apache.dubbo.remoting.transport.dispatcher.direct.DirectDispatcher
message=org.apache.dubbo.remoting.transport.dispatcher.message.MessageOnlyDispatcher
execution=org.apache.dubbo.remoting.transport.dispatcher.execution.ExecutionDispatcher
connection=org.apache.dubbo.remoting.transport.dispatcher.connection.ConnectionOrderedDispatcher

下面我们来具体分析:

2.1 AllDispatcher

all (AllDispatcher) : 所有的消息都派发到业务线程池,这些消息包括请求、响应、连接事件、断开事件、心跳事件等(话虽如此,但是在看AllChannelHandler 实现时发现sent 方法仍是由I/O线程处理,暂时不明),如下图,其中NettyServerBoss 、NettyServerWorker 为I/O 线程。
在这里插入图片描述

AllDispatcher 实现如下,可以看到,AllDispatcher 使用 AllChannelHandler 包装了原始 Handler :

// org.apache.dubbo.remoting.transport.dispatcher.all.AllDispatcher
public class AllDispatcher implements Dispatcher {

    public static final String NAME = "all";

    @Override
    public ChannelHandler dispatch(ChannelHandler handler, URL url) {
        return new AllChannelHandler(handler, url);
    }
}

2.2 DirectDispatcher

direct : 所有的消息都不派发到业务线程池,全都在 IO 线程上直接执行,如下
在这里插入图片描述

DirectDispatcher 策略是全部交给 IO 线程处理,所以这里不做任何处理直接返回。

public class DirectDispatcher implements Dispatcher {

    public static final String NAME = "direct";

    @Override
    public ChannelHandler dispatch(ChannelHandler handler, URL url) {
        return handler;
    }

}

2.3 MessageOnlyDispatcher

message :只有请求响应消息派发到业务线程池,其他的消息如连接消息、断开连接、心跳事件等直接在 IO 线程上执行
在这里插入图片描述
MessageOnlyDispatcher 实现如下,同样使用了 MessageOnlyChannelHandler 包装了原始 Handler

public class MessageOnlyDispatcher implements Dispatcher {

    public static final String NAME = "message";

    @Override
    public ChannelHandler dispatch(ChannelHandler handler, URL url) {
        return new MessageOnlyChannelHandler(handler, url);
    }

}

2.4 ExecutionDispatcher

execution :只把请求类消息派发到业务线程池,但是响应、连接、断开、心跳等消息直接在 IO上执行
在这里插入图片描述
ExecutionDispatcher 实现如下:

public class ExecutionDispatcher implements Dispatcher {

    public static final String NAME = "execution";

    @Override
    public ChannelHandler dispatch(ChannelHandler handler, URL url) {
        return new ExecutionChannelHandler(handler, url);
    }

}

2.5 ConnectionOrderedDispatcher

connection :在 IO 线程上将连接、断开消息放入队列,有序逐个执行,其他消息派发到业务线程池处理。
在这里插入图片描述

ConnectionOrderedDispatcher 实现如下:

public class ConnectionOrderedDispatcher implements Dispatcher {

    public static final String NAME = "connection";

    @Override
    public ChannelHandler dispatch(ChannelHandler handler, URL url) {
        return new ConnectionOrderedChannelHandler(handler, url);
    }
}

3. AllChannelHandler 的实现

上面我们看到, Dubbo线程策略的实现会通过 ChannelHandler 对原始的Handler进行包装后返回。下面我们以 AllDispatcher 为例进行分析, AllDispatcher 会使用 AllChannelHandler 包装Handler后返回。


AllChannelHandler 结构如下图:
在这里插入图片描述

其中 WrappedChannelHandler 封装了 Netty 消息通信的方法,如下:

  • WrappedChannelHandler#connected :通道中有客户端连接消息
  • WrappedChannelHandler#disconnected : 通道中有客户端断连消息
  • WrappedChannelHandler#sent :向通道中写入消息
  • WrappedChannelHandler#received : 从通道中读取消息
  • WrappedChannelHandler#caught: 通道中有异常消息

而 AllChannelHandler 继承了 WrappedChannelHandler,自然也继承了这些功能。AllChannelHandler 的 实现如下:

// org.apache.dubbo.remoting.transport.dispatcher.all.AllChannelHandler
public class AllChannelHandler extends WrappedChannelHandler {

    public AllChannelHandler(ChannelHandler handler, URL url) {
        super(handler, url);
    }
	// 链接完成事件,交由线程池来处理
    @Override
    public void connected(Channel channel) throws RemotingException {
    	// 获取业务线程池
        ExecutorService cexecutor = getExecutorService();
        try {
        	// 交由业务线程池来处理
            cexecutor.execute(new ChannelEventRunnable(channel, handler, ChannelState.CONNECTED));
        } catch (Throwable t) {
            throw new ExecutionException("connect event", channel, getClass() + " error when process connected event .", t);
        }
    }
	// 链接断开事件
    @Override
    public void disconnected(Channel channel) throws RemotingException {
        ExecutorService cexecutor = getExecutorService();
        try {
        	// 交由业务线程池来处理
            cexecutor.execute(new ChannelEventRunnable(channel, handler, ChannelState.DISCONNECTED));
        } catch (Throwable t) {
            throw new ExecutionException("disconnect event", channel, getClass() + " error when process disconnected event .", t);
        }
    }
	// 请求响应事件
    @Override
    public void received(Channel channel, Object message) throws RemotingException {
        ExecutorService cexecutor = getExecutorService();
        try {
        	// 交由业务线程池来处理
            cexecutor.execute(new ChannelEventRunnable(channel, handler, ChannelState.RECEIVED, message));
        } catch (Throwable t) {
        	if(message instanceof Request && t instanceof RejectedExecutionException){
        		Request request = (Request)message;
        		if(request.isTwoWay()){
        			String msg = "Server side(" + url.getIp() + "," + url.getPort() + ") threadpool is exhausted ,detail msg:" + t.getMessage();
        			Response response = new Response(request.getId(), request.getVersion());
        			response.setStatus(Response.SERVER_THREADPOOL_EXHAUSTED_ERROR);
        			response.setErrorMessage(msg);
        			channel.send(response);
        			return;
        		}
        	}
            throw new ExecutionException(message, channel, getClass() + " error when process received event .", t);
        }
    }
	
	// 异常处理事件
    @Override
    public void caught(Channel channel, Throwable exception) throws RemotingException {
        ExecutorService cexecutor = getExecutorService();
        try {
        	// 交由业务线程池来处理
            cexecutor.execute(new ChannelEventRunnable(channel, handler, ChannelState.CAUGHT, exception));
        } catch (Throwable t) {
            throw new ExecutionException("caught event", channel, getClass() + " error when process caught event .", t);
        }
    }
}

AllChannelHandler 的实现很简单,重写了 WrappedChannelHandler 中的通信方法,并且将具体处理投递到业务线程池中。

这里我们注意 :

  1. WrappedChannelHandler 构造函数,如下:

        public WrappedChannelHandler(ChannelHandler handler, URL url) {
            this.handler = handler;
            this.url = url;
            // SPI  获取 业务县城呼
            executor = (ExecutorService) ExtensionLoader.getExtensionLoader(ThreadPool.class).getAdaptiveExtension().getExecutor(url);
    
            String componentKey = Constants.EXECUTOR_SERVICE_COMPONENT_KEY;
            if (Constants.CONSUMER_SIDE.equalsIgnoreCase(url.getParameter(Constants.SIDE_KEY))) {
                componentKey = Constants.CONSUMER_SIDE;
            }
            // 将业务线程池保存到本地缓存中。当通信结束时会将线程池关闭
            DataStore dataStore = ExtensionLoader.getExtensionLoader(DataStore.class).getDefaultExtension();
            dataStore.put(componentKey, Integer.toString(url.getPort()), executor);
        }
    
  2. WrappedChannelHandler#sent 并没有被 AllChannelHandler 重写,也即是说 sent 事件是由I/O线程来完成的。


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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

猫吻鱼

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

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

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

打赏作者

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

抵扣说明:

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

余额充值