dubbo-最新版线程池模型

1,897 阅读6分钟

笔者分析采用的是当前最新版本的dubbo 2.7.8

背景

在微服务系统中,影响性能的处理硬件之外,首当其冲的应该就是通信框架了,dubbo底层默认采用netty作为通信框架,扩展了同步转异步等功能,今天我们要一起学习的是dubbo的线程池模型,分为消费端和生产端,下面是两端的线程大致模型。

图中省略其他自定义线程池和netty中的boss线程池。 生产端的模型比较简单,客户端相对来说比较复杂,在调用链方面也同样是客户端比较复杂。dubbo 线程池创建模式有 fixed,cached,Limited,Eager,默认采用fixed 固定线程个数。

思考?

  • 线程池设计的目的是啥?
  • dubbo 如此设计客户端线程池有什么优点?
  • 线程池模型怎么和异常事件联系,比如说超时?

分析

服务端

服务端是通过netty 的handler接收到事件,然后将接收到的信息传递给业务线程池。在DubboProtocol 暴露服务的过程中,将变量requestHandler 传递给了下游的Exchangers 层(可以去了解下dubbo分层架构)。

public class DubboProtocol extends AbstractProtocol {
    public static final String NAME = "dubbo";
    private ExchangeHandler requestHandler = new ExchangeHandlerAdapter(){
    	public void received(channel, Object message){}
        public CompletableFuture reply(channel, Object message){}
        private void invoke(channel, String methodKey) {}
    }
}
private ProtocolServer createServer(URL url) {
     //...
     Exchangers.bind(url, requestHandler);//将requestHandler 作为netty handler 传递
}

//ChannelEventRunnable.run()经过层层调用会调用到 ExchangeHandler.received()
org....transport.dispatcher.all.AllChannelHandler#received()
public void received(Channel channel, Object message) throws RemotingException {
// 将message 传给业务线程池去处理
    ExecutorService executor = getPreferredExecutorService(message);
    executor.execute(new ChannelEventRunnable(channel, handler, ChannelState.RECEIVED, message));
}

客户端

客户端业务线程通过invoker层层调用最后会来到DubboInvoker

org.apache.dubbo.rpc.protocol.dubbo.DubboInvoker#doInvoke
protected Result doInvoke(final Invocation invocation) throws Throwable {
  RpcInvocation inv = (RpcInvocation) invocation;
  ExchangeClient currentClient;
  boolean isOneway = RpcUtils.isOneway(getUrl(), invocation);
  int timeout = calculateTimeout(invocation, methodName);
  if (isOneway) {//注释1
      boolean isSent = getUrl().getMethodParameter(methodName, Constants.SENT_KEY, false);
      currentClient.send(inv, isSent);
      return AsyncRpcResult.newDefaultAsyncResult(invocation);
  } else {
      ExecutorService executor = getCallbackExecutor(getUrl(), inv);//注释2
      CompletableFuture<AppResponse> appResponseFuture =
              currentClient.request(inv, timeout, executor).thenApply(obj -> (AppResponse) obj);FutureContext.getContext().setCompatibleFuture(appResponseFuture);
      AsyncRpcResult result = new AsyncRpcResult(appResponseFuture, inv);
      result.setExecutor(executor);
      return result;
  }
}

注释1处是oneway 模式,既不需要关心服务端返回,优点rocketmq 里面的oneway味道,我们重点看注释2,注释2处获取了一个线程池,该线程池便是文章中红色框框所对应的线程池(ThreadlessExecutor),我们看看方法内容

org.apache.dubbo.rpc.protocol.AbstractInvoker#getCallbackExecutor()
protected ExecutorService getCallbackExecutor(URL url, Invocation inv) {
    ExecutorService sharedExecutor = ExtensionLoader.getExtensionLoader(ExecutorRepository.class).getDefaultExtension().getExecutor(url);//注释1
  if (InvokeMode.SYNC == RpcUtils.getInvokeMode(getUrl(), inv)) {//注释2
      return new ThreadlessExecutor(sharedExecutor);
  } else {
      return sharedExecutor;
  }
}

注释1处,创建一个共享线程池,注释2处如果为同步请求将使用ThreadlessExecutor线程池包装类包装共享线程池,这个类看名字就很好理解less 既减少线程的使用,我们看看任务提交方法怎么做的。

//org.apache.dubbo.common.threadpool.ThreadlessExecutor#execute()
public void execute(Runnable runnable) {
    if (!waiting) {
        sharedExecutor.execute(runnable);
    } else {queue.add(runnable);}
}

