Dubbo的两种调用模式和超时异常

先推荐你们阅读dubbo官网源码解读服务调用流程一节,传送门: dubbo.apache.org/zh-cn/docs/…html

1、调用模式

Dubbo同步调用仍是异步调用的逻辑是在DubboInvoker中,Dubbo 实现同步和异步调用比较关键的一点就在于由谁调用 ResponseFuture 的 get方法。前端

  1. 同步调用模式下,由框架自身调用 ResponseFuture 的 get 方法。
  2. 异步调用模式下,则由用户调用ResponseFuture的 get 方法。

ResponseFuture是一个接口,ResponseFuture的默认实现是DefaultFuture,当服务消费者还未接收到调用结果时,用户线程调用 get 方法会被阻塞住。apache

  1. 同步调用模式下,框架得到 DefaultFuture 对象后,会当即调用 get 方法进行等待。
  2. 异步模式下则是将该对象封装到 FutureAdapter 实例中,并将 FutureAdapter 实例设置到 RpcContext 中,供用户使用。FutureAdapter 是一个适配器,用于将 Dubbo 中的 ResponseFuture 与 JDK 中的 Future 进行适配。这样当用户线程调用 Future 的 get 方法时,通过 FutureAdapter 适配,最终会调用 ResponseFuture 实现类对象的 get 方法,也就是 DefaultFuture 的 get 方法。dubbo2.7.0使用了CompletableFuture,同时会将其设置到异步上下文中。

通常状况下,服务调用方会并发调用多个服务,每一个用户线程发送请求后,会调用不一样 DefaultFuture 对象的 get 方法进行等待。 一段时间后,服务调用方的线程池会收到多个响应对象。这个时候要考虑一个问题,如何将每一个响应对象传递给相应的 DefaultFuture 对象,且不出错。答案是经过调用编号。DefaultFuture 被建立时,会要求传入一个 Request 对象。此时 DefaultFuture 可从 Request 对象中获取调用编号,并将 <调用编号, DefaultFuture 对象> 映射关系存入到静态 Map 中,即 FUTURES。线程池中的线程在收到 Response 对象后,会根据 Response 对象中的调用编号到 FUTURES 集合中取出相应的 DefaultFuture 对象,而后再将 Response 对象设置到 DefaultFuture 对象中。最后再唤醒用户线程,这样用户线程便可从 DefaultFuture 对象中获取调用结果了。bash

2、超时异常

DefaultFuture中的sent变量在客户端向服务端发送请求成功后会写入,以代表消息发送完成。并发

-->NettyChannel#send
  -->io.netty.channel.ChannelOutboundInvoker#writeAndFlush
    -->NettyClientHandler#write
      -->.DefaultFuture#sent
复制代码

1.客户端超时

若是客户端没有成功发送消息,服务端不会返回响应(Response),DefaultFuture中的sent变量也没有被写入,在DefaultFuture#getTimeoutMessage会根据sent是否大于0,输出客户端超时异常。框架

2.服务端超时

若是客户端成功发送消息,服务端返回响应(Response),DefaultFuture中的sent变量被写入,在DefaultFuture#getTimeoutMessage会根据sent是否大于0,服务端超时异常。异步

private String getTimeoutMessage(boolean scan) {
       long nowTimestamp = System.currentTimeMillis();
       return (sent > 0 ? "Waiting server-side response timeout" : "Sending request timeout in client-side")
               + (scan ? " by scan timer" : "") + ". start time: "
               + (new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(new Date(start))) + ", end time: "
               + (new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(new Date())) + ","
               + (sent > 0 ? " client elapsed: " + (sent - start)
               + " ms, server elapsed: " + (nowTimestamp - sent)
               : " elapsed: " + (nowTimestamp - start)) + " ms, timeout: "
               + timeout + " ms, request: " + request + ", channel: " + channel.getLocalAddress()
               + " -> " + channel.getRemoteAddress();
   }
复制代码

3.服务端超时或者客户端超时dubbo如何构造 Response

当发生超时异常的时候是没有Response返回的,dubbo的客户端在建立DefaultFuture的时候会建立一个TimeoutCheckTask的延时任务,当超时时间到达后就会执行。这段代码不难理解。ide

public static DefaultFuture newFuture(Channel channel, Request request, int timeout) {
        final DefaultFuture future = new DefaultFuture(channel, request, timeout);
        // timeout check
        timeoutCheck(future);
        return future;
    }
    
 private static class TimeoutCheckTask implements TimerTask {

        private DefaultFuture future;

        TimeoutCheckTask(DefaultFuture future) {
            this.future = future;
        }

        @Override
        public void run(Timeout timeout) {
            if (future == null || future.isDone()) {
                return;
            }
            // create exception response.
            Response timeoutResponse = new Response(future.getId());
            // set timeout status.
            timeoutResponse.setStatus(future.isSent() ? Response.SERVER_TIMEOUT : Response.CLIENT_TIMEOUT);
            timeoutResponse.setErrorMessage(future.getTimeoutMessage(true));
            // handle response.
            DefaultFuture.received(future.getChannel(), timeoutResponse);

        }
    }
    
    private Object returnFromResponse() throws RemotingException {
        Response res = response;
        if (res == null) {
            throw new IllegalStateException("response cannot be null");
        }
        // 若是响应正常则返回调用结果
        if (res.getStatus() == Response.OK) {
            return res.getResult();
        }
        // 抛出超时异常
        if (res.getStatus() == Response.CLIENT_TIMEOUT || res.getStatus() == Response.SERVER_TIMEOUT) {
            throw new TimeoutException(res.getStatus() == Response.SERVER_TIMEOUT, channel, res.getErrorMessage());
        }
        // 消费方调用异常抛出RemotingException
        throw new RemotingException(channel, res.getErrorMessage());
    }
复制代码

使用延时任务的方式会在调用超时的时候也会使RPC调用流程完整,而不至于一直停留在!isDone()状态,相对来讲这种方式可能更好一些。ui

3、对超时问题的一些理解。

  • 首先要根据业务设置合适超时时间,全部的服务应当设置同样的超时时间。
  • 并非全部的超时异常都须要重试,有的业务场景应当由用户手动发起请求,必定要争对业务场景作合适的选择。
  • 发生超时异常并不意味这服务端处理失败(因此合适的超时时间尤其重要),这时能够经过查询接口主动拉取信息。
  • 发生超时前端应当设置遮罩层(有的人手贱没有返回一直点),不然可能引起雪崩。