以Dubbo為例解析RPC-非阻塞通訊下的同步API實現原理
Netty在Java NIO領域基本算是獨佔鰲頭,涉及到高效能網路通訊,基本都會以Netty為底層通訊架構,Dubbo 也不例外。以下將以Dubbo實現為例介紹其是如何在NIO非阻塞通訊基礎上實現同步通訊的。
Dubbo為一種RPC通訊架構,提供進程間的通訊,在使用dubbo協議+Netty作為傳輸層時,提供三種API調用方式:
- 同步介面
- 非同步帶回調介面
- 非同步不帶回調介面
同步介面適用在大部分環境,通訊方式簡單、可靠,用戶端發起調用,等待服務端處理,調用結果同步返回。這種方式下,在高吞吐、高效能(回應時間很快)的服務介面情境中最為適用,可以減少非同步帶來的額外的消耗,也方便用戶端做一致性保證。
非同步帶回調介面,用在任務處理時間較長,用戶端應用線程不願阻塞等待,而是為了提高自身處理能力希望服務端處理完成後可以非同步通知應用線程。這種方式可以大大提升用戶端的輸送量,避免因為服務端的耗時問題拖死用戶端。
非同步不帶回調介面,一些情境為了進一步提升用戶端的吞吐能力,只需發起一次服務端調用,不需關係調用結果,可以使用此種通訊方式。一般在不需要嚴格保證資料一致性或者有其他補償措施的情況下,選用這種,可以最小化遠程調用帶來的效能損耗。
來看一下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實現同步阻塞效果。
直接進入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)來清理,而是要額外開啟一個後台線程。
單獨啟動一個逾時線程有兩個好處:
- 提高逾時精度
get()方法內部的輪詢有一個timeout,每次逾時喚醒的時間間隔至少是timeout時間長度,最差的情況可能會等待2*timeout作出逾時反應。在逾時輪詢線程中,每隔30ms遍曆檢測一次,可以很大程度的提升逾時精度。
2. 提升效能,降低回應時間
剝離逾時處理邏輯到一個單獨線程,可以減少對業務線程的時間佔用,這個逾時後的處理對應用來說並無直接作用,完全可以放到後台非同步去處理。另外單獨在一個線程中,實際上有批量處理的表現。
以上是就NIO通訊基礎上實現三種API調用的實現原理,或許有更多優於Dubbo的處理方式,可以拿出來討論。