一致性 Hash 算法的實際應用

文章轉自:https://www.cnblogs.com/crossoverJie/p/10454349.html

一致性 Hash 算法的實際應用

前言

記得一年前分享過一篇《一致性 Hash 算法分析》,當時只是分析了這個算法的實現原理、解決了什麼問題等。

但沒有實際實現一個這樣的算法,畢竟要加深印象還得自己擼一遍,於是本次就當前的一個路由需求來着手實現一次。

背景

看過《爲自己搭建一個分佈式 IM(即時通訊) 系統》的朋友應該對其中的登錄邏輯有所印象。

先給新來的朋友簡單介紹下 cim 是幹啥的:

其中有一個場景是在客戶端登錄成功後需要從可用的服務端列表中選擇一臺服務節點返回給客戶端使用。

而這個選擇的過程就是一個負載策略的過程;第一版本做的比較簡單,默認只支持輪詢的方式。

雖然夠用,但不夠優雅😏。

因此我的規劃是內置多種路由策略供使用者根據自己的場景選擇,同時提供簡單的 API 供用戶自定義自己的路由策略。

先來看看一致性 Hash 算法的一些特點:

  • 構造一個 0 ~ 2^32-1 大小的環。
  • 服務節點經過 hash 之後將自身存放到環中的下標中。
  • 客戶端根據自身的某些數據 hash 之後也定位到這個環中。
  • 通過順時針找到離他最近的一個節點,也就是這次路由的服務節點。
  • 考慮到服務節點的個數以及 hash 算法的問題導致環中的數據分佈不均勻時引入了虛擬節點。

自定義有序 Map

根據這些客觀條件我們很容易想到通過自定義一個有序數組來模擬這個環。

這樣我們的流程如下:

  1. 初始化一個長度爲 N 的數組。
  2. 將服務節點通過 hash 算法得到的正整數,同時將節點自身的數據(hashcode、ip、端口等)存放在這裏。
  3. 完成節點存放後將整個數組進行排序(排序算法有多種)。
  4. 客戶端獲取路由節點時,將自身進行 hash 也得到一個正整數;
  5. 遍歷這個數組直到找到一個數據大於等於當前客戶端的 hash 值,就將當前節點作爲該客戶端所路由的節點。
  6. 如果沒有發現比客戶端大的數據就返回第一個節點(滿足環的特性)。

先不考慮排序所消耗的時間,單看這個路由的時間複雜度:

  • 最好是第一次就找到,時間複雜度爲O(1)
  • 最差爲遍歷完數組後才找到,時間複雜度爲O(N)

理論講完了來看看具體實踐。

我自定義了一個類:SortArrayMap

他的使用方法及結果如下:

可見最終會按照 key 的大小進行排序,同時傳入 hashcode = 101 時會按照順時針找到 hashcode = 1000 這個節點進行返回。


下面來看看具體的實現。

成員變量和構造函數如下:

其中最核心的就是一個 Node 數組,用它來存放服務節點的 hashcode 以及 value 值。

其中的內部類 Node 結構如下:


寫入數據的方法如下:

相信看過 ArrayList 的源碼應該有印象,這裏的寫入邏輯和它很像。

  • 寫入之前判斷是否需要擴容,如果需要則複製原來大小的 1.5 倍數組來存放數據。
  • 之後就寫入數組,同時數組大小 +1。

但是存放時是按照寫入順序存放的,遍歷時自然不會有序;因此提供了一個 Sort 方法,可以把其中的數據按照 key 其實也就是 hashcode 進行排序。

排序也比較簡單,使用了 Arrays 這個數組工具進行排序,它其實是使用了一個 TimSort 的排序算法,效率還是比較高的。

最後則需要按照一致性 Hash 的標準順時針查找對應的節點:

代碼還是比較簡單清晰的;遍歷數組如果找到比當前 key 大的就返回,沒有查到就取第一個。

這樣就基本實現了一致性 Hash 的要求。

ps:這裏並不包含具體的 hash 方法以及虛擬節點等功能(具體實現請看下文),這個可以由使用者來定,SortArrayMap 可作爲一個底層的數據結構,提供有序 Map 的能力,使用場景也不侷限於一致性 Hash 算法中。

TreeMap 實現

SortArrayMap 雖說是實現了一致性 hash 的功能,但效率還不夠高,主要體現在 sort 排序處。

