dubbo源碼分析22 -- consumer 發送與接收原理

在前面的文章中,我們分析了 dubbo 從 provider 進行服務暴露,然后把服務信息注冊到注冊中心上面解耦 consumerprovider 的調用。consumer 通過 javassist 創建代理對象引用遠程服務。當通過代理對象調用遠程服務的時候,講到進行真正調用的時候 dubbo 抽象出集群容錯(ClusterDirectoryRouterLoadBalance)從服務多個暴露方選取出一個合適的 Invoke 來進行調用。 dubbo 默認是通過 FailoverClusterInvoker 從多個 Invoke 中選擇出一個 Invoke 實例 InvokerWrapper 來進行遠程調用。本次分析主要包括以下 4 個部分:

  • consumer 發送擴展
  • consumer 發送原理
  • consumer 接收原理
  • dubbo 異步變同步

1、consumer 發送擴展

我們先來看一下 dubbo 中 consumer 端的請求發送原理,也就是從 InvokerWrapper#invoke 開始,在 consumer 服務引用分析的時候,我們知道根據 Invoke 調用的時候, dubbo 會創建 ProtocolListenerWrapper與 ProtocolFilterWrapper 來用集成框架使用者的擴展包含:InvokerListenerFilterProtocolListenerWrapper 在對象創建的時候就會調用InvokerListener#referred擴展,所以在遠程服務調用的時候最主要的還是 Filter 擴展,下面我們就看一下在遠程調用的時候默認包括哪些 Filter 擴展:

  • ConsumerContextFilter
  • FutureFilter
  • MonitorFilter

1.1 ConsumerContextFilter

ConsumerContextFilter 保存客戶端信息到 RpcContext

@Activate(group = Constants.CONSUMER, order = -10000)
public class ConsumerContextFilter implements Filter {

    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        RpcContext.getContext()
                .setInvoker(invoker)
                .setInvocation(invocation)
                .setLocalAddress(NetUtils.getLocalHost(), 0)
                .setRemoteAddress(invoker.getUrl().getHost(),
                        invoker.getUrl().getPort());
        if (invocation instanceof RpcInvocation) {
            ((RpcInvocation) invocation).setInvoker(invoker);
        }
        try {
            return invoker.invoke(invocation);
        } finally {
            RpcContext.getContext().clearAttachments();
        }
    }

}

RpcContext 使用 ThreadLocal 來記錄一個臨時狀態。當接收到 RPC 請求,或發起 RPC請求時,RpcContext 的狀態都會變化。

比如:A 調 B,B 再調 C,則 B 機器上,在 B 調 C 之前,RpcContext 記錄的是 A 調 B 的信息,在 B 調 C 之后,RpcContext 記錄的是 B 調 C 的信息。

可以通過 RpcContext 上的 setAttachmentgetAttachment 在服務消費方和提供方之間進行參數的隱式傳遞。

1.2 FutureFilter

FutureFilter 會來處理 dubbo 服務接口調用方配置 async="true" 來使用同步調用來是異步調用。

public class FutureFilter implements Filter {

    protected static final Logger logger = LoggerFactory.getLogger(FutureFilter.class);

    public Result invoke(final Invoker<?> invoker, final Invocation invocation) throws RpcException {
        final boolean isAsync = RpcUtils.isAsync(invoker.getUrl(), invocation);

        fireInvokeCallback(invoker, invocation);
        //需要在調用前配置好是否有返回值,已供invoker判斷是否需要返回future.
        Result result = invoker.invoke(invocation);
        if (isAsync) {
            asyncCallback(invoker, invocation);
        } else {
            syncCallback(invoker, invocation, result);
        }
        return result;
    }
}

同步調用 dubbo 就會同步的返回 provider 方法調用返回的響應.如果是異步調用在進行調用的時候就會把請求信息發送到 provider 然后返回一個空的 RpcResultconsumer 端如果要獲取響應需要通過以下方法獲取:

// 拿到調用的Future引用,當結果返回后,會被通知和設置到此Future
Future<Bar> barFuture = RpcContext.getContext().getFuture(); 
// 同理等待bar返回
Bar bar = barFuture.get(); 

1.3 MonitorFilter

MonitorFilter 其實是在分析之前 dubbo monitor 的時候就進行了詳細的分析。它主要是通過以下配置來激活 providerconsumer 端的指標監控。

<dubbo:monitor protocol="registry" />

我們還是簡單的來看一下它的源碼:

public class MonitorFilter implements Filter {

