我去,這是出BUG了呀!

你好呀,我是why。git

前兩天在 Git 上閒逛的時候又不知不覺逛到 Dubbo 那裏去了。github

看了一下最近一個月的數據,社區活躍度仍是很高的:web

而後看了一下最新的 issue,你們提問都很積極。算法

其中看到了這樣的一個 issue,發現有點意思:apache

https://github.com/apache/dubbo/issues/8055

因而寫下這篇文章給你分享一下這個 BUG 和 BUG 背後的故事。app

放心,就算你徹底不懂 Dubbo,也不影響你瞭解這個 BUG。框架

先說一下,下文中提到的 Dubbo 代碼,沒有特別說明的地方,都是我從 git 上拉下來的 Master 分支上的代碼測試

啥 BUG 啊?

先給你描述一個這個 BUG 是啥樣的。優化

其實就是這個 issue 的做者寫出來的:Dubbo 框架裏面的 Filter 排序過程有問題,即便按照框架要求寫好規則後,最終生成的 Filter 鏈並非咱們想要的。ui

那麼徹底不懂 Dubbo 的朋友可能就遇到了第一個問題:啥是 Filter 呢?

其實就是一個過濾器而已,和 web 服務裏面過濾器在概念上沒啥兩樣。而 Dubbo 有很是多的 Filter,這些 Filter 共同組成了一個 Filter 調用鏈。

引用官網上的一個調用鏈路圖,在 Filter 的地方我框起來了:

能夠看到 Filter 是 Dubbo 框架的一個很是核心的組成部分,不少不少的功能都是從 Filter 擴展出來的。

你要是還不明白也不要緊,你只要知道有這樣的一個 Filter 調用鏈就好了,鏈上的 Filter 各司其職,各幹各的事兒。

好的,那麼如今需求來了:

我如今要求鏈上的 Filter 的執行順序是我能控制的,即我定義 Filter 的時候你得給我留個地方設置它的優先級。

聽起來是很簡單的一個需求,對吧?

我直接給你留個口子,讓你輸入 order 參數,不輸入給個默認值,而後組裝 Filter 鏈的時候根據 Order 排個序。

不是我吹牛,十分鐘就能寫完,中間還帶着三分鐘的摸魚。

可是,就這麼個需求出 BUG 了。

具體啥現象呢?

我這裏把項目拉下來,基於官方的測試用例,改巴改巴,給你演示一下這個 BUG 的體現是啥。

在 Dubbo 裏面有這樣的一個註解:

org.apache.dubbo.common.extension.Activate

這裏的 Order 就是作排序用的。簡單演示一下,你看我如今有 5 個 Filter:

排序規則是 Order 越小的越先執行,那麼這個 Filter 鏈的執行順序應該是這樣的:

Filter4 -> Filter3 -> Filter2 -> Filter1 -> Filter5

搞個測試案例,咱們驗證一下:

符合預期,沒有任何毛病。

另外說明一下,官方的關於 Filter 的測試用例在這裏,你有興趣,源碼拉下來就能夠看:

org.apache.dubbo.common.extension.support.ActivateComparatorTest#testActivateComparator

不論是官方的案例,仍是我本身寫的案例,其中最關鍵的排序功能是這一行代碼實現的:

Collections.sort(filters, ActivateComparator.COMPARATOR)

而這一行代碼裏面最關鍵的就是 ActivateComparator.COMPARATOR 這個東西。

這個東西就是 BUG 之源,不慌,等下再說。

那麼爲何說它有 BUG 呢?

前面演示了正常的狀況下,是符合預期的。

可是你看 Activate 註解,裏面還有這樣的兩個東西:

before、after,含義是指定 Filter A 在 Filter B 以前或者以後執行。

可是被打上了 @Deprecated 註解,字段說明上也備註了:

Deprecated since 2.7.0

2.7.0 以後被廢除。

那麼就有點意思了,爲啥被廢除?

來,看個例子,仍是剛剛的那個測試用例。

我就稍微的這麼一改:

@Activate(before = "_2")
public class Filter5 implements Filter0{
}

改動點就是在 Filter5 上配置了:

@Activate(before = "_2")

含義就是 Filter5 在 「_2」 以前執行。

「_2」 是啥?

就是 Filter2 的一個映射而已:

那麼問題就來了,做爲一個正常的程序猿,自信的對 Filter5 進行了這個改動以後,他心裏的想法必定是想要把這樣的 Filter 鏈:

Filter4 -> Filter3 -> Filter2 -> Filter1 -> Filter5

修改成這樣:(Filter5 在 Filter2 以前執行):

Filter4 -> Filter3 -> Filter5 -> Filter2 -> Filter1

那麼實際狀況是怎樣的呢?

來跑一把:

