RPC-非阻塞通訊下的同步API實現原理,以Dubbo爲例

    Netty在Java NIO領域基本算是獨佔鰲頭,涉及到高性能網絡通訊,基本都會以Netty爲底層通訊框架,Dubbo 也不例外。如下將以Dubbo實現爲例介紹其是如何在NIO非阻塞通訊基礎上實現同步通訊的。java

    Dubbo爲一種RPC通訊框架,提供進程間的通訊,在使用dubbo協議+Netty做爲傳輸層時,提供三種API調用方式:react

  1. 同步接口
  2. 異步帶回調接口
  3. 異步不帶回調接口

    同步接口適用在大部分環境,通訊方式簡單、可靠,客戶端發起調用,等待服務端處理,調用結果同步返回。這種方式下,在高吞吐、高性能(響應時間很快)的服務接口場景中最爲適用,能夠減小異步帶來的額外的消耗,也方便客戶端作一致性保證。網絡

 

    異步帶回調接口,用在任務處理時間較長,客戶端應用線程不肯阻塞等待,而是爲了提升自身處理能力但願服務端處理完成後能夠異步通知應用線程。這種方式能夠大大提高客戶端的吞吐量,避免由於服務端的耗時問題拖死客戶端。框架

    異步不帶回調接口,一些場景爲了進一步提高客戶端的吞吐能力,只需發起一次服務端調用,不需關係調用結果,可使用此種通訊方式。通常在不須要嚴格保證數據一致性或者有其餘補償措施的狀況下,選用這種,能夠最小化遠程調用帶來的性能損耗。異步

    

    來看一下Dubbo是如何實現這三種API的。核心代碼在com.alibaba.dubbo.rpc.protocol.dubbo.DubboInvoker,以下圖對應的位置,屬於協議層的實現部分。爲方便你們能夠準肯定位代碼所在位置,使用截圖的方式,而不是直接貼代碼了。性能

    上文描述的是三種API方式,Dubbo裏面經過參數isOneway、isAsync來控制,isOneway=true表示異步不帶回調,isAsync=true表示異步帶回調,不然是同步API。具體是如何控制,看如下代碼:線程

    isOneway==true時,客戶端send完請求後,直接return一個空結果的RpcResult;isAsync==true時,客戶端發起請求,設置一個ResponseFuture,直接return一個空結果的RpcResult,接下來當服務端處理完成,客戶端Netty層在收到響應後會經過Future通知應用線程;最後是同步狀況下,客戶端發起請求,並經過get()方法阻塞等待服務端的響應結果。日誌

    異步API狀況下,結合NIO模型比較好理解是如何實現的(固然須要先了解NIO的reactor模型),接下來重點理解下,這個get()阻塞方法是如何作到基於非阻塞NIO實現同步阻塞效果。code

    直接進入get()方法內部。對象

    能夠看到是利用Java的鎖機制實現,循環判斷是否收到響應,若是收到或者等待超時則返回。done的實例對象以下:

private final Lock                            lock = new ReentrantLock();
private final Condition                       done = lock.newCondition();

    使用可重入鎖ReentrantLock,獲取一個Condition對象在其上作await操做。這裏有await操做,什麼時候被喚醒呢,有兩個條件,第一個是等待timeout超時,默認dubbo是1s,第二個就是被其餘線程喚醒,即收到了服務端的響應。

    signal信號一發出,上文循環檢測內的await操做會當即返回,下一次isDone判斷會變成true,直接跳出循環。

    仔細看代碼會發現,被喚醒的地方還有一個是在DefaultFuture內部有一個超時輪詢檢測的線程,這個線程主要是處理響應超時後觸發資源回收、記錄異常日誌等操做。    

private static class RemotingInvocationTimeoutScan implements Runnable {

        public void run() {
            while (true) {
                try {
                    for (DefaultFuture future : FUTURES.values()) {
                        if (future == null || future.isDone()) {
                            continue;
                        }
                        if (System.currentTimeMillis() - future.getStartTimestamp() > future.getTimeout()) {
                            // 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);
                        }
                    }
                    Thread.sleep(30);
                } catch (Throwable e) {
                    logger.error("Exception when scan the timeout invocation of remoting.", e);
                }
            }
        }
    }

    static {
        Thread th = new Thread(new RemotingInvocationTimeoutScan(), "DubboResponseTimeoutScanTimer");
        th.setDaemon(true);
        th.start();
    }

    可能會有疑問,這個觸發操做爲什麼不直接在get()方法內部檢測到超時直接調用DefaultFuture.received(Channel channel, Response response)來清理,而是要額外開啓一個後臺線程。

    單獨啓動一個超時線程有兩個好處:

  1.  提升超時精度

    get()方法內部的輪詢有一個timeout,每次超時喚醒的時間間隔至少是timeout時長,最差的狀況可能會等待2*timeout做出超時反應。在超時輪詢線程中,每隔30ms遍歷檢測一次,能夠很大程度的提高超時精度。

    2.  提高性能,下降響應時間

    剝離超時處理邏輯到一個單獨線程,能夠減小對業務線程的時間佔用,這個超時後的處理對應用來講並沒有直接做用,徹底能夠放到後臺異步去處理。另外單獨在一個線程中,實際上有批量處理的表現。

    以上是就NIO通訊基礎上實現三種API調用的實現原理,或許有更多優於Dubbo的處理方式,能夠拿出來討論。

相關文章
相關標籤/搜索