10. 一致性與共識

一句古老的格言告誡說:“千萬不要帶著兩塊計時器出海;要麼帶一塊,要麼帶三塊。”

弗雷德里克·P·布魯克斯,《人月神話:軟體工程隨筆》(1995)

正如在 第九章 中討論的,分散式系統中會出現許多問題。如果我們希望服務在出現這些問題時仍能正確工作,就需要找到容錯的方法。

我們擁有的最佳容錯工具之一是 複製。然而,正如我們在 第六章 中看到的,在多個副本上擁有多份資料副本會帶來不一致的風險。讀取可能由一個非最新的副本處理,從而產生過時的結果。如果多個副本可以接受寫入,我們必須處理在不同副本上併發寫入的值之間的衝突。從高層次來看,處理這些問題有兩種相互競爭的理念:

最終一致性
在這種理念中,系統被複制這一事實對應用程式是可見的,作為應用程式開發者,你需要處理可能出現的不一致和衝突。這種方法通常用於多主複製(見 “多主複製”)和無主複製(見 “無主複製”)的系統中。
強一致性
這種理念認為應用程式不應該擔心複製的內部細節,系統應該表現得就像單節點一樣。這種方法的優點是對你(應用程式開發者)來說更簡單。缺點是更強的一致性會帶來效能成本,並且某些最終一致系統能夠容忍的故障會導致強一致系統出現中斷。

一如既往,哪種方法更好取決於你的應用程式。如果你有一個應用程式,使用者可以在離線狀態下對資料進行更改,那麼最終一致性是不可避免的,如 “同步引擎與本地優先軟體” 中所討論的。然而,最終一致性對應用程式來說也可能很難處理。如果你的副本位於具有快速、可靠通訊的資料中心,那麼強一致性通常是合適的,因為其成本是可以接受的。

在本章中,我們將深入探討強一致性方法,關注三個領域:

  1. 一個挑戰是"強一致性"相當模糊,因此我們將制定一個更精確的定義,明確我們想要實現什麼:線性一致性
  2. 我們將研究生成 ID 和時間戳的問題。這可能聽起來與一致性無關,但實際上密切相關。
  3. 我們將探討分散式系統如何在保持容錯的同時實現線性一致性;答案是 共識 演算法。

在此過程中,我們將看到分散式系統中什麼是可能的,什麼是不可能的,存在一些基本限制。

本章的主題以難以正確實現而著稱;構建在沒有故障時表現良好,但在面對設計者沒有考慮到的不幸故障組合時完全崩潰的系統非常容易。已經發展了大量理論來幫助我們思考這些邊界情況,這使我們能夠構建可以穩健地容忍故障的系統。

本章只會觸及表面:我們將堅持非正式的直覺,避免演算法細節、形式化模型和證明。如果你想在共識系統和類似基礎設施上進行認真的工作,你需要更深入地研究理論,才有機會讓你的系統穩健。與往常一樣,本章中的文獻參考提供了一些初步的指引。

線性一致性

如果你希望複製的資料庫儘可能簡單易用,你應該讓它表現得就像根本沒有複製一樣。然後使用者就不必擔心複製延遲、衝突和其他不一致性。這將給我們帶來容錯的優勢,但不會因為必須考慮多個副本而帶來複雜性。

這就是 線性一致性 1 背後的想法(也稱為 原子一致性 2強一致性即時一致性外部一致性 3)。線性一致性的確切定義相當微妙,我們將在本節的其餘部分探討它。但基本思想是讓系統看起來好像只有一份資料副本,並且對它的所有操作都是原子的。有了這個保證,即使實際上可能有多個副本,應用程式也不需要擔心它們。

在線性一致系統中,一旦一個客戶端成功完成寫入,所有從資料庫讀取的客戶端都必須能夠看到剛剛寫入的值。維護單一資料副本的假象意味著保證讀取的值是最新的、最新的值,而不是來自過時的快取或副本。換句話說,線性一致性是一個 新鮮度保證。為了闡明這個想法,讓我們看一個非線性一致系統的例子。

圖 10-1. 如果這個資料庫是線性一致的,那麼 Alice 的讀取要麼返回 1 而不是 0,要麼 Bob 的讀取返回 0 而不是 1。

圖 10-1 顯示了一個非線性一致的體育網站示例 4。Aaliyah 和 Bryce 坐在同一個房間裡,都在檢視手機,想要了解他們最喜歡的球隊比賽的結果。就在最終比分宣佈後,Aaliyah 重新整理了頁面,看到了獲勝者的公告,並興奮地告訴了 Bryce。Bryce 懷疑地在自己的手機上點選了 重新整理,但他的請求傳送到了一個滯後的資料庫副本,因此他的手機顯示比賽仍在進行中。

如果 Aaliyah 和 Bryce 同時點選重新整理,他們得到兩個不同的查詢結果就不會那麼令人驚訝了,因為他們不知道他們各自的請求在伺服器上被處理的確切時間。然而,Bryce 知道他是在聽到 Aaliyah 宣佈最終比分 之後 點選重新整理按鈕(發起查詢)的,因此他期望他的查詢結果至少與 Aaliyah 的一樣新。他的查詢返回過時結果這一事實違反了線性一致性。

什麼使系統具有線性一致性?

為了更好地理解線性一致性,讓我們看一些更多的例子。圖 10-2 顯示了三個客戶端在線性一致資料庫中併發讀取和寫入同一個物件 x。在分散式系統理論中,x 被稱為 暫存器——在實踐中,它可能是鍵值儲存中的一個鍵,關係資料庫中的一行,或者文件資料庫中的一個文件,例如。

圖 10-2. Alice 觀察到 x = 0 且 y = 1,而 Bob 觀察到 x = 1 且 y = 0。就好像 Alice 和 Bob 的計算機對寫入發生的順序意見不一。

為簡單起見,圖 10-2 僅顯示了從客戶端角度看的請求,而不是資料庫的內部。每個條形代表客戶端發出的請求,條形的開始是傳送請求的時間,條形的結束是客戶端收到響應的時間。由於網路延遲可變,客戶端不知道資料庫確切何時處理了它的請求——它只知道必須在客戶端傳送請求和接收響應之間的某個時間發生。

在這個例子中,暫存器有兩種型別的操作:

  • read(x) ⇒ v 表示客戶端請求讀取暫存器 x 的值,資料庫返回值 v
  • write(x, v) ⇒ r 表示客戶端請求將暫存器 x 設定為值 v,資料庫返回響應 r(可能是 okerror)。

圖 10-2 中,x 的值最初為 0,客戶端 C 執行寫入請求將其設定為 1。在此期間,客戶端 A 和 B 反覆輪詢資料庫以讀取最新值。A 和 B 的讀取請求可能得到什麼響應?

  • 客戶端 A 的第一個讀取操作在寫入開始之前完成,因此它必須明確返回舊值 0。
  • 客戶端 A 的最後一次讀取在寫入完成後開始,因此如果資料庫是線性一致的,它必須明確返回新值 1,因為讀取必須在寫入之後被處理。
  • 與寫入操作在時間上重疊的任何讀取操作可能返回 0 或 1,因為我們不知道在讀取操作被處理時寫入是否已經生效。這些操作與寫入是 併發 的。

然而,這還不足以完全描述線性一致性:如果與寫入併發的讀取可以返回舊值或新值,那麼讀者可能會在寫入進行時多次看到值在舊值和新值之間來回翻轉。這不是我們對模擬"單一資料副本"的系統所期望的。

為了使系統線性一致,我們需要新增另一個約束,如 圖 10-3 所示。

圖 10-3. 如果 Alice 和 Bob 有完美的時鐘,線性一致性將要求返回 x = 1,因為 x 的讀取在寫入 x = 1 完成後開始。

在線性一致系統中,我們想象必須有某個時間點(在寫入操作的開始和結束之間),x 的值從 0 原子地翻轉到 1。因此,如果一個客戶端的讀取返回新值 1,所有後續讀取也必須返回新值,即使寫入操作尚未完成。

這種時序依賴關係在 圖 10-3 中用箭頭表示。客戶端 A 是第一個讀取新值 1 的。就在 A 的讀取返回後,B 開始新的讀取。由於 B 的讀取嚴格發生在 A 的讀取之後,它也必須返回 1,即使 C 的寫入仍在進行中。(這與 圖 10-1 中 Aaliyah 和 Bryce 的情況相同:在 Aaliyah 讀取新值後,Bryce 也期望讀取新值。)

我們可以進一步細化這個時序圖,以視覺化每個操作在某個時間點原子地生效 5,就像 圖 10-4 中顯示的更複雜的例子。在這個例子中,除了 readwrite 之外,我們添加了第三種操作型別:

  • cas(x, vold, vnew) ⇒ r 表示客戶端請求一個原子 比較並設定 操作(見 “條件寫入(比較並設定)”)。如果暫存器 x 的當前值等於 vold,它應該原子地設定為 vnew。如果 x 的值與 vold 不同,則操作應該保持暫存器不變並返回錯誤。r 是資料庫的響應(okerror)。

圖 10-4 中的每個操作都用一條垂直線(在每個操作的條形內)標記,表示我們認為操作執行的時間。這些標記按順序連線起來,結果必須是暫存器的有效讀寫序列(每次讀取必須返回最近寫入設定的值)。

線性一致性的要求是連線操作標記的線始終向前移動(從左到右),永不後退。這個要求確保了我們之前討論的新鮮度保證:一旦寫入或讀取了新值,所有後續讀取都會看到寫入的值,直到它再次被覆蓋。

圖 10-4. x 的讀取與寫入 x = 1 併發。由於我們不知道操作的確切時序,讀取可以返回 0 或 1。

圖 10-4 中有一些有趣的細節需要指出:

  • 首先客戶端 B 傳送了讀取 x 的請求,然後客戶端 D 傳送了將 x 設定為 0 的請求,然後客戶端 A 傳送了將 x 設定為 1 的請求。然而,返回給 B 的讀取值是 1(A 寫入的值)。這是可以的:這意味著資料庫首先處理了 D 的寫入,然後是 A 的寫入,最後是 B 的讀取。雖然這不是傳送請求的順序,但這是一個可接受的順序,因為這三個請求是併發的。也許 B 的讀取請求在網路中稍有延遲,因此它在兩次寫入之後才到達資料庫。
  • 客戶端 B 的讀取在客戶端 A 收到資料庫的響應之前返回了 1,表示值 1 的寫入成功。這也是可以的:這只是意味著從資料庫到客戶端 A 的 ok 響應在網路中稍有延遲。
  • 這個模型不假設任何事務隔離:另一個客戶端可以隨時更改值。例如,C 首先讀取 1,然後讀取 2,因為該值在兩次讀取之間被 B 更改了。原子比較並設定(cas)操作可用於檢查值是否未被另一個客戶端併發更改:B 和 C 的 cas 請求成功,但 D 的 cas 請求失敗(到資料庫處理它時,x 的值不再是 0)。
  • 客戶端 B 的最後一次讀取(在陰影條中)不是線性一致的。該操作與 C 的 cas 寫入併發,後者將 x 從 2 更新到 4。在沒有其他請求的情況下,B 的讀取返回 2 是可以的。然而,客戶端 A 在 B 的讀取開始之前已經讀取了新值 4,因此 B 不允許讀取比 A 更舊的值。同樣,這與 圖 10-1 中 Aaliyah 和 Bryce 的情況相同。

這就是線性一致性背後的直覺;形式化定義 1 更精確地描述了它。可以(儘管計算成本高昂)透過記錄所有請求和響應的時序,並檢查它們是否可以排列成有效的順序序列來測試系統的行為是否線性一致 6 7

就像除了可序列化之外還有各種弱隔離級別用於事務(見 “弱隔離級別”),除了線性一致性之外,複製系統也有各種較弱的一致性模型 8。實際上,我們在 “複製延遲問題” 中看到的 寫後讀單調讀一致性字首讀 屬性就是這種較弱一致性模型的例子。線性一致性保證所有這些較弱的屬性,以及更多。在本章中,我們將重點關注線性一致性,它是最常用的最強一致性模型。


線性一致性與可序列化

線性一致性很容易與可序列化混淆(見 “可序列化”),因為這兩個詞似乎都意味著類似"可以按順序排列"的東西。然而,它們是完全不同的保證,區分它們很重要:

可序列化
可序列化是事務的隔離屬性,其中每個事務可能讀取和寫入 多個物件(行、文件、記錄)。它保證事務的行為與它們按 某種 序列順序執行時相同:也就是說,就好像你首先執行一個事務的所有操作,然後執行另一個事務的所有操作,依此類推,而不交錯它們。該序列順序可以與事務實際執行的順序不同 9
線性一致性
線性一致性是對暫存器(單個物件)的讀寫保證。它不將操作分組到事務中,因此它不能防止涉及多個物件的問題,如寫偏差(見 “寫偏差和幻讀”)。然而,線性一致性是一個 新鮮度 保證:它要求如果一個操作在另一個操作開始之前完成,那麼後一個操作必須觀察到至少與前一個操作一樣新的狀態。可序列化沒有這個要求:例如,可序列化允許過時讀取 10

順序一致性 又是另外一回事 8,但我們不會在這裡討論它。)

資料庫可能同時提供可序列化和線性一致性,這種組合稱為 嚴格可序列化強單副本可序列化strong-1SR11 12。單節點資料庫通常是線性一致的。對於使用樂觀方法(如可序列化快照隔離)的分散式資料庫(見 “可序列化快照隔離(SSI)”),情況更加複雜:例如,CockroachDB 提供可序列化和對讀取的一些新鮮度保證,但不是嚴格可序列化 13,因為這需要事務之間進行昂貴的協調 14

也可以將較弱的隔離級別與線性一致性結合,或將較弱的一致性模型與可序列化結合;實際上,一致性模型和隔離級別可以在很大程度上相互獨立地選擇 15 16


依賴線性一致性

在什麼情況下線性一致性有用?檢視體育比賽的最終比分也許是一個無關緊要的例子:過時幾秒鐘的結果在這種情況下不太可能造成任何實際傷害。然而,有幾個領域中線性一致性是使系統正確工作的重要要求。

鎖定與領導者選舉

使用單主複製的系統需要確保確實只有一個主節點,而不是多個(腦裂)。選舉領導者的一種方法是使用租約:每個啟動的節點都嘗試獲取租約,成功的節點成為領導者 17。無論這種機制如何實現,它都必須是線性一致的:兩個不同的節點不應該能夠同時獲取租約。

像 Apache ZooKeeper 18 和 etcd 這樣的協調服務通常用於實現分散式租約和領導者選舉。它們使用共識演算法以容錯的方式實現線性一致的操作(我們將在本章後面討論這些演算法)。實現租約和領導者選舉正確仍然有許多微妙的細節(例如,參見 “分散式鎖和租約” 中的柵欄問題),像 Apache Curator 這樣的庫透過在 ZooKeeper 之上提供更高級別的配方來提供幫助。然而,線性一致的儲存服務是這些協調任務的基本基礎。


Note

嚴格來說,ZooKeeper 提供線性一致的寫入,但讀取可能是過時的,因為不能保證它們由當前領導者提供 18。etcd 從版本 3 開始預設提供線性一致的讀取。


分散式鎖也在一些分散式資料庫中以更細粒度的級別使用,例如 Oracle Real Application Clusters (RAC) 19。RAC 對每個磁碟頁使用一個鎖,多個節點共享對同一磁碟儲存系統的訪問。由於這些線性一致的鎖位於事務執行的關鍵路徑上,RAC 部署通常具有專用的叢集互連網路用於資料庫節點之間的通訊。

約束與唯一性保證

唯一性約束在資料庫中很常見:例如,使用者名稱或電子郵件地址必須唯一標識一個使用者,在檔案儲存服務中不能有兩個具有相同路徑和檔名的檔案。如果你想在資料寫入時強制執行此約束(這樣如果兩個人同時嘗試建立具有相同名稱的使用者或檔案,其中一個將返回錯誤),你需要線性一致性。

這種情況實際上類似於鎖:當用戶註冊你的服務時,你可以認為他們獲取了所選使用者名稱的"鎖"。該操作也非常類似於原子比較並設定,將使用者名稱設定為宣告它的使用者的 ID,前提是使用者名稱尚未被佔用。

如果你想確保銀行賬戶餘額永遠不會變為負數,或者你不會銷售超過倉庫庫存的物品,或者兩個人不會同時預訂同一航班或劇院的同一座位,也會出現類似的問題。這些約束都要求有一個所有節點都同意的單一最新值(賬戶餘額、庫存水平、座位佔用情況)。

在實際應用中,有時可以接受寬鬆地對待這些約束(例如,如果航班超售,你可以將客戶轉移到其他航班,併為不便提供補償)。在這種情況下,可能不需要線性一致性,我們將在 [Link to Come] 中討論這種寬鬆解釋的約束。

然而,硬唯一性約束,例如你通常在關係資料庫中找到的約束,需要線性一致性。其他型別的約束,例如外部索引鍵或屬性約束,可以在沒有線性一致性的情況下實現 20

跨通道時序依賴

注意 圖 10-1 中的一個細節:如果 Aaliyah 沒有大聲說出比分,Bryce 就不會知道他的查詢結果是過時的。他只會在幾秒鐘後再次重新整理頁面,最終看到最終比分。線性一致性違規之所以被注意到,只是因為系統中有一個額外的通訊通道(Aaliyah 的聲音到 Bryce 的耳朵)。

類似的情況可能出現在計算機系統中。例如,假設你有一個網站,使用者可以上傳影片,後臺程序將影片轉碼為較低質量,以便在慢速網際網路連線上流式傳輸。該系統的架構和資料流如 圖 10-5 所示。

影片轉碼器需要明確指示執行轉碼作業,此指令透過訊息佇列從 Web 伺服器傳送到轉碼器(見 [Link to Come])。Web 伺服器不會將整個影片放在佇列中,因為大多數訊息代理都是為小訊息設計的,而影片可能有許多兆位元組大小。相反,影片首先寫入檔案儲存服務,寫入完成後,轉碼指令被放入佇列。

圖 10-5. 一個非線性一致的系統:Alice 和 Bob 在不同時間看到上傳的影像,因此 Bob 的請求基於過時的資料。

如果檔案儲存服務是線性一致的,那麼這個系統應該工作正常。如果它不是線性一致的,就存在競態條件的風險:訊息佇列(圖 10-5 中的步驟 3 和 4)可能比儲存服務內部的複製更快。在這種情況下,當轉碼器獲取原始影片(步驟 5)時,它可能會看到檔案的舊版本,或者根本看不到任何內容。如果它處理影片的舊版本,檔案儲存中的原始影片和轉碼影片將永久不一致。

這個問題的出現是因為 Web 伺服器和轉碼器之間有兩個不同的通訊通道:檔案儲存和訊息佇列。如果沒有線性一致性的新鮮度保證,這兩個通道之間可能存在競態條件。這種情況類似於 圖 10-1,其中也存在兩個通訊通道之間的競態條件:資料庫複製和 Aaliyah 嘴巴到 Bryce 耳朵之間的現實音訊通道。

如果你有一個可以接收推送通知的移動應用程式,並且應用程式在收到推送通知時從伺服器獲取一些資料,就會發生類似的競態條件。如果資料獲取可能傳送到滯後的副本,可能會發生推送通知快速透過,但後續獲取沒有看到推送通知所涉及的資料。

線性一致性不是避免這種競態條件的唯一方法,但它是最容易理解的。如果你控制額外的通訊通道(如訊息佇列的情況,但不是 Aaliyah 和 Bryce 的情況),你可以使用類似於我們在 “讀己之寫” 中討論的替代方法,但代價是額外的複雜性。

實現線性一致性系統

現在我們已經看了線性一致性有用的幾個例子,讓我們思考如何實現一個提供線性一致語義的系統。

由於線性一致性本質上意味著"表現得好像只有一份資料副本,並且對它的所有操作都是原子的",最簡單的答案是真的只使用一份資料副本。然而,這種方法將無法容忍故障:如果持有該副本的節點失敗,資料將丟失,或者至少在節點重新啟動之前無法訪問。

讓我們重新審視 第六章 中的複製方法,並比較它們是否可以實現線性一致:

單主複製(可能線性一致)
在單主複製系統中,主節點擁有用於寫入的資料主副本,從節點在其他節點上維護資料的備份副本。只要你在主節點上執行所有讀寫操作,它們很可能是線性一致的。然而,這假設你確定知道誰是主節點。如 “分散式鎖和租約” 中所討論的,一個節點很可能認為自己是主節點,而實際上並不是——如果這個妄想的主節點繼續服務請求,很可能會違反線性一致性 21。使用非同步複製,故障切換甚至可能丟失已提交的寫入,這違反了永續性和線性一致性。

對單主資料庫進行分片,每個分片有一個單獨的主節點,不會影響線性一致性,因為它只是單物件保證。跨分片事務是另一回事(見 “分散式事務”)。

共識演算法(可能線性一致)
一些共識演算法本質上是帶有自動領導者選舉和故障切換的單主複製。它們經過精心設計以防止腦裂,使它們能夠安全地實現線性一致的儲存。ZooKeeper 使用 Zab 共識演算法 22,etcd 使用 Raft 23,例如。然而,僅僅因為系統使用共識並不能保證其上的所有操作都是線性一致的:如果它允許在不檢查節點是否仍然是領導者的情況下在節點上讀取,讀取的結果可能是過時的,如果剛剛選出了新的領導者。
多主複製(非線性一致)
具有多主複製的系統通常不是線性一致的,因為它們在多個節點上併發處理寫入,並將它們非同步複製到其他節點。因此,它們可能產生需要解決的衝突寫入(見 “處理衝突寫入”)。
無主複製(可能非線性一致)
對於具有無主複製的系統(Dynamo 風格;見 “無主複製”),人們有時聲稱可以透過要求仲裁讀寫(w + r > n)來獲得"強一致性"。根據確切的演算法,以及你如何定義強一致性,這並不完全正確。

基於日曆時鐘的"最後寫入獲勝"衝突解決方法(例如,在 Cassandra 和 ScyllaDB 中)幾乎肯定是非線性一致的,因為時鐘時間戳由於時鐘偏差而無法保證與實際事件順序一致(見 “依賴同步時鐘”)。即使使用仲裁,也可能出現非線性一致的行為,如下一節所示。

線性一致性與仲裁

直觀地說,在 Dynamo 風格的模型中,仲裁讀寫似乎應該是線性一致的。然而,當我們有可變的網路延遲時,可能會出現競態條件,如 圖 10-6 所示。

圖 10-6. 如果網路延遲是可變的,仲裁不足以確保線性一致性。

圖 10-6 中,x 的初始值為 0,寫入客戶端透過向所有三個副本傳送寫入(n = 3,w = 3)將 x 更新為 1。同時,客戶端 A 從兩個節點的仲裁(r = 2)讀取,並在其中一個節點上看到新值 1。同時與寫入併發,客戶端 B 從不同的兩個節點仲裁讀取,並從兩者獲得舊值 0。

仲裁條件得到滿足(w + r > n),但這種執行仍然不是線性一致的:B 的請求在 A 的請求完成後開始,但 B 返回舊值而 A 返回新值。(這又是 圖 10-1 中 Aaliyah 和 Bryce 的情況。)

可以使 Dynamo 風格的仲裁線性一致,但代價是降低效能:讀者必須同步執行讀修復(見 “追趕錯過的寫入”),然後才能將結果返回給應用程式 24。此外,在寫入之前,寫入者必須讀取節點仲裁的最新狀態以獲取任何先前寫入的最新時間戳,並確保新寫入具有更大的時間戳 25 26。然而,Riak 由於效能損失而不執行同步讀修復。Cassandra 確實等待仲裁讀取時的讀修復完成 27,但由於它使用日曆時鐘作為時間戳而失去了線性一致性。

此外,只有線性一致的讀寫操作可以以這種方式實現;線性一致的比較並設定操作不能,因為它需要共識演算法 28

總之,最安全的假設是,具有 Dynamo 風格複製的無主系統不提供線性一致性,即使使用仲裁讀寫。

線性一致性的代價

由於某些複製方法可以提供線性一致性而其他方法不能,因此更深入地探討線性一致性的利弊是很有趣的。

我們已經在 第六章 中討論了不同複製方法的一些用例;例如,我們看到多主複製通常是多區域複製的良好選擇(見 “地理分散式操作”)。圖 10-7 展示了這種部署的示例。

圖 10-7. 如果客戶端由於網路分割槽而無法聯絡足夠的副本,它們就無法處理寫入。

考慮如果兩個區域之間出現網路中斷會發生什麼。讓我們假設每個區域內的網路正常工作,客戶端可以到達其本地區域,但這些區域之間無法相互連線。這被稱為 網路分割槽

使用多主資料庫,每個區域可以繼續正常執行:由於來自一個區域的寫入被非同步複製到另一個區域,寫入只是排隊並在網路連線恢復時交換。

