區塊鏈核心技術:拜占庭共識演算法之PBFT全面理解
PBFT是Practical Byzantine Fault Tolerance的縮寫,意為實用拜占庭容錯演算法。該演算法是Miguel Castro (卡斯特羅)和Barbara Liskov(利斯科夫)在1999年提出來的,解決了原始拜占庭容錯演算法效率不高的問題,將演算法複雜度由指數級降低到多項式級,使得拜占庭容錯演算法在實際系統應用中變得可行。該論文發表在1999年的作業系統設計與實現國際會議上(OSDI99)。沒錯,這個Loskov就是提出著名的里氏替換原則(LSP)的人,2008年圖靈獎得主。
摘要部分
OSDI99這篇論文描述了一種副本複製(replication)演算法解決拜占庭容錯問題。作者認為拜占庭容錯演算法將會變得更加重要,因為惡意攻擊和軟體錯誤的發生將會越來越多,並且導致失效的節點產生任意行為。(拜占庭節點的任意行為有可能誤導其他副本節點產生更大的危害,而不僅僅是宕機失去響應。)而早期的拜占庭容錯演算法或者基於同步系統的假設,或者由於效能太低而不能在實際系統中運作。這篇論文中描述的演算法是實用的,因為該演算法可以工作在非同步環境中,並且通過優化在早期演算法的基礎上把響應效能提升了一個數量級以上。作者使用這個演算法實現了拜占庭容錯的網路檔案系統(NFS),效能測試證明了該系統僅比無副本複製的標準NFS慢了3%。
1.概要介紹
第一段大片廢話就是說明拜占庭演算法越來越重要了,然後說這篇論文提出解決拜占庭容錯的狀態機副本複製(state machine replication)演算法。這個演算法在保證活性和安全性(liveness & safety)的前提下提供了(n-1)/3的容錯性。從Lamport教授在1982年提出拜占庭問題開始,已經有一大堆演算法去解決拜占庭容錯了。但是一句話概括:這些演算法都是狗屎!PBFT演算法跟這些妖豔賤貨完全不同,在只讀操作中只使用1次訊息往返(message round trip),在只寫操作中只使用2次訊息往返,並且在正常操作中使用了訊息驗證編碼(Message Authentication Code,簡稱MAC),而造成妖豔賤貨效能低下的公鑰加密(public-key cryptography)只在發生失效的情況下使用。作者不僅提出演算法,而且使用這個演算法實現了一個牛逼的系統(拜占庭容錯的NFS),反正效能槓槓的。
作者先炫耀一下這邊論文的貢獻亮瞎你們的狗眼:
1)首次提出在非同步網路環境下使用狀態機副本複製協議
2)使用多種優化使效能顯著提升
3)實現了一種拜占庭容錯的分散式檔案系統
4)為副本複製的效能損耗提供試驗資料支援
2.系統模型
這部分主要對節點行為和網路環境進行劇情設定,然後賦予了訊息的加密屬性,最後對大魔王的能力進行設定。
系統假設為非同步分散式的,通過網路傳輸的訊息可能丟失、延遲、重複或者亂序。作者假設節點的失效必須是獨立發生的,也就是說程式碼、作業系統和管理員密碼這些東西在各個節點上是不一樣的。(那麼如果節點失效不獨立發生,PBFT演算法就失效了嗎?)
作者使用了加密技術來防止欺騙攻擊和重播攻擊,以及檢測被破壞的訊息。訊息包含了公鑰簽名(其實就是RSA演算法)、訊息驗證編碼(MAC)和無碰撞雜湊函式生成的訊息摘要(message digest)
系統允許大魔王可以操縱多個失效節點、延遲通訊、甚至延遲正確節點來毀滅世界。但是作者限定大魔王不能無限期地延遲正確的節點,並且大魔王算力有限不能破解加密演算法。例如,大魔王不能偽造正確節點的有效簽名,不能從摘要資料反向計算出訊息內容,或者找到兩個有同樣摘要的訊息。
3.服務屬性
這部分描述了副本複製服務的特性
論文演算法實現的是一個具有確定性的副本複製服務,這個服務包括了一個狀態(state)和多個操作(operations)。這些操作不僅能夠進行簡單讀寫,而且能夠基於狀態和操作引數進行任意確定性的計算。客戶端向副本複製服務發起請求來執行操作,並且阻塞以等待回覆。副本複製服務由n個節點組成。
針對安全性
演算法在失效節點數量不超過(n-1)/3的情況下同時保證安全性和活性(safety & liveness)。安全性是指副本複製服務滿足線性一致性(linearizability),就像中心化系統一樣原子化執行操作。安全性要求失效副本的數量不超過上限,但是對客戶端失效的數量和是否與副本串謀不做限制。系統通過訪問控制來限制失效客戶端可能造成的破壞,稽核客戶端並阻止客戶端發起無權執行的操作。同時,服務可以提供操作來改變一個客戶端的訪問許可權。因為演算法保證了許可權撤銷操作可以被所有客戶端觀察到,這種方法可以提供強大的機制從失效的客戶端攻擊中恢復。
針對活性
演算法不依賴同步提供安全性,因此必須依靠同步提供活性。否則,這個演算法就可以被用來在非同步系統中實現共識,而這是不可能的(由Fischer1985的論文證明)。本文的演算法保證活性,即所有客戶端最終都會收到針對他們請求的回覆,只要失效副本的數量不超過(n-1)/3,並且延遲delay(t)不會無限增長。這個delay(t)表示t時刻發出的訊息到它被目標最終接收的時間間隔,假設傳送者持續重傳直到訊息被接收。這時一個相當弱的同步假設,因為在真實系統中網路失效最終都會被修復。但是這就規避了Fischer1985提出的非同步系統無法達成共識的問題。
下面這段話是關鍵
本文的演算法彈性是達到最優的:當存在f個失效節點時必須保證存在至少3f+1
個副本數量,這樣才能保證在非同步系統中提供安全性和活性。這麼多數量的副本是需要的,因為在同n-f個節點通訊後系統必須做出正確判斷,由於f個副本有可能失效而不發回響應。但是,有可能f個沒有失效的副本不發回響應(是因為網路延遲嗎?),因此f個發回響應的副本有可能是失效的。儘管如此,系統仍舊需要足夠數量非失效節點的響應,並且這些非失效節點的響應數量必須超過失效節點的響應數量,即n-2f>f,因此得到n>3f。
演算法不能解決資訊保密的問題,失效的副本有可能將資訊洩露給攻擊者。在一般情況下不可能提供資訊保密,因為服務操作需要使用引數和服務狀態處理任意的計算,所有的副本都需要這些資訊來有效執行操作。當然,還是有可能在存在惡意副本的情況下通過祕密分享模式(secret sharing scheme)來實現私密性,因為引數和部分狀態對服務操作來說是不可見的。
4.演算法
PBFT是一種狀態機副本複製演算法,即服務作為狀態機進行建模,狀態機在分散式系統的不同節點進行副本複製。每個狀態機的副本都儲存了服務的狀態,同時也實現了服務的操作。將所有的副本組成的集合使用大寫字母R表示,使用0到|R|-1的整數表示每一個副本。為了描述方便,假設|R|=3f+1,這裡f是有可能失效的副本的最大個數。儘管可以存在多於3f+1個副本,但是額外的副本除了降低效能之外不能提高可靠性。
PBFT的劇情緩緩展開,首先介紹舞臺(view)、演員(replica)和角色(primary、backups)
所有的副本在一個被稱為檢視(View)的輪換過程(succession of configuration)中運作。在某個檢視中,一個副本作為主節點(primary),其他的副本作為備份(backups)。檢視是連續編號的整數。主節點由公式p = v mod |R|
計算得到,這裡v是檢視編號,p是副本編號,|R|是副本集合的個數。當主節點失效的時候就需要啟動檢視更換(view change)過程。Viewstamped Replication演算法和Paxos演算法就是使用類似方法解決良性容錯的。
PBFT演算法的狗血劇情如下:
1.客戶端向主節點發送請求呼叫服務操作
2.主節點通過廣播將請求傳送給其他副本
3.所有副本都執行請求並將結果發回客戶端
4.客戶端需要等待f+1個不同副本節點發回相同的結果,作為整個操作的最終結果。
同所有的狀態機副本複製技術一樣,PBFT對每個副本節點提出了兩個限定條件:(1)所有節點必須是確定性的。也就是說,在給定狀態和引數相同的情況下,操作執行的結果必須相同;(2)所有節點必須從相同的狀態開始執行。在這兩個限定條件下,即使失效的副本節點存在,PBFT演算法對所有非失效副本節點的請求執行總順序達成一致,從而保證安全性。
接下去描述簡化版本的PBFT演算法,忽略磁碟空間不足和訊息重傳等細節內容。並且,本文假設訊息驗證過程是通過數字簽名方法實現的,而不是更加高效的基於訊息驗證編碼(MAC)的方法。
4.1客戶端
客戶端c向主節點發送<REQUEST,o,t,c>
請求執行狀態機操作o,這裡時間戳t用來保證客戶端請求只會執行一次。客戶端c發出請求的時間戳是全序排列的,後續發出的請求比早先發出的請求擁有更高的時間戳。例如,請求發起時的本地時鐘值可以作為時間戳。
每個由副本節點發給客戶端的訊息都包含了當前的檢視編號,使得客戶端能夠跟蹤檢視編號,從而進一步推算出當前主節點的編號。客戶端通過點對點訊息向它自己認為的主節點發送請求,然後主節點自動將該請求向所有備份節點進行廣播。
副本發給客戶端的響應為<REPLY,v,t,c,i,r>
,v是檢視編號,t是時間戳,i是副本的編號,r是請求執行的結果。
客戶端等待f+1個從不同副本得到的同樣響應,同樣響應需要保證簽名正確,並且具有同樣的時間戳t和執行結果r。這樣客戶端才能把r作為正確的執行結果,因為失效的副本節點不超過f個,所以f+1個副本的一致響應必定能夠保證結果是正確有效的。
如果客戶端沒有在有限時間內收到回覆,請求將向所有副本節點進行廣播。如果請求已經在副本節點處理過了,副本就向客戶端重發一遍執行結果。如果請求沒有在副本節點處理過,該副本節點將把請求轉發給主節點。如果主節點沒有將該請求進行廣播,那麼就有認為主節點失效,如果有足夠多的副本節點認為主節點失效,則會觸發一次檢視變更。
本文假設客戶端會等待上一個請求完成才會發起下一個請求,但是隻要能夠保證請求順序,可以允許請求是非同步的。
4.2 PBFT演算法主線流程(正常情況)
世界格局
每個副本節點的狀態都包含了服務的整體狀態,副本節點上的訊息日誌(message log)包含了該副本節點接受(accepted)的訊息,並且使用一個整數表示副本節點的當前檢視編號。
事件的導火索
當主節點p收到客戶端的請求m,主節點將該請求向所有副本節點進行廣播,由此一場轟轟烈烈的三階段協議(three-phase protocol)拉開了序幕。在這裡,至於什麼訊息過多需要快取的情況我們就不管了,這不是重點。
三個階段的任務
我們重點討論預準備(pre-prepare)、準備(prepare)和確認(commit)這三個歷史性階段。預準備和準備兩個階段用來確保同一個檢視中請求傳送的時序性(即使對請求進行排序的主節點失效了),準備和確認兩個階段用來確保在不同的檢視之間的確認請求是嚴格排序的。
預準備階段
在預準備階段,主節點分配一個序列號n給收到的請求,然後向所有備份節點群發預準備訊息,預準備訊息的格式為<<PRE-PREPARE,v,n,d>,m>
,這裡v是檢視編號,m是客戶端傳送的請求訊息,d是請求訊息m的摘要。
請求本身是不包含在預準備的訊息裡面的,這樣就能使預準備訊息足夠小,因為預準備訊息的目的是作為一種證明,確定該請求是在檢視v中被賦予了序號n,從而在檢視變更的過程中可以追索。另外一個層面,將“請求排序協議”和“請求傳輸協議”進行解耦,有利於對訊息傳輸的效率進行深度優化。
備份節點對預準備訊息的態度
只有滿足以下條件,各個備份節點才會接受一個預準備訊息:
- 請求和預準備訊息的簽名正確,並且d與m的摘要一致。
- 當前檢視編號是v。
- 該備份節點從未在檢視v中接受過序號為n但是摘要d不同的訊息m。(許仙在這輩子從未見過名字叫白素貞的美貌女子)
- 預準備訊息的序號n必須在水線(watermark)上下限h和H之間。
水線存在的意義在於防止一個失效節點使用一個很大的序號消耗序號空間。
進入準備階段
如果備份節點i接受了預準備訊息<<PRE-PREPARE,v,n,d>,m>
,則進入準備階段。在準備階段的同時,該節點向所有副本節點發送準備訊息<PREPARE,v,n,d,i>
,並且將預準備訊息和準備訊息寫入自己的訊息日誌。如果看預準備訊息不順眼,就什麼都不做。
接受準備訊息需要滿足的條件
包括主節點在內的所有副本節點在收到準備訊息之後,對訊息的簽名是否正確,檢視編號是否一致,以及訊息序號是否滿足水線限制這三個條件進行驗證,如果驗證通過則把這個準備訊息寫入訊息日誌中。
準備階段完成的標誌
我們定義準備階段完成的標誌為副本節點i將(m,v,n,i)
記入其訊息日誌,其中m是請求內容,預準備訊息m在檢視v中的編號n,以及2f個從不同副本節點收到的與預準備訊息一致的準備訊息。每個副本節點驗證預準備和準備訊息的一致性主要檢查:檢視編號v、訊息序號n和摘要d。
預準備階段和準備階段確保所有正常節點對同一個檢視中的請求序號達成一致。接下去是對這個結論的形式化證明:如果prepared(m,v,n,i)
為真,則prepared(m',v,n,j)
必不成立,這就意味著至少f+1個正常節點在檢視v的預準備或者準備階段傳送了序號為n的訊息m。
進入確認階段
當(m,v,n,i)條件為真的時候,副本i將<COMMIT,v,n,D(m),i>
向其他副本節點廣播,於是就進入了確認階段。每個副本接受確認訊息的條件是:1)簽名正確;2)訊息的檢視編號與節點的當前檢視編號一致;3)訊息的序號n滿足水線條件,在h和H之間。一旦確認訊息的接受條件滿足了,則該副本節點將確認訊息寫入訊息日誌中。(補充:需要將針對某個請求的所有接受的訊息寫入日誌,這個日誌可以是在記憶體中的)。
接受確認訊息需要滿足的條件
我們定義確認完成committed(m,v,n)為真得條件為:任意f+1個正常副本節點集合中的所有副本i其prepared(m,v,n,i)為真;本地確認完成committed-local(m,v,n,i)為真的條件為:prepared(m,v,n,i)為真,並且i已經接受了2f+1個確認(包括自身在內)與預準備訊息一致。確認與預準備訊息一致的條件是具有相同的檢視編號、訊息序號和訊息摘要。
確認被接受的形式化描述
確認階段保證了以下這個不變式(invariant):對某個正常節點i來說,如果committed-local(m,v,n,i)為真則committed(m,v,n)也為真。這個不變式和檢視變更協議保證了所有正常節點對本地確認的請求的序號達成一致,即使這些請求在每個節點的確認處於不同的檢視。更進一步地講,這個不變式保證了任何正常節點的本地確認最終會確認f+1個更多的正常副本。
故事的終結
每個副本節點i在committed-local(m,v,n,i)為真之後執行m的請求,並且i的狀態反映了所有編號小於n的請求依次順序執行。這就確保了所有正常節點以同樣的順序執行所有請求,這樣就保證了演算法的正確性(safety)。在完成請求的操作之後,每個副本節點都向客戶端傳送回覆。副本節點會把時間戳比已回覆時間戳更小的請求丟棄,以保證請求只會被執行一次。
我們不依賴於訊息的順序傳遞,因此某個副本節點可能亂序確認請求。因為每個副本節點在請求執行之前已經將預準備、準備和確認這三個訊息記錄到了日誌中,這樣亂序就不成問題了。(為什麼?)
下圖展示了在沒有發生主節點失效的情況下演算法的正常執行流程,其中副本0是主節點,副本3是失效節點,而C是客戶端。
PBFT演算法流程
4.3 垃圾回收
為了節省記憶體,系統需要一種將日誌中的無異議訊息記錄刪除的機制。為了保證系統的安全性,副本節點在刪除自己的訊息日誌前,需要確保至少f+1個正常副本節點執行了訊息對應的請求,並且可以在檢視變更時向其他副本節點證明。另外,如果一些副本節點錯過部分訊息,但是這些訊息已經被所有正常副本節點刪除了,這就需要通過傳輸部分或者全部服務狀態實現該副本節點的同步。因此,副本節點同樣需要證明狀態的正確性。
在每一個操作執行後都生成這樣的證明是非常消耗資源的。因此,證明過程只有在請求序號可以被某個常數(比如100)整除的時候才會週期性地進行。我們將這些請求執行後得到的狀態稱作檢查點(checkpoint),並且將具有證明的檢查點稱作穩定檢查點(stable checkpoint)。
副本節點儲存了服務狀態的多個邏輯拷貝,包括最新的穩定檢查點,零個或者多個非穩定的檢查點,以及一個當前狀態。寫時複製技術可以被用來減少儲存額外狀態拷貝的空間開銷。
檢查點的正確性證明的生成過程如下:當副本節點i生成一個檢查點後,向其他副本節點廣播檢查點訊息<CHECKPOINT,n,d,i>
,這裡n是最近一個影響狀態的請求序號,d是狀態的摘要。每個副本節點都默默地在各自的日誌中收集並記錄其他節點發過來的檢查點訊息,直到收到來自2f+1個不同副本節點的具有相同序號n和摘要d的檢查點訊息。這2f+1
個訊息就是這個檢查點的正確性證明。
具有證明的檢查點成為穩定檢查點,然後副本節點就可以將所有序號小於等於n的預準備、準備和確認訊息從日誌中刪除。同時也可以將之前的檢查點和檢查點訊息一併刪除。
檢查點協議可以用來更新水線(watermark)的高低值(h和H),這兩個高低值限定了可以被接受的訊息。水線的低值h與最近穩定檢查點的序列號相同,而水線的高值H=h+k,k需要足夠大才能使副本不至於為了等待穩定檢查點而停頓。加入檢查點每100個請求產生一次,k的取值可以是200。
4.4 檢視變更,改朝換代
使用計時器的超時機制觸發檢視變更事件
檢視變更協議在主節點失效的時候仍然保證系統的活性。檢視變更可以由超時觸發,以防止備份節點無期限地等待請求的執行。備份節點等待一個請求,就是該節點接收到一個有效請求,但是還沒有執行它。當備份節點接收到一個請求但是計時器還未執行,那麼它就啟動計時器;當它不再等待請求的執行就把計時器停止,但是當它等待其他請求執行的時候再次情動計時器。
作者:李啟雷
連結:http://www.jianshu.com/p/fb5edf031afd
來源:簡書
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。