做法还是很简单的,如果waiting变量为true(默认为true),则将任务提交到队列中,否则提交给分享线程池去执行,这里能起到less作用的以应该就是else中的逻辑了,我们找找看哪里有任务的提交,由于我之前已经阅读过源码,所以这边直接给出了,execute 提交的任务为ChannelEventRunnable实现类。 AllChannelHandler 是netty 接收到事件之后的 handler 处理器,图中可以看到有 CONNECTED,DISCONNECTED,RECEIVED,CAUGHT 分别是连接,断开连接,接收读取,异常回调。

@Override
public void connected(Channel channel) throws RemotingException {
    ExecutorService executor = getExecutorService();//直接获取的共享线程池
    executor.execute(new ChannelEventRunnable(channel, handler, ChannelState.CONNECTED));
}
public void disconnected(Channel channel) throws RemotingException {
    ExecutorService executor = getExecutorService();//直接获取的共享线程池
    executor.execute(new ChannelEventRunnable(channel, handler, ChannelState.DISCONNECTED));
}

@Override
public void received(Channel channel, Object message) throws RemotingException {
	//获取的是less线程池
    ExecutorService executor = getPreferredExecutorService(message);
    executor.execute(new ChannelEventRunnable(channel, handler, ChannelState.RECEIVED, message));
}

这四个中其实只有RECEIVED事件会提交到less线程池的,那么我们把问题聚焦在提交线程的时候提交到队列还是共享线程池呢,我们把时间线拉到发请求之前,在AsyncToSyncInvoker 调用完invoke()发送完请求之后,紧接着执行了get()

public Result invoke(Invocation invocation) throws RpcException {
  Result asyncResult = invoker.invoke(invocation);
  if (InvokeMode.SYNC == ((RpcInvocation) invocation).getInvokeMode()) {
      asyncResult.get(Integer.MAX_VALUE, TimeUnit.MILLISECONDS);//
  }
}
public Result get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
    if (executor != null && executor instanceof ThreadlessExecutor) {
        ThreadlessExecutor threadlessExecutor = (ThreadlessExecutor) executor;
        threadlessExecutor.waitAndDrain();//注释1,重点
    }
    return responseFuture.get(timeout, unit);
}

get() 方法中判断对象如果持有线程池,并且为less线程池,则执行waitAndDrain(),这个方法有我们要的答案。

public void waitAndDrain() throws InterruptedException {
    if (finished) {
        return;
    }
    Runnable runnable = queue.take();//注释1
    synchronized (lock) {
        waiting = false;//注释2
        runnable.run();
    }
    runnable = queue.poll();
    while (runnable != null) {
        runnable.run();
        runnable = queue.poll();
    }
    finished = true;
}

注释1中调用了队列的take()方法,这是方法是阻塞的,如果队列没有任务会一直被挂起,注意这里是业务线程被park,一直要等到netty的worker 线程调用AllChannelHandler.received()将任务提交给less线程,放到queue中,业务线程才会被unpark,这样就变成业务线程去执行ChannelEventRunnable的run()方法,run()方法中会对消息进行解码,处理返回等。

注释2处我们记录下,当waitAndDrain()只要处理完一个请求之后就将waiting 设置为了false,也就是说后续如果还有任务提交到less线程池,任务就将分发给共享线程池了,而不是提交到队列。

org...support.header.HeaderExchangeHandler#handleResponse()
static void handleResponse(Channel channel, Response response) {
  if (response != null && !response.isHeartbeat()) {
      DefaultFuture.received(channel, response);
  }
}
public static void received(Channel channel, Response response, boolean timeout) {
  DefaultFuture future = FUTURES.remove(response.getId());
  if (future != null) {
      Timeout t = future.timeoutCheckTask;
      if (!timeout) { t.cancel();}
      future.doReceived(response);//注释1
  }
  CHANNELS.remove(response.getId());
}

注释1处最后调用当前请求的future的doReceived,让future完成,这样在业务线程在此执行responseFuture.get(timeout, unit) 时,当前future已经完成并不会再次阻塞。所以回过头去看从netty 的worker线程接收到读取事件之后,所有处理都交给了业务线程自己去做,这样减少了线程切换,并且优化了线程池。

什么时候回提交到共享线程池呢

我带着大家看一个案例,倘若客户端超时了呢,会执行到时间轮中的定时器,该定时器在DefaultFuture中。

private static class TimeoutCheckTask implements TimerTask {
  public void run(Timeout timeout) {
      DefaultFuture future = DefaultFuture.getFuture(requestID);
      if (future.getExecutor() != null) {
          future.getExecutor().execute(() -> notifyTimeout(future));//注释1
      } else { notifyTimeout(future);}
  }
}

注释1中,会提交一个超时任务,这样等服务器真正返回数据的时候就会提交到共享线程池去执行了,为啥这样设计呢,因为此时业务线程已经返回给前端了,不在关注服务器返回的内容,当然给共享线程池去处理比较合理。

总结

dubbo对于线程池的设计还是很精妙的,在很多的业务场景中我们可以借鉴和吸收。