另一方面,如果使用單主複製,那麼主節點必須在其中一個區域。任何寫入和任何線性一致的讀取都必須傳送到主節點——因此,對於連線到從節點區域的任何客戶端,這些讀寫請求必須透過網路同步傳送到主節點區域。

如果在單主設定中區域之間的網路中斷,連線到從節點區域的客戶端無法聯絡主節點,因此它們既不能對資料庫進行任何寫入,也不能進行任何線性一致的讀取。它們仍然可以從從節點讀取,但它們可能是過時的(非線性一致)。如果應用程式需要線性一致的讀寫,網路中斷會導致應用程式在無法聯絡主節點的區域中變得不可用。

如果客戶端可以直接連線到主節點區域,這不是問題,因為應用程式在那裡繼續正常工作。但只能訪問從節點區域的客戶端將在網路連結修復之前遇到中斷。

CAP 定理

這個問題不僅僅是單主和多主複製的結果:任何線性一致的資料庫都有這個問題,無論它如何實現。這個問題也不特定於多區域部署,而是可以發生在任何不可靠的網路上,即使在一個區域內。權衡如下:

  • 如果你的應用程式 需要 線性一致性,並且某些副本由於網路問題與其他副本斷開連線,那麼某些副本在斷開連線時無法處理請求:它們必須等待網路問題修復,或者返回錯誤(無論哪種方式,它們都變得 不可用)。這種選擇有時被稱為 CP(在網路分割槽下一致)。
  • 如果你的應用程式 不需要 線性一致性,那麼它可以以一種方式編寫,使每個副本可以獨立處理請求,即使它與其他副本斷開連線(例如,多主)。在這種情況下,應用程式可以在面對網路問題時保持 可用,但其行為不是線性一致的。這種選擇被稱為 AP(在網路分割槽下可用)。

因此,不需要線性一致性的應用程式可以更好地容忍網路問題。這種見解通常被稱為 CAP 定理 29 30 31 32,由 Eric Brewer 在 2000 年命名,儘管這種權衡自 1970 年代以來就為分散式資料庫設計者所知 33 34 35

CAP 最初是作為經驗法則提出的,沒有精確的定義,目的是開始關於資料庫中權衡的討論。當時,許多分散式資料庫專注於在具有共享儲存的機器叢集上提供線性一致語義 19,CAP 鼓勵資料庫工程師探索更廣泛的分散式無共享系統設計空間,這些系統更適合實現大規模 Web 服務 36。CAP 在這種文化轉變方面值得稱讚——它幫助觸發了 NoSQL 運動,這是 2000 年代中期左右的一系列新資料庫技術。

無用的 CAP 定理

CAP 有時被表述為 一致性、可用性、分割槽容錯性:從 3 箇中選擇 2 個。不幸的是,這樣表述是誤導性的 32,因為網路分割槽是一種故障,所以它們不是你可以選擇的:無論你喜歡與否,它們都會發生。

當網路正常工作時,系統可以同時提供一致性(線性一致性)和完全可用性。當發生網路故障時,你必須在線性一致性或完全可用性之間進行選擇。因此,CAP 的更好表述方式是 分割槽時要麼一致要麼可用 37。更可靠的網路需要更少地做出這種選擇,但在某個時候這種選擇是不可避免的。

CP/AP 分類方案還有幾個進一步的缺陷 4一致性 被形式化為線性一致性(定理沒有說任何關於較弱一致性模型的內容),可用性 的形式化 30 與該術語的通常含義不匹配 38。許多高可用(容錯)系統實際上不符合 CAP 對可用性的特殊定義。此外,一些系統設計者選擇(有充分理由)既不提供線性一致性也不提供 CAP 定理假設的可用性形式,因此這些系統既不是 CP 也不是 AP 39 40

總的來說,關於 CAP 有很多誤解和混淆,它並不能幫助我們更好地理解系統,因此最好避免使用 CAP。

正式定義的 CAP 定理 30 範圍非常狹窄:它只考慮一種一致性模型(即線性一致性)和一種故障(網路分割槽,根據 Google 的資料,這是不到 8% 事件的原因 41)。它沒有說任何關於網路延遲、死節點或其他權衡的內容。因此,儘管 CAP 在歷史上具有影響力,但對於設計系統幾乎沒有實際價值 4 38

已經有努力推廣 CAP。例如,PACELC 原則 觀察到系統設計者也可能選擇在網路正常工作時削弱一致性以減少延遲 39 40 42。因此,在網路分割槽(P)期間,我們需要在可用性(A)和一致性(C)之間進行選擇;否則(E),當沒有分割槽時,我們可能在低延遲(L)和一致性(C)之間進行選擇。然而,這個定義繼承了 CAP 的幾個問題,例如一致性和可用性的反直覺定義。

分散式系統中有許多更有趣的不可能性結果 43,CAP 現在已被更精確的結果所取代 44 45,因此它今天主要具有歷史意義。

線性一致性與網路延遲

儘管線性一致性是一個有用的保證,但令人驚訝的是,實際上很少有系統是線性一致的。例如,即使現代多核 CPU 上的 RAM 也不是線性一致的 46:如果在一個 CPU 核心上執行的執行緒寫入記憶體地址,而另一個 CPU 核心上的執行緒隨後讀取相同的地址,不能保證讀取第一個執行緒寫入的值(除非使用 記憶體屏障柵欄 47)。

這種行為的原因是每個 CPU 核心都有自己的記憶體快取和儲存緩衝區。預設情況下,記憶體訪問首先進入快取,任何更改都非同步寫出到主記憶體。由於訪問快取中的資料比訪問主記憶體快得多 48,這個特性對於現代 CPU 的良好效能至關重要。然而,現在有多份資料副本(一份在主記憶體中,可能還有幾份在各種快取中),這些副本是非同步更新的,因此線性一致性丟失了。

為什麼要做出這種權衡?使用 CAP 定理來證明多核記憶體一致性模型是沒有意義的:在一臺計算機內,我們通常假設可靠的通訊,我們不期望一個 CPU 核心在與計算機其餘部分斷開連線的情況下能夠繼續正常執行。放棄線性一致性的原因是 效能,而不是容錯 39

許多選擇不提供線性一致保證的分散式資料庫也是如此:它們這樣做主要是為了提高效能,而不是為了容錯 42。線性一致性很慢——這在任何時候都是真的,不僅在網路故障期間。

我們能否找到更高效的線性一致儲存實現?答案似乎是否定的:Attiya 和 Welch 49 證明,如果你想要線性一致性,讀寫請求的響應時間至少與網路中延遲的不確定性成正比。在具有高度可變延遲的網路中,例如大多數計算機網路(見 “超時和無界延遲”),線性一致讀寫的響應時間不可避免地會很高。更快的線性一致性演算法不存在,但較弱的一致性模型可能會快得多,因此這種權衡對於延遲敏感的系統很重要。在 [Link to Come] 中,我們將討論一些在不犧牲正確性的情況下避免線性一致性的方法。

ID 生成器和邏輯時鐘

在許多應用程式中,你需要在建立資料庫記錄時為它們分配某種唯一的 ID,這給了你一個可以引用這些記錄的主鍵。在單節點資料庫中,通常使用自增整數,它的優點是隻需要 64 位(如果你確定永遠不會有超過 40 億條記錄,甚至可以使用 32 位,但這是有風險的)來儲存。

這種自增 ID 的另一個優點是,ID 的順序告訴你記錄建立的順序。例如,圖 10-8 顯示了一個聊天應用程式,它在釋出聊天訊息時為其分配自增 ID。然後,你可以按 ID 遞增的順序顯示訊息,生成的聊天執行緒將有意義:Aaliyah 釋出了一個被分配 ID 1 的問題,而 Bryce 對該問題的回答被分配了一個更大的 ID,即 3。

圖 10-8. 兩個不同的節點可能生成衝突的 ID。

這個單節點 ID 生成器是線性一致系統的另一個例子。每個獲取 ID 的請求都是一個原子地遞增計數器並返回舊計數器值的操作(獲取並增加 操作);線性一致性確保如果 Aaliyah 的訊息釋出在 Bryce 的釋出開始之前完成,那麼 Bryce 的 ID 必須大於 Aaliyah 的。圖 10-8 中 Aaliyah 和 Caleb 的訊息是併發的,因此線性一致性不指定它們的 ID 必須如何排序,只要它們是唯一的。

記憶體中的單節點 ID 生成器很容易實現:你可以使用 CPU 提供的原子遞增指令,它允許多個執行緒安全地遞增同一個計數器。使計數器持久化需要更多的努力,這樣節點就可以崩潰並重新啟動而不重置計數器值,這將導致重複的 ID。但真正的問題是:

  • 單節點 ID 生成器不具容錯性,因為該節點是單點故障。
  • 如果你想在另一個區域建立記錄,速度會很慢,因為你可能必須往返地球的另一端才能獲得 ID。
  • 如果你有高寫入吞吐量,該單個節點可能成為瓶頸。

你可以考慮各種 ID 生成器的替代選項:

分片 ID 分配
你可以有多個分配 ID 的節點——例如,一個只生成偶數,一個只生成奇數。一般來說,你可以在 ID 中保留一些位來包含分片編號。這些 ID 仍然緊湊,但你失去了排序屬性:例如,如果你有 ID 為 16 和 17 的聊天訊息,你不知道訊息 16 是否實際上是先發送的,因為 ID 是由不同的節點分配的,其中一個節點可能領先於另一個。
預分配 ID 塊
不是從單節點 ID 生成器請求單個 ID,它可以分發 ID 塊。例如,節點 A 可能宣告從 1 到 1,000 的 ID 塊,節點 B 可能宣告從 1,001 到 2,000 的塊。然後每個節點可以獨立地從其塊中分發 ID,並在其序列號供應開始不足時從單節點 ID 生成器請求新塊。但是,這種方案也不能確保正確的排序:可能會發生這樣的情況,一條訊息被分配了 1,001 到 2,000 範圍內的 ID,而後來的訊息被分配了 1 到 1,000 範圍內的 ID,如果 ID 是由不同的節點分配的。
隨機 UUID
你可以使用 通用唯一識別符號(UUID),也稱為 全域性唯一識別符號(GUID)。它們的一大優點是可以在任何節點上本地生成,無需通訊,但它們需要更多空間(128 位)。有幾種不同版本的 UUID;最簡單的是版本 4,它本質上是一個如此長的隨機數,以至於兩個節點選擇相同的可能性非常小。不幸的是,這些 ID 的順序也是隨機的,因此比較兩個 ID 不會告訴你哪個更新。
時鐘時間戳使其唯一
如果你的節點的日曆時鐘使用 NTP 保持大致正確,你可以透過將該時鐘的時間戳放在最高有效位中,並用確保 ID 唯一的額外資訊填充剩餘位來生成 ID,即使時間戳不是——例如,分片編號和每分片遞增序列號,或長隨機值。這種方法用於版本 7 UUID 50、Twitter 的 Snowflake 51、ULID 52、Hazelcast 的 Flake ID 生成器、MongoDB ObjectID 和許多類似方案 50。你可以在應用程式程式碼或資料庫中實現這些 ID 生成器 53

所有這些方案都生成唯一的 ID(至少有足夠高的機率,使衝突極其罕見),但它們對 ID 的排序保證比單節點自增方案弱得多。

“為事件排序的時間戳” 中所討論的,時鐘時間戳最多隻能提供近似排序:如果較早的寫入從稍快的時鐘獲得時間戳,而較晚寫入的時間戳來自稍慢的時鐘,則時間戳順序可能與事件實際發生的順序不一致。由於使用非單調時鐘而導致的時鐘跳躍,即使單個節點生成的時間戳也可能排序錯誤。因此,基於時鐘時間的 ID 生成器不太可能是線性一致的。

你可以透過依賴高精度時鐘同步,使用原子鐘或 GPS 接收器來減少這種排序不一致。但如果能夠在不依賴特殊硬體的情況下生成唯一且正確排序的 ID 也會很好。這就是 邏輯時鐘 的用途。

邏輯時鐘

“不可靠的時鐘” 中,我們討論了日曆時鐘和單調時鐘。這兩種都是 物理時鐘:它們測量經過的秒數(或毫秒、微秒等)。

在分散式系統中,通常還使用另一種時鐘,稱為 邏輯時鐘。物理時鐘是計算已經過的秒數的硬體裝置,而邏輯時鐘是計算已發生事件的演算法。來自邏輯時鐘的時間戳因此不會告訴你現在幾點,但你 可以 比較來自邏輯時鐘的兩個時間戳,以判斷哪個更早,哪個更晚。

