raft算法mysql主从复制_Raft算法赏析
前言
最近抽空看了大名鼎鼎的Raft算法論文,看完后就一個感覺:如此復雜的算法居然可以設計得如此簡潔、巧妙。
反復看了幾遍,非常過癮,原文不僅通俗易懂,而且非常嚴謹,所有異常情況都做了充分的考慮以及給出解決方案。
原文請參考這里
如何保證一致性
保證一致性,即需要保證我寫入成功的數據,能夠被正確讀取的數據。
如果一個應用只有單個節點,我們能夠比較容易實現這個特性。
但一個節點無法滿足我們高可用的場景,我們需要多個節點互為備份,當單個節點故障時不影響整體服務。
一旦多于一個節點,我們就處在分布式的環境下,事情就變得復雜起來,我們需要考慮更多的因素:
節點之間應該如何進行數據進行同步,如何保證數據一致性?
如何處理不同節點并發收到同一份數據的修改請求?
若存在同一個數據在不同節點存在不同的值應該如何解決沖突?
如果新增或者刪除節點要會不會影響整個集群?
……
如果你的應用是一個無狀態應用,你無需考慮如此復雜的因素。
但我們大多數應用都是會產生數據的,保存這些數據的地方同樣面臨上面的問題,你只是把這個問題轉移到了另一個地方,它很可能是你的數據庫,比如MySQL。
在研究MySQL主從算法或則其他一致性算法時,我們先針對上面的問題想一個粗略的方案:
要解決并發數據修改問題,單節點我們會采取加鎖方案,確保同一時刻只有一個線程操作數據。同樣的,多節點之間,我們也要采取某種機制確保同一時刻只有一個節點進行所有修改操作,這個節點我們叫做領導者,而確定領導者的過程,通常叫做領導者選舉
要解決不同節點數據同步問題,我們首先要解決領導者對寫請求的數據持久化問題:
要能夠快速存儲,保證落盤性能
數據要絕對有序,才能保證領導者本地存儲數據的正確性,也才能保證后續同步數據時能夠做到有序增量
咦,聽著是不是有點隊列的味道?我們當然不需要引入一個重量級的隊列系統,我們需要的是它的核心——日志系統。
節點數據同步,其本質就是做是日志復制,即將領導者本地的日志復制到其他節點。
要解決異常情況下節點之間的數據不一致的問題,我們必須要有一種解決數據沖突的策略:
你肯定聯想到了Git,使用新數據解決沖突。
還要考慮大部分節點的數據狀態。
要保證新增或者刪除節點時整個集群的仍然可用,我們需要考慮:
新增的節點同步數據時,不應該耗盡領導者所有資源
刪除的節點為領導者時,不應該讓整個集群癱瘓
所以成員關系調整也是我們需要重點考慮的一點。
MySQL
在深入Raft算法前,讓我們先看看老牌數據庫MySQL怎么解決上面的問題。
領導者選舉
MySQL的領導者是由人工指定的——我們可以手工配置某一節點為主節點。
這個方案足夠簡單。
當然如果主節點掛了,我們也只能通過一系列人工操作進行領導者切換。
日志復制
MySQL提供了三種日志復制策略:
異步復制
主庫在執行完客戶端提交的事務后會立即將結果返給客戶端,并不關心從庫是否已經接收并處理。
全同步復制
當主庫執行完一個事務,所有的從庫都執行了該事務才返回給客戶端。
半同步復制
介于異步復制和全同步復制之間,主庫在執行完客戶端提交的事務后不是立刻返回給客戶端,而是等待至少一個從庫接收到并寫到 relay log 中才返回給客戶端
我們發現:
異步復制,從庫無法保證數據一致性
全同步復制,能夠保證數據一致性,但性能必然會受到嚴重的影響
半同步復制,只能保證部分從庫的數據一致性
看起來好像還有不少優化的空間?
有請本文的主角Raft登場_
領導者選舉
Raft會自動選舉領導者。
選舉時有三個角色參與:領導者(Leader)、跟從者(Follower)和候選人(Candidate)
候選人參與選舉有三種結果:成功、失敗、平局
主要流程
當節點啟動時,初始狀態為跟隨者身份。
如果一個跟隨者在一段時間里沒有接收到任何消息,就是選舉超時,將發起選舉以選出新的領導者。
跟隨者增加自己的當前任期號并且轉換到候選人狀態。
每一個節點最多會對一個任期號投出一張選票,按照先來先服務的原則。
當一個候選人獲得大多數選票時,將成為領導者,然后向其他節點發送心跳消息,阻止新的領導者產生。
如果領導者心跳消息中的任期號不小于候選人當前的任期號,那么候選人會承認領導者合法并回到跟隨者狀態,同時更新當前任期號為領導者的任期號。
任期號的設計,一定程度上防止舊數據的節點成為領導者。
異常情況
當出現候選人出現平局情況,如節點數量為偶數個,平局的情況可能會一直無限發生下去。
Raft 算法巧妙地使用隨機選舉超時時間的方法來避免這種情況,就算發生也能很快的解決。每個節點的選舉超時時間都是隨機決定的,一般在150~300毫秒之間,這樣兩個節點同時超時的情況就很罕見了。
寫請求處理
主要流程
只有領導者擁有寫請求處理權限
客戶端發送所有請求給領導者,隨機挑選一個服務器進行通信。如果挑選的服務器不是領導者,跟隨者會拒絕客戶端的請求并且返回領導者的信息(包含了網絡地址)。若客戶端的請求超時會再次重試上述過程。
可選方案:隨機挑選一個服務器進行通信。如果挑選的服務器不是領導者,跟隨者將請求重定向給領導者。
領導者在收到寫請求之后,在本地日志追加一條新的日志條目,然后并行的發起請求,讓其他節點復制這條日志條目。
當領導者將創建的日志條目復制到大多數的服務器上的時候,日志條目就會被提交當這條日志條目被安全的復制,領導者會應用這條日志條目到它的狀態機中然后把執行的結果返回給客戶端。
本地日志
日志由有序序號標記的條目組成。
每個條目都包含創建時的任期號和一個狀態機需要執行的指令。
領導者的一個條目被大部分節點復制成功后,應用到狀態機,就認為是已提交。
索引
1
2
3
任期號
1
1
2
指令
x=3
y=1
z=1
追隨者同步
追隨者的同步本質就是日志復制。
在正常的操作中,追隨者收到領導者的新日志復制請求后,將寫入本地日志;收到領導者提交日志請求后,將應用到狀態機,確保兩者的日志、狀態機數據保持一致性。
為了減少請求次數,已提交日志位置的通知可以合并到未來的所有請求 (包括心跳請求)。
異常情況
當跟隨者崩潰時,領導者可以通過無限重試,使追隨者完成日志恢復。因為請求都是冪等的,所以重試不會造成任何問題。
當領導者崩潰時,各個節點日志可能處于不一致的狀態,情況會變得復雜。如何得確定哪些日志已經被已提交了?你可能會想到收集各個副本(其他節點)已提交日志的信息去做決策,但這種做法在多輪領導者、追隨者崩潰后,變得難以實現。
Raft使用了一種非常簡潔的方法來解決這個難題——強領導者:
日志條目只從領導者發送給其他的服務器。
強制跟隨者直接復制領導者的日志,沖突的日志條目會被領導者的日志覆蓋。
領導者針對每一個跟隨者維護了一個下一個需要發送給跟隨者的日志條目的索引值(nextIndex )
如果一個跟隨者的日志和領導者不一致
若跟隨者日志少,領導者就會減小 nextIndex 并進行重試實現增量復制
若跟隨者日志多,則以領導者的日志全量覆蓋
最終領導者和跟隨者的日志達成一致。
那有沒有可能領導者本身的數據存在問題?
Raft用兩個巧妙的限制,確保被選舉出來的領導者數據是新的而且是正確的:
選舉請求包含了候選人的日志信息,投票人會拒絕掉那些日志沒有比自己新的投票請求。
候選人為了贏得選舉必須聯系集群中的大部分節點,這意味著候選人的日志至少和大多數的服務器節點一樣新。
Raft 通過比較兩份日志中最后一條日志條目的索引值和任期號定義:
任期號不同,任期號大的日志更加新。
任期號相同,索引值大的日志更加新。
時間和可用性
Raft 的特性之一就是不依賴時間,選舉領導者只要系統滿足下面的時間要求:
廣播時間(broadcastTime) << 選舉超時時間(electionTimeout) << 平均故障間隔時間(MTBF)
廣播時間指的是從一個服務器并行的發送請求給集群中的其他服務器并接收響應的平均時間;
平均故障間隔時間就是對于一臺服務器而言,兩次故障之間的平均時間。
廣播時間必須比選舉超時時間小一個量級,這樣領導者才能夠發送穩定的心跳消息來阻止跟隨者開始進入選舉狀態。
選舉超時時間應該要比平均故障間隔時間小上幾個數量級,這樣整個系統才能穩定的運行。
當領導者崩潰后,整個系統會大約相當于選舉超時的時間里不可用。
成員關系調整
到目前為止,我們都假設集群的配置是固定不變的。但是在實踐中,偶爾是會改變集群的配置的,例如替換那些宕機的機器或者改變配置。
我們的目的是為了變更時集群仍然能夠保持可用。
可能存在問題
我們先將成員變更請求當成普通的寫請求,由領導者得到多數節點響應后,每個節點提交成員變更日志,將從舊成員配置(Cold)切換到新成員配置(Cnew)。
但每個節點提交成員變更日志的時刻可能不同,這將造成各個服務器切換配置的時刻也不同,這就有可能選出兩個領導者,破壞安全性。
考慮以下這種情況:
集群配額從 3 臺機器變成了 5 臺,可能存在這樣的一個時間點,兩個不同的領導者在同一個任期里都可以被選舉成功。一個是通過舊的配置,一個通過新的配置。
image
如何解決這個問題呢?
Raft提供了兩種解決方案。
一階段成員變更
如果增強成員變更的限制,假設Cold與Cnew任意的多數派交集不為空,這兩個成員配置就無法各自形成多數派,那么成員變更方案就可能簡化為一階段。
那么如何限制Cold與Cnew,使之任意的多數派交集不為空呢?方法就是每次成員變更只允許增加或刪除一個成員。
可從數學上嚴格證明,只要每次只允許增加或刪除一個成員,Cold與Cnew不可能形成兩個不相交的多數派。
一階段成員變更:
成員變更限制每次只能增加或刪除一個成員(如果要變更多個成員,連續變更多次)。
成員變更由領導者發起,Cnew得到多數派確認后,返回客戶端成員變更成功。
一次成員變更成功前不允許開始下一次成員變更,因此新任領導者在開始提供服務前要將自己本地保存的最新成員配置重新投票形成多數派確認。
領導者只要開始同步新成員配置,即可開始使用新的成員配置進行日志同步。
二階段成員變更
集群先從舊成員配置Cold切換到一個過渡成員配置,稱為共同一致(joint consensus),共同一致是舊成員配置Cold和新成員配置Cnew的組合Cold U Cnew,一旦共同一致Cold U Cnew被提交,系統再切換到新成員配置Cnew。
領導者收到從Cold切成Cnew的成員變更請求,做了兩步操作:
第一階段
領導者在本地生成一個新的日志條目,其內容是Cold∪Cnew,代表當前時刻新舊成員配置共存,寫入本地日志,同時將該日志條目復制至Cold∪Cnew中的所有副本。在此之后新的日志同步需要保證得到Cold和Cnew兩個多數派的確認
跟隨者收到Cold∪Cnew的日志條目后更新本地日志,并且此時就以該配置作為自己的成員配置
如果Cold和Cnew中的兩個多數派確認了Cold U Cnew這條日志,領導者就提交這條日志條目。
第二階段
領導者生成一條新的日志條目,其內容是新成員配置Cnew,同樣將該日志條目寫入本地日志,同時復制到跟隨者上
跟隨者收到新成員配置Cnew后,將其寫入日志,并且從此刻起,就以該配置作為自己的成員配置;如果發現自己不在Cnew這個成員配置中會自動退出
領導者收到Cnew的多數派確認后,表示成員變更成功,后續的日志只要得到Cnew多數派確認即可。
領導者給客戶端回復成員變更執行成功。
異常情況
如果領導者的Cold U Cnew尚未推送到跟隨者,領導者就掛了,此后選出的新領導者并不包含這條日志,此時新領導者依然使用Cold作為自己的成員配置。
如果領導者的Cold U Cnew推送到大部分的跟隨者后就掛了,此后選出的新領導者可能是Cold也可能是Cnew中的某個跟隨者。
如果領導者在推送Cnew配置的過程中掛了,那么同樣,新選出來的領導者可能是Cold也可能是Cnew中的某一個,此后客戶端繼續執行一次改變配置的命令即可。
如果大多數的跟隨者確認了Cnew這個消息后,那么接下來即使領導者掛了,新選出來的領導者肯定位于Cnew中。
兩階段成員變更比較通用且容易理解,但是實現比較復雜,同時兩階段的變更協議也會在一定程度上影響變更過程中的服務可用性,因此我們期望增強成員變更的限制,以簡化操作流程。
兩階段成員變更,之所以分為兩個階段,是因為對Cold與Cnew的關系沒有做任何假設,為了避免Cold和Cnew各自形成不相交的多數派選出兩個領導者,才引入了兩階段方案。
添加新節點
添加一個新的節點到集群時,需要考慮一種情況,即新節點可能落后當前集群日志很多的情況,Raft針對這種情況做了以下處理:
添加進來的新節點首先將不加入到集群中,而是等待數據追上集群的進度。
領導者同步數據給新節點將劃分為多個輪次,每一輪同步一部分數據,而在同步的時候,領導者仍然可以寫入新的數據,只要等新的輪次到來繼續同步就好。
同步的輪次不能一直持續,需要限制輪次數量,如最多同步10輪。
下線領導者
領導者將發出一個變更節點配置的指令,只有在該指令被提交之后,領導者才下線,最后按照標準流程進行新的一輪選舉。
異常情況
如果某個節點在一次配置更新之后,被移出了新的集群,但是這個節點又不知道這個情況,那么按照前面描述的Raft算法流程來說,它應該在選舉超時之后,將任期號遞增1,發起一次新的選舉。雖然最終這個節點不會贏得選舉,但是畢竟對集群運行的狀態造成了干擾。而且如果這個節點一直不下線,那么上面這個發起新選舉的流程就會一直持續下去。
解決方案:如果領導者存活,則不允許發起新一輪的選舉。
如何判斷領導者是否存活?
如果領導者一直保持著與其它節點的心跳消息,那么就認為領導者節點是存活的。
讀請求處理
讀請求比寫請求簡單很多,可以直接處理而不需要記錄日志。
但這里可需要考慮臟讀問題,因為領導者可能不知道自己已經被作廢了。
保證不臟讀
Raft 中通過讓領導者在響應讀請求之前,先和集群中的大多數節點交換一次心跳信息,確保自己仍然是領導者。
如何優化性能
因為只讀操作也要經過一次請求,所以它并沒有我們想想的那么快,它可能和寫操作性能差不多,并且不能通過擴展節點數量來得到整體集群讀性能的提升,甚至不升反降。
折中方案
允許領導者不檢查是否存活。
該方案適用于一致性要求不高的場景,以通過擴展節點來提升讀性能。
優化方案
標準的強一致讀操作是完全是在領導者進行的,優化方案可將讀請求在任意節點處理:
跟隨者接收到只讀指令后,向領導者索要當前的已提交索引。
跟隨者等待自身的狀態機的提交完成后,即可以返回結果。
領導者向跟隨者返回已提交索引值依然要先和集群中的大多數節點交換一次心跳信息。
這個方案可以讓跟隨者分擔領導者的壓力,讓領導者有更多的資源來處理自身的讀寫操作。
進一步優化方案
領導者可以依賴心跳請求來實現租約機制,確保一段時間內都為某個領導者處理,但是這種方法強依賴時間來保證安全性。
日志壓縮
隨著數據的增長,日志會越來越大,Raft通過快照實現了日志壓縮。
在快照系統中,整個系統的狀態以及少量元數據以快照的形式寫入到穩定的持久化存儲中,然后到那個時間點之前的日志全部丟棄。
image
結尾
分布式一致性的算法非常重要,在眾多算法中,Raft以簡單易懂脫穎而出,且在很多細節都提供了具體的實現方式,獲得了越來越多開發者的關注與使用。
我們不禁遐想,如果MySQL能夠使用Raft算法進行重構,豈不是可以變成一個強一致、高可用的分布式數據庫?
事實上已經有人在做了,它就是TiDB。
總結
以上是生活随笔為你收集整理的raft算法mysql主从复制_Raft算法赏析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: anemometer mysql_MyS
- 下一篇: mfc ado 链接mysql 数据_M