當前位置:
首頁 > 知識 > dubbo源碼分析:超時原理以及應用場景

dubbo源碼分析:超時原理以及應用場景


超時問題

為了檢查對dubbo超時的理解,嘗試回答如下幾個問題,如果回答不上來或者不確定那麼說明此處需要再多研究研究。

我只是針對個人的理解提問題,並不代表我理解的就是全面深入的,但我的問題如果也回答不了,那至少說明理解的確是不夠細的。

  • 超時是針對消費端還是服務端?

  • 超時在哪設置?

  • 超時設置的優先順序是什麼?

  • 超時的實現原理是什麼?

  • 超時解決的是什麼問題?

問題解答

RPC場景

本文所有問題均以下圖做為業務場景,一個web api做為前端請求,product service是產品服務,其中調用comment service(評論服務)獲取產品相關評論,comment service從持久層中載入數據。

超時是針對消費端還是服務端?

  • 如果是爭對消費端,那麼當消費端發起一次請求後,如果在規定時間內未得到服務端的響應則直接返回超時異常,但服務端的代碼依然在執行。

  • 如果是爭取服務端,那麼當消費端發起一次請求後,一直等待服務端的響應,服務端在方法執行到指定時間後如果未執行完,此時返回一個超時異常給到消費端。

dubbo的超時是爭對客戶端的,由於是一種NIO模式,消費端發起請求後得到一個ResponseFuture,然後消費端一直輪詢這個ResponseFuture直至超時或者收到服務端的返回結果。雖然超時了,但僅僅是消費端不再等待服務端的反饋並不代表此時服務端也停止了執行。

按上圖的業務場景,看看生成的日誌:

product service:報超時錯誤,因為comment service 載入數據需要5S,但介面只等1S 。

Caused by: com.alibaba.dubbo.remoting.TimeoutException: Waiting server-side response timeout. start time: 2017-08-05 18:14:52.751, end time: 2017-08-05 18:14:53.764, client elapsed: 6 ms, server elapsed: 1006 ms, timeout: 1000 ms, request: Request [id=0, version=2.0.0, twoway=true, event=false, broken=false, data=RpcInvocation [methodName=getCommentsByProductId, parameterTypes=[class java.lang.Long], arguments=[1], attachments={traceId=6299543007105572864, spanId=6299543007105572864, input=259, path=com.jim.framework.dubbo.core.service.CommentService, interface=com.jim.framework.dubbo.core.service.CommentService, version=0.0.0}]], channel: /192.168.10.222:53204 -> /192.168.10.222:7777

at com.alibaba.dubbo.remoting.exchange.support.DefaultFuture.get(DefaultFuture.java:107) ~[dubbo-2.5.3.jar:2.5.3]

at com.alibaba.dubbo.remoting.exchange.support.DefaultFuture.get(DefaultFuture.java:84) ~[dubbo-2.5.3.jar:2.5.3]

at com.alibaba.dubbo.rpc.protocol.dubbo.DubboInvoker.doInvoke(DubboInvoker.java:96) ~[dubbo-2.5.3.jar:2.5.3]

... 42 common frames omitted

comment service : 並沒有異常,而是慢慢悠悠的執行自己的邏輯:


2017-08-05 18:14:52.760 INFO 846 --- [2:7777-thread-5] c.j.f.d.p.service.CommentServiceImpl : getComments start:Sat Aug 05 18:14:52 CST 2017

2017-08-05 18:14:57.760 INFO 846 --- [2:7777-thread-5] c.j.f.d.p.service.CommentServiceImpl : getComments end:Sat Aug 05 18:14:57 CST 2017

從日誌來看,超時影響的是消費端,與服務端沒有直接關係。

超時在哪設置?

消費端

  • 全局控制

<dubbo:consumer timeout="1000"></dubbo:consumer>

  • 介面控制

  • 方法控制

服務端

  • 全局控制

<dubbo:provider timeout="1000"></dubbo:provider>

  • 介面控制

  • 方法控制

可以看到dubbo針對超時做了比較精細化的支持,無論是消費端還是服務端,無論是介面級別還是方法級別都有支持。

超時設置的優先順序是什麼?

上面有提到dubbo支持多種場景下設置超時時間,也說過超時是針對消費端的。那麼既然超時是針對消費端,為什麼服務端也可以設置超時呢?

這其實是一種策略,其實服務端的超時配置是消費端的預設配置,即如果服務端設置了超時,任務消費端可以不設置超時時間,簡化了配置。

另外針對控制的粒度,dubbo支持了介面級別也支持方法級別,可以根據不同的實際情況精確控制每個方法的超時時間。所以最終的優先順序為:客戶端方法級>客戶端介面級>客戶端全局>服務端方法級>服務端介面級>服務端全局。

超時的實現原理是什麼?

