自定義一致性協議

  首先定義幾個摘要以便容易被搜索到:

  分佈式一致性協議  

  raft性能不好

  取代raft 替代raft

       ==========================================

  上一篇博文講了分佈式存儲的架構:https://mp.csdn.net/console/editor/html/105946387

  其中沒有詳述一致性協議,特意留到這篇博文來講。

  

  討論分佈式存儲,必定離不開一致性協議。

       先說說Raft。Raft的基本概念就不解釋了,網上有大量的相關帖子,官方paper(以及中文版)也可以找到。本人最初也是研讀了官方paper。

       我現在想說的是,raft並不適合高併發場景,原因有兩個:

  1) raft心跳太頻繁,對CPU和網絡的消耗很大。

  2) raft導致寫放大一倍,對磁盤IO的消耗不小。

  原因1)的分析:

  Raft的leader和followers之間依靠頻繁的心跳(間隔不能太大,基本要求每秒一次)保持聯繫以及傳遞數據。這樣一來在分佈式存儲場景中,假如一個存儲節點上有一萬個raft實例(每個存儲節點上,每個需要保證一致性的對象都必須有一個raft實例。一個10TB的存儲節點,每個文件1GB,就有10240個文件,也就有10240個raft實例),假設這一萬個raft實例都是leader,則這個存儲節點每秒要發送2萬個心跳到其他存儲節點,同時要接收處理2萬個心跳的回覆。平均來算,每秒要發送1.35萬個心跳和接收處理1.35萬個心跳回復。就算這個規模減低10倍,每秒也有1350個心跳和1350個心跳回復。這對CPU的壓力和對網絡的壓力太大了,特別是在光纖直連的多機房中跨機房部署時,心跳對光纖帶寬的消耗很大。

  原因2)的分析:

  從raft原理可知,raft先append日誌(要落盤),然後抄送給followers,最後提交。這樣,一個寫請求就需要2次寫磁盤操作(3副本總計需要6次寫磁盤)。這會導致性能的直接下降。特別是對大數據量的寫請求來說,寫放大現象特別明顯。

       本人比較奇怪的是,近2~3年說到分佈式存儲,大家必選raft做一些性協議,貌似非raft不可。真的是非raft不可嗎?有沒有深入去思考在分佈式存儲中使用raft的問題呢?有沒有想過去尋找其他一致性協議呢?

       當然,並非說raft怎麼不好。我只是想說,一項技術,首先要適合我們的應用場景才能被選用,不能一談到raft就覺得很高大上很怎麼着。

       除了raft(以及ZK,Paxos等,他們用在分佈式存儲系統中都有類似),其實還有更適合分佈式存儲的一致性協議!

       首先,從上下文的角度來分析,當多個參與者完全對等,同時沒有任何外力可借用的情況下,一致性協議只有raft(以及ZK,Paxos等)。但是,當多個參與者有外力可借用的場景下,就可以有其他選擇。在分佈式存儲場景中,有這樣的外力存在:管理節點,以及客戶端。這就爲我們打開了一扇門,否則下面講的都是廢話。

       先說幾個基礎概念。

       1)下文的大文件均是指上一篇博文中的大文件,也就是站在集羣角度的大文件,一致性的最小單位。

       2)管理節點爲一主兩從(至於主從選舉可用raft的無日誌狀態選舉來實現,這個相對完整的raft要簡單很多),主故障後會很快選出新主。

       3)各個大文件都有versionId和termId。每個寫請求對應一個versionId,versionId之遞增。每次副本的leader發生切換,termID遞增。VersionId和termID作爲大文件的元數據持久化在大文件各個副本的頭部空間。客戶端的每個寫請求先發送到管理節點,管理節點爲其分配versionId和termID,然後攜帶versionID和termID發送到leader副本的存儲節點。

  4)所有存儲節點和所有管理節點保持心跳(週期爲3秒)。存儲節點剛啓動時,在心跳中彙報自己所擁有的大文件(version,termID等必要元數據),後續存儲節點上新建大文件後只需彙報新的大文件元數據,以及version,termID發生變化的大文件元數據。這樣所有管理節點內存中都實時維護着集羣所有的大文件元數據,以及所有的存儲節點基本信息,主從管理節點的信息延遲只有一個心跳(3秒),從而,管理節點能很方便的進行主從切換。

       5)任何管理節點宕機重啓後,會要求存儲節點重新彙報其所有的大文件,這樣管理節點在幾十秒內即可彙總得知集羣所有的大文件(可能幾千萬個甚至幾億個)。

       6)寫請求必須先到管理節點(不要擔心性能,一般都是讀遠多於寫),分配一個version(遞增)和當前termID,記錄在內存隊列中,並且開始計時,一般是3個心跳還未完成及視爲超時。有任何寫請求超時之後管理節點暫停該大文件的新寫請求。

  7)寫請求的3副本中,多數副本(2個副本)成功即認爲請求最終成功。

      

  下面進入正題。以3副本爲例。

  3個副本在某一時刻必須有其僅有一個leader副本,leader副本的作用有2個:

  1) 保證寫入順序的一致性。某一段時間內來自客戶端的多個寫請求只能到達3副本中固定的一個。如果一個寫請求到達副本1,此時該請求還未在3副本中完全執行完,另一個寫請求到達副本2,這樣寫請求就亂了(Google的GFS允許這樣,這背後是有GFS的特定場景,一般場景下不能這樣做),無法保證多副本數據的一致性。

  2) 將寫請求數據抄送給其他副本,並做到有follower版本(version)落後時,一直向其做增量抄送,或全量抄送(在增量不足以追趕上version時),以保證最終一致性。

  所以這樣來看,我們的自定義一致性協議就是raft的變形。但是我們不需要選舉,不需要副本之間高頻率的心跳,寫請求不用先append到日誌中,而是隻記錄到內存中(循環隊列)即可。

  怎麼確定leader?

  通過死規則指定leader,可以指定IP最小者爲leader(可以優化,有些大文件指定IP最小者爲leader,有些指定IP最大者爲leader,但是規則最初定下來就不能變,持久化到副本的元數據中)。

