分散式系統中一個重要的問題就是資料複製,資料複製一般是為了增強系統的可用性或提高效能。而實現資料複製的一個主要難題就是保持各個副本的一致性。本文首先討論資料複製的場景中一致性模型如此重要的原因,然後討論一致性模型的含義,最後分析常用的一致性模型。
為什麼需要一致性模型
**資料複製主要的目的有兩個:可用性和效能。**首先資料複製可以提高系統的可用性。在保持多副本的情況,有一個副本不可用,系統切換到其他副本就會恢復。常用的 MySQL 主備同步方案就是一個典型的例子。另一方面,資料複製能夠提供系統的效能。當分散式系統需要在伺服器數量和地理區域上進行擴充套件時,資料複製是一個相當重要的手段。有了多個資料副本,就能將請求分流;在多個區域提供服務時,也能通過就近原則提高客戶端訪問資料的效率。常用的 CDN 技術就是一個典型的例子。 但是資料複製是要付出代價的。**資料複製帶來了多副本資料一致性的問題。**一個副本的資料更新之後,其他副本必須要保持同步,否則資料不一致就可能導致業務出現問題。因此,每次更新資料對所有副本進行修改的時間以及方式決定了複製代價的大小。全域性同步與效能實際上是矛盾的,而為了提高效能,往往會採用放寬一致性要求的方法。因此,我們需要用一致性模型來理解和推理在分散式系統中資料複製需要考慮的問題和基本假設。
什麼是一致性模型
首先我們要定義一下一致性模型的術語:
- 資料儲存:在分散式系統中指分散式共享資料庫、分散式檔案系統等。
- 讀寫操作:更改資料的操作稱為寫操作(包括新增、修改、刪除),其他操作稱為讀操作。
下面是一致性模型的定義: 一致性模型本質上是程式與資料儲存的約定:如果程式遵循某些規則,那麼程式對資料的讀寫操作都是可預期的。
上面的定義可能比較抽象,我們用常見的強一致性模型來通俗的解釋一下:**線上性一致性模型中,程式對一個資料項的讀操作,它期待資料儲存返回的是該資料在最後一次寫操作之後的結果。**這在單機系統裡面很容易實現,在 MySQL 中只要使用加鎖讀的方式就能保證讀取到資料在最後一次寫操作之後的結果。但在分散式系統中,因為沒有全域性時鐘,導致要精確定義哪次寫操作是最後一次寫操作是非常困難的事情,因此產生了一系列的一致性模型。**每種模型都有效限制了在對一個資料項執行讀操作所應該返回的值。**舉個例子:假設記錄值 X 在節點 M 和 N 上都有副本,當客戶端 A 修改了副本 M 上 X 的值,一段時間之後,客戶端 B 從 N 上讀取 X 的值,此時一致性模型會決定客戶端 B 是否能夠讀取到 A 寫入的值。
一致性模型主要可以分為兩類:能夠保證所有程式對資料的讀寫順序都保持一致的一致性模型稱為強一致性模型,而不能保證的一致性模型稱為弱一致性模型。
強一致性模型
線性一致性(Linearizable Consistency)
線性一致性也叫嚴格一致性(Strict Consistency)或者原子一致性(Atomic Consistency),它的條件是:
- 任何一次讀都能讀取到某個資料最近的一次寫的資料。
- 所有程式看到的操作順序都跟全域性時鐘下的順序一致。
線性一致性是對一致性要求最高的一致性模型,就現有技術是不可能實現的。因為它要求所有操作都實時同步,在分散式系統中要做到全域性完全一致時鐘現有技術是做不到的。首先通訊是必然有延遲的,一旦有延遲,時鐘的同步就沒法做到一致。當然不排除以後新的技術能夠做到,但目前而言線性一致性是無法實現的。
順序一致性(Sequential Consistency)
順序一致性是 Lamport(1979)在解決多處理器系統共享儲存器時首次提出來的。參考我之前寫的文章《分散式系統:Lamport 邏輯時鐘》。它的條件是:
- 任何一次讀寫操作都是按照某種特定的順序。
- 所有程式看到的讀寫操作順序都保持一致。
首先我們先來分析一下線性一致性和順序一致性的相同點在哪裡。他們都能夠保證所有程式對資料的讀寫順序保持一致。線性一致性的實現很簡單,就按照全域性時鐘(可以簡單理解為物理時鐘)為參考系,所有程式都按照全域性時鐘的時間戳來區分事件的先後,那麼必然所有程式看到的資料讀寫操作順序一定是一樣的,因為它們的參考系是一樣的。而順序一致性使用的是邏輯時鐘來作為分散式系統中的全域性時鐘,進而所有程式也有了一個統一的參考系對讀寫操作進行排序,因此所有程式看到的資料讀寫操作順序也是一樣的。
那麼線性一致性和順序一致性的區別在哪裡呢?通過上面的分析可以發現,**順序一致性雖然通過邏輯時鐘保證所有程式保持一致的讀寫操作順序,但這些讀寫操作的順序跟實際上發生的順序並不一定一致。**而線性一致性是嚴格保證跟實際發生的順序一致的。
弱一致性模型
因果一致性(Causal Consistency)
因果一致性是一種弱化的順序一致性模型,因為它將具有潛在因果關係的事件和沒有因果關係的事件區分開了。那麼什麼是因果關係?如果事件 B 是由事件 A 引起的或者受事件 A 的影響,那麼這兩個事件就具有因果關係。 舉個分散式資料庫的示例,假設程式 P1 對資料項 x 進行了寫操作,然後程式 P2 先讀取了 x,然後對 y 進行了寫操作,那麼對 x 的讀操作和對 y 的寫操作就具有潛在的因果關係,因為 y 的計算可能依賴於 P2 讀取到 x 的值(也就是 P1 寫的值)。 另一方面,如果兩個程式同時對兩個不同的資料項進行寫操作,那麼這兩個事件就不具備因果關係。無因果關係的操作稱為併發操作。這裡只是簡單陳述了一下,深入的分析見我之前寫的文章《分散式系統:向量時鐘》。 因果一致性的條件包括:
- 所有程式必須以相同的順序看到具有因果關係的讀寫操作。
- 不同程式可以以不同的順序看到併發的讀寫操作。
下面我們來分析一下為什麼說因果一致性是一種弱化的順序一致性模型。順序一致性雖然不保證事件發生的順序跟實際發生的保持一致,但是它能夠保證所有程式看到的讀寫操作順序是一樣的。而**因果一致性更進一步弱化了順序一致性中對讀寫操作順序的約束,僅保證有因果關係的讀寫操作有序,沒有因果關係的讀寫操作(併發事件)則不做保證。**也就是說如果是無因果關係的資料操作不同程式看到的值是有可能是不一樣,而有因果關係的資料操作不同程式看到的值保證是一樣的。
最終一致性(Eventual Consistency)
最終一致性是更加弱化的一致性模型,因果一致性起碼還保證了有因果關係的資料不同程式讀取到的值保證是一樣的,而最終一致性只保證所有副本的資料最終在某個時刻會保持一致。 從某種意義上講,最終一致性保證的資料在某個時刻會最終保持一致就像是在說:“人總有一天會死”一樣。實際上我們更加關心的是:
- “最終”到底是多久?通常來說,實際執行的系統需要能夠保證提供一個有下限的時間範圍。
- 多副本之間對資料更新採用什麼樣的策略?一段時間內可能資料可能多次更新,到底以哪個資料為準?一個常用的資料更新策略就是以時間戳最新的資料為準。
由於最終一致性對資料一致性的要求比較低,在對效能要求高的場景中是經常使用的一致性模型。
以客戶端為中心的一致性(Client-centric Consistency)
前面我們討論的一致性模型都是針對資料儲存的多副本之間如何做到一致性,考慮這麼一種場景:在最終一致性的模型中,如果客戶端在資料不同步的時間視窗內訪問不同的副本的同一個資料,會出現讀取同一個資料卻得到不同的值的情況。為了解決這個問題,有人提出了以客戶端為中心的一致性模型。以客戶端為中心的一致性為單一客戶端提供一致性保證,保證該客戶端對資料儲存的訪問的一致性,但是它不為不同客戶端的併發訪問提供任何一致性保證。 舉個例子:客戶端 A 在副本 M 上讀取 x 的最新值為 1,假設副本 M 掛了,客戶端 A 連線到副本 N 上,此時副本 N 上面的 x 值為舊版本的 0,那麼一致性模型會保證客戶端 A 讀取到的 x 的值為 1,而不是舊版本的 0。一種可行的方案就是給資料 x 加版本標記,同時客戶端 A 會快取 x 的值,通過比較版本來識別資料的新舊,保證客戶端不會讀取到舊的值。
以客戶端為中心的一致性包含了四種子模型:
- 單調讀一致性(Monotonic-read Consistency):如果一個程式讀取資料項 x 的值,那麼該程式對於 x 後續的所有讀操作要麼讀取到第一次讀取的值要麼讀取到更新的值。即保證客戶端不會讀取到舊值。
- 單調寫一致性(Monotonic-write Consistency):一個程式對資料項 x 的寫操作必須在該程式對 x 執行任何後續寫操作之前完成。即保證客戶端的寫操作是序列的。
- 讀寫一致性(Read-your-writes Consistency):一個程式對資料項 x 執行一次寫操作的結果總是會被該程式對 x 執行的後續讀操作看見。即保證客戶端能讀到自己最新寫入的值。
- 寫讀一致性(Writes-follow-reads Consistency):同一個程式對資料項 x 執行的讀操作之後的寫操作,保證發生在與 x 讀取值相同或比之更新的值上。即保證客戶端對一個資料項的寫操作是基於該客戶端最新讀取的值。
總結
資料複製導致了一致性的問題,為了保持副本的一致性可能會嚴重地影響效能,唯一的解決辦法就是放鬆一致性的要求。通過一致性模型我們可以理解和推理在分散式系統中資料複製需要考慮的問題和基本假設,便於結合具體的業務場景做權衡。每種模型都有效地限制了對一個資料項執行度操作應返回的值。通常來說限制越少的模型越容易應用,但一致性的保證就越弱。
參考資料
《分散式系統原理與範型》