當前這篇文章至少比計劃拖後了兩個月。在上一篇文章《條分縷析分布式:到底什麼是一致性?》中,我們仔細辨析了「一致性」相關的幾個容易混淆的概念。而在本文中,我們會沿著逐步深入的思路,跟大家繼續討論順序一致性、線性一致性、最終一致性等幾個概念。
為了避免產生歧義,我們先明確一下這幾個概念的英文表達:
順序一致性的英文是:sequential consistency。
線性一致性的英文是:linearizability。實際上,它就是CAP定理中的C,我們在上一篇文章中已經提到過。
最終一致性的英文是:eventual consistency。
在進行詳細的技術性討論之前,我們先把本文要討論的幾個重點問題和結論列出如下:
線性一致性和順序一致性,屬於分布式系統的一致性模型 (consistency model)。這代表了分布式系統的一個非常非常重要的方面。
通常人們把線性一致性稱為「強一致性」,把最終一致性稱為「弱一致性」,但線性一致性和最終一致性其實存在本質的區別。嚴格來說,它們並不是一個範疇的概念。
一致性模型之間的「強弱」比較,是一個相對的概念。比如,線性一致性是比順序一致性更強的一致性模型。當然,除了線性一致性和順序一致性,也存在其它一些一致性模型(其中很多都比順序一致性要弱)。
滿足線性一致性的系統,也必定滿足順序一致性,但反過來不一定。這是由一致性模型之間的強弱關係決定的。
下面,我們就開始詳細的解析。
我們之所以使用分布式系統,無非是因為分布式系統能帶來一些「好處」,比如容錯性、可擴展性等等。為了獲得這些「好處」,分布式系統實現上常用的方法是複製 (replication) 和分片 (sharding)。而我們將要討論的一致性模型 (consistency model),主要是與複製有關。因此這裡我們先關注一下複製的機制。
複製指的是將同一份數據保存在多個網絡節點上。而保存同一份數據拷貝的節點,被稱為副本 (replica)。複製帶來的具體「好處」主要是體現在兩個方面:
一方面,複製帶來了諸多好處;另一方面,它也帶來了很多挑戰,其中最重要的一個就是數據的一致性問題。由於同一份數據保存在了多個副本節點上,它們之間就存在數據不一致的風險。我們當然希望同一份數據的不同副本總是保持一致。換句話說,我們希望在其中一個副本上所做的修改,在其它副本上也能隨時觀察到(即讀取到)。
當然我們心裡都清楚,讓所有副本在任何時刻都保持一致,是不可能的。因為副本之間的數據同步即使速度再快,也是需要時間的。不過幸運的是,我們其實並不關心所有時刻的數據一致性情況。只要系統能夠保證,每當我們去「觀察」的時候(即讀取數據副本的時候),系統表現出來的行為是一致的,就可以了。換句話說,即使在兩次「觀察」之間,系統內部出現了短暫的數據不一致的情況,只要系統保證外部用戶無論如何都發現不了,我們也是可以滿意的。
這意味著,我們應該從系統用戶(使用系統的開發者)的角度,來對數據一致性的要求進行定義。
實際上,早期的分布式系統設計者們對系統設計的要求,也是按照類似的思路進行的。在理想情況下,系統應該維持類似SSI (single-system image)[1]或distribution transparency[2]的特性。這兩個概念要表達的核心意思是,系統內部有關分布式實現的複雜性應該對系統的外部用戶透明;也就是說,對於系統的外部用戶來說,系統應該表現得就好像只有一個單一的副本一樣。如果系統能夠提供這種「單一系統視圖」或「透明性」,那麼系統的使用者就能以比較簡單的方式來使用系統;否則就可能帶來很大的負擔。
系統「表現得就好像只有一個單一的副本」,這是一個相當「籠統」的說法。在此我們討論3個具體的例子:
我們先向一個副本節點寫入x=42,然後讀取數據對象x的值。顯然,不管我們從哪個副本節點上進行讀取,我們都希望讀到最新寫入的值(也就是42)。只有這樣才合理。
兩個系統用戶分別在兩個副本節點上同時執行寫操作。其中,用戶A在第1個副本上執行x=42;用戶B在第2個副本上執行x=43。然後用戶C讀取x的值。雖然兩個寫操作是「同時」進行的,但為了讓系統「表現得像只有一個副本」,我們還是需要對它們進行一個先後排序。又因為它們是「同時」執行的,所以誰先誰後都有可能是合理的。如果我們認為x=42在x=43之前先執行,那麼讀取到的x的值就應該是43;反過來,如果我們認為x=43在x=42之前先執行,那麼讀取到的x的值就應該是42。
用戶A先在第1個副本上執行x=42,然後用戶B再在第2個副本上執行x=43,最後用戶C在第3個副本上讀取x的值。仍然為了讓系統「表現得像只有一個副本」,直覺上看,用戶C讀取到的x的值似乎應該是43。但是,也不一定非要如此。因為兩個寫操作是分別由用戶A和用戶B發起的,他們並不知道彼此誰先誰後(雖然從時間上看用戶A的寫操作確實在先)。所以,我們也可以選擇認為用戶B執行x=43在用戶A執行x=42之前。這樣的話,用戶C讀取到的x的值就應該是42。當然,根據本文後面的討論,這種排序就不滿足線性一致性了,但卻滿足順序一致性。
從這些例子不難看出,一個系統在數據一致性上的具體表現如何,取決於系統對關鍵事件(讀寫操作)的排序和執行採取什麼樣的規則和限制。比如在上面第3個例子中,出現了兩種對於讀寫操作的排序。前一種排序是:
用戶A執行x=42。
用戶B執行x=43。
用戶C讀取到x的值是43。
而第3個例子中的後一種排序是:
用戶B執行x=43。
用戶A執行x=42。
用戶C讀取到x的值是42。
雖然這兩種排序結果不同,但它們都做到了讓系統「表現得像只有一個副本」。它們的不同在於,前一種排序遵循了不同用戶的操作的時間先後順序,而後一種排序沒有。實際上,如果我們要求系統滿足線性一致性,就只能得到前一種排序結果;而如果只要求系統滿足順序一致性,就有可能得到後一種排序結果(等看完本文後面的討論,你就能自己得到這些結論)。
可以這麼說,一個分布式系統對於讀寫操作的某種排序和執行規則,就定義了一種一致性模型 (consistency model)。當一個系統選定了某種特定的一致性模型(比如線性一致性或順序一致性),那麼你就只能看到這種一致性模型所允許的那些操作序列。還是拿前面第3個例子來說明:如果你選定了線性一致性模型,那麼系統就不會向你呈現後一種排序,你只能看到前一種排序。
另外,在前面的三個例子中,不管系統最終給出了哪種排序結果,所有系統的用戶其實都對那種操作序列達成了一致的看法。還有一些一致性模型,並不要求所有用戶對操作排序的結果達成唯一的一種看法。這樣的一致性模型稍顯複雜,我們會放在下一篇文章中再討論(比如因果一致性)。
接下來,為了更清晰地認識一致性模型,我們來深入到線性一致性和順序一致性的一些細節中去。
在討論之前,我們先把組成分布式系統的一些關鍵概念定義清楚:
整個系統可以看成由多個進程和一個共享的數據存儲組成。對於數據存儲的讀寫操作由進程發起。這裡的進程,相當於本文前面提到的系統用戶或系統使用者。
同一個進程發起的讀寫操作是先後順序執行的。注意,這裡的「進程」概念跟我們平常編程時用到的進程有所不同,進程裡面不再分多個線程了。
數據存儲可能有多個副本,但我們在討論一致性模型的時候,把它看成一個整體來看待,不區分讀寫操作提交到了具體哪個副本上。
每個操作的執行,從開始調用到執行結束,都需要花一定的時間。因此,一個進程發起的操作還沒有執行完的時候,另一個進程的操作可能就已經開始了。
可見,系統的多個進程是並發執行的。下面我們通過一個例子來說明這種並發執行的情況,進而解釋順序一致性的概念。
上面是一個類似「時空圖」的圖像,表達了3個進程(P1、P2和P3)對於數據存儲的讀寫執行過程。在這個圖中,橫向從左到右表示時間遞增,黑色的線段表示每個操作的執行起止。線段上面的符號表示具體的讀寫操作:
現在我們要考察的問題是:上圖的這樣一個執行過程,是否滿足順序一致性?要回答這個問題,我們首先得知道順序一致性的定義是什麼。
順序一致性定義[3,4]:如果一個並發執行過程所包含的所有讀寫操作能夠重排成一個全局線性有序的序列,並且這個序列滿足以下兩個條件,那麼這個並發執行過程就是滿足順序一致性的:
以上圖的執行過程為例,我們重排所有的6個讀寫操作,可以得到如下的有序序列:
A --> w1(x)
r3(x) --> A
C --> w2(x)
r3(x) --> C
B --> w1(x)
r3(x) --> B
很容易看出,這個序列是滿足前面順序一致性定義中的兩個條件的:
所以現在我們可以回答前面的問題了:上圖中的執行過程,是滿足順序一致性的。
你可能會問,順序一致性為什麼會這樣定義呢?這個定義的初衷是什麼?
我們可以試著這樣理解:首先,重排成一個全局線性有序的序列,相當於系統對外表現出了一種「假象」,原本多進程並發執行的操作,好像是順序執行的一樣。本文前面提到過,理想情況下,分布式系統應該「表現得像只有一個副本」一樣。順序一致性正是遵循了這種「系統假象」,系統對外表現就好像在操作一個單一的副本,執行順序也必然是可以看做順序執行的。而條件I規定了系統的表現是合理的(即合乎邏輯的);條件II則保證了以任何進程的視角來看,它所發起的操作執行順序都是符合它原本的預期的。總之,一個滿足順序一致性的系統,對外表現就好像總是在操作一個副本一樣。
我們再通過一個例子來看一看這個問題的反面——不滿足順序一致性的執行過程是怎樣的。
這個圖中的執行過程,與前面第一個圖的執行過程非常相似,只是進程P3的幾個操作的執行順序稍有變化。
我們根據前面順序一致性的定義再來試著對這個執行過程中的所有操作進行重排:首先根據條件II和進程P1的執行順序,我們知道,A --> w1(x)一定要排在B --> w1(x)前面;再根據條件I,進程P1的B --> w1(x)一定要排在進程P3的r3(x) --> B前面。最後,再結合條件II和進程P3的執行順序,我們能夠得出結論,進程P1和進程P3的所有操作,在最終重排後的完整序列中,必然保持以下的順序:
A --> w1(x)
B --> w1(x)
r3(x) --> B
r3(x) --> C
r3(x) --> A
我們會發現,上面的序列有兩個地方不滿足條件I:
我們還剩一個進程P2的寫操作,即C --> w2(x),沒有放到最後這個序列中。也許我們可以試著將它放置到第3和第4個操作之間,這樣就能把前面第一個不滿足條件I的地方修復掉。但無論如何,也無法得到一個完全符合條件I和條件II的完整序列了。因此,前面第二個圖中的執行過程,是不滿足順序一致性的。進一步說,如果一個系統的執行呈現出了這樣的一種執行過程(如前面第二個圖所示),那我們可以肯定地說,這個系統是沒有遵守順序一致性的。
我們再來考察一下線性一致性的概念。線性一致性的定義[5],與順序一致性非常相似,也是試圖把所有讀寫操作重排成一個全局線性有序的序列,但除了滿足前面的條件I和條件II之外,還要同時滿足一個條件:
根據最新定義的條件III,我們來重新評判一下前面第一個圖所展現出來的執行過程是不是滿足它。為了閱讀和討論方便,我們把第一個圖重新展示在下面:
針對條件III,我們分析一下各個操作之間的先後順序:
進程P1的B --> w1(x)和進程P2的C --> w2(x),在執行時間上是重疊的,所以它們的排序不受條件III的約束。即,在重排後的序列中,這兩個操作誰先誰後都可以。同樣,進程P1的B --> w1(x)和進程P3的r3(x) --> A,也是如此。
進程P1的A --> w1(x)和進程P2的C --> w2(x),在執行時間上是不重疊的,即前一個操作都執行完了,後一個操作才開始執行。那麼,這兩個操作就必須滿足條件III了:在重排後的序列中,A --> w1(x)必須排在C --> w2(x)前面。
與上面同樣的道理,在重排後的序列中,進程P2的C -->w2(x)必須排在進程P3的r3(x) --> A之前。
容易看出,在遵守這樣的先後關係約束的前提下,不管怎麼重排,都無法得到一個滿足條件I的完整序列了。所以說,前面第一個圖所示的滿足順序一致性的執行過程,是不滿足線性一致性的。
下面我們舉一個滿足線性一致性的例子:
上圖的執行過程,所有操作重排後,可以得到如下的有序序列:
A --> w1(x)
r3(x) --> A
C --> w2(x)
r3(x) --> C
B --> w1(x)
r3(x) --> B
不難看出,這個序列是滿足所有的條件I、條件II和條件III這三個條件的。因此,這個執行過程滿足線性一致性。
細心的你可能已經發現了,最後這個線性一致性的例子,得到的重排後的序列,與開始第一個順序一致性的例子重排後的序列,完全相同。當然,這兩個例子中原始的多進程並發執行過程,是不同的。這是符合預期的(沒有什麼可奇怪的)。
現在我們可以仔細分析一下條件II和條件III,它們囊括了任意兩個操作之間所有可能的先後關係:
最後,我們比較一下順序一致性和線性一致性:
注意一下上面第3點兩者在時間先後順序上的不同。這意味著:
我們在上一篇文章中提到過,CAP定理[6]中的C,指的就是線性一致性 (linearizability)。它也經常被稱為「強一致性」。
根據CAP定理,當存在網絡分區的時候,我們必須在可用性 (availability) 和強一致性之間進行取捨。
另外,即使在沒有網絡分區存在的情況下,我們也必須在延遲 (latency) 和強一致性之間進行取捨[7]。這是因為,系統維持強一致性是有成本的。想要維持越強的一致性,就需要在副本節點之間做更多的通信和協調工作,因此會增加操作的總延遲,進而降低整個系統的性能。
從20世紀90年代中期開始,網際網路開始蓬勃發展,系統的規模也變得越來越大。人們設計大型分布式系統的指導思想,也逐步開始更傾向於系統的高可用性和高性能。取捨的結果就是,降低系統提供的一致性保障。這其中非常重要的一條思路就是最終一致性[2]。
最終一致性的設計思路,不再試圖提供單一系統視圖 (SSI),即不再試圖讓系統「表現得像只有一個副本」一樣。它允許讀到舊版本的數據。最終一致性的原始出處是論文[2],作者在論文中給出的最終一致性的定義如下:
Eventual consistency. This is a specific form of weak consistency; the storage system guarantees that if no new updates are made to the object, eventually all accesses will return the last updated value.(譯文:最終一致性是弱一致性的一種特殊形式;存儲系統保證,如果對象沒有新的修改操作,那麼所有的訪問最終都會返回最新寫入的值。)
我們發現,雖然最終一致性和本文前面討論的線性一致性或順序一致性在命名上非常相似,但它的定義卻與後兩者存在非常大的差別。深層的原因在於,它們其實屬於不同類別的系統屬性 (property)。線性一致性和順序一致性屬於safety property(安全性);而最終一致性屬於liveness property(活性)[8]。
一個並發程序或者一個分布式系統,它們的執行所展現出來的系統屬性,可以分為兩大類:
safety:它表示「壞事」永遠不會發生。比如,一個系統如果遵守線性一致性或順序一致性,那麼就永遠不會出現違反三個(對於順序一致性來說是兩個)條件的執行過程。而一旦系統出現問題,safety被違反了,我們也能明確指出是在哪個時間點上出現意外的。
liveness:它表示「好事」最終會發生。這種屬性聽起來會比較神奇:在任何一個時間點,你都無法判定liveness被違反了。因為,即使你期望的「好事」還沒有發生,也不代表它未來不會發生。就像最終一致性一樣,即使當前系統處於不一致的狀態,也不代表未來系統就不會達到一致的狀態。而只要系統存在「在未來某個時刻達到一致狀態」的可能性,最終一致性就沒有被違反。另外,可用性 (availability) 也屬於liveness屬性。
由此可見,我們在前一小節之所以能夠將線性一致性和順序一致性放在一起討論和比較,是因為它們都屬於safety屬性。而最終一致性屬於liveness屬性,跟這兩者存在本質的區別。實際上,最終一致性有點名不副實,它更好的名字可能是收斂性 (convergence),表示所有副本最終都會收斂到相同的值[9]。
通常來說,只有當safety和liveness這兩種屬性被同時考慮時,一個系統才能提供有意義的系統保證[1]。而當系統設計者遵循最終一致性的設計思路時,相當於放棄了所有的safety屬性。這意味著,對於系統使用者來說,你必須針對數據不一致的可能性做好補償措施 (compensation)。這也是最終一致性系統難用的地方。但不管怎麼說,最終一致性仍然被認為是系統提供數據一致性的最低要求[1]。
在本文開頭,我們提到過,通常人們把線性一致性稱為「強一致性」,把最終一致性稱為「弱一致性」。但對於指代特定的一種一致性模型來說,「強一致性」和「弱一致性」都不是一個好名字。因為強和弱,是個相對的概念。
根據本文前面的討論,從線性一致性,到順序一致性,再到最終一致性,一致性的強度依次減弱。但是,一致性模型的強弱關係,其實是有更嚴格的定義的:
按照這個更嚴格的強弱關係定義,線性一致性是比順序一致性更強的一致性模型。這是因為,線性一致性比順序一致性多了一個條件III,所以凡是滿足線性一致性的執行過程,肯定也滿足順序一致性。
我們仔細分析一下也能知道,一致性模型的強弱關係定義,是基於safety屬性定義的。所以,將線性一致性或順序一致性與最終一致性比較強弱,這並不是一個嚴格的做法。實際上,就像我們前一小節所討論的,最終一致性在safety方面提供的保證為零,它是屬於liveness的概念。一個系統可以在提供最終一致性的同時,也提供另外一種更強一點的帶有safety屬性的一致性(比如因果一致性)。
就如同我在之前另外一篇文章《漫談分布式系統、拜佔庭將軍問題與區塊鏈》中所指出的,理解問題本身比知道問題的答案要重要的多。本文中,我們辨析了線性一致性、順序一致性、最終一致性這些概念,以及他們的關係和區別。由此我們了解到了分布式系統的一些核心問題,但我們並未討論怎麼解決這些問題。比如,採用什麼算法才能提供線性一致性;面對最終一致性的系統,應該怎樣編程,包括怎樣處理邊界情況,等等。相對於理解問題本身而言,這些反而都是細節。
在這個系列的下一篇文章中,我們將依然遵循這樣的思路,具體解析因果一致性,以及分布式系統更深層的事件排序問題。
(正文完。後臺發送「分布式」關鍵詞,獲取我的分布式系列文章)
[1] Peter Bailis, Ali Ghodsi, "Eventual Consistency Today: Limitations, Extensions, and Beyond", 2013.
[2] Werner Vogels, "Eventually Consistent", 2008.
[3] Leslie Lamport, "How to Make a Multiprocessor Computer That Correctly Executes Multiprocess Progranm", 1979.
[4] Mustaque Ahamad, Gil Neiger, James E. Burns, et al, "Causal Memory: Definitions, Implementation and Programming", 1994.
[5] Maurice P. Herlihy, Jeannette M. Wing, "Linearizability: A Correctness Condition for Concurrent Objects", 1990.
[6] Seth Gilbert, Nancy Lynch, "Brewer’s Conjecture and the Feasibility of Consistent, Available, Partition-Tolerant Web", 2002.
[7] Peter Bailis, Ali Ghodsi, et al, "Bolt-on Causal Consistency", 2013.
[8] Bowen Alpern, Fred B. Schneider, "Defining Liveness", 1985.
[9] Martin Kleppmann,《Designing Data-Intensive Applications》, 2017.
[10] Prince Mahajan, Lorenzo Alvisi, Mike Dahlin, "Consistency, Availability, and Convergence", 2011.
其它精選文章:
條分縷析分布式:到底什麼是一致性?
漫談分布式系統、拜佔庭將軍問題與區塊鏈
看得見的機器學習:零基礎看懂神經網絡
基於Redis的分布式鎖到底安全嗎
深度學習、資訊理論與統計學
蓄力十年,做一個成就
三個字節的歷險
由「精益創業」所想到的
知識的三個層次
掃碼或長按關注微信公眾號:張鐵蕾。
有時候寫點技術乾貨,有時候寫點有趣的文章。
這個公眾號有點科幻。