dubbo與trivial超時機制的深入思考
說在前面
trivial是根據之前設計的RPC框架而來的(還在增進當中),其中較為不同的一個點為,在客戶端去掉了業務執行緒池,因為既然都要等待,不必要再加一層。
進入正題
有在網上看到這樣的資訊,“之前有簡單提到過, dubbo預設採用了netty做為網路元件,它屬於一種NIO的模式。消費端發起遠端請求後,執行緒不會阻塞等待服務端的返回,而是馬上得到一個ResponseFuture,消費端通過不斷的輪詢機制判斷結果是否有返回。因為是通過輪詢,輪詢有個需要特別注要的就是避免死迴圈,所以為了解決這個問題就引入了超時機制,只在一定時間範圍內做輪詢,如果超時時間就返回超時異常”。
我認為這種說法是錯誤。
1.以上說法只關注結果,但是如果只關注結果的話何不阻塞等待?還需要輪詢判斷,耗費cpu資源?超時機制絕不是為了讓輪詢在一定時間內結束!
問題1:超時機制有什麼作用?
2.上述說“消費端通過不斷的輪詢機制判斷結果是否有返回”,沒有指明是消費端的什麼執行緒,但是容易讓人誤以為是呼叫者執行緒(下稱caller)。而事實上是由一個deamon執行緒去掃描判斷所有的caller發起的呼叫是否超時。
問題2:為什麼不讓caller自己去輪詢?
問題1個人觀點:
在正常情況下,即caller發起呼叫,而後只需阻塞等待服務提供方的結果即可,因為在正常情況下是能收到的。
那要是因為某些原因而收不到呢?比如,服務提供方的處理執行緒意外結束了,那caller豈不是要一直等下去?
所以要有超時。
dubbo中超時後重試的請求是路由到其他機器上的。咋一看合情合理,再細想大有學問(有可能是我想多了)。
除了剛剛說的因為處理執行緒意外結束使得caller得不到結果這種情況之外,有些人會想到另一種情況——在網路中丟失?這種情況也是不適合再發送到同一個機器的,因為有tcp的重傳,這樣你重試的請求若要到同一個機器,便到了協議棧同一個緩衝區,那麼最先發送成功的依然是上一次的請求,再按正常情況,首次收到的依然是上一次請求的結果,相當於重試沒有作用。
事實上,這只是我的猜想,對於tcp序列傳輸,並行傳輸什麼的還沒有去了解,這裡只算是提出一個問題來思考,如果有錯誤還望指出!
問題2個人觀點:
假設是由caller自己輪詢(有10個),那麼每個cpu時間片結束後,都會從執行態轉到就緒態(同樣有上下文的切換)。適合短時間輪詢
假設是由超時掃描執行緒掃描,這10個caller直接一次進入java執行緒的等待狀態(linux的阻塞態?),結束後由他人喚醒。適合較長時間輪詢
前者每次狀態切換耗費資源少,但次數多。
後者每次狀態切換耗費資源多,但只有一次。
所以多短算短,多長算長呢?未經測試。
同樣我並不知道dubbo是怎麼考慮的,但我自己是這樣想的,所以再次強調這是個人觀點,可能有錯誤。
dubbo超時細節
超時掃描執行緒
static { Thread th = new Thread(new RemotingInvocationTimeoutScan(), "DubboResponseTimeoutScanTimer"); //掃描超時 th.setDaemon(true); th.start(); }
DefaultFuture的get方法
@Override public Object get() throws RemotingException { return get(timeout); } @Override 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()) { // wait應該在迴圈當中 // 在呼叫的時候需要等待 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(); }
掃描執行緒細節
private static class RemotingInvocationTimeoutScan implements Runnable { @Override public void run() { while (true) { try { // 掃描DefaultFuture列表 for (DefaultFuture future : FUTURES.values()) { if (future == null || future.isDone()) { continue; } // 如果future未完成且超時 if (System.currentTimeMillis() - future.getStartTimestamp() > future.getTimeout()) { Response timeoutResponse = new Response(future.getId()); // 設定超時狀態 timeoutResponse.setStatus(future.isSent() ? Response.SERVER_TIMEOUT : Response.CLIENT_TIMEOUT); timeoutResponse.setErrorMessage(future.getTimeoutMessage(true)); DefaultFuture.received(future.getChannel(), timeoutResponse); } } Thread.sleep(30); } catch (Throwable e) { logger.error("Exception when scan the timeout invocation of remoting.", e); } } } }
可以看到該執行緒用於掃描所有caller註冊的呼叫資訊,檢查超時。值得注意的一個細節是,“Thread.sleep(30)”,也是在說明while(true)是不讓出cpu的嗎?
trivial超時細節
超時觀察者watcher
private class Watcher extends Thread{ @Override public void run() { while(!RPCClient.shutdown){//每次迴圈檢查是否已經關閉,同樣會讓出cpu try { CountDownNode head=waiterQueue.take();//阻塞獲取頭 if(System.currentTimeMillis()-head.createTime <RPCClient.timeout) waiterQueue.add(head);//如果沒有超時再加回到隊尾 else{//如果超時了 long callerId=head.message.getCallerId(); long count=head.message.getCount(); if(countMap.get(callerId)==null || countMap.get(callerId)!=count) continue;//實際上已經成功返回 if(head.retryNum>0){ head.retryNum--; log.error("執行緒——"+callerId+" 第 "+count +" 次呼叫超時,即將進行第 " +(RPCClient.retryNum-head.retryNum)+" 次重試"); context.writeAndFlush(head.message);//重發資訊 continue; } resultMap.put(callerId,"呼叫超時"); log.error("執行緒—— "+callerId+" 第 "+count +"次呼叫超時,已重試 "+RPCClient.retryNum+" 次,即將返回超時提示"); LockSupport.unpark(waiterMap.get(callerId)); waiterMap.remove(callerId); countMap.remove(callerId); } } catch (InterruptedException e) { e.printStackTrace(); } } log.info("超時觀察者退出"); } }
大致上是差不多的,都是要一個執行緒去掃描,但有一點較為不同的是,
dubbo的超時掃描執行緒雖然每次迴圈sleep(30),但即使沒有caller發起呼叫也會一直掃描,耗費cpu資源;
而trivial則會阻塞地從阻塞佇列中獲取,如果沒有caller發起呼叫則阻塞,不耗費cpu資源。
在頻繁發起呼叫的時候兩者差不多的,因為後者也不會總是進入阻塞,但在偶發呼叫時,或許trivial較好。當然取決於真實情況。
最後,如果有興趣的話,可以瞭解一下這個平凡的RPC框架,https://github.com/AllenDuke/trivia