    private MonitorFactory monitorFactory;

    public void setMonitorFactory(MonitorFactory monitorFactory) {
        this.monitorFactory = monitorFactory;
    }

    // 調用過程攔截
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        if (invoker.getUrl().hasParameter(Constants.MONITOR_KEY)) {
            RpcContext context = RpcContext.getContext(); // 提供方必須在invoke()之前獲取context信息
            String remoteHost = context.getRemoteHost();
            long start = System.currentTimeMillis(); // 記錄起始時間戮
            getConcurrent(invoker, invocation).incrementAndGet(); // 并發計數
            try {
                Result result = invoker.invoke(invocation); // 讓調用鏈往下執行
                collect(invoker, invocation, result, remoteHost, start, false);
                return result;
            } catch (RpcException e) {
                collect(invoker, invocation, null, remoteHost, start, true);
                throw e;
            } finally {
                getConcurrent(invoker, invocation).decrementAndGet(); // 并發計數
            }
        } else {
            return invoker.invoke(invocation);
        }
    }
}

當啟動 dubbo monitor 的時候會暴露一個遠程服務 MonitorService 接口服務服務,具體的處理類是 SimpleMonitorService。而在 MonitorFilter#collect 方法里面 MonitorFactory 會創建一個 Monitor 接口實例(繼承于 MonitorService)。其實就是 DubboMonitorFactroy#createMonitor 遠程引用 dubbo monitor 暴露的 MonitorService 服務。

public class DubboMonitorFactroy extends AbstractMonitorFactory {

    private Protocol protocol;

    private ProxyFactory proxyFactory;

    public void setProtocol(Protocol protocol) {
        this.protocol = protocol;
    }

    public void setProxyFactory(ProxyFactory proxyFactory) {
        this.proxyFactory = proxyFactory;
    }

    @Override
    protected Monitor createMonitor(URL url) {
        url = url.setProtocol(url.getParameter(Constants.PROTOCOL_KEY, "dubbo"));
        if (url.getPath() == null || url.getPath().length() == 0) {
            url = url.setPath(MonitorService.class.getName());
        }
        String filter = url.getParameter(Constants.REFERENCE_FILTER_KEY);
        if (filter == null || filter.length() == 0) {
            filter = "";
        } else {
            filter = filter + ",";
        }
        url = url.addParameters(Constants.CLUSTER_KEY, "failsafe", Constants.CHECK_KEY, String.valueOf(false),
                Constants.REFERENCE_FILTER_KEY, filter + "-monitor");
        Invoker<MonitorService> monitorInvoker = protocol.refer(MonitorService.class, url);
        MonitorService monitorService = proxyFactory.getProxy(monitorInvoker);
        return new DubboMonitor(monitorInvoker, monitorService);
    }

}

獲取到遠程服務 SimpleMonitorService,最后在 MonitorFilter#collect 調用 MonitorService#collect 進行監控數據采集提供給 dubbo monitor。調用過程如下所示:

consumer 發送擴展.jpg

2、consumer 發送原理

最終 consumer 會到 DubboInvoke 進行服務調用。它會在 AbstractInvoker#invoke 添加一些擴展參數到 RpcInvocation 這個遠程調用對象里面。添加的擴展參數包含:

  • interface : 遠程調用的接口名稱
  • group : 接口分組名稱
  • token : 調用的 token 信息
  • timeout : 調用服務的超時時間
  • async : 是否異步調用
  • id : 異步操作默認添加 invocation id,用于保證操作冪等

