當架構師大劉看到實習生小李提交的記帳流水亂序的問題的時候,他知道沒錯了:這一次,大劉又要用一致性哈希這個老夥計來解決這個問題了。redis
嗯,一致性哈希,分佈式架構師必備良藥,讓咱們一塊兒來嚐嚐它。算法
1. 滿眼都是本身二十年前的樣子,讓咱們從哈希開始
在 N 年前,互聯網的分佈式架構方興未艾。大劉所在的公司因爲業務須要,引入了一套由 IBM 團隊設計的業務架構。架構
這套架構採用了分佈式的思想,經過 RabbitMQ 的消息中間件來通訊。這套架構,在當時的年代裏,算是思想超前,技術少見的黑科技架構了。分佈式
可是,因爲當年分佈式技術落地並不普遍,有不少尚不成熟的地方。因此,這套架構在經年日久的使用中,一些問題逐漸突出。其中,最典型的問題有兩個:工具
-
RabbitMQ 是個單點,它一壞掉,整個系統就會所有癱瘓。優化
-
收、發消息的業務系統也是單點。任何一點出現問題,對應隊列的消息要麼無從消費,要麼海量消息堆積。spa
不管哪一種問題,最終是整套分佈式系統都沒法使用,後續處理很是麻煩。設計
對於 RabbitMQ 的單點問題,因爲當時 RabbitMQ 的集羣功能很是弱,普通模式有 queue 自己的單點問題,因此,最終使用了 Keepalived 配合了兩臺無關係的 RabbitMQ 搞出了高可用。3d
而對於業務系統單點問題,從一開始着手解決的時候就出現了波折。通常來講,咱們要解決單點問題,方法就是堆機器,堆應用。收發是單點,咱們直接多部署幾個應用就能夠了。若是僅僅從技術上看,無非就是多個收發消息的應用你們一塊兒競爭往 MQ 中放消息拿消息而已。中間件
可是,偏偏就是在把收發消息的應用集羣化後,系統出現了問題。
自己這套系統架構會被應用到公司的多類業務上,有些業務對消息的順序有着苛刻的要求。
好比,公司內部的 IM 應用,無論是點對點的聊天仍是羣聊消息,都須要對話消息嚴格有序。而當咱們把生產消息和消費消息的應用集羣化後,問題出現了:
聊天記錄出現了亂序
A 和 B 對話,會出現某些消息沒有嚴格按照 A 發出的前後順序被 B 接收,因而整個聊天順序亂成了一鍋粥。
通過排查,發現問題的根源就在於應用集羣上。因爲沒有對應用集羣收發消息作特殊的處理,當 A 發出一條聊天信息給B時,發送到 RabbitMQ 中的信息會被在 B 處的消費端所爭搶。若是 A 在短期內發出了幾條信息,那麼就可能會被集羣中的不一樣應用搶走。
這時候,亂序的問題就出現了。雖然應用業務邏輯是相同的,可是這些集羣中的應用依然可能在處理信息速度上出現差別,最終致使用戶看到的聊天信息錯亂。
問題找到了,解決辦法是什麼?
上面咱們說過了,消息順序錯亂是由於集羣中不一樣應用搶消息而後處理速度不同致使的。若是咱們能保證 A 和 B 會話,從開始以後到會話結束以前,永遠只會被 B 所在的消費消息集羣應用中的同一個應用消費,那麼咱們就能保證消息有序。這樣一來,咱們就能夠在消費消息的那個應用中,對搶到的消息進行排隊,而後依次處理。
那麼,這種保證怎麼實現呢?
首先,咱們在 RabbitMQ 中會創建有相同前綴的隊列,後面跟着隊列編號。而後,集羣中的不一樣應用會分別監聽這兩個有着不一樣編號的隊列。當在 A 發送信息時,咱們會對信息作一次簡單的哈希:
m = hash(id) mod n
這裏,id 是用戶的標識。n 是集羣中 B 所在業務系統部署的數量。最終的 m 是咱們須要發送到的目的隊列編號。
假設,hash(id) 的結果爲 2000,n 爲 2,通過計算 m = 0。此時,A 就會把他和 B 的對話信息都發送到 chat00 的隊列裏。B 收到消息後,就會依次顯示給終端用戶。這樣,聊天亂序的問題就解決了。
那麼,事情到此就結束了嗎?這個解決方案是完美的嗎?
2. 看來,咱們須要增長應用數量了
隨着公司的發展,公司的人數也急劇上升,公司內部的 IM 使用人數也跟着多了起來,新問題又隨之出現了。
最主要的問題是,人們收到聊天信息的速度變慢了。緣由也很簡單,收取聊天信息的集羣機器不夠用了。解決辦法能夠簡單直接點,再加臺機器就行了。
不過,因爲收消息的集羣中新加入了一臺機器,這時候,咱們還須要額外多作一些事情:
-
咱們須要爲新加入的這臺機器上的應用額外再多增長一個隊列 chat02。
-
咱們還須要修改下咱們的分配消息的規則,把原來的 hash(id) mod 2 修改成 hash(id) mod 3。
-
從新啓動發送消息的項目,以便修改的規則生效。
-
把收消息的應用部署到新機器上。
到這時,一切還都在可控範圍。開發人員只須要在須要的時候,新增長個隊列,而後把咱們的分配規則小小的修改下便可。
可是,他們不知道的是,暴風雨就要來了。
3. 新的問題來了,也許這就是人生吧
因爲公司內部不少人在使用這個 IM 工具。有些時候,爲了方便,公司的客戶還有一些合做方也用起了這個 IM。這讓事情變得複雜了起來。起初,開發人員仍是像往常同樣,每當人們抱怨說收消息過慢的時候,他們就會加一臺機器。
最糟糕的是,公司的客戶也會抱怨,他們發現 IM 有時候完全不可用。這可不是小事情。公司內部人員的問題還能夠內部溝通解決。可是公司客戶的問題,大意不得,由於這關係到公司產品的名譽。
那麼,這究竟是怎麼一回事呢?
原來,根本緣由還在於每次修改完配置規則後的重啓服務。每次修改完配置規則,就須要規劃好一個恰當的停機時間,去從新對項目作個上線。
可是,這種方法在公司的客戶也使用這個 IM 後就行不通了。由於公司的客戶有很多是在國外的。也就是說,無論白天仍是深夜,極可能老是有人在使用這個 IM。
這就迫使開發人員們,在增長機器時,還須要去和多方協調溝通出一個上線時間,而後發佈公告,再去上線。這種反覆溝通,再上線,再反覆溝通,再上線直接把開發人員們折騰了個半死。
每每溝通完,上線時間直接被放到了半個月之後。而在這半個月裏,開發人員還要承受無數內部 IM 使用人的口水。費心竭力的溝通,聲嘶力竭的解釋,缺眠少覺的上線,這一切的一切推進着開發人員們必須對眼前這套技術方案做出改變了。
4. 思路轉起來,隊列環起來
新的技術方案的需求本質就是:
不管是分配消息規則變化仍是集羣機器添加都不能停機停服務
對於這種狀況,一個很好的解決方案就是若是咱們對項目配置文件進行動態的定時檢測,當發現變更時,刷新配置規則便可。
一切看上去很美好,採用了動態的定時檢測後,每當咱們須要新增集羣中的機器時,咱們只須要以下三個步驟了:
-
增長一個隊列
-
修改分配消息的規則
-
部署新的機器
客戶毫無感知,開發人員們也不須要和用戶們協調溝通出專門的上線安排。但是,這個方案也存在一些問題:
-
隨着咱們的系統部署愈來愈多,咱們須要手工修改規則的系統也愈來愈多。
-
若是消費機器宕機了,咱們須要刪除隊列,同時還須要去刪除修改分配消息的規則,等到機器恢復了,咱們還要再把分配消息的規則改回去。
這個分配消息的規則真討厭啊,每次有變更,就要去關心這個分配消息的規則。有沒有什麼辦法能把這個分配變得更自動化一些呢?
若是咱們假設在 MQ 中有 100 個收發聊天信息的隊列(100:這是對咱們的IM不可能達到的一個數字),咱們只須要在配置規則中配置成:
m = hash(id) mod 100
而後,咱們的發送消息的應用啓動後,去動態的探測出真實的全部收發聊天信息的隊列信息。
當咱們經過哈希算出的編號發現沒有真實對應的隊列存在時,就根據必定的規則,去找到一個真實存在的隊列,這個隊列,就是咱們要發消息的隊列。
若是咱們作到這樣,那麼之後,每次隊列有變化,不管增多仍是減小,咱們都不須要再去考慮分配規則的事情了,只須要移除有問題的隊列或者增長有對應消費者的隊列便可。
這個思想,就是一致性哈希的思想。
具體怎麼作呢?
第一步,咱們假設有個 100 個收發聊天信息的隊列,而且這些隊列處於一個環上。
第二步,咱們獲取到真實的收發聊天信息的隊列數量,假設有 5 個。
第三步,咱們把真實的隊列映射到咱們第一步假設的環中。
第四步,咱們經過分配規則 hash(id) mod 100 計算出對應的隊列編號。
若是 hash(id) 的結果爲 2000,那麼算出的隊列編號 m = 0。這時候,咱們一查,發現對應編號 0 的 chat00 隊列確實存在,那麼就直接發送消息到 chat00 中。
若是咱們的 hash(id) 的結果爲 1999,那麼算出的隊列編號 m = 99。此時,咱們去查隊列映射關係,發現 99 編號並無對應的真實隊列。這時候怎麼辦?很簡單,咱們順時針繼續往下找,找到誰了呢?0 對應的 chat00 隊列,這是真實存在的,這時候,咱們就將消息發送到 chat00 隊列中。
上面四步就是一個基本的一致性哈希算法了。
那麼,這套一致性哈希算法知足咱們不想老是更新消息分配規則的需求嗎?讓咱們驗證一下:
-
假設咱們須要在消費信息端集羣增長一臺機器
咱們若是要增長一臺機器,那麼同時咱們也須要在 MQ 中增長一個隊列。這時候,咱們的分配規則是 hash(id) mod 100,增長了隊列後,真實的隊列數假設爲 6。此時,若是 hash(id) mod 100 的結果小於 6,那麼分配的規則和沒有增長機器的時候規則同樣,之前分配到哪一個隊列,如今仍是分配到哪一個隊列。可是對於結果等於 6 的狀況,則發生了變化。信息會被自動分配給 chat05。當分配給 chat05 後,新的消費者就會自動開始進入正常工做了,咱們不須要作任何人工干預,也不須要考慮分配規則的變化。增長機器之前:
增長機器以後:
-
假設消費信息端集羣一臺機器宕機了
模擬宕機,此時咱們會去減小一個隊列。減小後的真實隊列數量爲 5,則正好和增長隊列相反,m = 5 時,那麼行爲不會有任何變化,之前分到哪一個隊列,仍是分到哪一個隊列。若是 m = 6,因爲已經不存在真實的隊列了,就會作順時針查找,結果找到 chat00,之前會分到 chat05 的就會被分到 chat00。而此時,chat00 因爲正好有消費者,因此,系統的用戶是毫無感知的,咱們也專心修復咱們機器便可。當機器恢復後,就會和新增機器同樣,計算結果爲 6 的信息會被從新分配回 chat05。
目前,咱們能夠看到,當咱們引入一致性哈希後,咱們無論新增機器仍是集羣機器宕機,我只須要跟隨着機器的狀態,作一個操做便可:增長或者減小 MQ 中的隊列。一切簡單化了。
那麼,這個方案是否依然還有問題呢?
5. 失衡的圓環,壓垮駱駝的可能只是一根稻草
假設咱們目前有 5 個隊列存在,咱們的分配規則是 m = hash(id) mod 100。那麼,此時,問題就出來了。
若是 m 的值大於 5,因爲沒有對應的真實隊列存在,系統就會順時針順着咱們構造出來的哈希環找,最終會找到 chat00 這個隊列上。
而後,你會發現,只要是 m 值大於 5 的 id 對應用戶發的信息,最終都會落入到 chat00 隊列中。
在極端狀況下,若是大量的信息涌入到 chat00 隊列裏,因爲對應 chat00 的消費者處理不過來,極可能會致使這個消費者的崩潰。
而後,去除隊列後,根據規則,又會有大量的信息涌入到 chat00 後續的隊列 chat01 裏,這些信息又會致使 chat01 對應應用的崩潰,最終引起整個集羣的崩潰,這就是雪崩效應。
咱們須要一種更巧妙的辦法來解決這個問題。
6. 從實變虛,也許咱們應該更敢想一些
通過上面的論述,咱們發現,咱們在分配隊列時,之因此失衡,是由於咱們的隊列在圓環上的分配失衡。
咱們全部的真實隊列都是按照順時針依次排布在圓環上的。在上面的場景裏,咱們只有 5 個隊列。此時,咱們假設會有 100 個隊列。那麼,m = hash(id) mod 100 這個公式裏:
m 大於 5 的機率爲 95%
因爲咱們的 5 個隊列是按照編號順序依次排列的。那就說明全部 m 大於 5 的信息就都會映射到一個不存在的隊列上,最終,根據規則,順時針滑到了 0 對應的 chat00 隊列中。
若是,咱們可讓真實存在的隊列均勻分佈到環上,那麼,這種嚴重失衡的現象還會再出現嗎?
從上面的圖咱們能夠看出,若是咱們能讓真實的隊列均勻的在圓環上分佈,那麼這種嚴重失衡的現象就會獲得極大的緩解。
那麼如何讓這些隊列能均勻的分佈在這個圓環中呢?還記得咱們在苦惱分配信息規則的不斷修改時,咱們大膽的假設了一個咱們的 IM 系統永遠也不可能達到的隊列數字嗎?
咱們假設了 MQ 中有 100 個隊列,而後,咱們去判斷這些隊列是否真實存在。不存在,咱們就順時針滑動一直找到真實存在的隊列爲止。
若是咱們再大膽一點,偷偷的把咱們的假設進一步優化,把一些原本須要判斷爲不存在的隊列去映射到真正已經存在的隊列上,那麼咱們是否是就等於把這些真正存在的隊列均勻分佈到這個圓環上了?
像上圖這種,把已經存在的少許隊列去映射到多個假設隊列的方法,就是一致性哈希的虛擬節點辦法。
而對於怎麼讓少許的隊列映射到多個假設隊列,是有多種實現算法存在的。
好比,咱們能夠把真實存在的隊列名加上一些編號去分別哈希一下, 像hash(chat00) mod 100,hash(chat00#1) mod 100,而後根據獲得的餘數,去把 chat00 這個真實隊列和對應餘數的環中的位置映射上。
若是 hash(chat00) mod 100 = 31,那麼 31 號的位置就對應於 chat00,之後全部 m = hash(id) mod 100 中 m = 31的所對應的消息就會直接被髮送到 chat00 隊列。
而 hash(00#1) mod 100 = 56,則 m = 56對應的消息一樣也會直接發送到 chat00 隊列。
這樣,咱們就間接的把 MQ 中的真實存在的隊列作了均勻化分佈,從而大大減小了信息失衡的現象。
7. 理解算法的思想勝於算法的實現
好了,經過實際場景來對於一致性哈希的思想就暫時剖析到這裏了。
一致性哈希做爲一種很是經典的算法思想,被普遍的用於各大分佈式項目當中,用於解決各類分片問題,任務分發問題。
可是,在這裏,我要糾正一個觀點:不少人都在網上說 redis 使用了一致性哈希。這是錯的,redis 只是使用了一致性哈希的思想。好比一致性哈希中的環分佈,再好比虛擬節點對應真實節點的思想。
可是 redis 並無使用任何哈希算法去計算分佈,若是有興趣的讀者,能夠仔細去看下有關內容。從 redis 的例子上來講,咱們能夠看到,只有理解了算法的思想,咱們才能更容易更靈活地因地制宜的分解、修正、改進算法,讓算法能更切合實際的融入到咱們的項目之中。
經過這篇文章咱們從哈希開始,一直到用到一致性哈希的虛擬節點分佈,怎麼樣,您以爲一致性哈希這道良藥味道如何呢?
第一次寫圖解的文章,你們包容一下直男的審美!
第一次寫圖解的文章,畫圖真是累吐血了!求你們看完點個贊。