dubbo超時重試和異常處理

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源碼分析:超時原理以及應用場景

本篇主要記錄dubbo中關於超時的常見問題,實現原理,解決的問題以及如何在服務降級中體現做用等。apache

超時問題

爲了檢查對dubbo超時的理解,嘗試回答以下幾個問題,若是回答不上來或者不肯定那麼說明此處須要再多研究研究。bootstrap

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

  • 超時是針對消費端仍是服務端?
  • 超時在哪設置?
  • 超時設置的優先級是什麼?
  • 超時的實現原理是什麼?
  • 超時解決的是什麼問題?

問題解答

RPC場景

本文全部問題均如下圖作爲業務場景,一個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,消費端經過不斷的輪詢機制判斷結果是否有返回。由於是經過輪詢,輪詢有個須要特別注要的就是避免死循環,因此爲了解決這個問題就引入了超時機制,只在必定時間範圍內作輪詢,若是超時時間就返回超時異常。

源碼

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。

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

 

 

 

Dubbo超時機制致使的雪崩鏈接

BUG做者: 許曉

Bug 標題: Dubbo超時機制致使的雪崩鏈接

Bug 影響: Dubbo 服務提供者出現沒法獲取 Dubbo 服務處理線程異常,後端 DB 爆出拿不到數據庫鏈接池,致使前端響應時間異常飆高,系統處理能力降低,核心基礎服務沒法提供正常服務。

Bug 發現過程:

線 上,對於高併發的服務化接口應用,時常會出現Dubbo鏈接池爆滿狀況,一般,咱們理所應當的認爲,這是客戶端併發鏈接太高所致,一方面調整鏈接池大小, 一方面考慮去增長服務接口的機器,固然也會考慮去優化服務接口的應用。很天然的,當咱們在線上壓測一個營銷頁面(爲大促服務,具有高併發)時,咱們遇到了 這種狀況。而經過不斷的深刻研究,我發現了一個特別的狀況。

場景描述:

alt

壓力從Jmeter壓至前端web應用marketingfront,場景是批量獲取30個產品的信息。wsproductreadserver有一個批量接口,會循環從tair中獲取產品信息,若緩存不存在,則命中db。

壓測後有兩個現象:

1) Dubbo的服務端爆出大量鏈接拿不到的異常,還伴隨着沒法獲取數據庫鏈接池的狀況

alt

2) Dubbo Consumer端有大量的Dubbo超時和重試的異常,且重試3次後,均失敗。

3) Dubbo Consumer端的最大併發時91個

alt

Dubbo Provider端的最大併發倒是600個,而服務端配置的dubbo最大線程數即爲600。alt

這個時候,出於性能測試的警覺性,發現這兩個併發數極爲不妥。

按照正常的請求模式,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端的線程數,也證實了這個。

alt

那麼,可能仍是和超時有很大關係。

再觀察wsproductreadserver接口的處理時間分佈狀況:

alt

從 RT 的分佈來看 。基本上 78.5% 的響應時間是超過 1s 的。那麼這個接口方法的 dubbo 超時時間是 500ms ,此時 dubbo 的重試機制會帶來怎樣的 雪崩效應 呢?

alt

若是按照上圖,雖然客戶端只有1個併發在作操做,可是因爲服務端執行十分耗時,每一個請求的執行RT遠遠超過了超時時間500ms,此時服務端的最大併發會有多少呢?

和服務端處理的響應時間有特比特別大的關係。服務端處理時間變長,可是若是超時,客戶端的阻塞時間卻只有可憐的500ms,超過500ms,新一輪壓力又將發起。

上圖可直接看到的併發是8個,若是服務端RT再長些,那麼併發可能還會再大些!

這也是爲何從marketingfront consumer的dragoon監控來看,只有90個併發。可是到服務端,卻致使dubbo鏈接池爆掉的直接緣由。

查看了wsproductreadserver的堆棧,600個dubboServerHandle大部分都在作數據庫的讀取和數據庫鏈接獲取以及tair的操做。

alt

因此,爲何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就在後面喝喝茶好了!