咋回事?這不是我預期的執行鏈啊?

是的,這就是 BUG 的表現。

咋回事啊?

究竟是咋回事呢?

且聽我給你分析一波。

上一小節我說了,問題出在排序算法上。

org.apache.dubbo.common.extension.support.ActivateComparator

來,一塊兒看一下:

首先標號爲 ① 的地方就是把 before、after、order 封裝了一下,而後提供了幾個比較的方法。你知道 ActivateInfo 這個實體裏面有這些東西就好了,後面的代碼會用到。

而後說說標號爲 ② 的地方。

這個地方你別看挺長的,可是其實邏輯特別簡單,當前對比的兩個 filter 中的任何一個配置了 before、after 就會進入到標號爲 ② 的部分的邏輯。

而後這裏面的一坨邏輯是的這樣的:

具體邏輯不細說了,等會給你來個直觀的演示。

最後標號爲 ③ 這個地方,有點意思,稍微多說幾句。

能走到標號爲 ③ 的地方,說明當前對比的兩個 filter 都沒有配置 after、before 這兩個屬性。

直接對比 Order 就好了。

這個地方對 Order 相等的狀況還作了一個特殊處理:

o1.getSimpleName().compareTo(o2.getSimpleName()) > 0 ? 1 : -1

若是 Order 相等,再比較類名。

這樣作的緣由也是保證排序的穩定性。

舉個例子,好比這兩 Filter,都沒有指定 Order:

那若是咱們去掉這個判斷:

代碼就變成了這樣:

if (a1.order > a2.order) {
    return 1 
} else {
    return -1 
    
}

簡化一下就是這樣:

return a1.order > a2.order ? 1:-1

那麼這一塊的代碼,總體就會變成這樣:

這樣仔細一看:咦,好像還能再優化一下。78 行和 80 行是同樣的,因此能夠去掉 78 行。

好的,通過這樣的一番改造。

恭喜你,得到了一個老版本的代碼:

左邊是以前版本的代碼,右邊是如今 Master 分支的代碼:

爲何會發生變化,必然是有緣由的。

看一眼提交記錄:

此次提交指向了編號爲 7778 的提交:

https://github.com/apache/dubbo/pull/7778

而此次提交指向了編號爲 7757 的 issue:

https://github.com/apache/dubbo/issues/7757

而這個 issue 在前面提到的編號爲 8055 的 issue 裏也提到了:

這個 issue 主要就是兩張圖。

第一張圖是這樣的:

在沒有任何自定義 Filter,僅有官方原有的 Filter 的狀況下,構建出來的 Filter 鏈,ExecuteLimitFilter 在 MonitorFilter 以前。

第二張圖是這樣的:

在加入了一系列自定義的 Filter(沒有指定 Order)以後,ExecuteLimitFilter 就排在了 MonitorFilter 以後了。

至於這兩個 Filter 排前排後的影響是什麼,和文本關係不大,就不擴展了,你有興趣的能夠去看看對應的連接。

總之,只有這樣的判斷邏輯是不妥當的:

return a1.order > a2.order ? 1:-1

來個例子演示一下:

左邊是測試用例,右邊是排序規則,下面是輸出結果。

從輸出結果能夠看到,最終的 Filter 鏈取決於 list 的添加順序。

這也就是 7757 這個 issues 說的:

list 的遍歷順序會影響到排序的順序。

所以,纔會有了這樣的一次提交:

好,如今咱們把排序順序改回來,一樣的測試用例再跑一次,就穩定了:

眼睛尖的朋友可能還發現了一個問題。

這個地方還有一次提交:

  • 第一種判斷:return o1.getSimpleName().compareTo(o2.getSimpleName())
  • 第二種判斷:return o1.getSimpleName().compareTo(o2.getSimpleName()) > 0 ? 1 : -1;

你說這是在幹啥?

第一種判斷還疏忽了這樣的一種狀況,包名不一樣可是類名相同的狀況:

  • com.why.a.whyFilter
  • com.why.b.whyFilter

這個時候 o1.getSimpleName().compareTo(o2.getSimpleName()) 返回的是 0。

返回 0 會發生啥?

直接吞掉一個 Filter 你信不信?

好比你的集合是 HashSet,或者是 TreeMap。

這就巧了,Dubbo 用的就是 TreeMap。

來個測試用例演示一下。

若是採用第一種判斷,最後 TreeMap 裏面只有一個 Filter 了:

若是採用第二種判斷,最後 TreeMap 裏面會有兩個 Filter :

細節,魔鬼都在細節裏面。

哎呀,真的是防不勝防啊。

好了,比較器我就說完了,可是你發現沒有,我到如今都還沒給你說排序過程不穩定這個 BUG 究竟是啥,只是給你引伸了一個其餘的 BUG 出來。

莫慌,這不是我還沒想好怎麼給你描述嘛。

這個過程其實比較複雜,涉及到 Timsort 排序方法,就這方法就得另起一篇文章才能說清楚。

因此,我換了一個思路,主要給你看比較的過程,至於這個過程背後的緣由。

就是 Timsort 在搞鬼,歡迎你本身去探索一番。

那過程是啥呢?

我在比較方法的入口處加上這樣的輸出語句:

五個 Filter 是這樣的:

測試用例是這樣的:

@Test
public void whyTest(){
    List<Class> filters = new ArrayList<>();
    filters.add(Filter4.class);
    filters.add(Filter3.class);
    filters.add(Filter2.class);
    filters.add(Filter1.class);
    filters.add(Filter5.class);
    Collections.sort(filters, ActivateComparator.COMPARATOR);
    StringBuilder builder = new StringBuilder();
    for (int i = 0; i < filters.size(); i++) {
        builder.append(filters.get(i).getSimpleName()).append("->");
    }
    System.out.println(builder.toString());
}

輸出的日誌是這樣的:

發現問題了沒?

首先我很心機的控制了一下 list 的添加順序:

這樣前三次比較就能構建這樣的 Filter 鏈:

Filter4->Filter3->Filter2->Filter1->

而後,Filter5 進來後先和 Filter1 比,發現其 Order 爲 0 比 Filter1 的 -1 大,因而比較結束,獲得這樣的Filter 鏈:

Filter4->Filter3->Filter2->Filter1->Filter5->

整個過程當中,Filter5 與 Filter2 徹底沒有發生任何比較的操做,也就更不涉及到 Filter5 裏面的 before 標籤了:

可是當我把 list 的添加順序修改一下:

咦,就正確了,你就說神不神奇?

神奇吧?

爲啥呢?

去看看 Timsort 的原理吧。

追根溯源

其實寫到這裏的我產生了一個疑問:

是誰,何時,引入了 after/before 機制?

由於這個機制我我的以爲出發點是挺好的,多一個配置的地方,把選擇權留給用戶。

可是在實際的使用中,卻容易出現比較混亂的狀況。

因而我看了一下提交記錄:

這個註解最先是梁飛(就是 Dubbo 項目要的開創者之一)寫出來的,而設計之初沒有 before 和 after,可是有一個 match 和 mismatch。

而後在寫出這個註解一天以後的凌晨 1 點 54 分,提交了一個方法級別的匹配:

這三個方法使用起來甚至比 before/after 更加複雜了。

因而一覺睡醒以後的 12:34 分,梁飛又刪除了這三個配置:

兩個月以後的 2012 年 5 月 8 日,加入了 after 和 before 配置:

而後就一直留在 Dubbo 源碼裏面,直到 6 年後的 2018 年 8 月 7 日,打上了不建議使用的註解:

並提到了這個 issue:

https://github.com/apache/dubbo/issues/2180

裏面說:Dubbo 源碼中沒有使用 after 和 before,且排序是存在問題的。

因而這兩個方法,在 2.7.0 版本以後,被標註爲不建議使用,宣告了該方法的死亡。

我不知道 2012 年,梁飛爲何引入了這兩個方法,我也曾想從他的代碼提交記錄上找到點蛛絲馬跡,惋惜沒有。

可是,有了另外的一個想法:

當年梁飛引入這兩個方法後,他寫的比較器,是否考慮到了這樣的狀況呢?

因而我立刻又看了比較器的代碼提交記錄:

org.apache.dubbo.common.extension.support.ActivateComparator

而且把他的代碼拷貝了出來,用一樣的測試用例跑了一下:

很遺憾,也有同樣的問題。

或許,當年就不該該引入這兩個方法。

大道至簡,學 Spring 的 Order,就只有一個 Order:

而後我又忽然想了另一個框架:SofaRPC。

SofaRPC 和 Dubbo 和 HSF 之間有着千絲萬縷的愛恨情仇,因而我去瞅了一眼 SofaRPC 對應的地方:

com.alipay.sofa.rpc.ext.Extension

用於排序的,也就只是保留了 order。

這樣比較器的代碼就很簡單了:

com.alipay.sofa.rpc.common.struct.OrderedComparator

另外,我順便對比了一下樑飛最先寫的比較器和如今最新的比較器的代碼,功能徹底同樣,可是代碼卻差別較大:

不得不說,通過幾回重構以後,最新的比較器的可讀性高了不少。

我追蹤了一下這個類的提交記錄,也就看着這個類的一步步演化,其實算是一個比較好的代碼重構的例子。

有興趣的本身去翻一翻。

好了,就到這了,打完收工。

相關文章
相關標籤/搜索