Raft保證的safety
Leader Append-Only:leader從來不覆寫或者刪除日志英岭,只會追加新日志俄占。
Log Matching:如果兩個主機的副本上的日志文件中萝招,包含一條相同term和log id的entry矾飞,那么奕塑,這兩個日志文件在這條日志之前的內(nèi)容都是相同的(字節(jié)流級別的一致)械姻。
Leader Completeness:如果一條日志已經(jīng)commit了,那么這條日志一定會出現(xiàn)在term最大的leader的日志文件中恩脂。
State Machine Safety:如果一個主機應(yīng)用了一條日志帽氓,那么,其他主機不可能應(yīng)用一條相同log id而內(nèi)容卻不同的日志俩块。
初選Leader后的日志同步
當一個新的Leader被選出來時黎休,它的日志和其它的Follower的日志可能不一樣,這個時候玉凯,就需要一個機制來保證日志的一致性势腮。
主備不一致可能有如下幾種情況:少了一些日志(term可能相同或者少了);多了一些未commit的日志(term可能多了也可能少了)漫仆;某些term多了一些日志且某些term少了一些日志捎拯。Raft中如何解決這些不一致呢?leader強制讓follower的日志文件復(fù)制leader的日志文件盲厌,即follower上不一致的日志文件內(nèi)容被覆寫
首先記住一個前提, raft選舉保證選出來得leader擁有最新最多commit的日志署照。
因此leader只需要知道follower缺哪些日志(確定最后一條相同的日志),就可以主動給follower同步所缺的日志吗浩,follower只要覆蓋掉不一致的部分即可建芙。
如何確定不一致的點?
leader維護一個log id懂扼,初始為leader本地最大的log id禁荸,然后發(fā)送AppendEntries RPC到follower,follower在收到AppendEntries之后阀湿,檢查RPC中攜帶的term和log id(leader上被追加的這條日志的前面一條日志的term和log id)赶熟,如果follower本地沒有這條日志,就拒絕此次AppendEntriesRPC陷嘴,leader就能知道follower的同步點更靠前映砖,逐漸就能知道同步點的位置。當然罩旋,實際實現(xiàn)時啊央,會使用更有效率的方法。
例如涨醋,當附加日志 RPC 的請求被拒絕的時候瓜饥,跟隨者可以包含沖突的條目的任期號和自己存儲的那個任期的最早的索引地址。借助這些信息浴骂,領(lǐng)導(dǎo)人可以減小 nextIndex 越過所有那個任期沖突的所有日志條目乓土;這樣就變成每個任期需要一次附加條目 RPC 而不是每個條目一次。在實踐中溯警,我們十分懷疑這種優(yōu)化是否是必要的趣苏,因為失敗是很少發(fā)生的并且也不大可能會有這么多不一致的日志。
在階段a梯轻,term為2食磕,S1是Leader,且S1寫入日志(term, index)為(2, 2)喳挑,并且日志被同步寫入了S2彬伦;
在階段b,S1離線伊诵,觸發(fā)一次新的選主单绑,此時S5被選為新的Leader,此時系統(tǒng)term為3曹宴,且寫入了日志(term, index)為(3搂橙, 2);
S5尚未將日志推送到Followers變離線了,進而觸發(fā)了一次新的選主笛坦,而之前離線的S1經(jīng)過重新上線后被選中變成Leader区转,此時系統(tǒng)term為4,此時S1會將自己的日志同步到Followers版扩,按照上圖就是將日志(2蜗帜, 2)同步到了S3,而此時由于該日志已經(jīng)被同步到了多數(shù)節(jié)點(S1, S2, S3)资厉,因此厅缺,此時日志(2,2)可以被commit了(即更新到狀態(tài)機)宴偿;
在階段d湘捎,S1又很不幸地下線了,系統(tǒng)觸發(fā)一次選主窄刘,而S5有可能被選為新的Leader(這是因為S5可以滿足作為主的一切條件:1. term = 3 > 2, 2. 最新的日志index為2窥妇,比大多數(shù)節(jié)點(如S2/S3/S4的日志都新),然后S5會將自己的日志更新到Followers娩践,于是S2活翩、S3中已經(jīng)被提交的日志(2烹骨,2)被截斷了,這是致命性的錯誤材泄,因為一致性協(xié)議中不允許出現(xiàn)已經(jīng)應(yīng)用到狀態(tài)機中的日志被截斷沮焕。(論文描述)
這個問題的本質(zhì)是,S1上的term=2,log id=2的日志拉宗,在進行復(fù)制時峦树,使用的term仍然是term=2,而不是S1最新的term(4)旦事。在本地term較大的時候去復(fù)制term小的日志魁巩,這個是不合理的。但是為了維持Leader Append-Only的性質(zhì)姐浮,只能想辦法解決谷遂。
為了避免這種致命錯誤,需要對協(xié)議進行一個微調(diào):
只允許主節(jié)點提交包含當前term的日志
針對上述情況就是:即使日志(2卖鲤,2)已經(jīng)被大多數(shù)節(jié)點(S1埋凯、S2、S3)確認了扫尖,但是它不能被Commit白对,因為它是來自之前term(2)的日志,直到S1在當前term(4)產(chǎn)生的日志(4换怖, 3)被大多數(shù)Follower確認甩恼,S1方可Commit(4,3)這條日志.
commit的時候并不需要發(fā)給followers沉颂,commit就是回復(fù)client条摸。 leader只允許commit當前term的entry,其實是指積壓著之前已經(jīng)被majority認可的entry铸屉,直到當前term也被majority認可钉蒲,然后統(tǒng)一commit。
日志壓縮與快照
在實際的系統(tǒng)中彻坛,不能讓日志無限增長顷啼,否則系統(tǒng)重啟時需要花很長的時間進行回放,從而影響availability昌屉。Raft采用對整個系統(tǒng)進行snapshot來處理钙蒙,snapshot之前的日志都可以丟棄。Snapshot技術(shù)在Chubby和ZooKeeper系統(tǒng)中都有采用间驮。
Raft使用的方案是:每個副本獨立的對自己的系統(tǒng)狀態(tài)進行Snapshot躬厌,并且只能對已經(jīng)提交的日志記錄(已經(jīng)應(yīng)用到狀態(tài)機)進行snapshot。
Snapshot中包含以下內(nèi)容:
日志元數(shù)據(jù)竞帽,最后一條commited log entry的 (log index, last_included_term)扛施。這兩個值在Snapshot之后的第一條log entry的AppendEntriesRPC的consistency check的時候會被用上鸿捧,之前講過。一旦這個server做完了snapshot疙渣,就可以把這條記錄的最后一條log index及其之前的所有的log entry都刪掉匙奴。
系統(tǒng)狀態(tài)機:存儲系統(tǒng)當前狀態(tài)(這是怎么生成的呢?)
snapshot的缺點就是不是增量的昌阿,即使內(nèi)存中某個值沒有變饥脑,下次做snapshot的時候同樣會被dump到磁盤恳邀。當leader需要發(fā)給某個follower的log entry被丟棄了(因為leader做了snapshot)懦冰,leader會將snapshot發(fā)給落后太多的follower∫シ校或者當新加進一臺機器時刷钢,也會發(fā)送snapshot給它。發(fā)送snapshot使用新的RPC乳附,InstalledSnapshot内地。
做snapshot有一些需要注意的性能點,1. 不要做太頻繁赋除,否則消耗磁盤帶寬阱缓。 2. 不要做的太不頻繁,否則一旦節(jié)點重啟需要回放大量日志举农,影響可用性荆针。系統(tǒng)推薦當日志達到某個固定的大小做一次snapshot。3. 做一次snapshot可能耗時過長颁糟,會影響正常log entry的replicate航背。這個可以通過使用copy-on-write的技術(shù)來避免snapshot過程影響正常log entry的replicate。
客戶端命令執(zhí)行過程
當Leader被選出來后棱貌,即可接受客戶端發(fā)來的請求玖媚,每個請求包含一條需要被狀態(tài)機執(zhí)行的命令。leader會把它作為一個log entry append到日志中婚脱,然后給其它的server發(fā)AppendEntriesRPC請求今魔。
當Leader確定一個log entry被safely replicated了(大多數(shù)副本已經(jīng)將該命令寫入日志當中),就apply這條log entry到狀態(tài)機中然后返回結(jié)果給客戶端障贸。
如果某個Follower宕機了或者運行的很慢涡贱,或者網(wǎng)絡(luò)丟包了,則會一直給這個Follower發(fā)AppendEntriesRPC直到日志一致惹想。
當一條日志是commited時问词,Leader才可以將它應(yīng)用到狀態(tài)機中。Raft保證一條commited的log entry已經(jīng)持久化了并且會被所有的節(jié)點執(zhí)行嘀粱。