每個大文件的3個副本存放到哪3個存儲節點上,是由管理節點確定,同時客戶端的每個寫請求必須先到管理節點,獲取其3副本所在的3個存儲節點IP(還要遞增version,獲取當前termID)。存儲節點故障之後,必須要管理節點分配新的存儲節點,遞增termID。管理節點即我們這裏說的外力。

  先假設某個文件的3副本穩定,沒有故障發生。任何寫操作都會先到管理節點,得知當前寫請求對應的3個副本所在的3個存儲節點IP以及version和termID,其IP最小者爲leader。然後寫請求都從客戶端(攜帶version和termID)發送給這個leader副本所在的存儲節點。

  然後,假設有1個副本發生故障。

  1) 不是leader發生故障,且新分配的存儲節點IP比現leader副本的IP要大,則此時leader不發生轉移,讀寫不受影響。

  2) 不是leader發生故障,且新分配的存儲節點IP比現leader副本的IP要小,則此時leader要發生轉移。新的leader副本最初沒有任何數據,需要從其他follower上拉取數據,且能根據各個副本version得知最新的數據。這個過程大概要幾十秒到幾分鐘,這幾分鐘內當前大文件不可寫。實際中可以優化,新分配存儲節點時儘可能分配IP大的存儲節點,避免leader發生轉移。同時,管理節點(內存中記錄了各個大文件的前leader副本,從而能知道leader發生了轉移)做控制:等原leader上的請求都完成(成功或失敗,怎麼知道?根據version和超時來判斷),然後才能繼續對該文件寫,寫請求會發到新的leader。這個等待時間大概10秒以內。對具體某個文件來說,副本故障是極低概率的事情,不會造成什麼問題。

  3) 如果是leader發生故障,則新分配存儲節點後,一定會發生leader轉移(出現新leader),管理節點先遞增當前大文件的termID並在心跳中回覆給相關存儲節點。然後管理節點和2)一樣進行處理。

  這裏要注意是否有腦裂問題。通過管理節點的協助可避免腦裂:

  1)寫請求必須先到管理節點,leader副本發生轉移是,必須等到之前的發到原leader副本的寫請求全部結束後,才能開始新的寫請求(新的寫請求會發送到新leader副本上),新舊切換之間寫操作服務暫停,一般來說暫停大概3~5個心跳週期(10來秒內)(寫請求不會擠壓太多,有超時和限流)。

  2)還有,原來的leader副本被管理節點判定爲失效之後,實際上其可能還存活着,其上有一些列寫請求還在慢吞吞的執行(包括抄送給他認爲的followers)。這樣幾個心跳週期之後,新的leader副本生效了,此時可能同時存在兩個leader副本,所以還要補充手段。這種場景下,termID就排上用場了。存儲節點上,各個副本的每個寫請求,version必須比前面寫請求的version大,termId必須和當前副本的termID相同,否則就失敗。副本的leader發生轉移之後,管理節點會在心跳中及時通知存儲節點新termID,存儲節點就及時更新相應大文件副本的termID。這樣大文件的followers副本接收到原leader副本抄送的寫請求之後發現termID不匹配,就會拒絕這些寫請求,因此,就算原leader副本還存活着,也不起作用,其上剩餘的寫請求會失敗。

  如果管理節點切換了怎麼辦?新的主管理節點內存中沒有寫請求隊列,但是新的管理節點能知道某個大文件的leader副本是否發生了改變,因爲新的主管理節點知道每個大文件的所有副本的IP,如果有IP發生了變化(失聯或者來了新的IP),所有管理節點都同時知道,所以新的主管理節點對比大文件的前後IP即可知道leader是否有變化。如果leader發生了變化,更新termId後,死等幾個心跳,原來leader上的寫請求要不都結束(要不剩餘的因爲termID不匹配會失敗),然後開始受理新的寫請求(新的寫請求會發送到新的leader副本上)。這個時間大概3~5個心跳,而且發生管理節點主備切換的概率非常低,所以寫請求暫停3~5個心跳不會導致什麼問題。

  如果所有管理節點都重啓了怎麼辦?管理節點全部重啓之後,會立刻選出新的主,新主上沒有寫請求記錄,採用和主備切換時相同的邏輯來確保原來的寫請求都結束後,再開始受理新的寫請求。

  如果管理節點都宕機了,沒有管理節點可用,怎麼辦。很簡單,寫操作不能進行。管理節點一主兩備時,這種場景可以忽略。

  綜上,腦裂問題可以避免,且不影響服務可用性。

  Leader副本確定好之後,一致性保證就清晰了,也很簡單。採用和raft相同的做法。leader一直向followers抄送,直到成功。Leader副本的內存中,記錄有followers的version,同時記錄有最近的未確認的寫請求,如果follower的version落後,就向其發送對應的寫請求,直到其回覆成功。對客戶端來說,所有寫請求冪等,失敗(以及超時)後必須重試直到成功。

  一致性還涉及到管理節點。存儲節點上的任何一個大文件,其version發生變化之後,就在下一個心跳中彙報給(所有)管理節點,主管理節點以此判斷該大文件的寫請求執行情況,如果有某次寫請求超時時間到了,至少2個副本version還未彙報齊全,即認爲該次寫請求超時。寫請求超時之後,暫不再接收新的寫請求,以做限流(還可結合其他限流措施)。

   再然後,假設有2個副本發生故障。

  1) 如果leader副本發生故障,則有可能丟失最近的幾次寫請求的數據(如果該存儲節點無法及時恢復)。這在raft中也一樣。故障恢復流程和1個副本故障相同。

  2) 如果leader副本沒有發生故障,則不會丟數據。故障恢復流程,和1個副本故障相同。

  總結:我這裏自定義的一致性協議,和raft非常類似,最主要的區別是藉助管理節點,不用raft選舉流程,實現起來比raft要簡單很多倍。同時不用raft的心跳機制,性能上要好很多倍。個人覺得,在分佈式存儲場景下,完全可以取代raft。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章