(17)Reactor的調試——響應式Spring的道法術器

本系列文章索引《響應式Spring的道法術器》
前情提要 Reactor 3快速上手 | 響應式流規範
本文測試源碼

2.7 調試

在響應式編程中,調試是塊難啃的骨頭,這也是從命令式編程到響應式編程的切換過程中,學習曲線最陡峭的地方。

在命令式編程中,方法的調用關係擺在面上,我們通常可以通過stack trace追蹤的問題出現的位置。但是在異步的響應式編程中,一方面有諸多的調用是在水面以下的,作爲響應式開發庫的使用者是不需要了解的;另一方面,基於事件的異步響應機制導致stack trace並非很容易在代碼中按圖索驥的。

比如下邊的例子:

    @Test
    public void testBug() {
        getMonoWithException()
                .subscribe();
    }
  1. single()方法只能接收一個元素,多了的話就會導致異常。

上邊的代碼會報出如下的異常stack trace:

reactor.core.Exceptions$ErrorCallbackNotImplemented: java.lang.IndexOutOfBoundsException: Source emitted more than one item

Caused by: java.lang.IndexOutOfBoundsException: Source emitted more than one item
    at reactor.core.publisher.MonoSingle$SingleSubscriber.onNext(MonoSingle.java:129)
    at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.tryOnNext(FluxFilterFuseable.java:129)
    at reactor.core.publisher.FluxMapFuseable$MapFuseableConditionalSubscriber.tryOnNext(FluxMapFuseable.java:284)
    at reactor.core.publisher.FluxRange$RangeSubscriptionConditional.fastPath(FluxRange.java:273)
    at reactor.core.publisher.FluxRange$RangeSubscriptionConditional.request(FluxRange.java:251)
    at reactor.core.publisher.FluxMapFuseable$MapFuseableConditionalSubscriber.request(FluxMapFuseable.java:316)
    at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.request(FluxFilterFuseable.java:170)
    at reactor.core.publisher.MonoSingle$SingleSubscriber.request(MonoSingle.java:94)
    at reactor.core.publisher.LambdaMonoSubscriber.onSubscribe(LambdaMonoSubscriber.java:87)
    at reactor.core.publisher.MonoSingle$SingleSubscriber.onSubscribe(MonoSingle.java:114)
    at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.onSubscribe(FluxFilterFuseable.java:79)
    at reactor.core.publisher.FluxMapFuseable$MapFuseableConditionalSubscriber.onSubscribe(FluxMapFuseable.java:236)
    at reactor.core.publisher.FluxRange.subscribe(FluxRange.java:65)
    at reactor.core.publisher.FluxMapFuseable.subscribe(FluxMapFuseable.java:60)
    at reactor.core.publisher.FluxFilterFuseable.subscribe(FluxFilterFuseable.java:51)
    at reactor.core.publisher.MonoSingle.subscribe(MonoSingle.java:58)
    at reactor.core.publisher.Mono.subscribe(Mono.java:3077)
    at reactor.core.publisher.Mono.subscribeWith(Mono.java:3185)
    at reactor.core.publisher.Mono.subscribe(Mono.java:2962)
    at com.getset.Test_2_7.testBug(Test_2_7.java:19)
    ... 

比較明顯的信息大概就是那句“Source emitted more than one item”。下邊的內容基本都是在Reactor庫內部的調用,而且上邊的stack trace的問題是出自.subscribe()那一行的。

如果對響應式流內部的Publisher、Subscriber和Subscription的機制比較熟悉,大概可以根據subscribe()request()的順序大概猜測出來getMonoWithException()方法內大約經過了.map.filter.range的操作鏈,但是除此之外,確實獲取不到太多信息。

另一方面,命令式編程的方式比較容易使用IDE的調試工具進行單步或斷點調試,而在異步編程方式下,通常也不太好使。

以上這些都是在異步的響應式編程中可能會遇到的窘境。解鈴還須繫鈴人,對於響應式編程的調試還需要響應式編程庫本身提供調試工具。

2.7.1 開啓調試模式

Reactor提供了開啓調試模式的方法。

Hooks.onOperatorDebug();

這個方法能夠開啓調試模式,從而在拋出異常時打印出一些有用的信息。把這一行加上:

    @Test
    public void testBug() {
        Hooks.onOperatorDebug();
        getMonoWithException()
                .subscribe();
    }

