Dubbo作为一个优秀的RPC框架,以SPI方式抽象架构,可以自由选择通信层框架,已实现的有Netty、Netty4和Mina。
Dubbo 有提供线程模型的官方文档:
http://dubbo.apache.org/zh-cn/docs/user/demos/thread-model.html
本文深入源码,尝试解析dubbo线程模型具体细节。
对于dispatcher参数,有以下几种可选项:
- all 所有消息都派发到线程池,包括请求,响应,连接事件,断开事件,心跳等。
- direct 所有消息都不派发到线程池,全部在 IO 线程上直接执行。
- message 只有请求响应消息派发到线程池,其它连接断开事件,心跳等消息,直接在 IO 线程上执行。
- execution 只有请求消息派发到线程池,不含响应,响应和其它连接断开事件,心跳等消息,直接在 IO 线程上执行。
- connection 在 IO 线程上,将连接断开事件放入队列,有序逐个执行,其它消息派发到线程池。
dispatcher源码在 org.apache.dubbo.remoting.transport.dispatcher
包下面
由Dispatcher的定义知,all作为默认消息派发策略:
@SPI(AllDispatcher.NAME)
public interface Dispatcher {
@Adaptive({Constants.DISPATCHER_KEY, "dispather", "channel.handler"})
// The last two parameters are reserved for compatibility with the old configuration
ChannelHandler dispatch(ChannelHandler handler, URL url);
}
all作为默认的派发策略
服务暴露
虽然知道了线程分发种类以及具体代码,但是是如何和dubbo 服务端接口衔接起来的呢?
这就需要看看服务端暴露过程了。
服务暴露具体细节可以看博主前面几篇关于dubbo文章:
服务暴露过程
简要介绍下暴露过程:
- Dubbo服务抽象配置为ServiceConfig,而服务暴露具体方法则是其内部
export
方法。
public synchronized void export() {
...
if (shouldDelay()) {
DELAY_EXPORT_EXECUTOR.schedule(this::doExport, getDelay(), TimeUnit.MILLISECONDS);
} else {
doExport();
}
}
- 所有服务会以Exporter形式被保存在ServiceConfig中,因为服务可以暴露给多个注册中心,所以需要由url区分多份,export获取则是通过
Protocol.export
进行。 - 对于一个服务,首先要跟registry交互,所以dubbo将url换为了registry,通过SPI机制,让流程先进入
RegistryProtocol
的export方法
Exporter<?> exporter = protocol.export(wrapperInvoker);
exporters.add(exporter);
- 当向注册中心注册号服务链路之后,由SPI和Wrapper类装在,会先进入
ProtocolFilterWrapper
、ProtocolListenerWrapper
、QosProtocolWrapper
的export逻辑,而后进入DubboProtocol的export,针对具体协议,做真正的服务端暴露。
上面简要的介绍了服务端暴露流程,接下来从DubboProtocol 的export方法开始,则将正式进入我们的主题。
在export中有调用一个openServer,从名字上来看是开启一个网络服务:
DubboProtocol.java
@Override
public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
...
openServer(url);
optimizeSerialization(url);
return exporter;
}
如果根据ip+端口方式,没有获取到一个服务提供者,则会进入creatServer(url) 创建一个server。
DubboProtocol.java
private ProtocolServer createServer(URL url) {
...
ExchangeServer server;
try {
server = Exchangers.bind(url, requestHandler);
} catch (RemotingException e) {
throw new RpcException("Fail to start server(url: " + url + ") " + e.getMessage(), e);
}
str = url.getParameter(CLIENT_KEY);
if (str != null && str.length() > 0) {
Set<String> supportedTypes = ExtensionLoader.getExtensionLoader(Transporter.class).getSupportedExtensions();
if (!supportedTypes.contains(str)) {
throw new RpcException("Unsupported client type: " + str);
}
}
return new DubboProtocolServer(server);
}
回过头来,在服务端传入的requestHandler 是什么呢?
它主要是 ExchangeHandler
在DubboProtocol的一个内部类实现类,由于Dubbo支持多个传输层,所以从设计角度,会抽出一些共性的事件,从而顶一个内部一个包装类,来统一实现逻辑。
这个抽象的类封装类其实就是:org.apache.dubbo.remoting.exchange.support.ExchangeHandlerAdapter
而 ExchangeHandler 主要看看其replay方法:
DubboProtocol.java
@Override
public CompletableFuture<Object> reply(ExchangeChannel channel, Object message) throws RemotingException {
if (!(message instanceof Invocation)) {
throw new RemotingException(channel, "Unsupported request: "
+ (message == null ? null : (message.getClass().getName() + ": " + message))
+ ", channel: consumer: " + channel.getRemoteAddress() + " --> provider: " + channel.getLocalAddress());
}
Invocation inv = (Invocation) message;
Invoker<?> invoker = getInvoker(channel, inv);
...
RpcContext.getContext().setRemoteAddress(channel.getRemoteAddress());
Result result = invoker.invoke(inv);
return result.thenApply(Function.identity());
}
中间省略了回调机制的一些处理逻辑,总的来说,这个reply方法,应该就是收到调用消息后,封装成为invoker进行内部调用的过程。
Exchangers 封装了网络传输相关入口,包括服务端的bind和客户端的connect。
Dubbo通过SPI方式,获取一个Exchanger,再调用其connect方法进行绑定操作,Exchanger则进一步对底层传输框架进行封装,目前Exchanger的SPI实现类则只有一个,即HeaderExchanger,主要是在Server基础上,封装一些逻辑,例如心跳检测和其他读写事件。
Exchanger.java
public static ExchangeClient connect(URL url, ExchangeHandler handler) throws RemotingException {
...
return getExchanger(url).connect(url, handler);
}
public static Exchanger getExchanger(URL url) {
String type = url.getParameter(Constants.EXCHANGER_KEY, Constants.DEFAULT_EXCHANGER);
return getExchanger(type);
}
public static Exchanger getExchanger(String type) {
return ExtensionLoader.getExtensionLoader(Exchanger.class).getExtension(type);
}
接下来继续看Transporters的bind方法,其内部是获取对应 Transporter
的SPI实现类,从而调用其 bind方法,本文我们以org.apache.dubbo.remoting.transport.netty.NettyTransporter
的 bind为例:
@Override
public RemotingServer bind(URL url, ChannelHandler listener) throws RemotingException {
return new NettyServer(url, listener);
}
在NettyServer
内部,则是Netty服务端启动相关代码了。
在Dubbo中,除了编码和解码器,只使用了一个nettyHandle进行统一服务处理,我们所关心的派发逻辑,则需要从NettyServer构造方法中去获取:
NettyServer.java
public NettyServer(URL url, ChannelHandler handler) throws RemotingException {
super(ExecutorUtil.setThreadName(url, SERVER_THREAD_POOL_NAME), ChannelHandlers.wrap(handler, url));
}
首先是可以自定义线程名字,另一方面,对原来的handler和url,使用 ChannelHandlers 进行了进一步封装。
ChannelHandlers.java
public static ChannelHandler wrap(ChannelHandler handler, URL url) {
return ChannelHandlers.getInstance().wrapInternal(handler, url);
}
protected static ChannelHandlers getInstance() {
return INSTANCE;
}
static void setTestingChannelHandlers(ChannelHandlers instance) {
INSTANCE = instance;
}
protected ChannelHandler wrapInternal(ChannelHandler handler, URL url) {
return new MultiMessageHandler(new HeartbeatHandler(ExtensionLoader.getExtensionLoader(Dispatcher.class)
.getAdaptiveExtension().dispatch(handler, url)));
}
通过SPI来获取Dispatcher,并且调用其dispatch方法,对handler和url进行进一步封装,我们所关心的消息派发,就是从这里进入的。
派发类别
前面,聊完了消息派发组件Dispatcher的组装过程,接下来则仔细看下几种不同派发机制,是如何实现的。
AllChannelHandler
所有消息都派发到线程池,包括请求,响应,连接事件,断开事件,心跳等。
从代码中可以看到,所有网络事件,都封装为了 ChannelEventRunnable ,交由同一个线程池统一处理,而这个线程池也是通过SPI方式创建:
WrappedChannelHandler.java
public ExecutorService getSharedExecutorService() {
ExecutorRepository executorRepository =
ExtensionLoader.getExtensionLoader(ExecutorRepository.class).getDefaultExtension();
ExecutorService executor = executorRepository.getExecutor(url);
if (executor == null) {
executor = executorRepository.createExecutorIfAbsent(url);
}
return executor;
}
ExecutorRepository 目前只有一个SPI实现类:
DefaultExecutorRepository.java
public synchronized ExecutorService createExecutorIfAbsent(URL url) {
...
Integer portKey = url.getPort();
ExecutorService executor = executors.computeIfAbsent(portKey, k -> createExecutor(url));
// If executor has been shut down, create a new one
if (executor.isShutdown() || executor.isTerminated()) {
executors.remove(portKey);
executor = createExecutor(url);
executors.put(portKey, executor);
}
return executor;
}
private ExecutorService createExecutor(URL url) {
return (ExecutorService) ExtensionLoader.getExtensionLoader(ThreadPool.class).getAdaptiveExtension().getExecutor(url);
}
创建的业务线程池,是可以通过url参数指定的,默认为fixed,即固定大小线程池。
- fixed:固定大小线程池,启动时建立线程,不关闭,一直持有。
- coresize:200
- maxsize:200
- 队列:SynchronousQueue
- 回绝策略:AbortPolicyWithReport - 打印线程信息jstack,之后抛出异常
- cached:缓存线程池,空闲一分钟自动删除,需要时重建。
- limited:可伸缩线程池,但池中的线程数只会增长不会收缩。只增长不收缩的目的是为了避免收缩时突然来了大流量引起的性能问题。
- eager:当核心线程数都busy时候,继续创建线程而不是将任务放入队列。
最终receive逻辑:
@Override
public void received(Channel channel, Object message) throws RemotingException {
ExecutorService executor = getPreferredExecutorService(message);
try {
executor.execute(new ChannelEventRunnable(channel, handler, ChannelState.RECEIVED, message));
} catch (Throwable t) {
if(message instanceof Request && t instanceof RejectedExecutionException){
sendFeedback(channel, (Request) message, t);
return;
}
throw new ExecutionException(message, channel, getClass() + " error when process received event .", t);
}
}
再看看 DirectDispatcher
,其内部对handler封装了一层 DirectChannelHandler
,而内部用的线程池,如果是请求事件(client端),否则则仍然会则是从Futrue中获取io线程池,并进行执行调用。
public ExecutorService getPreferredExecutorService(Object msg) {
if (msg instanceof Response) {
Response response = (Response) msg;
DefaultFuture responseFuture = DefaultFuture.getFuture(response.getId());
// a typical scenario is the response returned after timeout, the timeout response may has completed the future
if (responseFuture == null) {
return getSharedExecutorService();
} else {
ExecutorService executor = responseFuture.getExecutor();
if (executor == null || executor.isShutdown()) {
executor = getSharedExecutorService();
}
return executor;
}
} else {
return getSharedExecutorService();
}
}
其他几种派发策略类似,博主就不一一读书了。
关注博主公众号: 六点A君。
哈哈哈,Dubbo小吃街不迷路: