修了Apache Dubbo的一個(gè)bug
這篇文章其實(shí)并沒(méi)有什么技術(shù)性的分享,從我的角度而言,更多是記錄和思考。
把我對(duì)于源碼和之前寫的部分文章反哺給我的一些東西,帶來(lái)的一點(diǎn)點(diǎn)思考分享給大家。
一行源碼
我很長(zhǎng)時(shí)間沒(méi)打開(kāi)我的 Outlook 郵箱了。
前兩天打開(kāi)的時(shí)候發(fā)現(xiàn)我之前給 Dubbo 提交的 pr 居然已經(jīng)被合并到 master 了:

這是第一次,我提交的 pr 被合并了。
這個(gè) pr 是修復(fù) LFU 緩存策略在 Dubbo 中即使配置了,也不起作用的 bug。
于是我也算是為開(kāi)源項(xiàng)目貢獻(xiàn)過(guò)源碼的人了。
什么你問(wèn)我貢獻(xiàn)了多少代碼?
一行,是的,就一行!

而且,說(shuō)起來(lái),這次提交真的是沒(méi)有什么技術(shù)含量的事情。因?yàn)檫@是一個(gè)必現(xiàn)的 bug,只是很少有人用到這個(gè)功能而已。
你知道的,當(dāng)一個(gè) bug 能穩(wěn)定復(fù)現(xiàn)的時(shí)候,其實(shí)它已經(jīng)就不算是一個(gè) bug 了。
但是我想聊聊這次提交背后的一些東西。

發(fā)現(xiàn)與解決
從宿命論的角度來(lái)說(shuō),當(dāng)我寫下面這篇文章的第一個(gè)字的時(shí)候,這個(gè) bug 就注定是等著我去發(fā)現(xiàn)并修復(fù)了:
其實(shí)吧,LRU也就那么回事。
而這篇文章我敲下第一個(gè)字的時(shí)間是 2020 年 12 月的下旬,這是我 2020 年的最后一篇技術(shù)原創(chuàng)文章。
當(dāng)我寫 LRU 的時(shí)候,我就知道 LFU 肯定也是需要專門寫一篇的。
于是 2021 年的第一篇技術(shù)原創(chuàng)文章,我就選題了 LFU。
產(chǎn)生了這篇文章:
哎,這讓人摳腦殼的 LFU。
寫這篇文章的時(shí)候,我想起之前看 Dubbo 的版本,好像是提到了一下 LFU。
于是我翻到了 2.7.7 版本的發(fā)布內(nèi)容:

果然是支持了 LFU 緩存策略,于是翻出了提交的代碼記錄:

雖然他的實(shí)現(xiàn)邏輯沒(méi)有問(wèn)題,Test 類也跑過(guò)去了。
但是毫不夸張的說(shuō),我看了一眼這個(gè)提交記錄,通過(guò)眼神編譯,就發(fā)現(xiàn)了這里勢(shì)必是有問(wèn)題的。

他僅僅是把 LFU 緩存策略集合到了 Dubbo 代碼中,但是卻未提供使用的入口。
因?yàn)檫@里是基于 SPI 實(shí)現(xiàn)的,他沒(méi)有在對(duì)應(yīng)的配置文件中加入配置。
這個(gè)問(wèn)題非常容易驗(yàn)證,我們可以看一下。
其源碼的位置是:org.apache.dubbo.common.utils.LFUCache
源碼里面告訴我這樣配置一下就可以使用 LFU 的緩存策略:

但是,當(dāng)我這樣配置,發(fā)起調(diào)用之后,是這樣的:

可以看到當(dāng)前請(qǐng)求的緩存策略確實(shí)是 LFU。
但是會(huì)拋出一個(gè)錯(cuò)誤:

No such extension org.apache.dubbo.cache.CacheFactory by name lfu
沒(méi)有 LFU 這個(gè)策略。
這不是玩我嗎?
再看一下具體的原因:

在 org.apache.dubbo.common.extension.ExtensionLoader#getExtensionClasses 處只獲取到了 4 個(gè)緩存策略,并沒(méi)有我們想要的 LFU。
所以,在這里拋出了異常:

為什么沒(méi)有找到我們想要的 LFU 呢?
那就得看你熟不熟悉 SPI 了。
在 SPI 文件中,確實(shí)沒(méi)有 LFU 的配置:

所以,這是個(gè) Bug,而這個(gè) Bug 的解決方案,就是在 SPI 文件里面加上一行 LFU 的配置即可。

經(jīng)過(guò)上面的分析,其實(shí)你也發(fā)現(xiàn)了,這個(gè)并不是一個(gè)有什么技術(shù)含量的提交。
更多的是運(yùn)氣成分。
只是由于對(duì)于 Dubbo 框架有些許的了解,所以對(duì)于這個(gè)地方,我發(fā)現(xiàn)問(wèn)題、定位問(wèn)題、解決問(wèn)題的速度非常的快。
這是運(yùn)氣給不了我的東西。
這需要日復(fù)一日的潛入到框架中去,去感受它的脈絡(luò),梳理它的結(jié)構(gòu),學(xué)習(xí)它的思想。
這是需要時(shí)間去沉淀和學(xué)習(xí)的東西。
注意,我說(shuō)的是“潛入”,而非是流于表面的。
什么是流于表面的呢?
比如,如果你之前沒(méi)有用過(guò) Dubbo 框架,但你又想去了解,學(xué)習(xí)它。
于是你看到了我的這篇或者其他的和 Dubbo 相關(guān)的文章,企圖從這些文章中入手。
記住魯迅先生的話:

這里只適合查漏補(bǔ)缺,不適合系統(tǒng)的學(xué)習(xí)。
亦或者是你在搜索框里面,輸入 “Dubbo”,然后漫無(wú)目的的看了起來(lái)。
哪怕你買了一本 Dubbo 相關(guān)的書或者看了 Dubbo 相關(guān)的系列視頻,進(jìn)行系統(tǒng)的學(xué)習(xí)。
我覺(jué)得,只要沒(méi)有自己親手去做,都屬于流于表面。
而自己動(dòng)手的第一步,就是搭建 Demo,從 Demo 入手。
到后面高階一點(diǎn)的就是你了解到了這個(gè)框架的前世今生,能在幾個(gè)大版本之間進(jìn)行橫向?qū)Ρ?,知道為什么升?jí)、怎么升級(jí)、升級(jí)之后是怎么樣的。
再之后,能細(xì)致到某一個(gè)大的模塊的演變是怎樣的,歷史上出現(xiàn)過(guò)哪些 Bug,是怎么去修復(fù)的,在哪個(gè)版本之后修復(fù)了,是穩(wěn)定的。
再舉個(gè)例子吧。
另外一個(gè)bug
回到最開(kāi)始的地方,我為什么會(huì)在寫 LFU 的時(shí)候聯(lián)想到 Dubbo 呢?
因?yàn)樵?2.7.7 這個(gè)版本發(fā)布的時(shí)候,我就關(guān)注到了它。
而當(dāng)時(shí)關(guān)注到它的原因并不是 LFU ,而是新增了一種負(fù)載均衡策略:

于是我把之前的文章進(jìn)行了匯總,寫下了這篇文章:
吐血輸出:2萬(wàn)字長(zhǎng)文帶你細(xì)細(xì)盤點(diǎn)五種負(fù)載均衡策略。
而其中一致性哈希負(fù)載均衡策略,我在實(shí)踐的時(shí)候也發(fā)現(xiàn)了一個(gè) bug。
其實(shí)這個(gè) bug 也是一個(gè)必現(xiàn)的 bug,為什么沒(méi)有被爆出來(lái)的原因,我想是因?yàn)楫?dāng)前的版本使用的人不多,而使用一致性哈希負(fù)載均衡策略的就更少了,甚至沒(méi)有。
這個(gè) bug 具體是這樣的:
https://github.com/apache/dubbo/issues/5429