下圖是目前主流排序算法的時間複雜度:

最好的也就是 O(N) 了。

這裏完全可以換一個思路,不用對數據進行排序;而是在寫入的時候就排好順序,只是這樣會降低寫入的效率。

比如二叉查找樹,這樣的數據結構 jdk 裏有現成的實現;比如 TreeMap 就是使用紅黑樹來實現的,默認情況下它會對 key 進行自然排序。


來看看使用 TreeMap 如何來達到同樣的效果。

運行結果:

127.0.0.1000

效果和上文使用 SortArrayMap 是一致的。

只使用了 TreeMap 的一些 API:

  • 寫入數據候,TreeMap 可以保證 key 的自然排序。
  • tailMap 可以獲取比當前 key 大的部分數據。
  • 當這個方法有數據返回時取第一個就是順時針中的第一個節點了。
  • 如果沒有返回那就直接取整個 Map 的第一個節點,同樣也實現了環形結構。

ps:這裏同樣也沒有 hash 方法以及虛擬節點(具體實現請看下文),因爲 TreeMap 和 SortArrayMap 一樣都是作爲基礎數據結構來使用的。

性能對比

爲了方便大家選擇哪一個數據結構,我用 TreeMapSortArrayMap 分別寫入了一百萬條數據來對比。

先是 SortArrayMap

耗時 2237 毫秒。

TreeMap:

耗時 1316毫秒。

結果是快了將近一倍,所以還是推薦使用 TreeMap 來進行實現,畢竟它不需要額外的排序損耗。

cim 中的實際應用

下面來看看在 cim 這個應用中是如何具體使用的,其中也包括上文提到的虛擬節點以及 hash 算法。

模板方法

在應用的時候考慮到就算是一致性 hash 算法都有多種實現,爲了方便其使用者擴展自己的一致性 hash 算法因此我定義了一個抽象類;其中定義了一些模板方法,這樣大家只需要在子類中進行不同的實現即可完成自己的算法。

AbstractConsistentHash,這個抽象類的主要方法如下:

  • add 方法自然是寫入數據的。
  • sort 方法用於排序,但子類也不一定需要重寫,比如 TreeMap 這樣自帶排序的容器就不用。
  • getFirstNodeValue 獲取節點。
  • process 則是面向客戶端的,最終只需要調用這個方法即可返回一個節點。

下面我們來看看利用 SortArrayMap 以及 AbstractConsistentHash 是如何實現的。

就是實現了幾個抽象方法,邏輯和上文是一樣的,只是抽取到了不同的方法中。

只是在 add 方法中新增了幾個虛擬節點,相信大家也看得明白。

把虛擬節點的控制放到子類而沒有放到抽象類中也是爲了靈活性考慮,可能不同的實現對虛擬節點的數量要求也不一樣,所以不如自定義的好。

但是 hash 方法確是放到了抽象類中,子類不用重寫;因爲這是一個基本功能,只需要有一個公共算法可以保證他散列地足夠均勻即可。

因此在 AbstractConsistentHash 中定義了 hash 方法。

這裏的算法摘抄自 xxl_job,網上也有其他不同的實現,比如 FNV1_32_HASH 等;實現不同但是目的都一樣。


這樣對於使用者來說就非常簡單了:

他只需要構建一個服務列表,然後把當前的客戶端信息傳入 process 方法中即可獲得一個一致性 hash 算法的返回。


同樣的對於想通過 TreeMap 來實現也是一樣的套路:

他這裏不需要重寫 sort 方法,因爲自身寫入時已經排好序了。

而在使用時對於客戶端來說只需求修改一個實現類,其他的啥都不用改就可以了。

運行的效果也是一樣的。

這樣大家想自定義自己的算法時只需要繼承 AbstractConsistentHash 重寫相關方法即可,客戶端代碼無須改動。

路由算法擴展性

但其實對於 cim 來說真正的擴展性是對路由算法來說的,比如它需要支持輪詢、hash、一致性hash、隨機、LRU等。

只是一致性 hash 也有多種實現,他們的關係就如下圖:

應用還需要滿足對這一類路由策略的靈活支持,比如我也想自定義一個隨機的策略。

因此定義了一個接口:RouteHandle

