dubbo超時重試和異常處理html
參考:前端
https://www.cnblogs.com/ASPNET2008/p/7292472.htmljava
https://www.tuicool.com/articles/YfA3Ubweb
https://www.cnblogs.com/binyue/p/5380322.html數據庫
https://blog.csdn.net/mj158518/article/details/51228649express
本篇主要記錄dubbo中關於超時的常見問題,實現原理,解決的問題以及如何在服務降級中體現做用等。apache
爲了檢查對dubbo超時的理解,嘗試回答以下幾個問題,若是回答不上來或者不肯定那麼說明此處須要再多研究研究。bootstrap
我只是針對我的的理解提問題,並不表明我理解的就是全面深刻的,但個人問題若是也回答不了,那至少說明理解的確是不夠細的。後端
本文全部問題均如下圖作爲業務場景,一個web api作爲前端請求,product service是產品服務,其中調用comment service(評論服務)獲取產品相關評論,comment service從持久層中加載數據。api
若是是爭對消費端,那麼當消費端發起一次請求後,若是在規定時間內未獲得服務端的響應則直接返回超時異常,但服務端的代碼依然在執行。
若是是爭取服務端,那麼當消費端發起一次請求後,一直等待服務端的響應,服務端在方法執行到指定時間後若是未執行完,此時返回一個超時異常給到消費端。
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,消費端經過不斷的輪詢機制判斷結果是否有返回。由於是經過輪詢,輪詢有個須要特別注要的就是避免死循環,因此爲了解決這個問題就引入了超時機制,只在必定時間範圍內作輪詢,若是超時時間就返回超時異常。
源碼
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(); }
只看它的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的源碼看下這下線程模型是怎麼用的:
主要是負責socket鏈接之類的工做。
將一個請求分給後端的某個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線程。
爲何會有業務線程池,這裏很少解釋,能夠參考我上面的文章。
缺省是採用固定大小的線程池,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)); } }
上面代碼的結論是:
當業務線程用完後,服務端會報以下的錯誤:
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(遠大於上游服務設置的超時時間),會發生超時異常,進而致使整個獲取產品明細的接口異常,這也就是日常說的強依賴。這類強依賴是超時不能解決的,解決方案通常是兩種:
只有經過作異常捕獲或者服務降級才能確保某些不重要的依賴出問題時不影響主服務的穩定性。而超時就能夠與服務降級結合起來,當消費端發生超時時自動觸發服務降級, 這樣即便咱們的評論服務一直慢,但不影響獲取產品明細的主體功能,只不過會犧牲部分體驗,用戶看到的評論不是真實的,但評論相對是個邊緣功能,相比看不到產品信息要輕的多,某種程度上是能夠捨棄的。
BUG做者: 許曉
Bug 標題: Dubbo超時機制致使的雪崩鏈接
Bug 影響: Dubbo 服務提供者出現沒法獲取 Dubbo 服務處理線程異常,後端 DB 爆出拿不到數據庫鏈接池,致使前端響應時間異常飆高,系統處理能力降低,核心基礎服務沒法提供正常服務。
Bug 發現過程:
線 上,對於高併發的服務化接口應用,時常會出現Dubbo鏈接池爆滿狀況,一般,咱們理所應當的認爲,這是客戶端併發鏈接太高所致,一方面調整鏈接池大小, 一方面考慮去增長服務接口的機器,固然也會考慮去優化服務接口的應用。很天然的,當咱們在線上壓測一個營銷頁面(爲大促服務,具有高併發)時,咱們遇到了 這種狀況。而經過不斷的深刻研究,我發現了一個特別的狀況。
場景描述:
壓力從Jmeter壓至前端web應用marketingfront,場景是批量獲取30個產品的信息。wsproductreadserver有一個批量接口,會循環從tair中獲取產品信息,若緩存不存在,則命中db。
壓測後有兩個現象:
1) Dubbo的服務端爆出大量鏈接拿不到的異常,還伴隨着沒法獲取數據庫鏈接池的狀況
2) Dubbo Consumer端有大量的Dubbo超時和重試的異常,且重試3次後,均失敗。
3) Dubbo Consumer端的最大併發時91個
Dubbo Provider端的最大併發倒是600個,而服務端配置的dubbo最大線程數即爲600。
這個時候,出於性能測試的警覺性,發現這兩個併發數極爲不妥。
按照正常的請求模式,DubboConsumer和DubboProvider展現出來的併發應該是一致的。此處爲什麼會出現服務端的併發數被放大6倍,甚至有可能不止6倍,由於服務端的dubbo鏈接數限制就是600。
此處開始發揮性能測試各類大膽猜測:
1)是不是由於服務端再dubboServerHandle處理請求時,開啓了多線程,而這塊兒的多線程會累計到Dubbo的鏈接上,dragoon採集的這個數據能夠真實的反應目前應用活動的線程對系統的壓力狀況;
2)壓測環境不純潔?個人小夥伴們在偷偷和我一塊兒壓測?(這個被我生生排除了,性能測試基本環境仍是要保持獨立性)
3)是不是由於超時所致?這裏超時會重試3次,那麼順其天然的想,併發有可能最多會被放大到3倍,3*91=273<<600....仍是不止3倍?
有了猜測,就得當心求證!
首先經過和dubbo開發人員 【草谷】分析,Dubbo鏈接數爆滿的緣由,猜測1被否決,Dubbo服務端鏈接池是計數DubboServerHandle個數的業務是否採用多線程無關。
經過在壓測時,Dump provider端的線程數,也證實了這個。
那麼,可能仍是和超時有很大關係。
再觀察wsproductreadserver接口的處理時間分佈狀況:
從 RT 的分佈來看 。基本上 78.5% 的響應時間是超過 1s 的。那麼這個接口方法的 dubbo 超時時間是 500ms ,此時 dubbo 的重試機制會帶來怎樣的 雪崩效應 呢?
若是按照上圖,雖然客戶端只有1個併發在作操做,可是因爲服務端執行十分耗時,每一個請求的執行RT遠遠超過了超時時間500ms,此時服務端的最大併發會有多少呢?
和服務端處理的響應時間有特比特別大的關係。服務端處理時間變長,可是若是超時,客戶端的阻塞時間卻只有可憐的500ms,超過500ms,新一輪壓力又將發起。
上圖可直接看到的併發是8個,若是服務端RT再長些,那麼併發可能還會再大些!
這也是爲何從marketingfront consumer的dragoon監控來看,只有90個併發。可是到服務端,卻致使dubbo鏈接池爆掉的直接緣由。
查看了wsproductreadserver的堆棧,600個dubboServerHandle大部分都在作數據庫的讀取和數據庫鏈接獲取以及tair的操做。
因此,爲何Dubbo服務端的鏈接池會爆掉?頗有可能就是由於你的服務接口,在高併發下的大部分RT分佈已經超過了你的Dubbo設置的超時時間!這將直接致使Dubbo的重試機制會不斷放大你的服務端請求併發。
所 以若是,你在線上曾經遇到過相似場景,您能夠採起去除Dubbo的重試機器,而且合理的設置Dubbo的超時時間。目前國際站的服務中心,已經開始去除 Dubbo的重試機制。固然Dubbo的重試機制實際上是很是好的QOS保證,它的路由機制,是會幫你把超時的請求路由到其餘機器上,而不是本機嘗試,因此 dubbo的重試機器也能必定程度的保證服務的質量。可是請必定要綜合線上的訪問狀況,給出綜合的評估。
------------ 等等等,彆着急,咱們彷佛又忽略了一些細節,元芳,你怎麼看? ------------------------
咱們從新回顧剛纔的業務流程架構,wsproductReadserver層有DB和tair兩級存儲。那麼對於一樣接口爲何服務化的接口RT如此之差,按照前面提到的架構,包含tair緩存,怎麼還會有數據庫鏈接獲取不到的狀況?
接續深刻追蹤,將問題暴露和開發討論,他們拿出tair
能夠看到,客戶端提交批量查詢30個產品的產品信息。在服務端,有一個緩存模塊,緩存的key是產品的ID。當產品命中tair時,則直接返回,若不命中,那麼回去db中取數,再放入緩存中。
這裏能夠發現一個潛在的性能問題:
客 戶端提交30個產品的查詢請求,而服務端,則經過for循環和tair交互,因此這個接口在一般狀況下的性能估計也得超過60-100ms。若是不是30 個產品,而是50或者100,那麼這個接口的性能將會衰減的很是厲害!(這純屬性能測試的yy,固然這個暫時還不是咱們本次關注的主要緣由)
那麼如此的架構,請求打在db上的可能性是比較小的, 由緩存命中率來保證。從線上真實的監控數據來看,tair的命中率在70%,應該說還不錯,爲何在咱們的壓測場景,DB的壓力確是如此兇殘,甚至致使db的鏈接池沒法獲取呢?
因此性能驗證場景就呼之欲出了:
場景: 準備30個產品ID,保持不變,這樣最多隻會第一次會去訪問DB,並將數據存入緩存,後面將會直接命中緩存,db就在後面喝喝茶好了!
可是從測試結果來看,有兩點能夠觀察到:
1)
2)
3)
因而開始檢查這30個產品到底有哪幾個沒有存入緩存。
通 過開發Debug預發佈環境代碼,最終發現,這兩個產品居然已經被用戶移到垃圾箱了。而經過和李浩和躍波溝通SellerCoponList的業務來 看,DA推送過來的產品是存在被用戶移除的可能性。於是,每次這兩個數據的查詢,因爲數據庫查詢不到記錄,tair也沒有存儲相關記錄,致使這些查詢都將 通過數據庫。數據庫壓力緣由也找到了。
可是問題尚未結束,這彷佛只像是冰山表面,咱們但願可以鳥瞰整個冰山!
細細品味這個問題的最終性能表象 , 這是一種變向擊穿緩存的作法啊!也就是具有必定的通用性。若是接口始終傳入數據庫和緩存都不可能存在的數據,那麼每次的訪問都就落到db上,致使緩存變相擊穿,這個現象頗有意思!
目前有一種解決方案,就是Null Object Pattern,將數據庫不存在的記錄也記錄到緩存中,可是value爲NULL,使得緩存能夠有效的攔截。因爲數據的超時時間是10min,因此若是數據有所改動,也能夠接受。
我相信這只是一種方案,可能還會有其餘方案,可是這種變向的緩存擊穿卻讓我很興奮。回過頭來,若是讓我本身去實現這樣的緩存機制,數據庫和緩存都不存在的 數據場景很容易被忽略,而且這個對於業務確實也不會有影響。在線上存在大量熱點數據狀況下,這樣的機制,每每並不會暴露性能問題。巧合的是,特定的場景, 性能卻會出現很大的誤差,這考驗的既是性能測試工程師的功力,也考驗的是架構的功力!
Bug 解決辦法:
其實這過程當中不只僅有一些方法論,也有一些是性能測試經驗的功底,更重要的是產出了一些通用性的性能問題解決方案,以及部分參數和技術方案的設計對系統架構的影響。
1)對於核心的服務中心,去除dubbo超時重試機制,並從新評估設置超時時間。
2)對於存在tair或者其餘中間件緩存產品,對NULL數據進行緩存,防止出現緩存的變相擊穿問題
dubbo啓動時默認有重試機制和超時機制。
超時機制的規則是若是在必定的時間內,provider沒有返回,則認爲本次調用失敗,
重試機制在出現調用失敗時,會再次調用。若是在配置的調用次數內都失敗,則認爲這次請求異常,拋出異常。
若是出現超時,一般是業務處理太慢,可在服務提供方執行:jstack PID > jstack.log 分析線程都卡在哪一個方法調用上,這裏就是慢的緣由。
若是不能調優性能,請將timeout設大。
某些業務場景下,若是不注意配置超時和重試,可能會引發一些異常。
DUBBO消費端設置超時時間須要根據業務實際狀況來設定,
若是設置的時間過短,一些複雜業務須要很長時間完成,致使在設定的超時時間內沒法完成正常的業務處理。
這樣消費端達到超時時間,那麼dubbo會進行重試機制,不合理的重試在一些特殊的業務場景下可能會引起不少問題,須要合理設置接口超時時間。
好比發送郵件,可能就會發出多份重複郵件,執行註冊請求時,就會插入多條重複的註冊數據。
(1)合理配置超時和重連的思路
1.對於核心的服務中心,去除dubbo超時重試機制,並從新評估設置超時時間。
2.業務處理代碼必須放在服務端,客戶端只作參數驗證和服務調用,不涉及業務流程處理
(2)Dubbo超時和重連配置示例
1
2
|
<!-- 服務調用超時設置爲5秒,超時不重試-->
<
dubbo:service
interface="com.provider.service.DemoService" ref="demoService" retries="0" timeout="5000"/>
|
dubbo在調用服務不成功時,默認會重試2次。
Dubbo的路由機制,會把超時的請求路由到其餘機器上,而不是本機嘗試,因此 dubbo的重試機制也能必定程度的保證服務的質量。
可是若是不合理的配置重試次數,當失敗時會進行重試屢次,這樣在某個時間點出現性能問題,調用方再連續重複調用,
系統請求變爲正常值的retries倍,系統壓力會大增,容易引發服務雪崩,須要根據業務狀況規劃好如何進行異常處理,什麼時候進行重試。
淺談dubbo的ExceptionFilter異常處理
給dubbo接口添加白名單——dubbo Filter的使用具體內容以下:
在開發中,有時候須要限制訪問的權限,白名單就是一種方法。對於Java Web應用,Spring的攔截器能夠攔截Web接口的調用;而對於dubbo接口,Spring的攔截器就無論用了。
dubbo提供了Filter擴展,能夠經過自定義Filter來實現這個功能。本文經過一個事例來演示如何實現dubbo接口的IP白名單。
這樣就能夠實現dubbo接口的IP白名單功能了。