邏輯時鐘的要求通常是:

  • 其時間戳緊湊(大小為幾個位元組)且唯一;
  • 你可以比較任意兩個時間戳(即它們是 全序 的);並且
  • 時間戳的順序與因果關係 一致:如果操作 A 發生在 B 之前,那麼 A 的時間戳小於 B 的時間戳。(我們之前在 ““先發生"關係和併發” 中討論了因果關係。)

單節點 ID 生成器滿足這些要求,但我們剛剛討論的分散式 ID 生成器不滿足因果排序要求。

Lamport 時間戳

幸運的是,有一種生成邏輯時間戳的簡單方法,它與因果關係 一致,你可以將其用作分散式 ID 生成器。它被稱為 Lamport 時鐘,由 Leslie Lamport 在 1978 年提出 54,現在是分散式系統領域被引用最多的論文之一。

圖 10-9 顯示了 Lamport 時鐘如何在 圖 10-8 的聊天示例中工作。每個節點都有一個唯一識別符號,在 圖 10-9 中是名稱"Aaliyah”、“Bryce"或"Caleb”,但在實踐中可能是隨機 UUID 或類似的東西。此外,每個節點都保留它已處理的運算元的計數器。Lamport 時間戳就是一對(計數器節點 ID)。兩個節點有時可能具有相同的計數器值,但透過在時間戳中包含節點 ID,每個時間戳都是唯一的。

圖 10-9. Lamport 時間戳提供與因果關係一致的全序。

每次節點生成時間戳時,它都會遞增其計數器值並使用新值。此外,每次節點看到來自另一個節點的時間戳時,如果該時間戳中的計數器值大於其本地計數器值,它會將其本地計數器增加到與時間戳中的值匹配。

圖 10-9 中,Aaliyah 在釋出自己的訊息時還沒有看到 Caleb 的訊息,反之亦然。假設兩個使用者都以初始計數器值 0 開始,因此都遞增其本地計數器並將新計數器值 1 附加到其訊息。當 Bryce 收到這些訊息時,他將本地計數器值增加到 1。最後,Bryce 向 Aaliyah 的訊息傳送回覆,為此他遞增本地計數器並將新值 2 附加到訊息。

要比較兩個 Lamport 時間戳,我們首先比較它們的計數器值:例如,(2, “Bryce”) 大於 (1, “Aaliyah”),也大於 (1, “Caleb”)。如果兩個時間戳具有相同的計數器,我們改為比較它們的節點 ID,使用通常的字典序字串比較。因此,此示例中的時間戳順序是 (1, “Aaliyah”) < (1, “Caleb”) < (2, “Bryce”)。

混合邏輯時鐘

Lamport 時間戳擅長捕獲事物發生的順序,但它們有一些限制:

  • 由於它們與物理時間沒有直接關係,你不能使用它們來查詢,比如說,在特定日期釋出的所有訊息——你需要單獨儲存物理時間。
  • 如果兩個節點從不通訊,一個節點的計數器遞增將永遠不會反映在另一個節點的計數器中。因此,可能會發生這樣的情況,即在不同節點上大約同一時間生成的事件具有極不相同的計數器值。

混合邏輯時鐘 結合了物理日曆時鐘的優勢和 Lamport 時鐘的排序保證 55。像物理時鐘一樣,它計算秒或微秒。像 Lamport 時鐘一樣,當一個節點看到來自另一個節點的時間戳大於其本地時鐘值時,它將自己的本地值向前移動以匹配另一個節點的時間戳。因此,如果一個節點的時鐘執行得很快,其他節點在通訊時也會類似地向前移動它們的時鐘。

每次生成混合邏輯時鐘的時間戳時,它也會遞增,這確保時鐘單調向前移動,即使底層物理時鐘由於 NTP 調整而向後跳躍。因此,混合邏輯時鐘可能略微領先於底層物理時鐘。演算法的細節確保這種差異儘可能小。

因此,你可以將混合邏輯時鐘的時間戳幾乎像傳統日曆時鐘的時間戳一樣對待,具有其排序與先發生關係一致的附加屬性。它不依賴於任何特殊硬體,只需要大致同步的時鐘。例如,CockroachDB 使用混合邏輯時鐘。

Lamport/混合邏輯時鐘 vs. 向量時鐘

“多版本併發控制(MVCC)” 中,我們討論了快照隔離通常是如何實現的:本質上,透過給每個事務一個事務 ID,並允許每個事務看到由 ID 較低的事務進行的寫入,但使 ID 較高的事務的寫入不可見。Lamport 時鐘和混合邏輯時鐘是生成這些事務 ID 的好方法,因為它們確保快照與因果關係一致 56

當併發生成多個時間戳時,這些演算法會任意排序它們。這意味著當你檢視兩個時間戳時,你通常無法判斷它們是併發生成的還是一個發生在另一個之前。(在 圖 10-9 的示例中,你實際上可以判斷 Aaliyah 和 Caleb 的訊息必須是併發的,因為它們具有相同的計數器值,但當計數器值不同時,你無法判斷它們是否併發。)

如果你想能夠確定記錄何時併發建立,你需要不同的演算法,例如 向量時鐘。缺點是向量時鐘的時間戳要大得多——可能是系統中每個節點一個整數。有關檢測併發的更多詳細資訊,請參見 “檢測併發寫入”

線性一致的 ID 生成器

儘管 Lamport 時鐘和混合邏輯時鐘提供了有用的排序保證,但該排序仍然弱於我們之前討論的線性一致單節點 ID 生成器。回想一下,線性一致性要求如果請求 A 在請求 B 開始之前完成,那麼 B 必須具有更高的 ID,即使 A 和 B 從未相互通訊。另一方面,Lamport 時鐘只能確保節點生成的時間戳大於該節點看到的任何其他時間戳,但它不能對它沒有看到的時間戳說任何話。

圖 10-10 顯示了非線性一致 ID 生成器如何導致問題。想象一個社交媒體網站,使用者 A 想要與朋友私下分享一張尷尬的照片。A 的賬戶最初是公開的,但使用他們的筆記型電腦,A 首先將他們的賬戶設定更改為私密。然後 A 使用他們的手機上傳照片。由於 A 按順序執行了這些更新,他們可能合理地期望照片上傳受到新的、受限的賬戶許可權的約束。

圖 10-10. 使用 Lamport 時間戳的許可權系統示例。

賬戶許可權和照片儲存在兩個單獨的資料庫(或同一資料庫的單獨分片)中,讓我們假設它們使用 Lamport 時鐘或混合邏輯時鐘為每次寫入分配時間戳。由於照片資料庫沒有從賬戶資料庫讀取,照片資料庫中的本地計數器可能稍微落後,因此照片上傳被分配了比賬戶設定更新更低的時間戳。

接下來,假設一個檢視者(不是 A 的朋友)正在檢視 A 的個人資料,他們的讀取使用快照隔離的 MVCC 實現。可能會發生這樣的情況,檢視者的讀取具有大於照片上傳的時間戳,但小於賬戶設定更新的時間戳。因此,系統將確定在讀取時賬戶仍然是公開的,因此向檢視者顯示他們不應該看到的尷尬照片。

你可以想象幾種可能的方法來解決這個問題。也許照片資料庫應該在執行寫入之前讀取使用者的賬戶狀態,但很容易忘記這樣的檢查。如果 A 的操作是在同一裝置上執行的,也許該裝置上的應用程式可以跟蹤該使用者寫入的最新時間戳——但如果使用者使用筆記型電腦和手機,如示例中所示,那就不那麼容易了。

在這種情況下,最簡單的解決方案是使用線性一致的 ID 生成器,這將確保照片上傳被分配比賬戶許可權更改更大的 ID。

實現線性一致的 ID 生成器

確保 ID 分配線性一致的最簡單方法實際上是為此目的使用單個節點。該節點只需要原子地遞增計數器並在請求時返回其值,持久化計數器值(以便在節點崩潰並重新啟動時不會生成重複的 ID),並使用單主複製進行容錯複製。這種方法在實踐中使用:例如,TiDB/TiKV 稱之為 時間戳預言機,受 Google 的 Percolator 57 啟發。

作為最佳化,你可以避免在每個請求上執行磁碟寫入和複製。相反,ID 生成器可以寫入描述一批 ID 的記錄;一旦該記錄被持久化和複製,節點就可以開始按順序向客戶端分發這些 ID。在它用完該批次中的 ID 之前,它可以為下一批持久化和複製記錄。這樣,如果節點崩潰並重新啟動或你故障轉移到從節點,某些 ID 將被跳過,但你不會發出任何重複或亂序的 ID。

你不能輕易地對 ID 生成器進行分片,因為如果你有多個分片獨立分發 ID,你就無法再保證它們的順序是線性一致的。你也不能輕易地將 ID 生成器分佈在多個區域;因此,在地理分散式資料庫中,所有 ID 請求都必須轉到單個區域的節點。從好的方面來說,ID 生成器的工作非常簡單,因此單個節點可以處理大量請求吞吐量。

如果你不想使用單節點 ID 生成器,可以使用替代方案:你可以做 Google 的 Spanner 所做的,如 “全域性快照的同步時鐘” 中所討論的。它依賴於物理時鐘,該時鐘不僅返回單個時間戳,還返回表示時鐘讀數不確定性的時間戳範圍。然後它等待該不確定性間隔的持續時間過去後再返回。

假設不確定性間隔是正確的(即真實的當前物理時間始終位於該間隔內),此過程還確保如果一個請求在另一個請求開始之前完成,後一個請求將具有更大的時間戳。這種方法確保了這種線性一致的 ID 分配,而無需任何通訊:即使不同區域的請求也將被正確排序,無需等待跨區域請求。缺點是你需要硬體和軟體支援,以使時鐘緊密同步並計算必要的不確定性間隔。

使用邏輯時鐘強制約束

“約束與唯一性保證” 中,我們看到線性一致的比較並設定操作可用於在分散式系統中實現鎖、唯一性約束和類似構造。這提出了一個問題:邏輯時鐘或線性一致的 ID 生成器是否也足以實現這些東西?

答案是:不完全。當你有幾個節點都試圖獲取同一個鎖或註冊同一個使用者名稱時,你可以使用邏輯時鐘為這些請求分配時間戳,並選擇具有最低時間戳的請求作為獲勝者。如果時鐘是線性一致的,你知道任何未來的請求都將始終生成更大的時間戳,因此你可以確定沒有未來的請求會收到比獲勝者更低的時間戳。

不幸的是,問題的一部分仍未解決:節點如何知道自己的時間戳是否最低?要確定,它需要聽到可能生成時間戳的 每個 其他節點 54。如果其他節點之一在此期間失敗,或者由於網路問題無法訪問,該系統將停止執行,因為我們無法確定該節點是否可能具有最低的時間戳。這不是我們需要的那種容錯系統。

要以容錯方式實現鎖、租約和類似構造,我們需要比邏輯時鐘或 ID 生成器更強大的東西:我們需要共識。

共識

在本章中,我們已經看到了幾個只有單個節點時很容易,但如果你想要容錯就會變得困難得多的例子:

  • 如果你只有一個主節點,並且在該主節點上進行所有讀寫,資料庫可以是線性一致的。但是,如果該主節點失敗,如何進行故障切換,同時避免腦裂?如何確保一個認為自己是主節點的節點實際上沒有被投票罷免?
  • 單節點上的線性一致 ID 生成器只是一個帶有原子獲取並增加指令的計數器,但如果它崩潰了怎麼辦?
  • 原子比較並設定(CAS)操作對許多事情都很有用,例如當多個程序競相獲取它時決定誰獲得鎖或租約,或確保具有給定名稱的檔案或使用者的唯一性。在單個節點上,CAS 可能就像一條 CPU 指令一樣簡單,但如何使其容錯?

事實證明,所有這些都是同一個基本分散式系統問題的例項:共識。共識是分散式計算中最重要和最基本的問題之一;它也是出了名的難以正確實現 58 59,許多系統在過去都出錯了。現在我們已經討論了複製(第六章)、事務(第八章)、系統模型(第九章)和線性一致性(本章),我們終於準備好解決共識問題了。

最著名的共識演算法是 Viewstamped Replication 60 61、Paxos 58 62 63 64、Raft 23 65 66 和 Zab 18 22 67。這些演算法之間有相當多的相似之處,但它們並不相同 68 69。這些演算法在非拜占庭系統模型中工作:也就是說,網路通訊可能會被任意延遲或丟棄,節點可能會崩潰、重啟和斷開連線,但演算法假設節點在其他方面正確遵循協議,不會惡意行為。

也有可以容忍某些拜占庭節點的共識演算法,即不正確遵循協議的節點(例如,向其他節點發送矛盾訊息)。一個常見的假設是少於三分之一的節點是拜占庭故障的 26 70。這種 拜占庭容錯(BFT)共識演算法用於區塊鏈 71。然而,如 “拜占庭故障” 中所解釋的,BFT 演算法超出了本書的範圍。


共識的不可能性

你可能聽說過 FLP 結果 72——以作者 Fischer、Lynch 和 Paterson 的名字命名——它證明如果存在節點可能崩潰的風險,就沒有演算法總是能夠達成共識。在分散式系統中,我們必須假設節點可能會崩潰,因此可靠的共識是不可能的。然而,在這裡我們正在討論實現共識的演算法。這是怎麼回事?

首先,FLP 並不是說我們永遠無法達成共識——它只是說我們不能保證共識演算法 總是 終止。此外,FLP 結果是在非同步系統模型中假設確定性演算法的情況下證明的(見 “系統模型與現實”),這意味著演算法不能使用任何時鐘或超時。如果它可以使用超時來懷疑另一個節點可能已經崩潰(即使懷疑有時是錯誤的),那麼共識就變得可解 73。即使只是允許演算法使用隨機數也足以繞過不可能性結果 74

因此,儘管 FLP 關於共識不可能性的結果具有重要的理論意義,但分散式系統通常可以在實踐中實現共識。


共識的多面性

共識可以用幾種不同的方式表達:

  • 單值共識 非常類似於原子 比較並設定 操作,它可用於實現鎖、租約和唯一性約束。
  • 構建 僅追加日誌 也需要共識;它通常形式化為 全序廣播。有了日誌,你可以構建 狀態機複製、基於主節點的複製、事件溯源和其他有用的東西。
  • 多資料庫或多分片事務的 原子提交 要求所有參與者就是否提交或中止事務達成一致。

我們很快就會探討所有這些。事實上,這些問題都是相互等價的:如果你有解決其中一個問題的演算法,你可以將其轉換為任何其他問題的解決方案。這是一個相當深刻且也許令人驚訝的見解!這就是為什麼我們可以將所有這些東西歸入"共識"之下,即使它們表面上看起來完全不同。

單值共識

共識的標準表述涉及讓多個節點就單個值達成一致。例如:

  • 當具有單主複製的資料庫首次啟動時,或者當現有主節點失敗時,多個節點可能會同時嘗試成為主節點。同樣,多個節點可能競相獲取鎖或租約。共識允許它們決定哪一個獲勝。
  • 如果幾個人同時嘗試預訂飛機上的最後一個座位,或劇院中的同一個座位,或嘗試使用相同的使用者名稱註冊賬戶,那麼共識演算法可以確定哪一個應該成功。

更一般地說,一個或多個節點可能 提議 值,共識演算法 決定 其中一個值。在上述示例中,每個節點可以提議自己的 ID,演算法決定哪個節點 ID 應該成為新的主節點、租約的持有者或飛機/劇院座位的購買者。在這種形式主義中,共識演算法必須滿足以下屬性 26

一致同意
沒有兩個節點決定不同。
完整性
一旦節點決定了一個值,它就不能透過決定另一個值來改變主意。
有效性
如果節點決定值 v,那麼 v 是由某個節點提議的。
終止
每個未崩潰的節點最終都會決定某個值。

如果你想決定多個值,你可以為每個值執行共識演算法的單獨例項。例如,你可以為劇院中的每個可預訂座位進行單獨的共識執行,這樣你就可以為每個座位獲得一個決定(一個買家)。

一致同意和完整性屬性定義了共識的核心思想:每個人都決定相同的結果,一旦你決定了,你就不能改變主意。有效性屬性排除了瑣碎的解決方案:例如,你可以有一個總是決定 null 的演算法,無論提議什麼;這個演算法將滿足同意和完整性屬性,但不滿足有效性屬性。

如果你不關心容錯,那麼滿足前三個屬性很容易:你可以硬編碼一個節點作為"獨裁者",讓該節點做出所有決定。然而,如果那個節點失敗,那麼系統就無法再做出任何決定——就像沒有故障切換的單主複製一樣。所有的困難都來自對容錯的需求。

終止屬性形式化了容錯的想法。它本質上是說共識演算法不能簡單地坐著什麼都不做——換句話說,它必須取得進展。即使某些節點失敗,其他節點仍必須達成決定。(終止是活性屬性,而其他三個是安全屬性——見 “安全性和活性”。)

如果崩潰的節點可能恢復,你可以等待它回來。然而,共識必須確保即使崩潰的節點突然消失並且永遠不會回來,它也會做出決定。(不要想象軟體崩潰,而是想象有地震,包含你的節點的資料中心被山體滑坡摧毀。你必須假設你的節點被埋在 30 英尺的泥土下,永遠不會重新上線。)

當然,如果 所有 節點都崩潰了,並且沒有一個在執行,那麼任何演算法都不可能決定任何事情。演算法可以容忍的故障數量是有限的:事實上,可以證明任何共識演算法都需要至少大多數節點正常執行才能確保終止 73。該多數可以安全地形成仲裁(見 “讀寫仲裁”)。

因此,終止屬性受到少於一半節點崩潰或不可達的假設的約束。然而,大多數共識演算法確保安全屬性——同意、完整性和有效性——始終得到滿足,即使大多數節點失敗或存在嚴重的網路問題 75。因此,大規模中斷可能會阻止系統處理請求,但它不能透過導致做出不一致的決定來破壞共識系統。

比較並設定作為共識

比較並設定(CAS)操作檢查某個物件的當前值是否等於某個期望值;如果是,它原子地將物件更新為某個新值;如果不是,它保持物件不變並返回錯誤。

如果你有容錯、線性一致的 CAS 操作,很容易解決共識問題:最初將物件設定為空值;每個想要提議值的節點都使用期望值為空、新值為它想要提議的值(假設它是非空的)呼叫 CAS。然後決定的值就是物件設定的任何值。

同樣,如果你有共識的解決方案,你可以實現 CAS:每當一個或多個節點想要使用相同的期望值執行 CAS 時,你使用共識協議提議 CAS 呼叫中的新值,然後將物件設定為共識決定的任何值。任何新值未被決定的 CAS 呼叫都返回錯誤。具有不同期望值的 CAS 呼叫使用共識協議的單獨執行。

這表明 CAS 和共識彼此等價 28 73。同樣,兩者在單個節點上都很簡單,但要使其容錯則具有挑戰性。作為分散式環境中 CAS 的示例,我們在 “由物件儲存支援的資料庫” 中看到了物件儲存的條件寫入操作,它允許寫入僅在自當前客戶端上次讀取以來具有相同名稱的物件未被另一個客戶端建立或修改時發生。

然而,線性一致的讀寫暫存器不足以解決共識。FLP 結果告訴我們,共識不能由非同步崩潰停止模型中的確定性演算法解決 72,但我們在 “線性一致性與仲裁” 中看到,線性一致的暫存器可以使用此模型中的仲裁讀/寫來實現 24 25 26。由此可見,線性一致的暫存器無法解決共識。

共享日誌作為共識

我們已經看到了幾個日誌的例子,例如複製日誌、事務日誌和預寫日誌。日誌儲存一系列 日誌條目,任何讀取它的人都會看到相同順序的相同條目。有時日誌有一個允許追加新條目的單個寫入者,但 共享日誌 是多個節點可以請求追加條目的日誌。單主複製的一個例子:任何客戶端都可以要求主節點進行寫入,主節點將其追加到複製日誌,然後所有從節點按照與主節點相同的順序應用寫入。

更正式地說,共享日誌支援兩種操作:你可以請求將值新增到日誌中,並且可以讀取日誌中的條目。它必須滿足以下屬性:

最終追加
如果節點請求將某個值新增到日誌中,並且節點不會崩潰,那麼該節點最終必須在日誌條目中讀取該值。
可靠交付
沒有日誌條目丟失:如果一個節點讀取某個日誌條目,那麼最終每個未崩潰的節點也必須讀取該日誌條目。
僅追加
一旦節點讀取了某個日誌條目,它就是不可變的,新的日誌條目只能在它之後新增,而不能在之前。節點可能會重新讀取日誌,在這種情況下,它會以與最初讀取它們時相同的順序看到相同的日誌條目(即使節點崩潰並重新啟動)。
一致性
如果兩個節點都讀取某個日誌條目 e,那麼在 e 之前,它們必須以相同的順序讀取完全相同的日誌條目序列。
有效性
如果節點讀取包含某個值的日誌條目,那麼某個節點先前請求將該值新增到日誌中。

Note

共享日誌在形式上被稱為 全序廣播原子廣播全序組播 協議 26 76 77。這是用不同的詞描述的同一件事:請求將值新增到日誌中然後稱為"廣播"它,讀取日誌條目稱為"交付"它。


如果你有共享日誌的實現,很容易解決共識問題:每個想要提議值的節點都請求將其新增到日誌中,第一個日誌條目中讀回的任何值就是決定的值。由於所有節點以相同的順序讀取日誌條目,它們保證就首先交付哪個值達成一致 28

相反,如果你有共識的解決方案,你可以實現共享日誌。細節有點複雜,但基本思想是這樣的 73

  1. 你為每個未來的日誌條目在日誌中都有一個槽,並且你為每個這樣的槽執行共識演算法的單獨例項,以決定該條目中應該包含什麼值。
  2. 當節點想要向日志新增值時,它為尚未決定的槽之一提議該值。
  3. 當共識演算法為其中一個槽做出決定,並且所有先前的槽都已經決定時,則決定的值作為新的日誌條目追加,並且已經決定的任何連續槽也將其決定的值追加到日誌中。
  4. 如果提議的值未被某個槽選擇,想要新增它的節點會透過為稍後的槽提議它來重試。

這表明共識等價於全序廣播和共享日誌。沒有故障切換的單主複製不滿足活性要求,因為如果主節點崩潰,它將停止傳遞訊息。像往常一樣,挑戰在於安全地自動執行故障切換。

獲取並增加作為共識

我們在 “線性一致的 ID 生成器” 中看到的線性一致 ID 生成器接近解決共識,但略有不足。我們可以使用獲取並增加操作實現這樣的 ID 生成器,該操作原子地遞增計數器並返回舊的計數器值。

如果你有 CAS 操作,很容易實現獲取並增加:首先讀取計數器值,然後執行 CAS,其中期望值是你讀取的值,新值是該值加一。如果 CAS 失敗,你將重試整個過程,直到 CAS 成功。當存在爭用時,這比本機獲取並增加操作效率低,但在功能上是等效的。由於你可以使用共識實現 CAS,你也可以使用共識實現獲取並增加。

相反,如果你有容錯的獲取並增加操作,你能解決共識問題嗎?假設你將計數器初始化為零,每個想要提議值的節點都呼叫獲取並增加操作來遞增計數器。由於獲取並增加操作是原子的,其中一個節點將讀取初始值零,其他節點都將讀取至少遞增過一次的值。

現在假設讀取零的節點是獲勝者,它的值被決定。這對於讀取零的節點有效,但其他節點有問題:它們知道自己不是獲勝者,但它們不知道其他節點中哪一個獲勝了。獲勝者可以向其他節點發送訊息,讓它們知道它已經獲勝,但如果獲勝者在有機會發送此訊息之前崩潰了怎麼辦?在這種情況下,其他節點將被掛起,無法決定任何值,因此共識不會終止。其他節點不能回退到另一個節點,因為讀取零的節點可能會回來並正確地決定它提議的值。

一個例外是,如果我們確定不超過兩個節點將提議值。在這種情況下,節點可以相互發送它們想要提議的值,然後每個都執行獲取並增加操作。讀取零的節點決定自己的值,讀取一的節點決定另一個節點的值。這解決了兩個節點之間的共識問題,這就是為什麼我們可以說獲取並增加的 共識數 為二 28。相比之下,CAS 和共享日誌解決了任意數量節點可能提議值的共識,因此它們的共識數為 ∞(無窮大)。

原子提交作為共識

“分散式事務” 中,我們看到了 原子提交 問題,即確保參與分散式事務的資料庫或分片都提交或中止事務。我們還看到了 兩階段提交 演算法,它依賴於作為單點故障的協調器。

共識和原子提交之間有什麼關係?乍一看,它們似乎非常相似——兩者都需要節點達成某種形式的一致。然而,有一個重要的區別:對於共識,可以決定提議的任何值,而對於原子提交,如果 任何 參與者投票中止,演算法 必須 中止。更準確地說,原子提交需要以下屬性 78

一致同意
沒有兩個節點決定不同的結果。
完整性
一旦節點決定了一個結果,它就不能透過決定另一個結果來改變主意。
有效性
如果節點決定提交,那麼所有節點必須先前投票提交。如果任何節點投票中止,節點必須中止。
非平凡性
如果所有節點都投票提交,並且沒有發生通訊超時,那麼所有節點必須決定提交。
終止
每個未崩潰的節點最終都會決定提交或中止。

有效性屬性確保事務只有在所有節點都同意時才能提交;非平凡性屬性確保演算法不能簡單地總是中止(但如果任何節點之間的通訊超時,它允許中止)。其他三個屬性基本上與共識相同。

如果你有共識的解決方案,有多種方法可以解決原子提交 78 79。一種方法是這樣的:當你想要提交事務時,每個節點將其提交或中止的投票傳送給每個其他節點。從自己和每個其他節點收到提交投票的節點使用共識演算法提議"提交";收到中止投票或經歷超時的節點使用共識演算法提議"中止"。當節點發現共識演算法決定了什麼時,它會相應地提交或中止。

在這個演算法中,只有當所有節點都投票提交時,才會提議"提交"。如果任何節點投票中止,所有共識演算法中的提議都將是"中止"。如果所有節點都投票提交但某些通訊超時,可能會發生某些節點提議"中止"而其他節點提議"提交";在這種情況下,節點是提交還是中止並不重要,只要它們都做同樣的事。

如果你有容錯的原子提交協議,你也可以解決共識。每個想要提議值的節點都在節點仲裁上啟動事務,並在每個節點上執行單節點 CAS,如果其值尚未被另一個事務設定,則將暫存器設定為提議的值。如果 CAS 成功,節點投票提交,否則投票中止。如果原子提交協議決定提交事務,其值將被決定用於共識;如果原子提交中止,提議節點將使用新事務重試。

這表明原子提交和共識也是彼此等價的。

共識的實踐

我們已經看到,單值共識、CAS、共享日誌和原子提交都彼此等價:你可以將其中一個的解決方案轉換為任何其他的解決方案。這是一個有價值的理論見解,但它沒有回答這個問題:在實踐中,這些許多共識表述中哪一個最有用?

答案是大多數共識系統提供共享日誌,也稱為全序廣播。Raft、Viewstamped Replication 和 Zab 直接提供共享日誌。Paxos 提供單值共識,但在實踐中,大多數使用 Paxos 的系統實際上使用稱為 Multi-Paxos 的擴充套件,它也提供共享日誌。

使用共享日誌

共享日誌非常適合資料庫複製:如果每個日誌條目代表對資料庫的寫入,並且每個副本使用確定性邏輯以相同的順序處理相同的寫入,那麼副本將全部處於一致狀態。這個想法被稱為 狀態機複製 80,它是事件溯源背後的原則,我們在 “事件溯源和 CQRS” 中看到了。共享日誌對於流處理也很有用,我們將在 [Link to Come] 中看到。

同樣,共享日誌可用於實現可序列化事務:如 “實際序列執行” 中所討論的,如果每個日誌條目代表要作為儲存過程執行的確定性事務,並且如果每個節點以相同的順序執行這些事務,那麼事務將是可序列化的 81 82


Note

具有強一致性模型的分片資料庫通常為每個分片維護一個單獨的日誌,這提高了可伸縮性,但限制了它們可以跨分片提供的一致性保證(例如,一致快照、外部索引鍵引用)。跨分片的可序列化事務是可能的,但需要額外的協調 83


共享日誌也很強大,因為它可以很容易地適應其他形式的共識:

  • 我們之前看到了如何使用它來實現單值共識和 CAS:只需決定日誌中首先出現的值。
  • 如果你想要許多單值共識例項(例如,幾個人試圖預訂的劇院中每個座位一個),請在日誌條目中包含座位編號,並決定包含給定座位編號的第一個日誌條目。
  • 如果你想要原子獲取並增加,請將要新增到計數器的數字放入日誌條目中,當前計數器值是到目前為止所有日誌條目的總和。日誌條目上的簡單計數器可用於生成柵欄令牌(見 “柵欄化殭屍和延遲請求”);例如,在 ZooKeeper 中,此序列號稱為 zxid 18

從單主複製到共識

我們之前看到,如果你有一個單一的"獨裁者"節點做出決定,單值共識很容易,同樣,如果單個主節點是唯一允許向其追加條目的節點,共享日誌也很容易。問題是如果該節點失敗如何提供容錯。

傳統上,具有單主複製的資料庫沒有解決這個問題:它們將主節點故障切換作為人類管理員必須手動執行的操作。不幸的是,這意味著大量的停機時間,因為人類反應的速度是有限的,並且它不滿足共識的終止屬性。對於共識,我們要求演算法可以自動選擇新的主節點。(並非所有共識演算法都有主節點,但常用的演算法有 84。)

然而,有一個問題。我們之前討論過腦裂的問題,並說所有節點都需要就誰是主節點達成一致——否則兩個不同的節點可能各自認為自己是主節點,從而做出不一致的決定。因此,似乎我們需要共識來選舉主節點,而我們需要主節點來解決共識。我們如何擺脫這個難題?

事實上,共識演算法不要求在任何時候只有一個主節點。相反,它們做出了較弱的保證:它們定義了一個 紀元編號(在 Paxos 中稱為 投票編號,在 Viewstamped Replication 中稱為 檢視編號,在 Raft 中稱為 任期編號)並保證在每個紀元內,主節點是唯一的。

當節點因為在某個超時時間內沒有收到主節點的訊息而認為當前主節點已死時,它可能會開始投票選舉新的主節點。這次選舉被賦予一個大於任何先前紀元的新紀元編號。如果兩個不同紀元中的兩個不同主節點之間存在衝突(也許是因為先前的主節點實際上並沒有死),那麼具有更高紀元編號的主節點獲勝。

在主節點被允許將下一個條目追加到共享日誌之前,它必須首先檢查是否有其他具有更高紀元編號的主節點可能追加不同的條目。它可以透過從節點仲裁收集投票來做到這一點——通常但不總是大多數節點 85。只有在節點不知道任何其他具有更高紀元的主節點時,節點才會投贊成票。

因此,我們有兩輪投票:一次選擇主節點,第二次對主節點提議的下一個要追加到日誌的條目進行投票。這兩次投票的仲裁必須重疊:如果對提議的投票成功,投票支援它的節點中至少有一個也必須參與了最近成功的主節點選舉 85。因此,如果對提議的投票透過而沒有透露任何更高編號的紀元,當前主節點可以得出結論,沒有選出具有更高紀元編號的主節點,因此它可以安全地將提議的條目追加到日誌中 26 86

這兩輪投票表面上看起來類似於兩階段提交,但它們是非常不同的協議。在共識演算法中,任何節點都可以開始選舉,它只需要節點仲裁的響應;在 2PC 中,只有協調器可以請求投票,它需要 每個 參與者的"是"投票才能提交。

共識的微妙之處

這個基本結構對於 Raft、Multi-Paxos、Zab 和 Viewstamped Replication 的所有都是通用的:節點仲裁的投票選舉主節點,然後主節點想要追加到日誌的每個條目都需要另一個仲裁投票 68 69。每個新的日誌條目在確認給請求寫入的客戶端之前都會同步複製到節點仲裁。這確保如果當前主節點失敗,日誌條目不會丟失。

然而,魔鬼在細節中,這也是這些演算法採用不同方法的地方。例如,當舊主節點失敗並選出新主節點時,演算法需要確保新主節點遵守舊主節點在失敗之前已經追加的任何日誌條目。Raft 透過只允許其日誌至少與其大多數追隨者一樣最新的節點成為新主節點來做到這一點 69。相比之下,Paxos 允許任何節點成為新主節點,但要求它在開始追加自己的新條目之前使其日誌與其他節點保持最新。


主節點選舉中的一致性與可用性

如果你希望共識演算法嚴格保證 “共享日誌作為共識” 中列出的屬性,那麼新主節點在處理任何寫入或線性一致讀取之前必須瞭解任何已確認的日誌條目,這一點至關重要。如果具有過時資料的節點成為新主節點,它可能會將新值寫入已經由舊主節點寫入的日誌條目,從而違反共享日誌的僅追加屬性。

在某些情況下,你可能選擇削弱共識屬性,以便更快地從主節點故障中恢復。例如,Kafka 提供了啟用 不乾淨的主節點選舉 的選項,它允許任何副本成為主節點,即使它不是最新的。此外,在具有非同步複製的資料庫中,當主節點失敗時,你無法保證任何從節點是最新的。

如果你放棄新主節點必須是最新的要求,你可能會提高效能和可用性,但你是在薄冰上,因為共識理論不再適用。雖然只要沒有故障,事情就會正常工作,但 第九章 中討論的問題很容易導致大量資料丟失或損壞。


另一個微妙之處是如何處理演算法處理舊主節點在失敗之前提議的日誌條目,但對於追加到日誌的投票尚未完成。你可以在本章的參考文獻中找到這些細節的討論 23 69 86

對於使用共識演算法進行復制的資料庫,不僅寫入需要轉換為日誌條目並複製到仲裁。如果你想保證線性一致的讀取,它們也必須像寫入一樣透過仲裁投票,以確認認為自己是主節點的節點確實仍然是最新的。例如,etcd 中的線性一致讀取就是這樣工作的。

在其標準形式中,大多數共識演算法假設一組固定的節點——也就是說,節點可能會宕機並重新啟動,但允許投票的節點集在建立叢集時是固定的。在實踐中,通常需要在系統配置中新增新節點或刪除舊節點。共識演算法已經擴充套件了 重新配置 功能,使這成為可能。這在向系統新增新區域或從一個位置遷移到另一個位置(透過首先新增新節點,然後刪除舊節點)時特別有用。

共識的利弊

儘管它們複雜而微妙,但共識演算法是分散式系統的巨大突破。共識本質上是"正確完成的單主複製",在主節點故障時自動故障切換,確保沒有已提交的資料丟失,也不可能出現腦裂,即使面對我們在 第九章 中討論的所有問題。

由於單主複製與自動故障切換本質上是共識的定義之一,任何提供自動故障切換但不使用經過驗證的共識演算法的系統都可能是不安全的 87。使用經過驗證的共識演算法並不能保證整個系統的正確性——仍然有很多其他地方可能潛伏著錯誤——但這是一個好的開始。

然而,共識並不是到處都使用,因為好處是有代價的。共識系統總是需要嚴格的多數才能執行——容忍一個故障需要三個節點,或者容忍兩個故障需要五個節點。每個操作都需要與仲裁通訊,因此你不能透過新增更多節點來增加吞吐量(事實上,你新增的每個節點都會使演算法變慢)。如果網路分割槽將某些節點與其餘節點隔離,只有網路的多數部分可以取得進展,其餘部分被阻塞。

共識系統通常依賴超時來檢測失敗的節點。在具有高度可變網路延遲的環境中,特別是跨多個地理區域分佈的系統,調整這些超時可能很困難:如果它們太大,從故障中恢復需要很長時間;如果它們太小,可能會有很多不必要的主節點選舉,導致糟糕的效能,因為系統最終花費更多時間選擇主節點而不是做有用的工作。

有時,共識演算法對網路問題特別敏感。例如,Raft 已被證明具有不愉快的邊緣情況 88 89:如果除了一個始終不可靠的特定網路連結之外,整個網路都正常工作,Raft 可能會進入主節點身份在兩個節點之間不斷跳躍的情況,或者當前主節點不斷被迫辭職,因此係統實際上從未取得進展。設計對不可靠網路更穩健的演算法仍然是一個開放的研究問題。

對於想要高可用但不想接受共識成本的系統,唯一真正的選擇是使用較弱的一致性模型,例如 第六章 中討論的無主或多主複製提供的模型。這些方法通常不提供線性一致性,但對於不需要它的應用程式來說這很好。

總結

在本章中,我們研究了容錯系統中強一致性的主題:它是什麼,以及如何實現它。我們深入研究了線性一致性,這是強一致性的一種流行形式化:它意味著複製的資料看起來好像只有一個副本,所有操作都以原子方式作用於它。我們看到,當你需要在讀取時某些資料是最新的,或者需要解決競爭條件(例如,如果多個節點併發地嘗試做同樣的事情,比如建立具有相同名稱的檔案)時,線性一致性是有用的。

雖然線性一致性很有吸引力,因為它易於理解——它使資料庫的行為像單執行緒程式中的變數一樣——但它的缺點是速度慢,特別是在網路延遲較大的環境中。許多複製演算法不能保證線性一致性,即使表面上看起來它們可能提供強一致性。

接下來,我們在 ID 生成器的背景下應用了線性一致性的概念。單節點自增計數器是線性一致的,但不是容錯的。許多分散式 ID 生成方案不能保證 ID 的順序與事件實際發生的順序一致。像 Lamport 時鐘和混合邏輯時鐘這樣的邏輯時鐘提供了與因果關係一致的順序,但沒有線性一致性。

這引導我們進入了共識的概念。我們看到,達成共識意味著以一種所有節點都同意決定的方式決定某事,並且他們不能改變主意。廣泛的問題實際上可以歸約為共識,並且彼此等價(即,如果你有一個問題的解決方案,你可以將其轉換為所有其他問題的解決方案)。這些等價的問題包括:

線性一致的比較並設定操作
暫存器需要根據其當前值是否等於操作中給定的引數,原子地 決定 是否設定其值。
鎖和租約
當多個客戶端併發地嘗試獲取鎖或租約時,鎖 決定 哪一個成功獲取它。
唯一性約束
當多個事務併發地嘗試建立具有相同鍵的衝突記錄時,約束必須 決定 允許哪一個,哪一個應該因約束違反而失敗。
共享日誌
當多個節點併發地想要向日志追加條目時,日誌 決定 它們被追加的順序。全序廣播也是等價的。
原子事務提交
參與分散式事務的資料庫節點必須都以相同的方式 決定 是提交還是中止事務。
線性一致的 fetch-and-add 操作
這個操作可以用來實現 ID 生成器。多個節點可以併發地呼叫該操作,它 決定 它們遞增計數器的順序。這種情況實際上只解決了兩個節點之間的共識,而其他的適用於任意數量的節點。

如果你只有一個節點,或者如果你願意將決策能力分配給單個節點,所有這些都是簡單的。這就是單領導者資料庫中發生的事情:所有的決策權都授予了領導者,這就是為什麼這樣的資料庫能夠提供線性一致的操作、唯一性約束、複製日誌等等。

然而,如果那個單一的領導者失敗,或者如果網路中斷使領導者無法訪問,這樣的系統就無法取得任何進展,直到人工執行手動故障轉移。廣泛使用的共識演算法如 Raft 和 Paxos 本質上是帶有內建自動領導者選舉和故障轉移的單領導者複製(如果當前領導者失敗)。

共識演算法經過精心設計,以確保在故障轉移期間不會丟失任何已提交的寫入,並且系統不會進入腦裂狀態(多個節點接受寫入)。這要求每個寫入和每個線性一致的讀取都由節點的仲裁(通常是多數)確認。這可能是昂貴的,特別是跨地理區域,但如果你想要共識提供的強一致性和容錯性,這是不可避免的。

像 ZooKeeper 和 etcd 這樣的協調服務也是建立在共識演算法之上的。它們提供鎖、租約、故障檢測和變更通知功能,這些功能對於管理分散式應用程式的狀態很有用。如果你發現自己想要做那些可以歸約為共識的事情之一,並且你希望它是容錯的,建議使用協調服務。它不會保證你做對,但它可能會有所幫助。

共識演算法是複雜而微妙的,但它們得到了自 1980 年代以來發展起來的豐富理論體系的支援。這個理論使得構建能夠容忍我們在第 9 章中討論的所有故障的系統成為可能,同時仍然確保你的資料不會損壞。這是一個了不起的成就,本章末尾的參考文獻展示了這項工作的一些亮點。

然而,共識並不總是正確的工具:在某些系統中,不需要它提供的強一致性屬性,使用較弱的一致性以獲得更高的可用性和更好的效能會更好。在這些情況下,通常使用無領導者或多領導者複製,這是我們之前在第 6 章中討論過的。我們在本章中討論的邏輯時鐘在那種情況下是有幫助的。

參考文獻


  1. Maurice P. Herlihy and Jeannette M. Wing. Linearizability: A Correctness Condition for Concurrent Objects. ACM Transactions on Programming Languages and Systems (TOPLAS), volume 12, issue 3, pages 463–492, July 1990. doi:10.1145/78969.78972 ↩︎ ↩︎

  2. Leslie Lamport. On interprocess communication. Distributed Computing, volume 1, issue 2, pages 77–101, June 1986. doi:10.1007/BF01786228 ↩︎

  3. David K. Gifford. Information Storage in a Decentralized Computer System. Xerox Palo Alto Research Centers, CSL-81-8, June 1981. Archived at perma.cc/2XXP-3JPB ↩︎

  4. Martin Kleppmann. Please Stop Calling Databases CP or AP. martin.kleppmann.com, May 2015. Archived at perma.cc/MJ5G-75GL ↩︎ ↩︎ ↩︎

  5. Kyle Kingsbury. Call Me Maybe: MongoDB Stale Reads. aphyr.com, April 2015. Archived at perma.cc/DXB4-J4JC ↩︎

  6. Kyle Kingsbury. Computational Techniques in Knossos. aphyr.com, May 2014. Archived at perma.cc/2X5M-EHTU ↩︎

  7. Kyle Kingsbury and Peter Alvaro. Elle: Inferring Isolation Anomalies from Experimental Observations. Proceedings of the VLDB Endowment, volume 14, issue 3, pages 268–280, November 2020. doi:10.14778/3430915.3430918 ↩︎

  8. Paolo Viotti and Marko Vukolić. Consistency in Non-Transactional Distributed Storage Systems. ACM Computing Surveys (CSUR), volume 49, issue 1, article no. 19, June 2016. doi:10.1145/2926965 ↩︎ ↩︎

  9. Peter Bailis. Linearizability Versus Serializability. bailis.org, September 2014. Archived at perma.cc/386B-KAC3 ↩︎

  10. Daniel Abadi. Correctness Anomalies Under Serializable Isolation. dbmsmusings.blogspot.com, June 2019. Archived at perma.cc/JGS7-BZFY ↩︎

  11. Peter Bailis, Aaron Davidson, Alan Fekete, Ali Ghodsi, Joseph M. Hellerstein, and Ion Stoica. Highly Available Transactions: Virtues and Limitations. Proceedings of the VLDB Endowment, volume 7, issue 3, pages 181–192, November 2013. doi:10.14778/2732232.2732237, extended version published as arXiv:1302.0309 ↩︎

  12. Philip A. Bernstein, Vassos Hadzilacos, and Nathan Goodman. Concurrency Control and Recovery in Database Systems. Addison-Wesley, 1987. ISBN: 978-0-201-10715-9, available online at microsoft.com↩︎

  13. Andrei Matei. CockroachDB’s consistency model. cockroachlabs.com, February 2021. Archived at perma.cc/MR38-883B ↩︎

  14. Murat Demirbas. Strict-serializability, but at what cost, for what purpose? muratbuffalo.blogspot.com, August 2022. Archived at perma.cc/T8AY-N3U9 ↩︎

  15. Ben Darnell. How to talk about consistency and isolation in distributed DBs. cockroachlabs.com, February 2022. Archived at perma.cc/53SV-JBGK ↩︎

  16. Daniel Abadi. An explanation of the difference between Isolation levels vs. Consistency levels. dbmsmusings.blogspot.com, August 2019. Archived at perma.cc/QSF2-CD4P ↩︎

  17. Mike Burrows. The Chubby Lock Service for Loosely-Coupled Distributed Systems. At 7th USENIX Symposium on Operating System Design and Implementation (OSDI), November 2006. ↩︎

  18. Flavio P. Junqueira and Benjamin Reed. ZooKeeper: Distributed Process Coordination. O’Reilly Media, 2013. ISBN: 978-1-449-36130-3 ↩︎ ↩︎ ↩︎ ↩︎

  19. Murali Vallath. Oracle 10g RAC Grid, Services & Clustering. Elsevier Digital Press, 2006. ISBN: 978-1-555-58321-7 ↩︎ ↩︎

  20. Peter Bailis, Alan Fekete, Michael J. Franklin, Ali Ghodsi, Joseph M. Hellerstein, and Ion Stoica. Coordination Avoidance in Database Systems. Proceedings of the VLDB Endowment, volume 8, issue 3, pages 185–196, November 2014. doi:10.14778/2735508.2735509 ↩︎

  21. Kyle Kingsbury. Call Me Maybe: etcd and Consul. aphyr.com, June 2014. Archived at perma.cc/XL7U-378K ↩︎

  22. Flavio P. Junqueira, Benjamin C. Reed, and Marco Serafini. Zab: High-Performance Broadcast for Primary-Backup Systems. At 41st IEEE International Conference on Dependable Systems and Networks (DSN), June 2011. doi:10.1109/DSN.2011.5958223 ↩︎ ↩︎

  23. Diego Ongaro and John K. Ousterhout. In Search of an Understandable Consensus Algorithm. At USENIX Annual Technical Conference (ATC), June 2014. ↩︎ ↩︎ ↩︎

  24. Hagit Attiya, Amotz Bar-Noy, and Danny Dolev. Sharing Memory Robustly in Message-Passing Systems. Journal of the ACM, volume 42, issue 1, pages 124–142, January 1995. doi:10.1145/200836.200869 ↩︎ ↩︎

  25. Nancy Lynch and Alex Shvartsman. Robust Emulation of Shared Memory Using Dynamic Quorum-Acknowledged Broadcasts. At 27th Annual International Symposium on Fault-Tolerant Computing (FTCS), June 1997. doi:10.1109/FTCS.1997.614100 ↩︎ ↩︎

  26. Christian Cachin, Rachid Guerraoui, and Luís Rodrigues. Introduction to Reliable and Secure Distributed Programming, 2nd edition. Springer, 2011. ISBN: 978-3-642-15259-7, doi:10.1007/978-3-642-15260-3 ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  27. Niklas Ekström, Mikhail Panchenko, and Jonathan Ellis. Possible Issue with Read Repair? Email thread on cassandra-dev mailing list, October 2012. ↩︎

  28. Maurice P. Herlihy. Wait-Free Synchronization. ACM Transactions on Programming Languages and Systems (TOPLAS), volume 13, issue 1, pages 124–149, January 1991. doi:10.1145/114005.102808 ↩︎ ↩︎ ↩︎ ↩︎

  29. Armando Fox and Eric A. Brewer. Harvest, Yield, and Scalable Tolerant Systems. At 7th Workshop on Hot Topics in Operating Systems (HotOS), March 1999. doi:10.1109/HOTOS.1999.798396 ↩︎

  30. Seth Gilbert and Nancy Lynch. Brewer’s Conjecture and the Feasibility of Consistent, Available, Partition-Tolerant Web Services. ACM SIGACT News, volume 33, issue 2, pages 51–59, June 2002. doi:10.1145/564585.564601 ↩︎ ↩︎ ↩︎

  31. Seth Gilbert and Nancy Lynch. Perspectives on the CAP Theorem. IEEE Computer Magazine, volume 45, issue 2, pages 30–36, February 2012. doi:10.1109/MC.2011.389 ↩︎

  32. Eric A. Brewer. CAP Twelve Years Later: How the ‘Rules’ Have Changed. IEEE Computer Magazine, volume 45, issue 2, pages 23–29, February 2012. doi:10.1109/MC.2012.37 ↩︎ ↩︎

  33. Susan B. Davidson, Hector Garcia-Molina, and Dale Skeen. Consistency in Partitioned Networks. ACM Computing Surveys, volume 17, issue 3, pages 341–370, September 1985. doi:10.1145/5505.5508 ↩︎

  34. Paul R. Johnson and Robert H. Thomas. RFC 677: The Maintenance of Duplicate Databases. Network Working Group, January 1975. ↩︎

  35. Michael J. Fischer and Alan Michael. Sacrificing Serializability to Attain High Availability of Data in an Unreliable Network. At 1st ACM Symposium on Principles of Database Systems (PODS), March 1982. doi:10.1145/588111.588124 ↩︎

  36. Eric A. Brewer. NoSQL: Past, Present, Future. At QCon San Francisco, November 2012. ↩︎

  37. Adrian Cockcroft. Migrating to Microservices. At QCon London, March 2014. ↩︎

  38. Martin Kleppmann. A Critique of the CAP Theorem. arXiv:1509.05393, September 2015. ↩︎ ↩︎

  39. Daniel Abadi. Problems with CAP, and Yahoo’s little known NoSQL system. dbmsmusings.blogspot.com, April 2010. Archived at perma.cc/4NTZ-CLM9 ↩︎ ↩︎ ↩︎

  40. Daniel Abadi. Hazelcast and the Mythical PA/EC System. dbmsmusings.blogspot.com, October 2017. Archived at perma.cc/J5XM-U5C2 ↩︎ ↩︎

  41. Eric Brewer. Spanner, TrueTime & The CAP Theorem. research.google.com, February 2017. Archived at perma.cc/59UW-RH7N ↩︎

  42. Daniel J. Abadi. Consistency Tradeoffs in Modern Distributed Database System Design. IEEE Computer Magazine, volume 45, issue 2, pages 37–42, February 2012. doi:10.1109/MC.2012.33 ↩︎ ↩︎

  43. Nancy A. Lynch. A Hundred Impossibility Proofs for Distributed Computing. At 8th ACM Symposium on Principles of Distributed Computing (PODC), August 1989. doi:10.1145/72981.72982 ↩︎

  44. Prince Mahajan, Lorenzo Alvisi, and Mike Dahlin. Consistency, Availability, and Convergence. University of Texas at Austin, Department of Computer Science, Tech Report UTCS TR-11-22, May 2011. Archived at perma.cc/SAV8-9JAJ ↩︎

  45. Hagit Attiya, Faith Ellen, and Adam Morrison. Limitations of Highly-Available Eventually-Consistent Data Stores. At ACM Symposium on Principles of Distributed Computing (PODC), July 2015. doi:10.1145/2767386.2767419 ↩︎

  46. Peter Sewell, Susmit Sarkar, Scott Owens, Francesco Zappa Nardelli, and Magnus O. Myreen. x86-TSO: A Rigorous and Usable Programmer’s Model for x86 Multiprocessors. Communications of the ACM, volume 53, issue 7, pages 89–97, July 2010. doi:10.1145/1785414.1785443 ↩︎

  47. Martin Thompson. Memory Barriers/Fences. mechanical-sympathy.blogspot.co.uk, July 2011. Archived at perma.cc/7NXM-GC5U ↩︎

  48. Ulrich Drepper. What Every Programmer Should Know About Memory. akkadia.org, November 2007. Archived at perma.cc/NU6Q-DRXZ ↩︎

  49. Hagit Attiya and Jennifer L. Welch. Sequential Consistency Versus Linearizability. ACM Transactions on Computer Systems (TOCS), volume 12, issue 2, pages 91–122, May 1994. doi:10.1145/176575.176576 ↩︎

  50. Kyzer R. Davis, Brad G. Peabody, and Paul J. Leach. Universally Unique IDentifiers (UUIDs). RFC 9562, IETF, May 2024. ↩︎ ↩︎

  51. Ryan King. Announcing Snowflake. blog.x.com, June 2010. Archived at archive.org ↩︎

  52. Alizain Feerasta. Universally Unique Lexicographically Sortable Identifier. github.com, 2016. Archived at perma.cc/NV2Y-ZP8U ↩︎

  53. Rob Conery. A Better ID Generator for PostgreSQL. bigmachine.io, May 2014. Archived at perma.cc/K7QV-3KFC ↩︎

  54. Leslie Lamport. Time, Clocks, and the Ordering of Events in a Distributed System. Communications of the ACM, volume 21, issue 7, pages 558–565, July 1978. doi:10.1145/359545.359563 ↩︎ ↩︎

  55. Sandeep S. Kulkarni, Murat Demirbas, Deepak Madeppa, Bharadwaj Avva, and Marcelo Leone. Logical Physical Clocks. 18th International Conference on Principles of Distributed Systems (OPODIS), December 2014. doi:10.1007/978-3-319-14472-6_2 ↩︎

  56. Manuel Bravo, Nuno Diegues, Jingna Zeng, Paolo Romano, and Luís Rodrigues. On the use of Clocks to Enforce Consistency in the Cloud. IEEE Data Engineering Bulletin, volume 38, issue 1, pages 18–31, March 2015. Archived at perma.cc/68ZU-45SH ↩︎

  57. Daniel Peng and Frank Dabek. Large-Scale Incremental Processing Using Distributed Transactions and Notifications. At 9th USENIX Conference on Operating Systems Design and Implementation (OSDI), October 2010. ↩︎

  58. Tushar Deepak Chandra, Robert Griesemer, and Joshua Redstone. Paxos Made Live – An Engineering Perspective. At 26th ACM Symposium on Principles of Distributed Computing (PODC), June 2007. doi:10.1145/1281100.1281103 ↩︎ ↩︎

  59. Will Portnoy. Lessons Learned from Implementing Paxos. blog.willportnoy.com, June 2012. Archived at perma.cc/QHD9-FDD2 ↩︎

  60. Brian M. Oki and Barbara H. Liskov. Viewstamped Replication: A New Primary Copy Method to Support Highly-Available Distributed Systems. At 7th ACM Symposium on Principles of Distributed Computing (PODC), August 1988. doi:10.1145/62546.62549 ↩︎

  61. Barbara H. Liskov and James Cowling. Viewstamped Replication Revisited. Massachusetts Institute of Technology, Tech Report MIT-CSAIL-TR-2012-021, July 2012. Archived at perma.cc/56SJ-WENQ ↩︎

  62. Leslie Lamport. The Part-Time Parliament. ACM Transactions on Computer Systems, volume 16, issue 2, pages 133–169, May 1998. doi:10.1145/279227.279229 ↩︎

  63. Leslie Lamport. Paxos Made Simple. ACM SIGACT News, volume 32, issue 4, pages 51–58, December 2001. Archived at perma.cc/82HP-MNKE ↩︎

  64. Robbert van Renesse and Deniz Altinbuken. Paxos Made Moderately Complex. ACM Computing Surveys (CSUR), volume 47, issue 3, article no. 42, February 2015. doi:10.1145/2673577 ↩︎

  65. Diego Ongaro. Consensus: Bridging Theory and Practice. PhD Thesis, Stanford University, August 2014. Archived at perma.cc/5VTZ-2ADH ↩︎

  66. Heidi Howard, Malte Schwarzkopf, Anil Madhavapeddy, and Jon Crowcroft. Raft Refloated: Do We Have Consensus? ACM SIGOPS Operating Systems Review, volume 49, issue 1, pages 12–21, January 2015. doi:10.1145/2723872.2723876 ↩︎

  67. André Medeiros. ZooKeeper’s Atomic Broadcast Protocol: Theory and Practice. Aalto University School of Science, March 2012. Archived at perma.cc/FVL4-JMVA ↩︎

  68. Robbert van Renesse, Nicolas Schiper, and Fred B. Schneider. Vive La Différence: Paxos vs. Viewstamped Replication vs. Zab. IEEE Transactions on Dependable and Secure Computing, volume 12, issue 4, pages 472–484, September 2014. doi:10.1109/TDSC.2014.2355848 ↩︎ ↩︎

  69. Heidi Howard and Richard Mortier. Paxos vs Raft: Have we reached consensus on distributed consensus?. At 7th Workshop on Principles and Practice of Consistency for Distributed Data (PaPoC), April 2020. doi:10.1145/3380787.3393681 ↩︎ ↩︎ ↩︎ ↩︎

  70. Miguel Castro and Barbara H. Liskov. Practical Byzantine Fault Tolerance and Proactive Recovery. ACM Transactions on Computer Systems, volume 20, issue 4, pages 396–461, November 2002. doi:10.1145/571637.571640 ↩︎

  71. Shehar Bano, Alberto Sonnino, Mustafa Al-Bassam, Sarah Azouvi, Patrick McCorry, Sarah Meiklejohn, and George Danezis. SoK: Consensus in the Age of Blockchains. At 1st ACM Conference on Advances in Financial Technologies (AFT), October 2019. doi:10.1145/3318041.3355458 ↩︎

  72. Michael J. Fischer, Nancy Lynch, and Michael S. Paterson. Impossibility of Distributed Consensus with One Faulty Process. Journal of the ACM, volume 32, issue 2, pages 374–382, April 1985. doi:10.1145/3149.214121 ↩︎ ↩︎

  73. Tushar Deepak Chandra and Sam Toueg. Unreliable Failure Detectors for Reliable Distributed Systems. Journal of the ACM, volume 43, issue 2, pages 225–267, March 1996. doi:10.1145/226643.226647 ↩︎ ↩︎ ↩︎ ↩︎

  74. Michael Ben-Or. Another Advantage of Free Choice: Completely Asynchronous Agreement Protocols. At 2nd ACM Symposium on Principles of Distributed Computing (PODC), August 1983. doi:10.1145/800221.806707 ↩︎

  75. Cynthia Dwork, Nancy Lynch, and Larry Stockmeyer. Consensus in the Presence of Partial Synchrony. Journal of the ACM, volume 35, issue 2, pages 288–323, April 1988. doi:10.1145/42282.42283 ↩︎

  76. Xavier Défago, André Schiper, and Péter Urbán. Total Order Broadcast and Multicast Algorithms: Taxonomy and Survey. ACM Computing Surveys, volume 36, issue 4, pages 372–421, December 2004. doi:10.1145/1041680.1041682 ↩︎

  77. Hagit Attiya and Jennifer Welch. Distributed Computing: Fundamentals, Simulations and Advanced Topics, 2nd edition. John Wiley & Sons, 2004. ISBN: 978-0-471-45324-6, doi:10.1002/0471478210 ↩︎

  78. Rachid Guerraoui. Revisiting the Relationship Between Non-Blocking Atomic Commitment and Consensus. At 9th International Workshop on Distributed Algorithms (WDAG), September 1995. doi:10.1007/BFb0022140 ↩︎ ↩︎

  79. Jim N. Gray and Leslie Lamport. Consensus on Transaction Commit. ACM Transactions on Database Systems (TODS), volume 31, issue 1, pages 133–160, March 2006. doi:10.1145/1132863.1132867 ↩︎

  80. Fred B. Schneider. Implementing Fault-Tolerant Services Using the State Machine Approach: A Tutorial. ACM Computing Surveys, volume 22, issue 4, pages 299–319, December 1990. doi:10.1145/98163.98167 ↩︎

  81. Alexander Thomson, Thaddeus Diamond, Shu-Chun Weng, Kun Ren, Philip Shao, and Daniel J. Abadi. Calvin: Fast Distributed Transactions for Partitioned Database Systems. At ACM International Conference on Management of Data (SIGMOD), May 2012. doi:10.1145/2213836.2213838 ↩︎

  82. Mahesh Balakrishnan, Dahlia Malkhi, Ted Wobber, Ming Wu, Vijayan Prabhakaran, Michael Wei, John D. Davis, Sriram Rao, Tao Zou, and Aviad Zuck. Tango: Distributed Data Structures over a Shared Log. At 24th ACM Symposium on Operating Systems Principles (SOSP), November 2013. doi:10.1145/2517349.2522732 ↩︎

  83. Mahesh Balakrishnan, Dahlia Malkhi, Vijayan Prabhakaran, Ted Wobber, Michael Wei, and John D. Davis. CORFU: A Shared Log Design for Flash Clusters. At 9th USENIX Symposium on Networked Systems Design and Implementation (NSDI), April 2012. ↩︎

  84. Vasilis Gavrielatos, Antonios Katsarakis, and Vijay Nagarajan. Odyssey: the impact of modern hardware on strongly-consistent replication protocols. At 16th European Conference on Computer Systems (EuroSys), April 2021. doi:10.1145/3447786.3456240 ↩︎

  85. Heidi Howard, Dahlia Malkhi, and Alexander Spiegelman. Flexible Paxos: Quorum Intersection Revisited. At 20th International Conference on Principles of Distributed Systems (OPODIS), December 2016. doi:10.4230/LIPIcs.OPODIS.2016.25 ↩︎ ↩︎

  86. Martin Kleppmann. Distributed Systems lecture notes. University of Cambridge, October 2024. Archived at perma.cc/SS3Q-FNS5 ↩︎ ↩︎

  87. Kyle Kingsbury. Call Me Maybe: Elasticsearch 1.5.0. aphyr.com, April 2015. Archived at perma.cc/37MZ-JT7H ↩︎

  88. Heidi Howard and Jon Crowcroft. Coracle: Evaluating Consensus at the Internet Edge. At Annual Conference of the ACM Special Interest Group on Data Communication (SIGCOMM), August 2015. doi:10.1145/2829988.2790010 ↩︎

  89. Tom Lianza and Chris Snook. A Byzantine failure in the real world. blog.cloudflare.com, November 2020. Archived at perma.cc/83EZ-ALCY ↩︎

最後更新於