public interface RouteHandle {
<span class="hljs-comment">/**
 * 再一批服務器裏進行路由
 * <span class="hljs-doctag">@param</span> values
 * <span class="hljs-doctag">@param</span> key
 * <span class="hljs-doctag">@return</span>
 */</span>
<span class="hljs-function">String <span class="hljs-title">routeServer</span><span class="hljs-params">(List&lt;String&gt; values,String key)</span> </span>;

}

其中只有一個方法,也就是路由方法;入參分別是服務列表以及客戶端信息即可。

而對於一致性 hash 算法來說也是只需要實現這個接口,同時在這個接口中選擇使用 SortArrayMapConsistentHash 還是 TreeMapConsistentHash 即可。

這裏還有一個 setHash 的方法,入參是 AbstractConsistentHash;這就是用於客戶端指定需要使用具體的那種數據結構。


而對於之前就存在的輪詢策略來說也是同樣的實現 RouteHandle 接口。

這裏我只是把之前的代碼搬過來了而已。

接下來看看客戶端到底是如何使用以及如何選擇使用哪種算法。

爲了使客戶端代碼幾乎不動,我將這個選擇的過程放入了配置文件。

  1. 如果想使用原有的輪詢策略,就配置實現了 RouteHandle 接口的輪詢策略的全限定名。
  2. 如果想使用一致性 hash 的策略,也只需要配置實現了 RouteHandle 接口的一致性 hash 算法的全限定名。
  3. 當然目前的一致性 hash 也有多種實現,所以一旦配置爲一致性 hash 後就需要再加一個配置用於決定使用 SortArrayMapConsistentHash 還是 TreeMapConsistentHash 或是自定義的其他方案。
  4. 同樣的也是需要配置繼承了 AbstractConsistentHash 的全限定名。

不管這裏的策略如何改變,在使用處依然保持不變。

只需要注入 RouteHandle,調用它的 routeServer 方法。

@Autowired
private RouteHandle routeHandle ;
String server = routeHandle.routeServer(serverCache.getAll(),String.valueOf(loginReqVO.getUserId()));

既然使用了注入,那其實這個策略切換的過程就在創建 RouteHandle bean 的時候完成的。

也比較簡單,需要讀取之前的配置文件來動態生成具體的實現類,主要是利用反射完成的。

這樣處理之後就比較靈活了,比如想新建一個隨機的路由策略也是同樣的套路;到時候只需要修改配置即可。

感興趣的朋友也可提交 PR 來新增更多的路由策略。

總結

希望看到這裏的朋友能對這個算法有所理解,同時對一些設計模式在實際的使用也能有所幫助。

相信在金三銀四的面試過程中還是能讓面試官眼前一亮的,畢竟根據我這段時間的面試過程來看聽過這個名詞的都在少數😂(可能也是和候選人都在 1~3 年這個層級有關)。

以上所有源碼:

https://github.com/crossoverJie/cim

如果本文對你有所幫助還請不吝轉發。

作者: crossoverJie

出處: https://crossoverjie.top

歡迎關注博主公衆號與我交流。

本文版權歸作者所有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出, 如有問題, 可郵件(crossoverJie#gmail.com)諮詢。

標籤: GitHub, Java, Netty, IM, 算法
4
0
« 上一篇:利用策略模式優化過多 if else 代碼
» 下一篇:一個線程罷工的詭異事件
	</div>
	<div class="postDesc">posted @ <span id="post-date">2019-03-01 08:28</span> <a href="https://www.cnblogs.com/crossoverJie/">crossoverJie</a> 閱讀(<span id="post_view_count">548</span>) 評論(<span id="post_comment_count">2</span>)  <a href="https://i.cnblogs.com/EditPosts.aspx?postid=10454349" rel="nofollow">編輯</a> <a href="#" onclick="AddToWz(10454349);return false;">收藏</a></div>
</div>
<script src="//common.cnblogs.com/highlight/9.12.0/highlight.min.js"></script><script>markdown_highlight();</script><script type="text/javascript">var allowComments=true,cb_blogId=441144,cb_entryId=10454349,cb_blogApp=currentBlogApp,cb_blogUserGuid='48619d9a-ff90-4bde-9cd2-08d5d82bd790',cb_entryCreatedDate='2019/3/1 8:28:00';loadViewCount(cb_entryId);var cb_postType=1;var isMarkdown=true;</script>
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章