我已經(jīng)知道了在一致性哈希算法中的這行代碼就是導(dǎo)致 bug 的原因:
System.identityHashCode(invokers)
甚至我也知道了,這行代碼導(dǎo)致 bug 的原因是 invokers 這個(gè)集合的地址變了。
這個(gè)集合里面,放的就是服務(wù)提供者列表。
集合里面的服務(wù)者列表其實(shí)并沒(méi)有變化,只是每次都用了一個(gè)新的 list 來(lái)裝這些服務(wù)提供者。
而為什么每次都用一個(gè)新的 list 來(lái)裝,我也找到了。
問(wèn)題就出在 TagRouter 中:
org.apache.dubbo.rpc.cluster.router.tag.TagRouter#filterInvoker

基本上到這里,也就明確原因了。
但是我前面說(shuō)了,更高一級(jí)的是了解這個(gè)框架的前世今生。
問(wèn)題出在 TagRouter,那么這個(gè) TagRouter 怎么來(lái)的呢?
如果了解 Dubbo 2.7.x 版本新特性的朋友可能知道,標(biāo)簽路由是 Dubbo2.7 引入的新功能。

巧就巧在我還真的清楚這個(gè)地方的來(lái)龍去脈。
因?yàn)槲业牡谝黄夹g(shù)文章就是寫的 Dubbo 2.7 新特性,當(dāng)時(shí)進(jìn)行了一個(gè)了解。
沒(méi)想到一年多以后,竟然還呼應(yīng)上了。
而這個(gè) bug,其實(shí)也是一行代碼就能修復(fù);

而我當(dāng)時(shí)為什么沒(méi)有去修復(fù)呢?
因?yàn)樽铋_(kāi)始找到這個(gè) bug 的時(shí)候,我想到的解決方案是寫個(gè)工具類。
思路也是只關(guān)心 List 里面的元素,而不關(guān)心 List 這個(gè)容器,但是實(shí)現(xiàn)方式比較復(fù)雜,改動(dòng)點(diǎn)較多,還需要寫一個(gè)工具類。
當(dāng)時(shí)就沒(méi)動(dòng)手,想著先提個(gè) issue 放著,有時(shí)間了再弄。
結(jié)果,沒(méi)想到 issue 放上去的當(dāng)天就有人回復(fù)并了一個(gè)我沒(méi)有想到的解決方案:

看到這個(gè)回復(fù)的時(shí)候,我才一下回過(guò)神來(lái),原來(lái)一行代碼就能代替我寫的工具類了啊。
而對(duì)于其中涉及到的知識(shí)點(diǎn),我是知道的。
我反思了一下自己為什么沒(méi)有想到這個(gè)方案。
其實(shí)就是對(duì)于已知道的知識(shí)點(diǎn),掌握不夠深刻導(dǎo)致的,沒(méi)有達(dá)到融會(huì)貫通的地步。
知其然,也知其所以然,可惜在需要使用的場(chǎng)景稍稍一變的情況下,就想不起來(lái)了。
知道知識(shí)點(diǎn),但是該用的時(shí)候卻記不起來(lái),這種情況其實(shí)挺常見(jiàn)的,那怎么解決呢?
于是我寫下了這篇文章:
夠強(qiáng)!一行代碼就修復(fù)了我提的Dubbo的Bug。
這篇文章就是我的解決方案,記錄下來(lái)嘛。
就像高中的時(shí)候人手一本的錯(cuò)題本,做錯(cuò)的題,不會(huì)的題都抄下來(lái)嘛。沒(méi)事的時(shí)候翻一翻,總有下次碰到的時(shí)候。再次碰到時(shí),就是“一雪前恥”的機(jī)會(huì)。
寫過(guò)但沒(méi)有發(fā)現(xiàn)的bug
我之前還寫過(guò)一樣的一篇文章:
Dubbo 2.7.5在線程模型上的優(yōu)化
當(dāng)時(shí)這個(gè)版本推出之后,我就趕緊去研究了一下對(duì)應(yīng)部分的源碼,然后寫下這篇自稱為全網(wǎng)第一篇解析 Dubbo 2.7.5 里程碑版本中的客戶端線程模型優(yōu)化的文章。
但是前兩天我看提交記錄的時(shí)候,發(fā)現(xiàn)了這樣的一個(gè)提交:

并找到了對(duì)應(yīng)的 issue:
https://github.com/apache/dubbo/issues/7054