以及 RpcContext 傳遞過來的擴展參數(RpcContext#attachments)。然后在 DubboInvoker#doInvoke 中會添加 path (接口全類名) 以及 version(版本信息)。再根據 dubbo 的調用模式進行遠程調用,包含以下三種調用模式:

  • oneway 模式:<dubbo:method>標簽的 return 屬性配置為false,則是oneway模式,利用ExchangeClient 對象向服務端發送請求消息之后,立即返回空 RpcResult 對象
  • 異步模式:<dubbo:method>標簽的 async 屬性配置為 ture,則是異步模式,直接返回空 RpcResult對象,由 FutureFilterDefaultFuture 完成異步處理工作
  • 同步模式:默認即是同步,則發送請求之后線程進入等待狀態,直到收到服務端的響應消息或者超時。

下面我們看一下 dubbo 同步調用時序圖:

DubboInvoke.png
ChannelFuture future = channel.write(message);

最終是調用 org.jboss.netty.channel.Channel 通過 socket 發送消息到從集群中選擇出的一個暴露服務信息的服務器發送網絡數據。

3、consumer 接收原理

我們都知道 dubbo 其實是通過 netty 來進行 socket 通信的。而在使用 netty 進行網絡編程的時候,其實核心就是就是實現 ChannelHandler。而在 dubbo 中對應的實現類就是 NettyHandler(高版本支持支持 netty 4 使用的是 NettyClientHandler ,NettyHandler 使用的是 netty 3.x)。如果在 consumer 端(provider 也支持)需要使用 netty 4 進行業務處理,需要進行進行以下配置:

<dubbo:consumer client="netty4" />

所以 consumer 接收 provider 響應的入口就在 NettyClientHandler#channelRead

NettyClientHandler.jpg

首先 ChannelHandler 用于接收 provider 端響應回來的請求,然后經過 5 個 dubbo 自定義的 ChannelHandler

  • MultiMessageHandler:支持 MultiMessage 消息處理,也就是多條消息處理。
  • HeartbeatHandler:netty 心條檢測。如果心跳請求,發送心跳然后直接 return,如果是心跳響應直接 return
  • AllChannelHandler:使用線程池通過 ChannelEventRunnable 工作類來處理網絡事件。
  • DecodeHandler:解碼 message,解析成 dubbo 中的 Response 對象
  • HeaderExchangeHandler:處理解析后的 provider 端返回的 Response 響應信息,把響應結果賦值到 DefaultFuture 響應獲取阻塞對象中。

4、dubbo 異步變同步

我們都知道 dubbo 是基于 netty NIO 的非阻塞 并行調用通信。所以 dubbo 在 consumer 請求 provider 后響應都是異步的。但是在 dubbo 里面默認是同步返回的,那么 dubbo 是如何把異步響應變成同步請求的呢?帶著這個問題,首先我們來看一下 dubbo 里面的幾種請求方式。

4.1 異步且無返回值

這種請求最簡單,consumer 把請求信息發送給 provider 就行了。只是需要在 consumer 端把請求方式配置成異步請求就好了。如下:

<dubbo:method name="sayHello" return="false"></dubbo:method>

4.2 異步且有返回值

這種情況下 consumer 首先把請求信息發送給 provider 。這個時候在 consumer 端不僅把請求方式配置成異步,并且需要 RpcContext 這個 ThreadLocal 對象獲取到 Future 對象,然后通過 Future#get() 阻塞式獲取到 provider 的響應。那么這個 Future 是如果添加到 RpcContext 中呢?

在第二小節講服務發送的時候, 在 DubboInvoke 里面有三種調用方式,之前只具體請求了同步請求的發送方式而且沒有異步請求的發送。異步請求發送代碼如下:

DubboInvoker#doInvoke 中的 else if (isAsync) 分支

    ResponseFuture future = currentClient.request(inv, timeout);
    FutureAdapter<T> futureAdapter = new FutureAdapter<>(future);
    RpcContext.getContext().setFuture(futureAdapter);
    Result result;
    if (RpcUtils.isAsyncFuture(getUrl(), inv)) {
        result = new AsyncRpcResult<>(futureAdapter);
    } else {
        result = new RpcResult();
    }
    return result;

上面的代碼邏輯是直接發送請求到 provider 返回一個 ResponseFuture 實例,然后把這個 Future 對象保存到 RpcContext#LOCAL 這個 ThreadLocal 當前線程對象當中,并且返回一個空的 RpcResult對象。如果要獲取到 provider 響應的信息,需要進行以下操作:

// 拿到調用的Future引用,當結果返回后,會被通知和設置到此Future
Future<String> temp= RpcContext.getContext().getFuture();
// 同理等待bar返回
hello=temp.get();

4.3 異步變同步(默認)

下面我們就來討論一下 dubbo 是如何把異步請求轉化成同步請求的。其實原理和異步請求的通過 Future#get 等待 provider 響應返回一樣,只不過異步有返回值是顯示調用而默認是 dubbo 內部把這步完成了。下面我們就來分析一下 dubbo 是如何把 netty 的異步響應變成同步返回的。(當前線程怎么讓它 "暫停",等結果回來后,再執行?)

我們都知道在 consumer 發送請求的時候會調用 HeaderExchangeChannel#request 方法:

HeaderExchangeChannel#request

    public ResponseFuture request(Object request, int timeout) throws RemotingException {
        if (closed) {
            throw new RemotingException(this.getLocalAddress(), null, "Failed to send request " + request + ", cause: The channel " + this + " is closed!");
        }
        // create request.
        Request req = new Request();
        req.setVersion(Version.getProtocolVersion());
        req.setTwoWay(true);
        req.setData(request);
        DefaultFuture future = new DefaultFuture(channel, req, timeout);
        try {
            channel.send(req);
        } catch (RemotingException e) {
            future.cancel();
            throw e;
        }
        return future;
    }

它首先會通過 dubbo 自定義的 ChannelRequesttimeout(int) 構造一個 DefaultFuture 對象。然后再通過 NettyChannel 發送請求到 provider,最后返回這個 DefaultFuture。下面我們來看一下通過構造方法是如何創建 DefaultFuture 的。我只把主要涉及到的屬性展示出來:

public class DefaultFuture implements ResponseFuture {

    private static final Map<Long, Channel> CHANNELS = new ConcurrentHashMap<Long, Channel>();

    private static final Map<Long, DefaultFuture> FUTURES = new ConcurrentHashMap<Long, DefaultFuture>();

    private final long id;
    private final Channel channel;
    private final Request request;
    private final int timeout;

    public DefaultFuture(Channel channel, Request request, int timeout) {
        this.channel = channel;
        this.request = request;
        this.id = request.getId();
        this.timeout = timeout > 0 ? timeout : channel.getUrl().getPositiveParameter(Constants.TIMEOUT_KEY, Constants.DEFAULT_TIMEOUT);
        // put into waiting map.
        FUTURES.put(id, this);
        CHANNELS.put(id, channel);
    }
}

這個 id 是在創建 Request 的時候使用 AtomicLong#getAndIncrement 生成的。從 1 開始并且如果它一直增加直到生成負數也能保證這臺機器這個值是唯一的,且不沖突的。符合唯一主鍵原則。 dubbo 默認同步變異步其實和異步調用一樣,也是在 DubboInvoker#doInvoke 實現的。

DubboInvoker#doInvoke

    RpcContext.getContext().setFuture(null);
    return (Result) currentClient.request(inv, timeout).get();

關鍵就在 ResponseFuture#get 方法上面,下面我們來看一下這個方法的源碼:

    public Object get(int timeout) throws RemotingException {
        if (timeout <= 0) {
            timeout = Constants.DEFAULT_TIMEOUT;
        }
        if (!isDone()) {
            long start = System.currentTimeMillis();
            lock.lock();
            try {
                while (!isDone()) {
                    done.await(timeout, TimeUnit.MILLISECONDS);
                    if (isDone() || System.currentTimeMillis() - start > timeout) {
                        break;
                    }
                }
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                lock.unlock();
            }
            if (!isDone()) {
                throw new TimeoutException(sent > 0, channel, getTimeoutMessage(false));
            }
        }
        return returnFromResponse();
    }

其實就是 while 循環,利用 java 的 lock 機制判斷如果在超時時間范圍內 DefaultFuture#response 如果賦值成不為空就返回響應,否則拋出 TimeoutException 異常。下面我們就來看一下 DefaultFuture#response 是如何被賦值的。

還記得 consumer 接收 provider 響應的最后一步嗎?就是 DefaultFuture#received,在 provider 端會帶回 consumer 請求的 id。我們來看一下它的具體處理邏輯:

    public static void received(Channel channel, Response response) {
        try {
            DefaultFuture future = FUTURES.remove(response.getId());
            if (future != null) {
                future.doReceived(response);
            } else {
                logger.warn("The timeout response finally returned at "
                        + (new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(new Date()))
                        + ", response " + response
                        + (channel == null ? "" : ", channel: " + channel.getLocalAddress()
                        + " -> " + channel.getRemoteAddress()));
            }
        } finally {
            CHANNELS.remove(response.getId());
        }
    }

它會從最開始通過構造函數傳進去的 DefaultFuture#FUTURES 根據請求的 id 拿到 DefaultFuture ,然后根據這個 DefaultFuture 調用 DefaultFuture#doReceived 方法。通過 Java 里面的 lock 機制把 provider 的值賦值給 DefaultFuture#response。此時 consumer 也正在調用 DefaultFuture#get 方法進行阻塞,當這個 DefaultFuture#response 被賦值后,它的值就不為空。阻塞操作完成,且根據請求號的 idconsumer 端的 Request 以及 Provider 端返回的 Response 關聯了起來。

這個就是 Dubbo 異步轉同步的原理,是不是很巧妙,很簡單。 :)

參考資料:

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容