alt

可是從測試結果來看,有兩點能夠觀察到:

1)

2)

3)

因而開始檢查這30個產品到底有哪幾個沒有存入緩存。

通 過開發Debug預發佈環境代碼,最終發現,這兩個產品居然已經被用戶移到垃圾箱了。而經過和李浩和躍波溝通SellerCoponList的業務來 看,DA推送過來的產品是存在被用戶移除的可能性。於是,每次這兩個數據的查詢,因爲數據庫查詢不到記錄,tair也沒有存儲相關記錄,致使這些查詢都將 通過數據庫。數據庫壓力緣由也找到了。

可是問題尚未結束,這彷佛只像是冰山表面,咱們但願可以鳥瞰整個冰山!

細細品味這個問題的最終性能表象  這是一種變向擊穿緩存的作法啊!也就是具有必定的通用性。若是接口始終傳入數據庫和緩存都不可能存在的數據,那麼每次的訪問都就落到db上,致使緩存變相擊穿,這個現象頗有意思!

目前有一種解決方案,就是Null Object Pattern,將數據庫不存在的記錄也記錄到緩存中,可是value爲NULL,使得緩存能夠有效的攔截。因爲數據的超時時間是10min,因此若是數據有所改動,也能夠接受。

我相信這只是一種方案,可能還會有其餘方案,可是這種變向的緩存擊穿卻讓我很興奮。回過頭來,若是讓我本身去實現這樣的緩存機制,數據庫和緩存都不存在的 數據場景很容易被忽略,而且這個對於業務確實也不會有影響。在線上存在大量熱點數據狀況下,這樣的機制,每每並不會暴露性能問題。巧合的是,特定的場景, 性能卻會出現很大的誤差,這考驗的既是性能測試工程師的功力,也考驗的是架構的功力!

Bug 解決辦法:

其實這過程當中不只僅有一些方法論,也有一些是性能測試經驗的功底,更重要的是產出了一些通用性的性能問題解決方案,以及部分參數和技術方案的設計對系統架構的影響。

1)對於核心的服務中心,去除dubbo超時重試機制,並從新評估設置超時時間。

2)對於存在tair或者其餘中間件緩存產品,對NULL數據進行緩存,防止出現緩存的變相擊穿問題

 

 

 

Dubbo超時和重連機制

dubbo啓動時默認有重試機制和超時機制。
超時機制的規則是若是在必定的時間內,provider沒有返回,則認爲本次調用失敗,
重試機制在出現調用失敗時,會再次調用。若是在配置的調用次數內都失敗,則認爲這次請求異常,拋出異常。

若是出現超時,一般是業務處理太慢,可在服務提供方執行:jstack PID > jstack.log 分析線程都卡在哪一個方法調用上,這裏就是慢的緣由。
若是不能調優性能,請將timeout設大。

某些業務場景下,若是不注意配置超時和重試,可能會引發一些異常。

1.超時設置

DUBBO消費端設置超時時間須要根據業務實際狀況來設定,
若是設置的時間過短,一些複雜業務須要很長時間完成,致使在設定的超時時間內沒法完成正常的業務處理。
這樣消費端達到超時時間,那麼dubbo會進行重試機制,不合理的重試在一些特殊的業務場景下可能會引起不少問題,須要合理設置接口超時時間。
好比發送郵件,可能就會發出多份重複郵件,執行註冊請求時,就會插入多條重複的註冊數據。

(1)合理配置超時和重連的思路

1.對於核心的服務中心,去除dubbo超時重試機制,並從新評估設置超時時間。
2.業務處理代碼必須放在服務端,客戶端只作參數驗證和服務調用,不涉及業務流程處理

(2)Dubbo超時和重連配置示例

1
2
<!-- 服務調用超時設置爲5秒,超時不重試--> 
< dubbo:service  interface="com.provider.service.DemoService" ref="demoService"  retries="0" timeout="5000"/>

2.重連機制

