這是 why 技術的第 28 篇原創文章
之前在《Dubbo 一致性哈希負載均衡的源碼和 Bug,了解一下?》中寫到了我發現了一個 Dubbo 一致性哈希負載均衡算法的 Bug。
對於解決方案我是這樣寫的:
特別簡單,把獲取identityHashCode的方法從System.identityHashCode(invokers)修改為invokers.hashCode()即可。此方案是我提的issue里面的評論,這里System.identityHashCode和 hashCode之間的聯系和區別就不進行展開講述了,不清楚的大家可以自行了解一下。
我說:這里 System.identityHashCode 和 hashCode 之間的聯系和區別就不進行展開講述了,不清楚的大家可以自行了解一下。
但是有讀者在后台問我詳細原因,我已經和他聊清楚了。
再加上這個BUG 已於近期修復了,且只用了一行代碼就修復了,那我就寫一下解決方案,以及背后的原理。
即是對之前文章的一個補充,也是一個獨立的知識點。
所以本文主要是回答下面這三個問題:
1.什么是 System.identityHashCode?
2.什么是 hashCode?
3.為什么一行代碼就修復了這個 BUG?
注:本文 Dubbo 源碼 2.7.4.1 版本。如果閱讀過《Dubbo 一致性哈希負載均衡的源碼和 Bug,了解一下?》可以更好的理解這篇文章。但是沒有讀過也不會影響閱讀。
前情回顧
先通過一個前情回顧,引出本文所要分享的內容。
Dubbo 一致性哈希負載均衡算法的設計初衷應該是如果沒有服務上下線的操作,后續請求根據已經映射好的哈希環進行處理,不需要重新映射。
然而我在研究其源碼時,我發現實際情況是即使在服務端沒有上下線操作的時候,一致性哈希負載均衡算法每次都需要重新進行 hash 環的映射。
實際情況與設計初衷不符。
於是給 Dubbo 提了一個 issue,地址如下:
https://github.com/apache/dubbo/issues/5429
以下內容是對該 issue 的詳細說明:
在 Dubbo 對應的源碼中,只需要一行代碼。就可以判斷是否有服務上下線的操作:
就是下面這一行代碼:
int identityHashCode = System.identityHashCode(invokers);
通過判斷 invokers(服務提供方 List 集合)的 identityHashCode 是否發生了變化,從而判斷是否有服務上下線的操作。
但是這行代碼,在Dubbo2.7.0 版本之后就失效了。
問題出在 Dubbo2.7.0 版本引入的新特性之一:標簽路由。
其對應的源碼如下:
org.apache.dubbo.rpc.cluster.router.tag.TagRouter#filterInvoker
通過源碼可以看出:在 TagRouter 中的 stream 操作,改變了 invokers,導致每次調用時其 System.identityHashCode(invokers)返回的值不一樣。
所以每次調用都會進行哈希環的映射操作,在服務節點多,虛擬節點多的情況下一定會有性能問題。
該問題對應的 PR 鏈接如下:
https://github.com/apache/dubbo/pull/5440
修復方法也是特別簡單:把獲取 identityHashCode 的方法從 System.identityHashCode(invokers)修改為 invokers.hashCode()即可。如下圖所示:
為什么一行代碼就能修復?
為什么把獲取 identityHashCode 的方法從 System.identityHashCode(invokers)修改為 invokers.hashCode()就可以了呢?
要回答這個問題,我們首先得明白什么是 identityHashCode?什么是 hashCode?
**什么是 identityHashCode?**我們看看 API 里面的注釋:
返回與默認方法 hashCode()返回的給定對象相同的哈希碼,無論給定對象的類是否覆蓋了 hashCode()。空引用的哈希碼為零。
另外關於 identityHashCode 還有下面的三條規則:
1.所以如果兩個對象 A == B,那么 A、B 的 System.identityHashCode() 必定相等;
2.如果兩個對象的 System.identityHashCode() 不相等,那他們必定不是同一個對象;
3.但是如果兩個對象的 System.identityHashCode()相等,並不保證 A==B,因為 identityHashCode 的底層實現是基於一個偽隨機數實現的。
什么是 hashCode? 大家應該都比較熟了,還是看 API 上的注釋:
再結合下面兩個示例代碼,深入理解。
示例一:WhyHashCodeDto沒有重寫 hashCode()方法,所以 identityHashCode 和 hashCode 的值是一樣的:
示例二:如下所示,String 是重寫了 hashCode()的方法,所以在下面的例子中 identityHashCode 不等於 hashCode:
帶入場景
有了前面的知識鋪墊,我們就可以回到 Dubbo 的一致性哈希算法的場景中去了。
在 PR 中有一行注釋是這樣寫的:
using the hashcode of list to compute the hash only pay attention to the elements in the list
我們應該只注意 list 里面的元素就可以了。 而這個 list 里面的元素,就是一個個的服務提供方。
所以,在 Dubbo 的一致性哈希算法的場景中,我們只需要關心 List 里面的服務提供方是否有上下線的操作,而不關心這個 List 是否每次都是新的。
我們再回到源碼中,結合源碼,然后簡化源碼:
把上面的源碼抽離一下,簡化一下,如下:
filterInvoker 方法是根據條件過濾 invokers,並返回一個 List。而我傳入的條件是,過濾出 invokers 中 invoker 大於 0 的數據:
filterInvoker(invokers, invoker -> invoker > 0);
執行結果如下:
可以看到經過 filterInvoker 方法后,由於集合中所有的元素都滿足條件,所以過濾前后,集合中的元素並沒有發生變化,導致 hashCode 沒有變化。但是由於裝元素的容器(集合)已經不是原來的容器了,所以 identityHashCode 發生了變化。
"因為集合中的元素沒有發生變化,導致 hashCode 沒有變化。"這句話的理由是什么?
因為 List 重寫了 hashCode()方法,其算出的 hashCode 只和 list 中的元素相關:
經過 filterInvoker 方法后元素還是【1,2,3】,與過濾之前一樣,所以 hashCode 沒有變。
"由於裝元素的容器(集合)已經不是原來的容器了,所以 identityHashCode 發生了變化。"這句話的理由又是什么?
可以看到在源碼中,Collectors.toList()方法會 new List。所以都是新的,那么每次的 identityHashCode 必不相同。
上面的示例代碼,模擬的是沒有服務上下線的操作。
接下來,我們模擬一下服務下線的場景:
這次傳入的過濾條件為,過濾出 invokers 中 invoker 大於 1 的數據:
filterInvoker(invokers, invoker -> invoker > 1);
輸出結果如下:
可以看到,過濾后的集合中只有【2,3】了,所以 hashCode 發生了變化。
上面的示例在 Dubbo 的一致性哈希算法的場景中相當於 1 號服務器下線了,服務列表發生了變化,需要重新進行哈希環的映射。
對應源碼如下(PR 提交的源碼):
因為在標號為 ① 處得到的 invokersHashCode 和之前的不一樣了,所以在標號為 ② 處判斷條件為真,進入標號為 ③ 的代碼處,重新進行 Hash 環的映射,並選擇某個虛擬節點執行該請求。
通過上面模擬的兩個示例,再結合下面的源碼:
也就回答了為什么把上圖中編號為 ① 處的代碼替換為標號為 ② 的代碼,這一行代碼就能修復這個 Bug,核心思想就是只關心 List 集合里面的元素變化,而不關心 List 集合容器是否發生變化。
最后說一句
最開始找到這個 BUG 的時候,我自己也是有一套解決方案的。思路也是只關心 List 里面的元素,而不關心 List 這個容器,但是實現方式比較復雜,改動點較多,還需要寫一個工具類。
但是看到 issue 下面的這個評論,
我才一下回過神來,原來一行代碼就能代替我寫的工具類了啊。而對於這個知識點,我之前其實是知道的。
我反思了一下自己為什么沒有想到這個方案。
其實就是對於已知道的知識點,掌握不夠深刻導致的,沒有達到融會貫通的地步。知其然,也知其所以然,可惜在需要使用的場景稍稍一變的情況下,就想不起來了。
知道知識點,但是該用的時候卻記不起來,這種情況其實挺常見的,那怎么解決呢?
這篇文章就是我的解決方案,記錄下來嘛。就像高中的時候人手一本的錯題本,做錯的題,不會的題都抄下來嘛。沒事的時候翻一翻,總有下次碰到的時候。再次碰到時,就是"一雪前恥"的機會。
好了。
才疏學淺,難免會有紕漏,如果你發現了錯誤的地方,還請你留言給我指出來,我對其加以修改。
感謝您的閱讀,感謝您的關注。
以上。
歡迎關注公眾號【why 技術】,堅持輸出原創。願你我共同進步。