這時候,除了上邊的那一套stack trace之外,增加了以下內容:

    Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: 
Assembly trace from producer [reactor.core.publisher.MonoSingle] :
    reactor.core.publisher.Flux.single(Flux.java:6473)
    com.getset.Test_2_7.getMonoWithException(Test_2_7.java:13)
    com.getset.Test_2_7.testBug(Test_2_7.java:19)
Error has been observed by the following operator(s):
    |_  Flux.single(Test_2_7.java:13)

這裏就可以明確找出問題根源了。

Hooks.onOperatorDebug()的實現原理在於在組裝期包裝各個操作符的構造方法,加入一些監測功能,所以這個 hook 應該在早於聲明的時候被激活,最保險的方式就是在你程序的最開始就激活它。以map操作符爲例:

    public final <V> Flux<V> map(Function<? super T, ? extends V> mapper) {
        if (this instanceof Fuseable) {
            return onAssembly(new FluxMapFuseable<>(this, mapper));
        }
        return onAssembly(new FluxMap<>(this, mapper));
    }

可以看到,每次在返回新的Flux對象的時候,都會調用onAssembly方法,這裏就是Reactor可以在組裝期插手“搞事情”的地方。

Hooks.onOperatorDebug()是一種全局性的Hook,會影響到應用中所有的操作符,所以其帶來的性能成本也是比較大的。如果我們大概知道可能的問題在哪,而對整個應用開啓調試模式,也容易被茫茫多的調試信息淹沒。這時候,我們需要一種更加精準且廉價的定位方式。

2.7.2 使用 checkpoint() 來定位

如果你知道問題出在哪個鏈上,但是由於這個鏈的上游或下游來自其他的調用,就可以針對這個鏈使用checkpoint()進行問題定位。

checkpoint()操作符就像一個Hook,不過它的作用範圍僅限於這個鏈上。

    @Test
    public void checkBugWithCheckPoint() {
        getMonoWithException()
                .checkpoint()
                .subscribe();
    }

通過增加checkpoint()操作符,仍然可以打印出調試信息:

    Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: 
Assembly trace from producer [reactor.core.publisher.MonoSingle] :
    reactor.core.publisher.Mono.checkpoint(Mono.java:1367)
    reactor.core.publisher.Mono.checkpoint(Mono.java:1317)
    com.getset.Test_2_7.checkBugWithCheckPoint(Test_2_7.java:25)
Error has been observed by the following operator(s):
    |_  Mono.checkpoint(Test_2_7.java:25)

checkpoint()方法還有變體checkpoint(String description),你可以傳入一個獨特的字符串以方便在 assembly traceback 中進行識別。 這樣會省略掉stack trace,不過你可以依賴這個字符串來定位到出問題的組裝點。checkpoint(String) 比 checkpoint 有更低的執行成本。如下:

    @Test
    public void checkBugWithCheckPoint2() {
        getMonoWithException()
                .checkpoint("checkBugWithCheckPoint2")
                .subscribe();
    }

加入用於標識的字符串(方法名),輸出如下:

    Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: 
Assembly site of producer [reactor.core.publisher.MonoSingle] is identified by light checkpoint [I_HATE_BUGS]."description" : "checkBugWithCheckPoint2"

可以看到這裏確實省略了調試的assembly traceback,但是我們通過上邊的信息也可以定位到是single的問題。

上邊的例子比較簡單,當有許多的調試信息打印出來的時候,這個標識字符串能夠方便我們在許多的控制檯輸出中定位到問題。

如果既希望有調試信息assembly traceback,也希望用上標識字符串,還可以checkpoint(description, true)來實現,第二個參數true標識要打印assembly traceback。

2.7.3 使用log()操作符瞭解執行過程

最後一個方便調試的工具就是我們前邊多次用到的log()操作符了,它能夠記錄其上游的Flux或 Mono的事件(包括onNextonErroronComplete, 以及onSubscribecancel、和request)。

log操作符可以通過SLF4J使用類似Log4J和Logback這樣的公共的日誌工具來記錄日誌,如果SLF4J不存在的話,則直接將日誌輸出到控制檯。

控制檯使用 System.err 記錄WARNERROR級別的日誌,使用 System.out 記錄其他級別的日誌。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章