dubbo在調用服務不成功時,默認會重試2次。
Dubbo的路由機制,會把超時的請求路由到其餘機器上,而不是本機嘗試,因此 dubbo的重試機制也能必定程度的保證服務的質量。
可是若是不合理的配置重試次數,當失敗時會進行重試屢次,這樣在某個時間點出現性能問題,調用方再連續重複調用,
系統請求變爲正常值的retries倍,系統壓力會大增,容易引發服務雪崩,須要根據業務狀況規劃好如何進行異常處理,什麼時候進行重試。

 

 

 

 

 

淺談dubbo的ExceptionFilter異常處理

背景

咱們的項目使用了dubbo進行不一樣系統之間的調用。
每一個項目都有一個全局的異常處理,對於業務異常,咱們會拋出自定義的業務異常(繼承RuntimeException)。
全局的異常處理會根據不一樣的異常類型進行不一樣的處理。
最近咱們發現,某個系統調用dubbo請求,provider端(服務提供方)拋出了自定義的業務異常,但consumer端(服務消費方)拿到的並非自定義的業務異常。
這是爲何呢?還須要從dubbo的ExceptionFilter提及。

ExceptionFilter

若是Dubbo的 provider端 拋出異常(Throwable),則會被 provider端 的ExceptionFilter攔截到,執行如下invoke方法:
[java]  view plain  copy
 
  1. /* 
  2.  * Copyright 1999-2011 Alibaba Group. 
  3.  *   
  4.  * Licensed under the Apache License, Version 2.0 (the "License"); 
  5.  * you may not use this file except in compliance with the License. 
  6.  * You may obtain a copy of the License at 
  7.  *   
  8.  *      http://www.apache.org/licenses/LICENSE-2.0 
  9.  *   
  10.  * Unless required by applicable law or agreed to in writing, software 
  11.  * distributed under the License is distributed on an "AS IS" BASIS, 
  12.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
  13.  * See the License for the specific language governing permissions and 
  14.  * limitations under the License. 
  15.  */  
  16. package com.alibaba.dubbo.rpc.filter;  
  17.   
  18. import java.lang.reflect.Method;  
  19.   
  20. import com.alibaba.dubbo.common.Constants;  
  21. import com.alibaba.dubbo.common.extension.Activate;  
  22. import com.alibaba.dubbo.common.logger.Logger;  
  23. import com.alibaba.dubbo.common.logger.LoggerFactory;  
  24. import com.alibaba.dubbo.common.utils.ReflectUtils;  
  25. import com.alibaba.dubbo.common.utils.StringUtils;  
  26. import com.alibaba.dubbo.rpc.Filter;  
  27. import com.alibaba.dubbo.rpc.Invocation;  
  28. import com.alibaba.dubbo.rpc.Invoker;  
  29. import com.alibaba.dubbo.rpc.Result;  
  30. import com.alibaba.dubbo.rpc.RpcContext;  
  31. import com.alibaba.dubbo.rpc.RpcException;  
  32. import com.alibaba.dubbo.rpc.RpcResult;  
  33. import com.alibaba.dubbo.rpc.service.GenericService;  
  34.   
  35. /** 
  36.  * ExceptionInvokerFilter 
  37.  * <p> 
  38.  * 功能: 
  39.  * <ol> 
  40.  * <li>不指望的異常打ERROR日誌(Provider端)<br> 
  41.  *     不指望的日誌便是,沒有的接口上聲明的Unchecked異常。 
  42.  * <li>異常不在API包中,則Wrap一層RuntimeException。<br> 
  43.  *     RPC對於第一層異常會直接序列化傳輸(Cause異常會String化),避免異常在Client出不能反序列化問題。 
  44.  * </ol> 
  45.  *  
  46.  * @author william.liangf 
  47.  * @author ding.lid 
  48.  */  
  49. @Activate(group = Constants.PROVIDER)  
  50. public class ExceptionFilter implements Filter {  
  51.   
  52.     private final Logger logger;  
  53.       
  54.     public ExceptionFilter() {  
  55.         this(LoggerFactory.getLogger(ExceptionFilter.class));  
  56.     }  
  57.       
  58.     public ExceptionFilter(Logger logger) {  
  59.         this.logger = logger;  
  60.     }  
  61.       
  62.     public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {  
  63.         try {  
  64.             Result result = invoker.invoke(invocation);  
  65.             if (result.hasException() && GenericService.class != invoker.getInterface()) {  
  66.                 try {  
  67.                     Throwable exception = result.getException();  
  68.   
  69.                     // 若是是checked異常,直接拋出  
  70.                     if (! (exception instanceof RuntimeException) && (exception instanceof Exception)) {  
  71.                         return result;  
  72.                     }  
  73.                     // 在方法簽名上有聲明,直接拋出  
  74.                     try {  
  75.                         Method method = invoker.getInterface().getMethod(invocation.getMethodName(), invocation.getParameterTypes());  
  76.                         Class<?>[] exceptionClassses = method.getExceptionTypes();  
  77.                         for (Class<?> exceptionClass : exceptionClassses) {  
  78.                             if (exception.getClass().equals(exceptionClass)) {  
  79.                                 return result;  
  80.                             }  
  81.                         }  
  82.                     } catch (NoSuchMethodException e) {  
  83.                         return result;  
  84.                     }  
  85.   
  86.                     // 未在方法簽名上定義的異常,在服務器端打印ERROR日誌  
  87.                     logger.error("Got unchecked and undeclared exception which called by " + RpcContext.getContext().getRemoteHost()  
  88.                             + ". service: " + invoker.getInterface().getName() + ", method: " + invocation.getMethodName()  
  89.                             + ", exception: " + exception.getClass().getName() + ": " + exception.getMessage(), exception);  
  90.   
  91.                     // 異常類和接口類在同一jar包裏,直接拋出  
  92.                     String serviceFile = ReflectUtils.getCodeBase(invoker.getInterface());  
  93.                     String exceptionFile = ReflectUtils.getCodeBase(exception.getClass());  
  94.                     if (serviceFile == null || exceptionFile == null || serviceFile.equals(exceptionFile)){  
  95.                         return result;  
  96.                     }  
  97.                     // 是JDK自帶的異常,直接拋出  
  98.                     String className = exception.getClass().getName();  
  99.                     if (className.startsWith("java.") || className.startsWith("javax.")) {  
  100.                         return result;  
  101.                     }  
  102.                     // 是Dubbo自己的異常,直接拋出  
  103.                     if (exception instanceof RpcException) {  
  104.                         return result;  
  105.                     }  
  106.   
  107.                     // 不然,包裝成RuntimeException拋給客戶端  
  108.                     return new RpcResult(new RuntimeException(StringUtils.toString(exception)));  
  109.                 } catch (Throwable e) {  
  110.                     logger.warn("Fail to ExceptionFilter when called by " + RpcContext.getContext().getRemoteHost()  
  111.                             + ". service: " + invoker.getInterface().getName() + ", method: " + invocation.getMethodName()  
  112.                             + ", exception: " + e.getClass().getName() + ": " + e.getMessage(), e);  
  113.                     return result;  
  114.                 }  
  115.             }  
  116.             return result;  
  117.         } catch (RuntimeException e) {  
  118.             logger.error("Got unchecked and undeclared exception which called by " + RpcContext.getContext().getRemoteHost()  
  119.                     + ". service: " + invoker.getInterface().getName() + ", method: " + invocation.getMethodName()  
  120.                     + ", exception: " + e.getClass().getName() + ": " + e.getMessage(), e);  
  121.             throw e;  
  122.         }  
  123.     }  
  124.   
  125. }  