之前有簡單提到過, dubbo默認採用了netty做為網路組件,它屬於一種NIO的模式。消費端發起遠程請求後,線程不會阻塞等待服務端的返回,而是馬上得到一個ResponseFuture,消費端通過不斷的輪詢機制判斷結果是否有返回。因為是通過輪詢,輪詢有個需要特別注要的就是避免死循環,所以為了解決這個問題就引入了超時機制,只在一定時間範圍內做輪詢,如果超時時間就返回超時異常。

源碼

ResponseFuture介面定義


public interface ResponseFuture {

/**

* get result.

*

* @return result.

*/

Object get() throws RemotingException;

/**

* get result with the specified timeout.

*

* @param timeoutInMillis timeout.

* @return result.

*/

Object get(int timeoutInMillis) throws RemotingException;

/**

* set callback.

*

* @param callback

*/

void setCallback(ResponseCallback callback);

/**

* check is done.

*

* @return done or not.

*/

boolean isDone();

}

ReponseFuture的實現類:DefaultFuture

只看它的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();

}

超時解決的是什麼問題?

設置超時主要是解決什麼問題?如果沒有超時機制會怎麼樣?

回答上面的問題,首先要了解dubbo這類rpc產品的線程模型。下圖是我之前個人RPC學習產品的示例圖,與dubbo的線程模型大致是相同的。

dubbo源碼分析:超時原理以及應用場景

我們從dubbo的源碼看下這下線程模型是怎麼用的:

netty boss

主要是負責socket連接之類的工作。

netty wokers

將一個請求分給後端的某個handle去處理,比如心跳handle ,執行業務請求的 handle等。

Netty Server中可以看到上述兩個線程池是如何初始化的:

首選是open方法,可以看到一個boss一個worker線程池。


protected void doOpen() throws Throwable {

NettyHelper.setNettyLoggerFactory();

ExecutorService boss = Executors.newCachedThreadPool(new NamedThreadFactory("NettyServerBoss", true));

ExecutorService worker = Executors.newCachedThreadPool(new NamedThreadFactory("NettyServerWorker", true));

ChannelFactory channelFactory = new NioServerSocketChannelFactory(boss, worker, getUrl().getPositiveParameter(Constants.IO_THREADS_KEY, Constants.DEFAULT_IO_THREADS));

bootstrap = new ServerBootstrap(channelFactory);

// ......

}

再看ChannelFactory的構造函數:


public NioServerSocketChannelFactory(Executor bossExecutor, Executor workerExecutor, int workerCount) {

this(bossExecutor, 1, workerExecutor, workerCount);

}

可以看出,boss線程池的大小為1,worker線程池的大小也是可以配置的,默認大小是當前系統的核心數+1,也稱為IO線程。

busines(業務線程池)

為什麼會有業務線程池,這裡不多解釋,可以參考我上面的文章。

預設是採用固定大小的線程池,dubbo提供了三種不同類型的線程池供用戶選擇。我們看看這個類:AllChannelHandler,它是其中一種handle,處理所有請求,它的一個作用就是調用業務線程池去執行業務代碼,其中有獲取線程池的方法:


private ExecutorService getExecutorService() {

ExecutorService cexecutor = executor;

if (cexecutor == null || cexecutor.isShutdown()) {

cexecutor = SHARED_EXECUTOR;

}

return cexecutor;

}

上面代碼中的變數executor來自於AllChannelHandler的父類WrappedChannelHandler,看下它的構造函數:


public WrappedChannelHandler(ChannelHandler handler, URL url) {

//......

executor = (ExecutorService) ExtensionLoader.getExtensionLoader(ThreadPool.class).getAdaptiveExtension().getExecutor(url);

//......

}

獲取線程池來自於SPI技術,從代碼中可以看出線程池的預設配置就是上面提到的固定大小線程池。


@SPI("fixed")

public interface ThreadPool {

/**

* 線程池

*

* @param url 線程參數

* @return 線程池

*/

@Adaptive({Constants.THREADPOOL_KEY})

Executor getExecutor(URL url);

}

最後看下是如何將請求丟給線程池去執行的,在AllChannelHandler中有這樣的方法:


public void received(Channel channel, Object message) throws RemotingException {

ExecutorService cexecutor = getExecutorService();

try {

cexecutor.execute(new ChannelEventRunnable(channel, handler, ChannelState.RECEIVED, message));

} catch (Throwable t) {

throw new ExecutionException(message, channel, getClass() + " error when process received event .", t);

}

}

典型問題:拒絕服務

如果上面提到的dubbo線程池模型理解了,那麼也就容易理解一個問題,當前端大量請求並發出現時,很有可以將業務線程池中的線程消費完,因為默認預設的線程池是固定大小(我現在版本預設線程池大小為200),此時會出現服務無法按預期響應的結果,當然由於是固定大小的線程池,當核心線程滿了後也有隊列可排,但默認是不排隊的,需要排隊需要單獨配置,我們可以從線程池的具體實現中看:


public class FixedThreadPool implements ThreadPool {

public Executor getExecutor(URL url) {

String name = url.getParameter(Constants.THREAD_NAME_KEY, Constants.DEFAULT_THREAD_NAME);

int threads = url.getParameter(Constants.THREADS_KEY, Constants.DEFAULT_THREADS);

int queues = url.getParameter(Constants.QUEUES_KEY, Constants.DEFAULT_QUEUES);

return new ThreadPoolExecutor(threads, threads, 0, TimeUnit.MILLISECONDS,

queues == 0 ? new SynchronousQueue<Runnable>() :

(queues < 0 ? new LinkedBlockingQueue<Runnable>()

: new LinkedBlockingQueue<Runnable>(queues)),

new NamedThreadFactory(name, true), new AbortPolicyWithReport(name, url));

}

}

上面代碼的結論是:

  • 默認線程池大小為200(不同的dubbo版本可能此值不同)

  • 默認線程池不排隊,如果需要排隊,需要指定隊列的大小

當業務線程用完後,服務端會報如下的錯誤:


Caused by: java.util.concurrent.RejectedExecutionException: Thread pool is EXHAUSTED! Thread Name: DubboServerHandler-192.168.10.222:9999, Pool Size: 1 (active: 1, core: 1, max: 1, largest: 1), Task: 8 (completed: 7), Executor status:(isShutdown:false, isTerminated:false, isTerminating:false), in dubbo://192.168.10.222:9999!

at com.alibaba.dubbo.common.threadpool.support.AbortPolicyWithReport.rejectedExecution(AbortPolicyWithReport.java:53) ~[dubbo-2.5.3.jar:2.5.3]

at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:823) [na:1.8.0_121]

at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1369) [na:1.8.0_121]

at com.alibaba.dubbo.remoting.transport.dispatcher.all.AllChannelHandler.caught(AllChannelHandler.java:65) ~[dubbo-2.5.3.jar:2.5.3]

... 17 common frames omitted

通過上面的分析,對調用的服務設置超時時間,是為了避免因為某種原因導致線程被長時間佔用,最終出現線程池用完返回拒絕服務的異常。


超時與服務降級

按我們文章之前的場景,web api 請求產品明細時調用product service,為了查詢產品評論product service調用comment service。如果此時由於comment service異常,響應時間增大到10S(遠大於上游服務設置的超時時間),會發生超時異常,進而導致整個獲取產品明細的介面異常,這也就是平常說的強依賴。這類強依賴是超時不能解決的,解決方案一般是兩種:

  • 調用comment service時做異常捕獲,返回空值或者返回具體的錯誤碼,消費端根據不同的錯誤碼做不同的處理。

  • 調用coment service做服務降級,比如發生異常時返回一個mock的數據,dubbo默認支持mock。

只有通過做異常捕獲或者服務降級才能確保某些不重要的依賴出問題時不影響主服務的穩定性。而超時就可以與服務降級結合起來,當消費端發生超時時自動觸發服務降級, 這樣即使我們的評論服務一直慢,但不影響獲取產品明細的主體功能,只不過會犧牲部分體驗,用戶看到的評論不是真實的,但評論相對是個邊緣功能,相比看不到產品信息要輕的多,某種程度上是可以捨棄的。

dubbo源碼分析:超時原理以及應用場景



  • 更多優質內容推薦:

  • 有錢任性,某公司豪擲500萬幫助20左右年輕人找工作,起因是做善良的人:

  • http://www.ujiuye.com/zt/jyfc/?wt.bd=zdy35845tt

  • 學IT,用周末給自己加薪!

  • http://www.ujiuye.com/zt/zmb/?wt.bd=zdy35845tt

  • IT職業教育:http://xue.ujiuye.com/

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 IT優就業 的精彩文章:

同一個表單,傳遞到不同的處理器中

TAG:IT優就業 |

您可能感興趣

Prometheus原理和源碼分析
LruCache原理分析
如何拿下Gooligan(一):對Gooligan起源和工作原理的初步分析
SpringBoot自動裝配原理分析
Linux內存映射mmap原理分析
android 結合源碼深入剖析AsyncTask機制原理
Kafka、ActiveMQ、RabbitMQ、RocketMQ 區別以及高可用原理
RabbitMq運行原理淺析
AlfredoChen:如何看待用經濟、市場原理來解決優質內容的發現問題 二
Linux調度原理介紹和應用
教學筆記:Web應用伺服器Tomcat的架構設計及原理實現詳解
AlfredoChen:如何看待用經濟、市場原理來解決優質內容的發現問題 一
如何拿下Gooligan(二):深度分析內部工作原理
深入理解 Web Server 原理與實踐:Nginx
ajax,long poll,websocket連接的區別原理
Oculus分享Lipsync工作原理,如何讓虛擬角色唇部說話栩栩如生
adb使用技巧和usb通信原理探索
榮耀play遊戲體驗測試,刺激戰場幀率擊敗一加6,GPU Turbo原理分析
SpringBoot自動配置原理
opencv+python Hough變換的基本原理