根據(jù)這個(gè) issue,我去看了一下對(duì)應(yīng)的源碼,確實(shí)是存在他描述的問(wèn)題。
于是我就在想,我當(dāng)時(shí)寫文章的時(shí)候也是深入到源碼里面了呀,為什么沒(méi)有發(fā)現(xiàn)這樣的問(wèn)題呢?
我想原因還是在于自己當(dāng)時(shí)思考的深度不夠,僅僅是搭建了一個(gè)非常簡(jiǎn)陋的 Demo,而且把心思聚焦到了前后版本差異對(duì)比上。
只是摸到了一個(gè)大概的樣子,于是被源碼牽著走了,并沒(méi)有跳出源碼的包圍,帶著質(zhì)疑的眼光去審視它。
所以,對(duì)于這種比較深層次的、一環(huán)扣一環(huán)的問(wèn)題,自己還是流于表面了一些。
一旦被源碼牽著走了,大概率的情況下就會(huì)無(wú)條件的相信源碼。毫無(wú)質(zhì)疑之心。
怎么看源碼
前面舉了三個(gè)例子,一個(gè)是發(fā)現(xiàn)并解決了 bug,一個(gè)是僅發(fā)現(xiàn)未解決的 bug,一個(gè)是有 bug 但沒(méi)有發(fā)現(xiàn)。
前兩個(gè) bug 都有一個(gè)共性,在簡(jiǎn)單的 Demo 下就是必現(xiàn)的,只要跑到了對(duì)應(yīng)的地方,就會(huì)出現(xiàn)和預(yù)期不符的情況,比較容易發(fā)現(xiàn)。
最后一個(gè) bug 隱藏的比較深入一點(diǎn),也許你觸發(fā)了,但是程序自愈了。
當(dāng)有一天我能發(fā)現(xiàn)并解決這樣的 bug 時(shí),我就不會(huì)說(shuō)這是運(yùn)氣了。
但是發(fā)現(xiàn)這些 bug 的前提是得動(dòng)手搭建 Demo 呀。
你不看源碼,只是看網(wǎng)上的文章,是永遠(yuǎn)發(fā)現(xiàn)不了問(wèn)題的,也是潛入不進(jìn)去的。
分享一下我看源碼的方法吧。
我們知道開(kāi)源框架的設(shè)計(jì)和理念大多是非常優(yōu)秀的,但是源碼里面的細(xì)枝末節(jié)特別的多,一不小心就容易在源碼里面迷失,直接就是一波勸退。
所以,對(duì)于初讀源碼的同學(xué),首先要做的就是把核心流程梳理出來(lái),邊梳理邊畫圖,要多畫圖,別怕麻煩。
對(duì)于幾處關(guān)鍵的源碼,一定要寫上自己的備注。因?yàn)槟阒赖?,?dāng)時(shí)也許你對(duì)這個(gè)地方為什么這樣寫門清,但是隔段時(shí)間再回來(lái)看,就摸不著頭腦了。這個(gè)時(shí)候,備注就顯得非常重要了。
對(duì)于看不明白的地方,打斷點(diǎn),瘋狂的調(diào)試,反復(fù)的調(diào)試。
等待主流程摸清楚之后,再去進(jìn)入到源碼的細(xì)節(jié)部分。
舉個(gè)簡(jiǎn)單的例子,比如你看 Dubbo 源碼,先摸清楚它一次請(qǐng)求大概的調(diào)用鏈路之后,再去細(xì)致了解其中負(fù)載均衡的部分。
然后,就是多復(fù)習(xí),多鞏固了。

你發(fā)現(xiàn)沒(méi)有,我說(shuō)的這些其實(shí)你也知道,或者其他人也是這樣說(shuō)的。
為什么你看的時(shí)候就老是看不進(jìn)去呢?不得要領(lǐng)呢?
是的,我開(kāi)始也是這樣的。但是,無(wú)它,唯反復(fù)練習(xí)爾。
共勉之。
--end--
掃描下方二維碼 添加好友,備注【交流】 可私聊交流,也可進(jìn)資源豐富學(xué)習(xí)群
更文不易,點(diǎn)個(gè)“在看”支持一下??