代碼分析

按邏輯順序進行分析,知足其中一個即返回,再也不繼續執行判斷。

邏輯0

[java]  view plain  copy
 
  1. if (result.hasException() && GenericService.class != invoker.getInterface()) {  
  2.     //...  
  3. }  
  4. return result;  
調用結果有異常且未實現GenericService接口,進入後續判斷邏輯,不然直接返回結果。
[java]  view plain  copy
 
  1. /** 
  2.  * 通用服務接口 
  3.  *  
  4.  * @author william.liangf 
  5.  * @export 
  6.  */  
  7. public interface GenericService {  
  8.   
  9.     /** 
  10.      * 泛化調用 
  11.      *  
  12.      * @param method 方法名,如:findPerson,若是有重載方法,需帶上參數列表,如:findPerson(java.lang.String) 
  13.      * @param parameterTypes 參數類型 
  14.      * @param args 參數列表 
  15.      * @return 返回值 
  16.      * @throws Throwable 方法拋出的異常 
  17.      */  
  18.     Object $invoke(String method, String[] parameterTypes, Object[] args) throws GenericException;  
  19.   
  20. }  
泛接口實現方式主要用於服務器端沒有API接口及模型類元的狀況,參數及返回值中的全部POJO均用Map表示,一般用於框架集成,好比:實現一個通用的遠程服務Mock框架,可經過實現GenericService接口處理全部服務請求。
不適用於此場景,不在此處探討。
 

