dubbo 超時設定和原始碼分析
在工作中碰到一個業務介面時間比較長,需要修改超時時間,不知道原理,在網上搜尋,看到有人說如果你覺得自己瞭解了dubbo的超時機制,那麼問問自己以下問題:
-
超時是針對消費端還是服務端?
-
超時在哪設定?
-
超時設定的優先順序是什麼?
-
超時的實現原理是什麼?
-
超時解決的是什麼問題 ?
如果連這些都回答不上了,那隻能說明還沒有完全掌握 dubbo的超時機制。
於是索性就自己本地搭了個環境,研究了一下原始碼。 先來說一說結論:
-
超時是針對消費端的,消費端會丟擲TimeoutException 而伺服器端僅僅是一個 warn日誌
-
超時在消費端、伺服器端設定,dubbo會合並這兩個設定
-
consumer方法級別 > provider 方法級別 > consumer 介面級別 > provider 介面級別 > consumer 全域性級別 > provider 全域性級別。如果都沒配置,那麼就是dubbo預設的1秒
-
見下面分析
-
最主要是寶貴的執行緒,客戶端的使用者執行緒不能因為服務端超時而一直類似wait, 導致無法正常響應其他業務。
一、超時時間設定
全域性超時配置
<dubbo:consumer timeout="5000" />
指定介面以及特定方法超時配置
<dubbo:service interface="me.kimi.samples.dubbo.facade.QuestionFacade" ref="questionFacade" timeout="6000"> <dubbo:method name="getQuestionById" timeout="7000"/> </dubbo:service>
觀察控制檯列印的註冊URL:
consumer://172.16.71.30/me.kimi.samples.dubbo.facade.QuestionFacade?application=demo-consumer&category=providers,configurators,routers&check=false&default.proxy=jdk&default.timeout=5000&dubbo=2.6.2&getQuestionById.timeout=7000&interface=me.kimi.samples.dubbo.facade.QuestionFacade&logger=log4j&methods=getQuestionById&pid=13884&side=consumer&timeout=6000×tamp=1536630294523
可以看到:
-
default.timeout=5000
-
timeout=6000
-
getQuestionById.timeout=7000
分別對應了全域性、類級別、方法級別的超時設定。
省略一部分呼叫鏈,最終會來到這裡 DubboInvoker,讀取超時時間:
com.alibaba.dubbo.rpc.protocol.dubbo.DubboInvoker
@Override protected Result doInvoke(final Invocation invocation) throws Throwable { RpcInvocation inv = (RpcInvocation) invocation; final String methodName = RpcUtils.getMethodName(invocation); inv.setAttachment(Constants.PATH_KEY, getUrl().getPath()); inv.setAttachment(Constants.VERSION_KEY, version); ExchangeClient currentClient; if (clients.length == 1) { currentClient = clients[0]; } else { currentClient = clients[index.getAndIncrement() % clients.length]; } try { boolean isAsync = RpcUtils.isAsync(getUrl(), invocation); boolean isOneway = RpcUtils.isOneway(getUrl(), invocation); // 讀取超時時間,這裡dubbo已經把服務端的timeout引數和消費端的timeout引數合併 int timeout = getUrl().getMethodParameter(methodName, Constants.TIMEOUT_KEY, Constants.DEFAULT_TIMEOUT); if (isOneway) { boolean isSent = getUrl().getMethodParameter(methodName, Constants.SENT_KEY, false); currentClient.send(inv, isSent); RpcContext.getContext().setFuture(null); return new RpcResult(); } else if (isAsync) { ResponseFuture future = currentClient.request(inv, timeout); RpcContext.getContext().setFuture(new FutureAdapter<Object>(future)); return new RpcResult(); } else { RpcContext.getContext().setFuture(null); // 返回 DefaultFuture // get()在沒返回值之前會 阻塞 await return (Result) currentClient.request(inv, timeout).get(); } } catch (TimeoutException e) { throw new RpcException(RpcException.TIMEOUT_EXCEPTION, "Invoke remote method timeout. method: " + invocation.getMethodName() + ", provider: " + getUrl() + ", cause: " + e.getMessage(), e); } catch (RemotingException e) { throw new RpcException(RpcException.NETWORK_EXCEPTION, "Failed to invoke remote method: " + invocation.getMethodName() + ", provider: " + getUrl() + ", cause: " + e.getMessage(), e); } }
看一下引數獲取的方法:
public int getMethodParameter(String method, String key, int defaultValue) { // 首先查 getQuestionById.timeout String methodKey = method + "." + key; // 從數字快取中先獲取,不需要每次都 parseInt Number n = getNumbers().get(methodKey); if (n != null) { return n.intValue(); } // 沒得話,去取字串值 String value = getMethodParameter(method, key); if (value == null || value.length() == 0) { // 三個地方都沒配置,返回預設值,預設是1秒 return defaultValue; } // 放入快取中 int i = Integer.parseInt(value); getNumbers().put(methodKey, i); return i; }
public String getMethodParameter(String method, String key) { // 首先查 getQuestionById.timeout String value = parameters.get(method + "." + key); if (value == null || value.length() == 0) { // 沒有設定方法級別的,去查介面級別或全域性的 return getParameter(key); } return value; }
public String getParameter(String key) { // 介面級別去查 timeout String value = parameters.get(key); if (value == null || value.length() == 0) { // 沒的話查詢全域性級別 default.timeout value = parameters.get(Constants.DEFAULT_KEY_PREFIX + key); } return value; }
從程式碼中可以看出超時時間的設定:方法級別 > 介面級別 > 全域性級別。
這裡要特殊提一點,就是dubbo會合並服務端客戶端的設定。
修改客戶端配置, 只留下全域性設定:
<dubbo:consumer timeout="2000" proxy="jdk"/> <dubbo:service interface="me.kimi.samples.dubbo.facade.QuestionFacade" ref="questionFacade"/>
服務端配置如下:
<dubbo:provider timeout="10000" accepts="500"/> <!-- service implementation, as same as regular local bean --> <bean id="questionFacade" class="me.kimi.samples.dubbo.provider.service.QuestionFacadeImpl"/> <!-- declare the service interface to be exported --> <dubbo:service interface="me.kimi.samples.dubbo.facade.QuestionFacade" ref="questionFacade" timeout="9000"/>
最後在客戶端呼叫的時候,發現timeout是9000ms, debug發現客戶端合併了url, 合併結果如下:
dubbo://172.16.71.30:20880/me.kimi.samples.dubbo.facade.QuestionFacade?anyhost=true&application=demo-provider&default.accepts=500&default.timeout=10000&dubbo=2.6.2&generic=false&interface=me.kimi.samples.dubbo.facade.QuestionFacade&logger=log4j&methods=getQuestionById&pid=17508&side=provider&timeout=9000×tamp=1536660132286
檢視原始碼 com.alibaba.dubbo.registry.integration.RegistryDirectory#mergeUrl:
private URL mergeUrl(URL providerUrl) { providerUrl = ClusterUtils.mergeUrl(providerUrl, queryMap); // Merge the consumer side parameters List<Configurator> localConfigurators = this.configurators; // local reference if (localConfigurators != null && !localConfigurators.isEmpty()) { for (Configurator configurator : localConfigurators) { providerUrl = configurator.configure(providerUrl); } } providerUrl = providerUrl.addParameter(Constants.CHECK_KEY, String.valueOf(false)); // Do not check whether the connection is successful or not, always create Invoker! // 這裡就是合併伺服器端的引數,所以除了timeout引數,其他很多引數也是這樣的 // 即已客戶端優先 this.overrideDirectoryUrl = this.overrideDirectoryUrl.addParametersIfAbsent(providerUrl.getParameters()); if ((providerUrl.getPath() == null || providerUrl.getPath().length() == 0) && "dubbo".equals(providerUrl.getProtocol())) { // Compatible version 1.0 //fix by tony.chenl DUBBO-44 String path = directoryUrl.getParameter(Constants.INTERFACE_KEY); if (path != null) { int i = path.indexOf('/'); if (i >= 0) { path = path.substring(i + 1); } i = path.lastIndexOf(':'); if (i >= 0) { path = path.substring(0, i); } providerUrl = providerUrl.setPath(path); } } return providerUrl; }
所以綜合,超時時間的優先順序為:
consumer方法級別 > provider 方法級別 > consumer 介面級別 > provider 介面級別 > consumer 全域性級別 > provider 全域性級別。
二、超時實現
有了超時時間,那麼dubbo是怎麼實現超時的呢?
再看上面的DubboInvoker,對於一般的有返回值的呼叫,最終呼叫:
return (Result) currentClient.request(inv, timeout).get();
先看一下request方法,來到 com.alibaba.dubbo.remoting.exchange.support.header.HeaderExchangeChannel的Request方法:
@Override 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("2.0.0"); 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; }
重點是 DefaultFuture:
static { Thread th = new Thread(new RemotingInvocationTimeoutScan(), "DubboResponseTimeoutScanTimer"); th.setDaemon(true); th.start(); }
類載入的時候會啟動一個超時掃描執行緒:
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); // 每個 DefaultFuture 都有一個 id, 對應當前請求id, 然後被放到 靜態Map中。 FUTURES.put(id, this); // id 對應的 Channel 也存起來,後續超時需要處理 CHANNELS.put(id, channel); }
再看下get方法:
@Overridepublic Object get() throws RemotingException { return get(timeout); }@Overridepublic 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(); }
從上面程式碼上可以看到,get方法,會使當前執行緒掛起等待。那麼什麼時候會被恢復呢,可以想到兩類情況:
-
超時
-
服務端正常返回
那麼回過頭來看看超時掃描執行緒,看一下掃描執行緒做了什麼事情:
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 Response timeoutResponse = new Response(future.getId()); // set timeout status. 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); } } } }
看下 received方法
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()); } }
private void doReceived(Response res) { lock.lock(); try { // 設定響應 // 這樣isDone就是true了 response = res; if (done != null) { // 恢復掛起的執行緒 done.signal(); } } finally { lock.unlock(); } if (callback != null) { invokeCallback(callback); } }
顯然這裡掃描執行緒把使用者請求執行緒恢復了。 恢復以後,順著剛才的 DefaultFuture 的get方法,來到 returnFromResponse方法:
private Object returnFromResponse() throws RemotingException { Response res = response; if (res == null) { throw new IllegalStateException("response cannot be null"); } // 正常返回,返回 Result 物件 if (res.getStatus() == Response.OK) { return res.getResult(); } // 超時處理 if (res.getStatus() == Response.CLIENT_TIMEOUT || res.getStatus() == Response.SERVER_TIMEOUT) { // 重新丟擲異常 throw new TimeoutException(res.getStatus() == Response.SERVER_TIMEOUT, channel, res.getErrorMessage()); } throw new RemotingException(channel, res.getErrorMessage()); }
超時掃描執行緒,構建了一個 超時 Response, 在這裡丟擲 超時異常。
超時拋異常是看見了,那麼正常返回是怎麼處理的呢,因為 done還 await在那裡。 這裡暫時不細說dubbo其他元件的原理,只要知道在網路事件完成(即伺服器端在規定時間內正常返回)的時候,會有個回撥,在整個回撥過程中,最終會回撥到 com.alibaba.dubbo.remoting.exchange.support.header.HeaderExchangeHandler 的 received 方法,看下程式碼:
@Overridepublic void received(Channel channel, Object message) throws RemotingException { channel.setAttribute(KEY_READ_TIMESTAMP, System.currentTimeMillis()); ExchangeChannel exchangeChannel = HeaderExchangeChannel.getOrAddChannel(channel); try { if (message instanceof Request) { // handle request. Request request = (Request) message; if (request.isEvent()) { handlerEvent(channel, request); } else { if (request.isTwoWay()) { Response response = handleRequest(exchangeChannel, request); channel.send(response); } else { handler.received(exchangeChannel, request.getData()); } } } else if (message instanceof Response) { // 請求會回撥到這裡 handleResponse(channel, (Response) message); } else if (message instanceof String) { if (isClientSide(channel)) { Exception e = new Exception("Dubbo client can not supported string message: " + message + " in channel: " + channel + ", url: " + channel.getUrl()); logger.error(e.getMessage(), e); } else { String echo = handler.telnet(channel, (String) message); if (echo != null && echo.length() > 0) { channel.send(echo); } } } else { handler.received(exchangeChannel, message); } } finally { HeaderExchangeChannel.removeChannelIfDisconnected(channel); } }
處理響應:
static void handleResponse(Channel channel, Response response) throws RemotingException { // 不是心跳包,是正常的業務返回 if (response != null && !response.isHeartbeat()) { DefaultFuture.received(channel, response); } }
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31557424/viewspace-2219290/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- Dubbo之SPI原始碼分析原始碼
- Dubbo之限流TpsLimitFilter原始碼分析MITFilter原始碼
- dubbo消費者原始碼分析原始碼
- Dubbo 原始碼分析 - SPI 機制原始碼
- Dubbo原始碼分析(一)Dubbo與Spring整合例項原始碼Spring
- Dubbo原始碼分析(五)Dubbo呼叫鏈-服務端原始碼服務端
- Dubbo原始碼分析(三)Dubbo的服務引用Refer原始碼
- Dubbo原始碼分析(六)Dubbo通訊的編碼解碼機制原始碼
- dubbo原始碼分析之叢集Cluster原始碼
- dubbo原始碼分析02:服務引用原始碼
- Dubbo原始碼分析之服務引用原始碼
- Dubbo原始碼分析之服務暴露原始碼
- Dubbo原始碼分析十一、服務路由原始碼路由
- Redis中Lua指令碼的使用和設定超時坍淑Redis指令碼
- Dubbo原始碼分析(三)Dubbo中的SPI和自適應擴充套件機制原始碼套件
- MongoDB 超時設定MongoDB
- SSH 超時設定
- Dubbo原始碼分析(一)Dubbo的擴充套件點機制原始碼套件
- Dubbo原始碼分析(七)服務目錄原始碼
- Dubbo RPC執行緒模型 原始碼分析RPC執行緒模型原始碼
- Dubbo原始碼分析(二)Dubbo是從哪裡初始化的?原始碼
- Dubbo原始碼分析-叢集容錯之Router原始碼
- 原始碼分析Dubbo網路通訊篇NettyClient原始碼Nettyclient
- CSocket設定超時(轉)
- Dubbo原始碼分析(十)同步呼叫與非同步呼叫原始碼非同步
- Dubbo原始碼分析(八)叢集容錯機制原始碼
- Dubbo原始碼分析(九)負載均衡演算法原始碼負載演算法
- 2.6.7 設定DDL鎖超時
- FFmpeg轉碼音影片時間戳設定分析時間戳
- drf 許可權校驗設定與原始碼分析原始碼
- 原始碼解讀Dubbo分層設計思想原始碼
- **java設定一段程式碼執行超時時間(轉)**Java
- 巢狀滾動設計和原始碼分析巢狀原始碼
- Dubbo原理和原始碼解析之服務引用原始碼
- 影片直播系統原始碼,例項原始碼系列-設定系統時間原始碼
- 深度解剖dubbo原始碼原始碼
- 6.原始碼分析---和dubbo相比SOFARPC是如何實現負載均衡的?原始碼RPC負載
- Dubbo原始碼分析(四)Dubbo呼叫鏈-消費端(叢集容錯機制)原始碼