本篇主要記錄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的線程模型大致是相同的,有興趣的可參考我的筆記:簡單RPC框架-業務線程池
我們從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。
只有通過做異常捕獲或者服務降級才能確保某些不重要的依賴出問題時不影響主服務的穩定性。而超時就可以與服務降級結合起來,當消費端發生超時時自動觸發服務降級, 這樣即使我們的評論服務一直慢,但不影響獲取產品明細的主體功能,只不過會犧牲部分體驗,用戶看到的評論不是真實的,但評論相對是個邊緣功能,相比看不到產品信息要輕的多,某種程度上是可以舍棄的。