CAP定理在分散式系統設計中的最新應用

banq發表於2018-01-05
本文翻譯自國外InfoQ和計算機雜誌上一篇2012年舊文,本文就有關資料同步進行了討論,特別關注業務事務的不變性與一致性如何在分散式系統中巧妙保證,探討了長時間執行的事務的補償機制。這些對分散式系統設計都有很大幫助。

原文大意如下:

CAP理論認為,任何聯網的共享資料系統只能在三個屬性中的兩個。但是,透過明確處理分割槽,設計人員可以最佳化一致性和可用性,從而實現三者之間的某種權衡。

CAP定理推出以來的十年中,設計師和研究人員已經使用(有時濫用)CAP理論作為探索各種新型分散式系統的依據。NoSQL運動也將其作為反對傳統資料庫的論據。

CAP定理指出,任何網路共享資料系統最多可以有三個理想的屬性中的兩個:

1.一致性(C)相當於擁有一份最新的資料;
2.該資料的高可用性(A)(用於更新);
3.容忍網路分割槽(P)。

(banq注,關於CAP定理所有你不知道的中CAP解釋更加易懂:

1.一致性。每一次讀取都會讓你得到最近的寫入結果
2.可用性。每個節點(如果沒有失敗)總是能執行查詢(讀取和寫入)操作
3.分割槽容忍。即使節點之間的連線關閉,其他兩個屬性也會得到保證。

分割槽P基本可以理解為出現網路故障導致的通訊中斷,形成兩個以上的各自為政的孤島伺服器節點。


CAP的這種表達是服務於它的目的,這是為了讓設計師的思想敞開,上升到更廣泛的系統中進行權衡; 的確,在過去的十年中,已經出現了大量的新系統,以及關於一致性和可用性的相對優點的爭論。“三分之二”的表述卻是有誤導性的,因為它往往過分簡化了屬性之間的緊張關係。而這些細微差別卻很重要。CAP僅禁止設計空間的一小部分:在分割槽存下的完美可用性和完美一致性。這種情況是很少能實現的。

雖然設計師仍然需要在分割槽存在情況下在一致性和可用性之間進行選擇,但是處理分割槽和從分割槽中恢復的靈活性有著令人難以置信的好處。現代CAP目標應該是最大限度地提高一致性和可用性的組合,這對於特定的應用是有意義的。這種方法結合了分割槽發生和事後恢復的兩種方式,從而幫助設計人員考慮CAP,超越其歷史上的限制。

為什麼“2/3”是誤導

理解CAP最簡單的方法是考慮分割槽兩側的兩個節點。允許其中任意一個節點更新狀態將導致兩個節點變得不一致,從而放棄C,同樣,如果選擇是保持一致性,分割槽中任意一側必須是一種不可用的狀態,從而放棄A. 只有當節點一直完美通訊時才有可能保持兩者的一致性和可用性,這又喪失P。因此普遍認為,對於廣域系統來說,設計者不能放棄P,因此在C和A之間有一個困難的選擇。從某種意義上說,NoSQL運動是關於首先關注可用性和其次才是一致性的選擇; 遵循ACID屬性(原子性,一致性,隔離性和永續性)的資料庫則正好相反。

在20世紀90年代中期,我和我的同事們正在構建各種基於群集的廣域系統(本質上是早期的雲端計算),包括搜尋引擎,代理快取和內容分發系統。由於收入目標和合同規範,系統可用性非常重要,所以我們發現自己經常選擇透過採用快取或日誌記錄更新等策略來最佳化可用性,實現之後的衝突解決校驗。雖然這些策略確實提高了可用性,但是增加的代價是一致性降低。

這個一致性與可用性的爭論的第一個版本表現為ACID與BASE,BASE在當時並沒有得到很好的接受,主要是因為人們喜歡ACID的特性而不願放棄。CAP定理的目的是證明探索更廣闊的設計空間,因此是一種“2/3”的設計。

CAP定理首先出現在1998年秋季,1999年出版,並在2000年的“分散式計算原理專題討論會”上發表了主題演講 ,從而證明了這一點。
(這個定理被Eric Brewer在2000年分散式計算原理研討會上提出。2002年,來自麻省理工學院的Seth Gilbert和Nancy Lynch發表了一個Brewer猜想的正式證明,使其成為了一個定理。根據Brewer的說法,他只是想讓社群開始談論這個問題,但他的話最終被解釋為一個定理了。)

但是“2/3”的觀點在幾個方面有誤導性。

首先,因為當分割槽很少時,或者當系統沒有被分割槽時就沒有理由放棄C或A. 其次,C和A之間的選擇可以在相同的系統中以非常細的粒度出現多次; 子系統不僅可以做出不同的選擇,而且可以根據操作甚至特定的資料或使用者來選擇。
最後,這三個屬性是連續的,不是像二進位制那樣不是0就是1。可用性顯然是從0到100%連續的,同時也有很多級別的一致性,甚至分割槽也有細微差別,包括系統內部是否存在分割槽的高低程度。

探索這些細微差別需要推動傳統處理分割槽的方式,這是基本的挑戰。因為在分割槽很少出現情況下,CAP應該在大多數情況下可以允許完美的C和A,但是當分割槽存在或被感知時,檢測分割槽並明確說明分割槽的策略是有序的。這個策略應該有三個步驟:檢測分割槽,進入明確的分割槽模式,可以限制一些操作,並啟動恢復過程來恢復一致性,並彌補分割槽過程中犯的錯誤。


ACID,BASE和CAP

ACID和BASE代表了在一致性 - 可用性兩點之間進行選擇的設計哲學。ACID事務屬性注重一致性,是關聯式資料庫的傳統方法。我和我的同事在20世紀90年代後期建立了BASE,以捕捉新興的高可用性設計方法,並明確選擇和範圍。包括雲在內的現代大規模廣域系統都使用了兩種方法的組合。

儘管這兩個術語都比較精確,BASE這個縮寫代表:基本可用,軟狀態,最終一致。軟狀態和最終一致性是存在分割槽的情況下能夠很好地工作一種技術手段,這種手段能夠提高可用性。

CAP和ACID之間的關係比較複雜,常常被誤解,部分原因是ACID中的C和A雖然和CAP中C和A是相同的字母,但是表達不同的概念,選擇可用性只會影響一些ACID保證。四個ACID屬性是:

原子(A)。所有的系統都受益於原子操作。當我們將焦點放在可用性時,分割槽的兩邊應該仍然使用原子操作。而且,更高階別的原子操作(ACID暗示的那種)實際上簡化了分割槽發生故障以後的恢復過程。

一致性(C)。在ACID中,C意味著事務預處理所有的資料庫規則,例如唯一鍵。相比之下,CAP中的C僅指單一複製一致性,這是ACID一致性的嚴格子集。ACID的一致性也無法在分割槽之間保持。分割槽恢復將需要恢復ACID一致性。更一般地說,在分割槽中保持不變性也許是不可能的,因此需要仔細考慮哪些操作是不允許的,以及如何在恢復過程中恢復不變性。

隔離(I)。隔離性是CAP定理的核心:如果系統需要ACID隔離,則在分割槽過程中最多可以在一側進行操作。一般而言,可序列化需要通訊,這樣就會面臨跨分割槽的失敗情況。在分割槽恢復過程中,透過補償機制實現跨越分割槽的相對弱的正確性是可行的。

耐久性(D)。與原子性一樣,儘管開發人員可能選擇透過軟狀態(以BASE的形式)以避免它,因為其開銷昂貴,但是沒有理由禁止選擇永續性。一個微妙之處是,在分割槽恢復過程中,可以反轉在操作過程中在不知不覺中違反了不變的持久操作。然而,在恢復的時候,透過雙方比較長的歷史資料對比可以發現和糾正違反不變性的操作。一般來說,在分割槽的每一邊執行ACID事務能夠使得分割槽恢復變得更容易,並且使用一個框架來實現補償事務有助於分割槽恢復。

Cap-latency連線

在其經典的解釋中,CAP定理忽略了延遲,儘管在實踐中,延遲和分割槽是深度相關的。在操作上,CAP的本質是,在發生了分割槽(網路故障)以後,有一段timeout時間,在這個時間內程式必須做出基本的決定:

1.取消操作,從而降低可用性,或

2.繼續進行操作,從而導致風險不一致。

例如重試通訊可以實現一致性,比如透過Paxos或兩階段提交2PC,這些都是隻是延遲了決策。程式在某個時刻總是必須做出決定; 無限期地重試通訊本質上是選擇C而不是A。

因此,實際上,一個分割槽是通訊的一段時間範圍。在一段時間範圍內未能達到一致意味著存在一個分割槽,因此這個操作必須在C和A之間進行選擇。這些概念反映了關於延遲的核心設計問題:雙方如果沒有溝通通訊情況下會繼續執行前進嗎?

這種務實的觀點引起了一些重要的後果。首先是沒有分割槽的全域性概念,因為一些節點可能檢測到一個分割槽,而另一些節點可能不會。第二個結果是節點可以檢測分割槽並進入分割槽模式,這是最佳化C和A的核心部分。

最後,這個觀點意味著設計者可以根據目標響應時間故意設定時間範圍; 邊界更緊的系統可能會更頻繁地進入分割槽模式,有時網路只是緩慢的,而不是實際的分割槽。

有時為了避免在大範圍內保持一致性的高延遲,放棄強C是有意義的。雅虎的PNUTS系統透過非同步實現維護遠端副本同步而導致不一致。但是,它在本地機器實現主節點,從而減少延遲。這個策略在實踐中執行良好,因為使用者可據根據使用者(正常)地理位置實現自然分割槽。理想情況下,每個使用者是最靠近主資料的。

Facebook使用相反的策略:主資料始終在一個位置,所以遠端使用者通常有一個更接近但可能是陳舊的副本。但是,當使用者更新其頁面時,即使有更長的延遲時間,更新也會直接寫入主資料節點。20秒後,使用者會看到到最近的資料副本,在這個時候資料應該是反映最新資料。

CAP混亂

CAP定理的各個方面經常被誤解,特別是可用性和一致性的範圍,這可能會導致不希望的結果。如果使用者根本無法訪問服務,除非部分服務在客戶端上執行,否則C和A之間沒有選擇。這種通常被稱為斷線操作或離線模式,這種例外情況變得越來越重要。某些HTML5功能(特別是客戶端永續性儲存)使未連線的操作更容易。這些系統通常選擇A而不是C,因此必須需要長時間的分割槽恢復(以保證一致性)。

一致性的範圍反映了這樣的想法:在某個邊界內,狀態是一致的,但是在這個邊界之外就無法保證。例如,在主分割槽內,可以確保完整的一致性和可用性,而在分割槽之外,服務不可用。Paxos和原子多播系統通常符合這種情況。在Google中,主分割槽通常駐留在一個資料中心內;而Paxos被廣泛用於確保全球範圍內實現共識,如Chubby, 和高度可用的持久儲存如Megastore。

獨立的,自洽的子集可以在分割槽的情況下自行執行,儘管不能確保全域性的不變性。例如,對於設計人員在節點間預分配資料的分片(sharding),很有可能每個分片在分割槽故障發生過程中都會持續獨立執行。相反,如果相關狀態被跨分割槽劃分,或者全域性不變數是非常必要時,那麼充其量只有一方可以繼續保持執行,最壞的情況是都無法繼續執行。

選擇一致性和可用性(CA)作為“2/3”是否合理?正如一些研究人員指出的那樣,精確地說放棄P的意思是不夠清晰的。設計師是否可以選擇不分割槽呢?如果選擇是CA,然後才是分割槽?最好從機率上考慮:選擇CA意味著分割槽(網路故障)的機率遠小於其他系統故障的機率如災難或多個同時發生的故障。

這樣的觀點是有道理的,因為真實的系統會在一些失敗都下失去了C和A,所以這三個屬性都是程度的問題。在實踐中,大多數叢集組都假定在一個資料中心(單個站點)內沒有分割槽,因此在單個站點內可以設計CA; 這樣的設計,包括傳統的資料庫,都是CAP之前的預設選擇。考慮到全球地區的高延遲,為了獲得更好的效能,在大範圍內放棄完美的一致性是相對常見的。

CAP混淆的另一個方面是喪失一致性的隱藏成本,這是需要掌握系統的不變性。一致性系統的微妙之處在於即使設計者不知道它們是什麼,不變性也會保持不變。因此,廣泛的合理的不變性將工作得很好。相反,當設計者選擇A時,則需要在分割槽之後恢復不變性,因此必須對所有不變性都是明確掌握的,這是既具有挑戰性又容易出錯的。在微觀CPU核程式設計方面,類似相同的併發更新問題,多執行緒程式設計比順序程式設計更難一樣。

管理分割槽

設計師面臨的挑戰是減輕分割槽(網路故障)對一致性和可用性的影響。關鍵的想法是非常明確地管理分割槽(網路故障),不僅包括檢測,還包括一個特定的恢復過程和對一個分割槽中可能違反的所有不變性的總結。這種管理方法有三個步驟:


1.檢測分割槽的開始,
2.進入明確的分割槽模式,可能會限制一些操作
3.通訊恢復時啟動分割槽恢復。

最後一步旨在恢復一致性,並補償程式在分割槽時各自執行犯的錯誤(資料不一致)。

正常情況下的操作是一系列的原子操作,因此分割槽總是在正常操作之間開始。系統超時後,檢測到分割槽,檢測端進入分割槽模式。如果確實存在分割槽,則雙方都進入此模式,但也可以進行單向分割槽。在這種情況下,另一方根據需要進行通訊,或者該方正確響應或不需要進行通訊; 無論如何,操作應保持一致。但是由於檢測端操作不一致,必須進入分割槽模式。使用法定數量選取的系統就是這種單向分割槽的一個例子。一方區域如果有符合法定數量的節點(比如共3個節點,有兩個節點在一個區域就是符合法定數量)則可以繼續,但另一方不能。支援斷開操作的系統顯然具有分割槽模式的概念,

一旦系統進入分割槽模式,兩種策略是可能的。首先是限制一些操作,從而降低可用性。其次是記錄有關在分割槽恢復過程中將有幫助的操作的額外資訊。繼續嘗試通訊將使系統能夠識別分割槽何時結束。

哪些操作在發生分割槽時應該繼續進行?

決定限制哪些操作主要取決於系統必須維護的不變性。給定一個不變性集合,設計者必須決定是否在分割槽模式下保持一個特定的不變性,或者有意在恢復過程中恢復它。例如,對於表中的鍵必須是唯一的這種不變性約束,設計人員通常決定冒風險違背這個不變性,並允許在分割槽中使用重複相同鍵。重複鍵很容易在恢復過程中檢測到,假設可以合併,設計人員可以很容易地恢復全域性的不變性(全域性鍵的唯一性約束)。

然而,對於在分割槽中必須保持的不變性,設計者必須禁止或修改可能違反它的一些操作。(一般情況下,沒有辦法預知操作是否實際上將違反不變性,因為對方的狀態並不可知。)一些外部化的活動,如信用卡充值,屬於這種情況。在這種情況下,對付辦法是記錄下意圖(使用者操作意圖,如命令/事件等)並在恢復後執行。這種情況通常屬於工作流的一部分,比如有明確的訂單處理狀態的,在分割槽結束之前推遲操作幾乎沒有什麼壞處。設計師以一種使用者看不到的方式放棄了A。使用者只知道他們下了訂單,系統稍後會執行。

更一般地說,分割槽模式引起了使用者介面體驗的挑戰,即使用者傳達任務是正在進行但沒有完成。研究人員已經詳細探討了這個問題,對於斷開連線的操作,這只是一個很長的分割槽。例如,Bayou的日曆應用程式以不同的顏色顯示潛在的不一致(暫定)條目。這樣的情況在工作流程應用程式(如使用電子郵件通知的商務應用程式)和離線模式的雲服務(例如Google Docs)中都會定期顯現。

關注明確的原子操作而不僅僅是讀寫操作的一個原因是:分析高階操作對不變性的影響要容易得多。本質上,設計者必須建立一個表格,檢視所有操作和所有不變性規則的交叉乘積,併為每個條目決定操作是否違反不變性約束。如果是這樣,設計師必須決定是否禁止,推遲或修改操作。在實踐中,這些決定還可以取決於已知狀態等。例如,在存放某些資料的家庭節點的系統中,通常可以在家庭節點上進行5個操作,但是不能在其他節點上進行。

跟蹤雙方操作歷史的最好方法是使用版本向量( version vectors),它捕獲操作之間的因果關係。向量的元素是一對(節點,邏輯時間),每個已更新物件的節點和最後一次更新的時間都有一個條目。如果一個物件有A和B的兩個操作版本,如果A的時間大於或等於B,並且A的時間中至少有一次更大(時間數值是不斷增大的),則A比B新。

如果無法對版本向量排序,則更新是併發的,可能不一致。因此,根據雙方的版本向量歷史資料,系統可以容易地知道哪些操作已經按照已知的順序執行,哪些操作是同時執行的。最近的研究證明,如果設計者選擇關注可用性,那麼這種因果一致性通常有最好的結果。

分割槽恢復

在某個時候,通訊恢復,分割槽結束。在分割槽過程中,每一邊都是可用的,各自都執行了一些操作,其中因為分割槽推遲了一些操作,並且違反了一些不變性約束。此時,系統知道雙方的當前狀態和歷史操作記錄,因為它在分割槽模式下保持了仔細的歷史操作事件日誌。這種情況下當前狀態不如歷史事件日誌有用,系統可以從中推斷出哪些操作實際上違反了不變性,哪些結果已經無法收回,包括髮送給使用者的響應。設計師必須在恢復過程中解決兩個難題:

1.雙方的狀態必須保持一致
2.必須對分割槽模式下的錯誤進行補償(讓雙方狀態一致性,符合全域性不變性約束)。

更容易修復當前狀態的辦法是:從分割槽時的狀態開始,以某種方式向前滾動兩組操作,從而一直保持兩邊一致的狀態,。Bayou透過將資料庫回滾到正確的時間來顯式執行此操作,並以明確的,確定的順序重播全套操作,以使所有節點達到相同的狀態。類似地,原始碼控制系統,如並行版本系統(CVS)從共享一致點和前滾更新開始合併分支(banq注:事件溯源也屬於這種,區塊鏈也是)。

大多數系統不能總是合併衝突。例如,CVS偶爾會出現使用者必須手動解決的衝突,而具有離線模式的wiki系統通常會在生成的文件中留下需要手動編輯的衝突。

相反,一些系統總是可以透過選擇某些操作來合併衝突。一個恰當的例子就是Google文件中的文字編輯,它限制了應用樣式和新增或刪除文字等的操作。因此,雖然解決衝突在一般意義上是不可解決的,但實際上,設計者可以選擇在分割槽過程中限制某些操作的使用,以便系統在恢復過程中自動合併狀態。推遲具有風險的操作(banq注:所謂風險是可能違背不變性約束的操作,或在分割槽的情況下乾脆停止寫操作。)是一個相對簡單的實施辦法。

市面上通用框架一般是使用交換操作(commutative operations)實現狀態自動狀態收斂一致。系統連線歷史操作日誌,按照某種順序排序,然後執行它們。交換性意味著能夠將操作重新排列為以全域性順序為優先的順序。不幸的是,僅使用交換操作比看起來更難; 例如,加法是可交換的,但是邊界檢查則不是(例如零餘額)。

馬克·夏皮羅(Marc Shapiro)及其同事在INRIA 18,19最近的工作大大改善了交換操作在狀態融合中的應用。該團隊開發了可交換的複製資料型別(CRDT),這是一類在分割槽之後可證明地收斂的資料結構,並描述瞭如何使用這些結構:

1.確保分割槽過程中的所有操作都是可交換的,或者
2.在格上表示值,並確保分割槽期間的所有操作相對於該格是單調遞增的。

後一種方法透過移動到每一邊的最大值來收斂狀態。這是一個正規化formalization,亞馬遜在其購物車中就是這麼做的,分割槽之後,收斂值是兩個兩個購物車的聯盟,這個聯盟是一個單調集合操作。選擇這種方案的結果是被刪除的專案可能會重新出現。

但是,CRDT也可以實現新增和刪除專案的分割槽容忍性。這種方法的本質是保持兩套資料集:每套都有新增和刪除的專案,不同的是集合的成員。每個簡化集合進行收斂,因此差異化也是如此進行。在某些時候,系統可以簡單地透過從兩個集合中移除走已刪除的專案來進行清理。但是,這種清理通常只有在系統沒有分割槽的情況下才有可能。換句話說,設計者必須在分割槽期間禁止或推遲某些操作,但是這些操作不會限制敏感的可用性。因此,透過CRDT來實現狀態,設計者可以選擇A,並且在分割槽之後仍然保證狀態自動收斂。

相關文章