首先標題有點譁衆取寵之嫌,可是暫時想不到更加合適的標題,就姑且這麼使用吧。分佈式共識算法一直是一個熱門的研究話題,之因此要分佈式共識,無外乎就是單點服務容易宕機,異常,出錯,從而致使系統不可用,因而就有了備份容錯的機制,那麼一份數據多地(location)存儲,若是不發生修改操做那就無需一致性協議的引入,可是這僅僅是理想狀況,真實的應用中絕大多數都是須要執行更新操做的,這纔有了分佈式共識的需求。目前最爲認同的共識算法就是lamport大神在98年發表的論文中說起的Paxos協議(然而因爲太難以理解又在01年發表了paxos made simple),即便過了這麼多年,Paxos依然難以理解和難以實現,工程實現大多都是精簡版,最爲出名的也有Raft,以及Zookeeper中的Zab。筆者以前讀過paxos made simple,雖然能理解,可是總以爲有點只知其一;不知其二(也寫了一篇小博客理解paxos協議-分佈式共識算法(consensus))。最近恰好看了一篇新的論文,結合Paxos來從新梳理下什麼是分佈式共識算法,怎麼實現分佈式共識算法。linux
Just Say NO to Paxos Overhead:Replacing Consensus with Network Ordering 這篇論文是2016年osdi上的一篇論文,第一做者是華盛頓大學計算機系統實驗的一個PHD。標題看上去也很是的奪人眼球,拜讀完以後,對於做者的想法還持有一點疑惑,可是這篇文章很好的闡述了怎麼實現分佈式共識算法,對於理解Paxos有難度的同窗不妨先去閱讀下這篇論文,能更好的去理解,下面筆者就用純大白話的形式來一步步說明分佈式共識算法。git
lamport大神在其論文中也說起到,所謂的分佈式共識要達成的目標:github
若是直接從上面的話來理解,那麼就會陷入一個誤區,或者說,看不明一致性的完整過程,換一個角度,咱們如何來保證多個replica一致性呢,很簡單,目前最爲流行的機制就是State Machine Replication(aka SMR)。SMR是這麼定義的,(1).初始state要相同,(2).對於同一個state,給定相同的輸入參數,執行相同的操做,輸出結果是相同的。因此用SMR來保證一致性的要求就是,相同的狀態,輸入相同的參數,執行相同的操做。相同的狀態屬於上一步的結果,因此約束其實就是兩個,相同的參數(argument),相同的操做(operation),說到底,paxos那些複雜的過程就是爲了保證這兩個約束條件。算法
相同的參數:看起來簡單的約束,實際實現並不是易事,首先在異步網絡的狀況,消息亂序狀況就嚴重干擾了這一約束,你想一想看,節點1先執行request n後執行m,節點2先執行m後執行n,結果能同樣嗎?那麼paxos是如何保證這個有序性的呢?Paxos在其運行過程,一旦提案p被大多數的acceptors接受,那麼後續提出的更高編號的提案都應該包含這個p,看明白了嗎,就是一個提案未被確認執行前,全部的acceptors都不容許新的提案(這裏的新的提案指的是value不一樣的提案)發生,這就間接解決了亂序的問題,paxos保證全部的節點在每一次paxos提案期間只能執行一個提案(同一個value),從而來保證參數相同,消息有序。編程
相同的操做:客戶端發起狀態修改必然會帶着一個operation的請求(實際工程中實現是經過調用不一樣的接口如insert,update,delete等),那麼當這個請求廣播給全部的節點,那麼執行天然是相同的操做。問題就在於異步網絡沒法保證可靠性,假如部分節點網絡失效,有些沒收到request,天然不會去執行。那麼一部分節點執行,一部分節點不執行纔是致使操做不一樣的緣由(commit和do-nothing)。體如今paxos就是一旦通過大部分的acceptor贊成的提案到被learner學習的過程。工程上常見的實現方法是用leader來管理複製日誌來實現操做相同的。segmentfault
有了上述的認知,再來看一致性,是否是就會以爲明朗許多,本文標題中的另外一個名字是NOPaxos,天然重點就是講解這篇論文了,下面就來看看論文的做者是如何實現分佈式共識算法。網絡
聲明:這裏不是純翻譯論文,若是想了解所有的過程,能夠點閱前面的連接查看。 傳統的一致性算法如paxos是把上述兩個約束條件放在應用層去實現,這樣的好處是不依賴於特定的網絡結構,可是同時也有一些弊端,首先是一致性的時間比較長,性能較低,第二是實現難度比較大且複雜。做者另闢蹊徑,若是網絡層能保證消息有序,那麼paxos前面整個投票過程就無需存在了,這樣就把一致性的責任分攤在了網絡層(並不是指TCP/IP協議棧中的網絡層)和應用層。論文主要作了四方面的工做:session
按照論文自己的說法,最簡單的實現,就是給每一個消息增長一個單調遞增1的序列號(sequence number),這樣,節點在接受到消息的時候,就能知道每一個消息的前後順序了。除此以外,這一層無需再提供任何的保證,這樣使得設計和實現都比較簡單。簡單的狀況下,OUM爲每一個request都添加一個sequence number,應用層經過libOUM調用接收到這個request以後,判斷是不是當前想要接受的信息。只要sequence number是遞增1,就能判斷出是否亂序或者正常狀況。若是出現了跳躍,好比當前須要的消息序列號是n,而後卻接受到了一個序列號爲m(m > n)的消息,那麼上層應用就知道n-m中間的消息是丟失,進而執行其餘操做,這樣保證每一個replica接收到的消息是相同的順序。下圖是NOPaxos的一個總體架構:架構
每一個客戶端都須要集成兩個庫,一個是用於處理網絡消息的libOUM,一個是用於協調多個replica之間操做的NOPaxos。底層網絡有一個sequencer,負責爲每一個消息加上一個序列號,爲了防止sequencer故障或者失效,須要一個controller來進行監控。總的來講,這個架構總共有三種角色,controller,sequencer,client,其中client有兩種協議OUM和NOPaxos。下面就一個個的來介紹這些角色分別承擔的功能和用途。app
1. sequencer
正如前面所說,sequencer的功能很簡單,就是爲每一個消息添加一個序列號,這個序列號必須是單調遞增1的。論文中,做者提供了三種不一樣的實現方式,分別是,基於可編程的交換機內實現,基於middlebox原型的硬件實現,純軟件實現。在介紹這三種實現以前,先來考慮這樣一個網絡結構,
上圖是一個三層胖樹的結構3-level fat-tree,根據設計,全部的客戶端發出來的信息都要通過sequencer,若是本來客戶端與replica在同一個局域網內,這樣的設計首先會致使消息路徑增加,由於消息首先要走到sequencer,再從sequencer轉發回來,明顯消息走過的路徑就變長了。因此這裏對於sequencer最合理的位置,就是放在root switch(至於爲啥是放在這個位置會使得性能最好,能夠參考網絡拓撲fat-tree的設計思路,筆者對於網絡拓撲沒有深刻研究,這裏就不展開)。且論文做者在真實的環境下測試獲得,對於這樣一個三層胖樹結構的網絡,88%的狀況下,添加一個序列號並不會增長額外的延遲開銷,99%的狀況下,只有5us的延遲。所以,增長這樣一個sequencer並不會帶來性能的降低(論文解釋的緣由是,無論存不存在這個sequencer,大部分的package都是須要走到root-switch才能達到大多數的group)。sequencer的位置肯定了,那麼接下來就是實現了:
2.controller
有了sequencer天然能實現消息有序性,可是同時也引進了一個問題,sequencer是一個單節點,一會兒就使得整個系統脆弱了不少,雖說發生故障的機率不高,可是一旦發生故障,整個系統不可用不說,還可能出錯。這個時候就須要controller出場了,controller的主要目的是監視sequencer,一旦發現sequencer不可用或者不可達,則會選擇一個替換的sequencer,並更新其路由表。這一過程引入一個新的概念,session。每當一個sequencer失效了,須要挑選新的sequencer的時候,首先從新選定一個session number,並將信息更新到新的sequencer上。這樣用一個session的概念就能來維護跨sequencer的消息有序了(會在消息頭上加上一個二元組 [session-number, sequencer-number] )。一旦libOUM接收到了一個更高編號的session number,則說明,舊的session已經失效了,可是此時,libOUM並不知道是否有丟失舊的session中的package,因此不能返回一個drop-notification,只能返回一個session-terminated,由上層應用去決定改如何處理。至於上層如何處理,後面會講。這裏session number能夠採用本地時間戳,或者將session number持久化到磁盤而後遞增。此外controller的可靠性能夠由多個節點來保證,controller選舉sequencer的算法甚至能夠直接使用paxos或者raft等,畢竟節點失效並非一個常常發生的事。
NOPaxos從架構圖中可知,屬於一致性協議最上層的協議了,經過調用底層的libOUM保證消息有序,剩下的如何保證操做一致就是在這個協議中實現的。下面會分別介紹不一樣的狀況下,NOPaxos是如何執行的,首先講明一些概念和變量:
系統運行只有,協議的運行過程,只會出現四種不一樣的狀況,正常的操做,出現消息丟失,發生視圖轉換,系統狀態同步。下面就分別講解這四種狀況下接收到不一樣消息時該如何處理,另外關於leader選舉能夠參考viewstamped-replication。
1. Normal Operation
正常的狀況下,客戶端廣播(broadcast)一個request的消息(消息內容爲,[request, op, request-id],其中request-id是用於response的時候,判斷是哪一個消息,理解爲消息的unique key)當replica接受到這樣一個消息的時候,首先OUM帶過來的一個session-msg-num判斷是否是本身正在等待的消息,若是是,則遞增session-msg-num並將op寫入日誌(注意,這裏並不執行)。若是replica是leader的話,那麼就執行這個操做,並寫入日誌。而後每一個replica會回覆客戶端一個消息(內容爲,[reply, view-id, log-slot-num, request-id, result],這裏的result只有leader纔有回覆,其餘爲null)。客戶端會等待f+1個reply,能match上view-id和log-slot-nums的回覆,其中必須有一個是leader,若是沒有接收到足夠的回覆,則會超時甚至重試。上述是正常的狀況下,正常的處理過程。【問題:若是leader提交了,然而client並無收到f+1個reply,這個時候怎麼辦,沒有任何機制能反駁leader?raft的機制就是確認replica已經寫入了日誌才commit的,因此他這裏沒寫明我也不明白是爲啥】
2. Gap Agrement
假如此時libOUM原本在等待session-msg-num編號的請求,卻來了一個更大的請求,說明,中間發生丟包了。那麼此時,libOUM就會向上層返回一個drop-notification,告知session-msg-num丟失了(同一個session內)而且遞增session-msg-num。若是是非leader接收到drop-notification,那麼能夠向相鄰節點copy請求,或者不作任何事。若是leader節點接收到了這樣一個返回值,則會在日誌中追加一個NO-OP,而且執行下面的操做:
對於drop操做,客戶端是不須要顯式通知到的,由於能夠等待客戶端超時。固然這裏在實際開發的時候能夠進行一些優化,好比leader沒有接收到的時候,也能夠向其餘的節點進行copy,減小NO-OP的數目。
3. view change
前文也說到了,NOPaxos這一層有一個leader的概念,且OUM有一個session的概念,若是這兩個一旦有一個發生改變,就須要進行view change操做了。view change協議能保證新老視圖中間的狀態一致性,且能很好的從老的視圖切換到新的視圖。NOPaxos中的視圖變換協議相似於Viewstamped Replication[42]。算法闡述以下:當一個replica懷疑當前的leader掛了,或者接收到了一個session-terminated,亦或者接收到一個view-change/view-change-req的消息。此時他就適當的增長leader-num或者session-num,而且將狀態status設置爲viewchange。一旦session-num發生改變,session-msg-num則重置爲0。而後廣播一個消息[view-change-req, view-id]給其餘的replica,而且發送一個[view-change, view-id, v`, session-msg
-num, log]給新的leader,v`表示上一個狀態status爲normal的視圖的view-id。當一個replica處於viewchange狀態的時候,會忽略其餘消息,除了start-view, view-change,view-change-req。若是超時,則從新廣播和發送指定消息給新leader。當新leader接收到f+1個view-change的消息的時候,則會執行下列操做:從最近最新的view且status爲normal的replica合併日誌(每一個replica都會發送一個log信息給新的leader)。合併規則是,若是你們都是no-op,那就是no-op,若是有一個是request,那就是request。leader拿到新的view-id以後設置session-msg-num比合並日志大的數字,而後廣播一個消息[start-view, view-id, session-msg-num, log]給全部的replica.當其餘的replica收到了這個start-view的消息以後,會更新本身監聽的信息,包括view-id,session-msg-num等,並要先同步下日誌,若是發生了日誌更新,則會發送reply信息給客戶端。最後把本身的status設置爲normal。
4. Synchronization
定時同步,這個屬於優化範疇,前文也說到,在正常的狀況下,leader進行commit操做,replica只進行日誌append,可是隨着系統的運行,會致使log愈來愈大,若是leader發生變動,那麼就會有一個問題,新leader恢復時間很是長。爲了在leader變動的時候,恢復時間縮短,NOPaxos決定週期性的進行數據同步。這一步驟的目的就是確保在sync-point前的全部數據,log狀態都是一致的。小論文並無詳細的指出如何解決這一問題,放在了大論文中去講了。
首先定義正確性是:一個request被執行或者NO-OP這樣的日誌被寫進f+1個節點中,且若是客戶端確保一個request執行成功的標記是收到f+1個回覆,而且咱們說一個view v的log是stable的,代表這個會成爲全部高於view v的視圖的前綴日誌。
(1).stable log中的成功操做,在resulting log中也是一樣正確的。
(2).replica老是開始於一個view中一個正確的session-msg-num
值得注意的是,一個操做(request或者no-op一旦被commit到日誌中,將會永遠呆着),由於若是在view change期間,同時發生了操做commit,意味着f+1個節點贊成了view change,而f+1個節點提交了日誌,也就說明了,至少有一個節點同時執行了這兩件事。並且新leader會跟其餘的節點同步日誌,因此若是是大多數的節點認可的日誌會同步到leader中去。
後記:閱讀完以後,總以爲有點問題,github上有這個代碼的開源實現,NOPaxos的源碼,筆者還沒來得及去看,後續若是看了會繼續開更,但願更多喜歡分佈式共識和分佈式一致性的朋友能一塊兒談論這個話題。