邏輯1

[java]  view plain  copy
 
  1. // 若是是checked異常,直接拋出  
  2. if (! (exception instanceof RuntimeException) && (exception instanceof Exception)) {  
  3.     return result;  
  4. }  
不是RuntimeException類型的異常,而且是受檢異常(繼承Exception),直接拋出。
provider端想拋出受檢異常,必須在api上明確寫明拋出受檢異常;consumer端若是要處理受檢異常,也必須使用明確寫明拋出受檢異常的api。
provider端api新增 自定義的 受檢異常, 全部的 consumer端api都必須升級,同時修改代碼,不然沒法處理這個特定異常。

consumer端DecodeableRpcResult的decode方法會對異常進行處理


此處會拋出IOException,上層catch後會作toString處理,放到mErrorMsg屬性中:
[java]  view plain  copy
 
  1. try {  
  2.     decode(channel, inputStream);  
  3. catch (Throwable e) {  
  4.     if (log.isWarnEnabled()) {  
  5.         log.warn("Decode rpc result failed: " + e.getMessage(), e);  
  6.     }  
  7.     response.setStatus(Response.CLIENT_ERROR);  
  8.     response.setErrorMessage(StringUtils.toString(e));  
  9. finally {  
  10.     hasDecoded = true;  
  11. }  

DefaultFuture判斷請求返回的結果,最後拋出RemotingException:
[java]  view plain  copy
 
  1. private Object returnFromResponse() throws RemotingException {  
  2.     Response res = response;  
  3.     if (res == null) {  
  4.         throw new IllegalStateException("response cannot be null");  
  5.     }  
  6.     if (res.getStatus() == Response.OK) {  
  7.         return res.getResult();  
  8.     }  
  9.     if (res.getStatus() == Response.CLIENT_TIMEOUT || res.getStatus() == Response.SERVER_TIMEOUT) {  
  10.         throw new TimeoutException(res.getStatus() == Response.SERVER_TIMEOUT, channel, res.getErrorMessage());  
  11.     }  
  12.     throw new RemotingException(channel, res.getErrorMessage());  
  13. }  

DubboInvoker捕獲RemotingException,拋出RpcException:
[java]  view plain  copy
 
  1. try {  
  2.     boolean isAsync = RpcUtils.isAsync(getUrl(), invocation);  
  3.     boolean isOneway = RpcUtils.isOneway(getUrl(), invocation);  
  4.     int timeout = getUrl().getMethodParameter(methodName, Constants.TIMEOUT_KEY,Constants.DEFAULT_TIMEOUT);  
  5.     if (isOneway) {  
  6.         boolean isSent = getUrl().getMethodParameter(methodName, Constants.SENT_KEY, false);  
  7.         currentClient.send(inv, isSent);  
  8.         RpcContext.getContext().setFuture(null);  
  9.         return new RpcResult();  
  10.     } else if (isAsync) {  
  11.         ResponseFuture future = currentClient.request(inv, timeout) ;  
  12.         RpcContext.getContext().setFuture(new FutureAdapter<Object>(future));  
  13.         return new RpcResult();  
  14.     } else {  
  15.         RpcContext.getContext().setFuture(null);  
  16.         return (Result) currentClient.request(inv, timeout).get();  
  17.     }  
  18. catch (TimeoutException e) {  
  19.     throw new RpcException(RpcException.TIMEOUT_EXCEPTION, "Invoke remote method timeout. method: " + invocation.getMethodName() + ", provider: " + getUrl() + ", cause: " + e.getMessage(), e);  
  20. catch (RemotingException e) {  
  21.     throw new RpcException(RpcException.NETWORK_EXCEPTION, "Failed to invoke remote method: " + invocation.getMethodName() + ", provider: " + getUrl() + ", cause: " + e.getMessage(), e);  
  22. }  

調用棧:
FailOverClusterInvoker.doInvoke -...-> DubboInvoker.doInvoke -> ReferenceCountExchangeClient.request -> HeaderExchangeClient.request -> HeaderExchangeChannel.request -> AbstractPeer.send -> NettyChannel.send -> AbstractChannel.write -> Channels.write --back_to--> DubboInvoker.doInvoke -> DefaultFuture.get -> DefaultFuture.returnFromResponse -> throw new RemotingException
 
異常示例:
[java]  view plain  copy
 
  1. com.alibaba.dubbo.rpc.RpcException: Failed to invoke the method triggerCheckedException in the service com.xxx.api.DemoService. Tried 1 times of the providers [192.168.1.101:20880] (1/1) from the registry 127.0.0.1:2181 on the consumer 192.168.1.101 using the dubbo version 3.1.9. Last error is: Failed to invoke remote method: triggerCheckedException, provider: dubbo://192.168.1.101:20880/com.xxx.api.DemoService?xxx, cause: java.io.IOException: Response data error, expect Throwable, but get {cause=(this Map), detailMessage=null, suppressedExceptions=[], stackTrace=[Ljava.lang.StackTraceElement;@23b84919}  
  2. java.io.IOException: Response data error, expect Throwable, but get {cause=(this Map), detailMessage=null, suppressedExceptions=[], stackTrace=[Ljava.lang.StackTraceElement;@23b84919}  
  3.     at com.alibaba.dubbo.rpc.protocol.dubbo.DecodeableRpcResult.decode(DecodeableRpcResult.java:94)  

 

邏輯2

[java]  view plain  copy
 
  1. // 在方法簽名上有聲明,直接拋出  
  2. try {  
  3.     Method method = invoker.getInterface().getMethod(invocation.getMethodName(), invocation.getParameterTypes());  
  4.     Class<?>[] exceptionClassses = method.getExceptionTypes();  
  5.     for (Class<?> exceptionClass : exceptionClassses) {  
  6.         if (exception.getClass().equals(exceptionClass)) {  
  7.             return result;  
  8.         }  
  9.     }  
  10. catch (NoSuchMethodException e) {  
  11.     return result;  
  12. }  
若是在provider端的api明確寫明拋出運行時異常,則會直接被拋出。
 
若是拋出了這種異常,可是consumer端又沒有這種異常,會發生什麼呢?
答案是和上面同樣,拋出RpcException。

所以若是consumer端不care這種異常,則不須要任何處理;
consumer端有這種異常(路徑要徹底一致,包名+類名),則不須要任何處理;
沒有這種異常,又想進行處理,則須要引入這個異常進行處理(方法有多種,好比升級api,或引入/升級異常所在的包)。

邏輯3

[java]  view plain  copy
 
  1. // 異常類和接口類在同一jar包裏,直接拋出  
  2. String serviceFile = ReflectUtils.getCodeBase(invoker.getInterface());  
  3. String exceptionFile = ReflectUtils.getCodeBase(exception.getClass());  
  4. if (serviceFile == null || exceptionFile == null || serviceFile.equals(exceptionFile)){  
  5.     return result;  
  6. }  
若是異常類和接口類在同一個jar包中,直接拋出。
 

邏輯4

[java]  view plain  copy
 
  1. // 是JDK自帶的異常,直接拋出  
  2. String className = exception.getClass().getName();  
  3. if (className.startsWith("java.") || className.startsWith("javax.")) {  
  4.     return result;  
  5. }  
以java.或javax.開頭的異常直接拋出。
 

邏輯5

[java]  view plain  copy
 
  1. // 是Dubbo自己的異常,直接拋出  
  2. if (exception instanceof RpcException) {  
  3.     return result;  
  4. }  
dubbo自身的異常,直接拋出。
 

邏輯6

[java]  view plain  copy
 
  1. // 不然,包裝成RuntimeException拋給客戶端  
  2. return new RpcResult(new RuntimeException(StringUtils.toString(exception)));  
不知足上述條件,會作toString處理並被封裝成RuntimeException拋出。
 

核心思想

盡力避免反序列化時失敗(只有在jdk版本或api版本不一致時纔可能發生)。

如何正確捕獲業務異常

瞭解了ExceptionFilter,解決上面提到的問題就很簡單了。
有多種方法能夠解決這個問題,每種都有優缺點,這裏不作詳細分析,僅列出供參考:
1. 將該異常的包名以"java.或者"javax. " 開頭
2. 使用受檢異常(繼承Exception)
3. 不用異常,使用錯誤碼
4. 把異常放到provider-api的jar包中
5. 判斷異常message是否以XxxException.class.getName()開頭(其中XxxException是自定義的業務異常)
6. provider實現GenericService接口
7. provider的api明確寫明throws XxxException,發佈provider(其中XxxException是自定義的業務異常)
8. 實現dubbo的filter,自定義provider的異常處理邏輯(方法可參考以前的文章 給dubbo接口添加白名單——dubbo Filter的使用
 

 給dubbo接口添加白名單——dubbo Filter的使用具體內容以下:

在開發中,有時候須要限制訪問的權限,白名單就是一種方法。對於Java Web應用,Spring的攔截器能夠攔截Web接口的調用;而對於dubbo接口,Spring的攔截器就無論用了。

dubbo提供了Filter擴展,能夠經過自定義Filter來實現這個功能。本文經過一個事例來演示如何實現dubbo接口的IP白名單。

 

  1. 擴展Filter
    實現com.alibaba.dubbo.rpc.Filter接口:
    [java]  view plain  copy
     
    1. public class AuthorityFilter implements Filter {  
    2.     private static final Logger LOGGER = LoggerFactory.getLogger(AuthorityFilter.class);  
    3.   
    4.     private IpWhiteList ipWhiteList;  
    5.   
    6.     //dubbo經過setter方式自動注入  
    7.     public void setIpWhiteList(IpWhiteList ipWhiteList) {  
    8.         this.ipWhiteList = ipWhiteList;  
    9.     }  
    10.   
    11.     @Override  
    12.     public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {  
    13.         if (!ipWhiteList.isEnabled()) {  
    14.             LOGGER.debug("白名單禁用");  
    15.             return invoker.invoke(invocation);  
    16.         }  
    17.   
    18.         String clientIp = RpcContext.getContext().getRemoteHost();  
    19.         LOGGER.debug("訪問ip爲{}", clientIp);  
    20.         List<String> allowedIps = ipWhiteList.getAllowedIps();  
    21.         if (allowedIps.contains(clientIp)) {  
    22.             return invoker.invoke(invocation);  
    23.         } else {  
    24.             return new RpcResult();  
    25.         }  
    26.     }  
    27. }  
    注意:只能經過setter方式來注入其餘的bean,且不要標註註解!
    dubbo本身會對這些bean進行注入,不須要再標註@Resource讓Spring注入,參見擴展點加載
  2. 配置文件
    參考:調用攔截擴展
    在resources目錄下添加純文本文件META-INF/dubbo/com.alibaba.dubbo.rpc.Filter,內容以下:
    [plain]  view plain  copy
     
    1. xxxFilter=com.xxx.AuthorityFilter  
    修改dubbo的provider配置文件,在dubbo:provider中添加配置的filter,以下:
    [html]  view plain  copy
     
    1. <dubbo:provider filter="xxxFilter" />  

這樣就能夠實現dubbo接口的IP白名單功能了。