1. 程式人生 > >CAP二十年:“規則”變了

CAP二十年:“規則”變了

本文是原文https://www.cnblogs.com/cobbliu/p/7494961.html的譯文,原連結:http://www.infoq.com/cn/articles/cap-twelve-years-later-how-the-rules-have-changed

CAP理論斷言任何基於網路的資料共享系統,最多隻能滿足資料一致性、可用性、分割槽容忍性三要素中的兩個要素。但是通過顯式處理分割槽情形,系統設計師可以做到優化資料一致性和可用性,進而取得三者之間的平衡。

自打引入CAP理論的十幾年裡,設計師和研究者已經以它為理論基礎探索了各式各樣新穎的分散式系統,甚至到了濫用的程度。NoSQL運動也將CAP理論當作對抗傳統關係型資料庫的依據。

CAP理論主張任何基於網路的資料共享系統,都最多隻能擁有以下三條中的兩條:

  • 資料一致性(C),等同於所有節點訪問同一份最新的資料副本;
  • 對資料更新具備高可用性(A);
  • 能容忍網路分割槽(P)。

CAP理論的表述很好地服務了它的目的,即開闊設計師的思路,在多樣化的取捨方案下設計出多樣化的系統。在過去的十幾年裡確實湧現了不計其數的新系統,也隨之在資料一致性和可用性的相對關係上產生了相當多的爭論。“三選二”的公式一直存在著誤導性,它會過分簡單化各性質之間的相互關係。現在我們有必要辨析其中的細節。實際上只有“在分割槽存在的前提下呈現完美的資料一致性和可用性”這種很少見的情況是CAP理論不允許出現的。

 

雖然設計師仍然需要在分割槽的前提下對資料一致性和可用性做取捨,但具體如何處理分割槽和恢復一致性,這裡面有不計其數的變通方案和靈活度。當代CAP實踐應將目標定為針對具體的應用,在合理範圍內最大化資料一致性和可用性的“合力”。這樣的思路延伸為如何規劃分割槽期間的操作和分割槽之後的恢復,從而啟發設計師加深對CAP的認識,突破過去由於CAP理論的表述而產生的思維侷限。

為什麼“三選二”公式有誤導性

理解CAP理論的最簡單方式是想象兩個節點分處分割槽兩側。允許至少一個節點更新狀態會導致資料不一致,即喪失了C性質。如果為了保證資料一致性,將分割槽一側的節點設定為不可用,那麼又喪失了A性質。除非兩個節點可以互相通訊,才能既保證C又保證A,這又會導致喪失P性質。一般來說跨區域的系統,設計師無法捨棄P性質,那麼就只能在資料一致性和可用性上做一個艱難選擇。不確切地說,NoSQL運動的主題其實是創造各種可用性優先、資料一致性其次的方案;而傳統資料庫堅守ACID特性(原子性、一致性、隔離性、永續性),做的是相反的事情。下文“ACID、BASE、CAP”小節詳細說明了它們的差異。

事實上,CAP理論本身就是在類似的討論中誕生的。早在1990年代中期,我和同事構建了一系列的基於叢集的跨區域系統(實質上是早期的雲端計算),包括搜尋引擎、快取代理以及內容分發系統1。從收入目標以及合約規定來講,系統可用性是首要目標,因而我們常規會使用快取或者事後校核更新日誌來優化系統的可用性。儘管這些策略提升了系統的可用性,但這是以犧牲系統資料一致性為代價的。

關於“資料一致性 VS 可用性”的第一回合爭論,表現為ACID與BASE之爭2。當時BASE還不怎麼被人們接受,主要是大家看重ACID的優點而不願意放棄。提出CAP理論,目的是證明有必要開拓更廣闊的設計空間,因此才有了“三選二”公式。CAP理論最早在1998年秋季提出,1999年正式發表3,並在2000年登上Symposium on Principles of Distributed Computing大會的主題演講4,最終確立了該理論的正確性。

“三選二”的觀點在幾個方面起了誤導作用,詳見下文“CAP之惑”小節的解釋。首先,由於分割槽很少發生,那麼在系統不存在分割槽的情況下沒什麼理由犧牲C或A。其次,C與A之間的取捨可以在同一系統內以非常細小的粒度反覆發生,而每一次的決策可能因為具體的操作,乃至因為牽涉到特定的資料或使用者而有所不同。最後,這三種性質都可以在程度上衡量,並不是非黑即白的有或無。可用性顯然是在0%到100%之間連續變化的,一致性分很多級別,連分割槽也可以細分為不同含義,如系統內的不同部分對於是否存在分割槽可以有不一樣的認知。

要探索這些細微的差別,就要突破傳統的分割槽處理方式,而這是一項根本性的挑戰。因為分割槽很少出現,CAP在大多數時候允許完美的C和A。但當分割槽存在或可感知其影響的情況下,就要預備一種策略去探知分割槽並顯式處理其影響。這樣的策略應分為三個步驟:探知分割槽發生,進入顯式的分割槽模式以限制某些操作,啟動恢復過程以恢復資料一致性並補償分割槽期間發生的錯誤。

ACID、BASE、CAP

ACID和BASE代表了兩種截然相反的設計哲學,分處一致性-可用性分佈圖譜的兩極。ACID注重一致性,是資料庫的傳統設計思路。我和同事在1990年代晚期提出BASE,目的是抓住當時正逐漸成型的一些針對高可用性的設計思路,並且把不同性質之間的取捨和消長關係擺上檯面。現代大規模跨區域分佈的系統,包括雲在內,同時運用了這兩種思路。

這兩個術語都好記有餘而精確不足,出現較晚的BASE硬湊的感覺更明顯,它是“Basically Available, Soft state, Eventually consistent(基本可用、軟狀態、最終一致性)”的首字母縮寫。其中的軟狀態和最終一致性這兩種技巧擅於對付存在分割槽的場合,並因此提高了可用性。

CAP與ACID的關係更復雜一些,也因此引起更多誤解。其中一個原因是ACID的C和A字母所代表的概念不同於CAP的C和A。還有一個原因是選擇可用性只部分地影響ACID約束。ACID四項特性分別為:

原子性(A)。所有的系統都受惠於原子性操作。當我們考慮可用性的時候,沒有理由去改變分割槽兩側操作的原子性。而且滿足ACID定義的、高抽象層次的原子操作,實際上會簡化分割槽恢復。

一致性(C)。ACID的C指的是事務不能破壞任何資料庫規則,如鍵的唯一性。與之相比,CAP的C僅指單一副本這個意義上的一致性,因此只是ACID一致性約束的一個嚴格的子集。ACID一致性不可能在分割槽過程中保持,因此分割槽恢復時需要重建ACID一致性。推而廣之,分割槽期間也許不可能維持某些不變性約束,所以有必要仔細考慮哪些操作應該禁止,分割槽後又如何恢復這些不變性約束。

隔離性(I)。隔離是CAP理論的核心:如果系統要求ACID隔離性,那麼它在分割槽期間最多可以在分割槽一側維持操作。事務的可序列性(serializability)要求全域性的通訊,因此在分割槽的情況下不能成立。只要在分割槽恢復時進行補償,在分割槽前後保持一個較弱的正確性定義是可行的。

永續性(D)。犧牲永續性沒有意義,理由和原子性一樣,雖然開發者有理由(永續性成本太高)選擇BASE風格的軟狀態來避免實現永續性。這裡有一個細節,分割槽恢復可能因為回退永續性操作,而無意中破壞某項不變性約束。但只要恢復時給定分割槽兩側的永續性操作歷史記錄,破壞不變性約束的操作還是可以被檢測出來並修正的。通常來講,讓分割槽兩側的事務都滿足ACID特性會使得後續的分割槽恢復變得更容易,並且為分割槽恢復時事務的補償工作奠定了基本的條件。

CAP和延遲的聯絡

CAP理論的經典解釋,是忽略網路延遲的,但在實際中延遲和分割槽緊密相關。CAP從理論變為現實的場景發生在操作的間歇,系統需要在這段時間內做出關於分割槽的一個重要決定:

  • 取消操作因而降低系統的可用性,還是
  • 繼續操作,以冒險損失系統一致性為代價

依靠多次嘗試通訊的方法來達到一致性,比如Paxos演算法或者兩階段事務提交,僅僅是推遲了決策的時間。系統終究要做一個決定;無限期地嘗試下去,本身就是選擇一致性犧牲可用性的表現。

因此以實際效果而言,分割槽相當於對通訊的時限要求。系統如果不能在時限內達成資料一致性,就意味著發生了分割槽的情況,必須就當前操作在C和A之間做出選擇。這就從延遲的角度抓住了設計的核心問題:分割槽兩側是否在無通訊的情況下繼續其操作?

從這個實用的觀察角度出發可以匯出若干重要的推論。第一,分割槽並不是全體節點的一致見解,因為有些節點檢測到了分割槽,有些可能沒有。第二,檢測到分割槽的節點即進入分割槽模式——這是優化C和A的核心環節。

最後,這個觀察角度還意味著設計師可以根據期望中的響應時間,有意識地設定時限;時限設得越短,系統進入分割槽模式越頻繁,其中有些時候並不一定真的發生了分割槽的情況,可能只是網路變慢而已。

有時候在跨區域的系統,放棄強一致性來避免保持資料一致所帶來的高延遲是非常有意義的。Yahoo的PNUTS系統因為以非同步的方式維護遠端副本而帶來資料一致性的問題5。但好處是主副本就放在本地,減小操作的等待時間。這個策略在實際中很實用,因為一般來講,使用者資料大都會根據使用者的(日常)地理位置做分割槽。最理想的狀況是每一位使用者都在他的資料主副本附近。

Facebook使用了相反的策略6:主副本被固定在一個地方,因此遠端使用者一般訪問到的是離他較近,但可能已經過時的資料副本。不過當使用者更新其頁面的時候是直接對主副本進行更新,而且該使用者的所有讀操作也被短暫轉向從主副本讀取,儘管這樣延遲會比較高。20秒後,該使用者的流量被重新切換回離他較近的副本,此時副本應該已經同步好了剛才的更新。

CAP之惑

CAP理論經常在不同方面被人誤解,對於可用性和一致性的作用範圍的誤解尤為嚴重,可能造成不希望看到的結果。如果使用者根本獲取不到服務,那麼其實談不上C和A之間做取捨,除非把一部分服務放在客戶端上執行,即所謂的無連線操作或稱離線模式7。離線模式正變得越來越重要。HTML5的一些特性,特別是客戶端持久化儲存特性,將會促進離線操作的發展。支援離線模式的系統通常會在C和A中選擇A,那麼就不得不在長時間處於分割槽狀態後進行恢復。

“一致性的作用範圍”其實反映了這樣一種觀念,即在一定的邊界內狀態是一致的,但超出了邊界就無從談起。比如在一個主分割槽內可以保證完備的一致性和可用性,而在分割槽外服務是不可用的。Paxos演算法和原子性多播(atomic multicast)系統一般符合這樣的場景8。像Google的一般做法是將主分割槽歸屬在單一個數據中心裡面,然後交給Paxos演算法去解決跨區域的問題,一方面保證全域性協商一致(global consensus)如Chubby9,一方面實現高可用的永續性儲存如Megastore10。

分割槽期間,獨立且能自我保證一致性的節點子集合可以繼續執行操作,只是無法保證全域性範圍的不變性約束不受破壞。資料分片(sharding)就是這樣的例子,設計師預先將資料劃分到不同的分割槽節點,分割槽期間單個數據分片多半可以繼續操作。相反,如果被分割槽的是內在關係密切的狀態,或者有某些全域性性的不變性約束非保持不可,那麼最好的情況是隻有分割槽一側可以進行操作,最壞情況是操作完全不能進行。

“三選二”的時候取CA而舍P是否合理?已經有研究者指出了其中的要害——怎樣才算“舍P”含義並不明確11,12。設計師可以選擇不要分割槽嗎?哪怕原來選了CA,當分割槽出現的時候,你也只能回頭重新在C和A之間再選一次。我們最好從概率的角度去理解:選擇CA意味著我們假定,分割槽出現的可能性要比其他的系統性錯誤(如自然災難、併發故障)低很多。

這種觀點在實際中很有意義,因為某些故障組合可能導致同時丟掉C和A,所以說CAP三個性質都是一個度的問題。實踐中,大部分團體認為(位於單一地點的)資料中心內部是沒有分割槽的,因此在單一資料中心之內可以選擇CA;CAP理論出現之前,系統都預設這樣的設計思路,包括傳統資料庫在內。然而就算可能性不高,單一資料中心完全有可能出現分割槽的情況,一旦出現就會動搖以CA為取向的設計基礎。最後,考慮到跨區域時出現的高延遲,在資料一致性上讓步來換取更好效能的做法相對比較常見。

CAP還有一個方面很多人認識不清,那就是放棄一致性其實有隱藏負擔,即需要明確瞭解系統中存在的不變性約束。滿足一致性的系統有一種保持其不變性約束的自然傾向,即便設計師不清楚系統中所有的不變性約束,相當一部分合理的不變性約束會自動地維持下去。相反,當設計師選擇可用性的時候,因為需要在分割槽結束後恢復被破壞的不變性約束,顯然必須將各種不變性約束一一列舉出來,可想而知這件工作很有挑戰又很容易犯錯。放棄一致性為什麼難,其核心還是“併發更新問題”,跟多執行緒程式設計比順序程式設計難的原因是一樣的。

管理分割槽

怎樣緩和分割槽對一致性和可用性的影響是對設計師的挑戰。其關鍵是以非常明確、公開的方式去管理分割槽,不僅需要主動察覺分割槽的發生,還需要為分割槽期間所有可能受侵害的不變性約束預備專門的恢復過程和計劃。管理分割槽有三個步驟:

(點選看大圖)

  • 檢測到分割槽開始
  • 明確進入分割槽模式,限制某些操作,並且
  • 當通訊恢復後啟動分割槽恢復過程

最後一步的目的是恢復一致性,以及補償在系統分割槽期間程式產生的錯誤。

圖1可見分割槽的演變過程。普通的操作都是順序的原子操作,因此分割槽總是在兩筆操作之間開始。一旦系統在操作間歇檢測到分割槽發生,檢測方一側即進入分割槽模式。如果確實發生了分割槽的情況,那麼一般分割槽兩側都會進入到分割槽模式,不過單方面完成分割槽也是可能的。單方面分割槽要求在對方按需要通訊的時候,本方要麼能正確響應,要麼不需要通訊;總之操作不得破壞一致性。但不管怎麼樣,由於檢測方可能有不一致的操作,它必須進入分割槽模式。採取了quorum決定機制的系統即為單方面分割槽的例子。其中一方擁有“法定通過節點數”,因此可以執行操作,而另一方不可以執行操作。支援離線操作的系統明顯地含有“分割槽模式”的概念,一些支援原子多播(atomic multicast)的系統也含有這個概念,如Java平臺的JGroups。

當系統進入到分割槽模式,它有兩種可行的策略。其一是限制部分操作,因此會削弱可用性。其二是額外記錄一些有利於後面分割槽恢復的操作資訊。系統可通過持續嘗試恢復通訊來察覺分割槽何時結束。

哪些操作可以執行?

決定限制哪些操作,主要取決於系統需要維持哪幾項不變性約束。在給定了不變性約束條件之後,設計師需要決定在分割槽模式下,是否堅持不觸動某項不變性約束,抑或以事後恢復為前提去冒險觸犯它。例如,對於“表中鍵的惟一性”這項不變性約束,設計師一般都選擇在分割槽期間放寬要求,容許重複的鍵。重複的鍵很容易在恢復階段檢查出來,假如重複鍵可以合併,那麼設計師不難恢復這項不變性約束。

對於分割槽期間必須維持的不變性約束,設計師應當禁止或改動可能觸犯該不變性約束的操作。(一般而言,我們沒辦法知道操作是否真的會破壞不變性約束,因為無法知道分割槽另一側的狀態。)信用卡扣費等具有外部化特徵的事件常以這種方式工作。適合這種情況的策略,是記錄下操作意圖,然後在分割槽恢復後再執行操作。這類事務往往從屬於一些更大的工作流,在工作流明確含有類似“訂單處理中”狀態的情況下,將操作推遲到分割槽結束並無明顯的壞處。設計師以使用者不易察覺的方式犧牲了可用性。使用者只知道自己下了指令,系統稍後會執行。

說得更概括一點,分割槽模式給使用者介面提出了一種根本性的挑戰,即如何傳達“任務正在進行尚未完成”的資訊。研究者已經從離線操作的角度對此問題進行了一些深入的探索,離線操作可以看成時間很長的一次分割槽。例如Bayou的日曆程式用顏色來區分顯示可能(暫時)不一致的條目13。工作流應用和帶離線模式的雲服務中也常見類似的提醒,前者的例子如交易中的電子郵件通知,後者的例子如Google Docs。

在分割槽模式的討論中,我們將關注點放在有明確意義的原子操作而非單純的讀寫,其中一個原因是操作的抽象級別越高,對不變性約束的影響通常就越容易分析清楚。大體來說,設計師要建立一張所有操作與所有不變性約束的叉乘表格(cross product),觀察並確定其中每一處操作可能與不變性約束相沖突的地方。對於這些衝突情況,設計師必須決定是否禁止、推遲或修改相應的操作。在實踐中,這類決定還受到分割槽前狀態和/或環境引數的影響。例如有的系統為特定的資料設立了主節點,那麼一般允許主節點執行操作,不允許其他節點操作。

對分割槽兩側跟蹤操作歷史的最佳方式是使用版本向量,版本向量可以反映操作間的因果依賴關係。向量的元素是(節點, 邏輯時間)數值對,分別對應一個更新了物件的節點和它最後更新的時間。對於同一物件的兩個給定的版本A和B,當所有結點的版本向量一致有A的時間大於或等於B的時間,且至少有一個節點的版本向量有A的時間較大,則A新於B。

如果不可能對版本向量排序,那麼更新操作是併發的,而且有可能出現不一致的情況。只要知道分割槽兩側版本向量的沿革。系統不難判斷哪些操作的執行順序是確定的,哪些操作是併發的。最近的研究成果證明14,當設計師選擇可用性優先,一般最多隻能將一致性收緊到這樣的程度。

分割槽恢復

到了某個時刻,通訊恢復,分割槽結束。由於每一側在分割槽期間都是可用的,其狀態仍繼續向前進展,但是分割槽會推遲某些操作並侵犯一些不變性約束。分割槽結束的時刻,系統知道分割槽兩側的當前狀態和歷史記錄,因為它在分割槽模式下記錄了詳盡的日誌。當前狀態不如歷史記錄有價值,因為通過歷史記錄,系統可以判斷哪些操作違反了不變性約束,產生了何種外在的後果(如傳送了響應給使用者)。在分割槽恢復過程中,設計師必須解決兩個問題:

  • 分割槽兩側的狀態最終必須保持一致,
  • 並且必須補償分割槽期間產生的錯誤。

通常情況,矯正當前狀態最簡單的解決方法是回退到分割槽開始時的狀態,以特定方式推進分割槽兩側的一系列操作,並在過程中一直保持一致的狀態。Bayou就是這個實現機制,它會回滾資料庫到正確的時刻並按無歧義的、確定性的順序重新執行所有的操作,最終使所有的節點達到相同的狀態15。同樣地,併發版本控制系統CVS在合併分支的時候,也是從從一個共享的狀態一致點開始,逐步將更新合併上去。。

大部分系統都存在不能自動合併的衝突。比如,CVS時不時有些衝突需要手動介入,帶離線模式的wiki系統總是把衝突留在產生的文件裡給使用者處理16。

相反,有些系統用了限制操作的辦法來保證衝突總能合併。一個例子就是Google Docs將其文字編輯操作17精簡為應用樣式、新增文字和刪除文字。因此,雖然總的來說衝突問題不可解,但現實中設計師可以選擇在分割槽期間限制使用部分操作,以便系統在恢復的時候能夠自動合併狀態。如果要實施這種策略,推遲有風險的操作是相對簡單的實現方式。

還有一種辦法是讓操作可以交換順序,這種辦法最接近於形成一種解決自動狀態合併問題的通用框架。此類系統將線性合併各日誌並重排操作的順序,然後執行。操作滿足交換率,意味著操作有可能重新排列成一種全域性一致的最佳順序。不幸的是,只允許滿足交換率的操作這個想法實現起來沒那麼容易。比如加法操作可以交換順序,但是加入了越界檢查的加法就不行了。

Marc Shapiro及其INRIA同事最近的工作18,19對於可交換順序的操作在狀態合併方面的應用起了很大的促進作用。該團隊提出一種從理論上證明可以保證分割槽後合併的資料型別,稱為可交換多副本資料型別(commutative replicated data types,CRDTs)。他們介紹瞭如何使用此類資料結構來

  • 保證分割槽期間進行的所有操作都是可交換順序的,或者
  • 用“格(lattice)”的數學概念來表示資料,並保證相對於“格”來說,分割槽期間的所有操作都是單調遞增的。

用後一種方法合併狀態會彙總分割槽兩邊的最大集合。這種方法是對亞馬遜購物車合併演算法20的形式化總結和改良,合併後的資料是兩邊購物車的並集,而並運算是一種單調的集合運算。這種策略的壞處是刪掉的購物車商品有可能再次出現。

其實CRDTs完全可以實現同時支援增、刪操作的分割槽耐受集合。此方法的本質是維護兩個集合:一個放增加的專案,一個放刪除的專案,兩集合之差即為真正的集合成員。增集合、刪集合分別合併起來都不困難,因而增刪集合之差合併起來也不困難。在某個時間點上,系統可以從兩個集合中清理掉刪除的資料項。假如按照一般的設計,像這種清理操作僅在系統沒分割槽的時候才可行,屬於設計師必須在分割槽期間禁止或推遲的特定操作,但是CRDTs的清理操作並不會對可用性產生外在的影響。因此通過CRDTs來實現狀態,設計師既保證了可用性,又保證了分割槽後系統自動合併狀態。

補償錯誤

比計算分割槽後狀態更難解決的問題是如何彌補分割槽期間造成的錯誤。跟蹤和限制分割槽模式下的操作,這兩種措施足以使設計師確知哪些不變性約束可能被違反,然後分別為它們制定恢復策略。一般系統在分割槽恢復期間檢查違反情況,修復工作也必須在這段時間內完成。

恢復不變性約束的方法有很多,粗陋一點的辦法如“最後寫入者勝”(因此會忽略部分更新),聰明一點的辦法如合併操作和人為跟進事態(human escalation)。人為跟進事態的例子如飛機航班“超售”的情形:可以把乘客登機看作是對之前售票情況的分割槽恢復,必須恢復“座位數不少於乘客數”這項不變性約束。那麼當乘客太多的時候,有些乘客將失去座位,客服最好能設法補償他們。

航班的例子揭示了一個外在錯誤(externalized mistake):假如航空公司沒說過乘客一定有座位,這個問題會好解決得多。因此我們看到推遲有風險的操作的又一個理由——到了分割槽恢復的時候,我們才知道真實的情況。矯正此類錯誤的核心概念是“補償(compensation)”;設計師必須設立補償操作,除了恢復不變性約束,還要糾正外在錯誤。

技術上CRDTs只允許區域性可驗證的不變性約束,所以沒有補償的必要,雖然這種限制降低了CRDTs方法本身的能力。用了CRDTs來處理狀態合併的設計方案可以允許暫時違反全域性性的不變數約束,分割槽結束後才合併狀態,以及履行必要的補償。

恢復外在錯誤通常要求知道一些有關外在輸出的歷史資訊。以“喝醉酒打電話”為例,一位老兄不記得自己昨晚喝高了的時候打過幾個電話,雖然他第二天白天恢復了正常狀態,但通話日誌上的記錄都還在,其中有些通話很可能是錯誤的。撥出的電話就是這位老兄的狀態(喝高了)的外在影響。而由於這位老兄不記得打過什麼電話,也就很難補償其中可能造成的麻煩。

又以機器為例,電腦可能在分割槽期間把一份訂單執行了兩次。如果系統能區分兩份一樣的訂單是有意的還是重複了,它就能取消掉一份重複的訂單。如果這次錯誤產生了外在影響,補償策略可以是自動生成一封電子郵件,向顧客解釋系統意外將訂單執行了兩次,現在錯誤已經被糾正,附上一張優惠券下次可以用。假如沒有完善的歷史記錄,就只好靠顧客親自去發現錯誤了。

曾經有人正式研究過將補償性事務作為處理長壽命事務(long-lived transactions)的一種手段21,22。長時間執行的事務會面臨另一種形態的分割槽決策:是長時間持有鎖來保證一致性比較好呢?還是及早釋放鎖向其他事務暴露未提交的資料,提高併發能力比較好呢?比如在單筆事務中更新所有的員工記錄就是一個典型例子。按照一般的方式序列化這筆事務,將導致所有的記錄都被鎖定,阻止併發。而補償性事務採取另一種方式,它將大事務拆成多個分別提交的子事務。如果要中止大事務,系統必須發起一筆新的、起糾正作用的事務,逐一撤銷所有已經提交的子事務,這筆新事務就是所謂的補償性事務。

總的來說,補償性事務的目的是避免中止其他用了未正確提交資料的事務(即不允許級聯取消)。這種方案不依賴序列化或隔離的手段來保障正確性,其正確性取決於事務序列對狀態和輸出所產生的淨影響。那麼,經過補償,資料庫的狀態究竟是不是相當於那些子事務根本沒執行過一樣呢?考慮等價必須連外在行為也包括在內;舉個例子,把重複扣取的交易款退還給顧客,很難說成等於一開始就沒多收顧客的錢,但從結果上看勉強算扯平了。分割槽恢復也延續同樣的思路。雖然服務不一定總能直接撤銷其錯誤,但起碼承認錯誤並做出新的補償行為。怎樣在分割槽恢復中運用這種思路效果最好,這個問題沒有固定的答案。“自動櫃員機上的補償問題”小節以一個很小的應用領域為例點出了一些思考方向。

當系統中存在分割槽,系統設計師不應該盲目地犧牲一致性或可用性。運用以上討論的方法,設計師通過細緻地管理分割槽期間的不變性約束,兩方面的性質都可以取得最佳的表現。隨著版本向量和CRDTs等比較新的技術逐漸被納入一些簡化其用法的框架,這方面的優化手段會得到比較普遍的應用。但引入CAP實踐畢竟不像引入ACID事務那麼簡單,實施的時候需要對過去的策略進行全面的考慮,最佳的實施方案極大地依賴於具體服務的不變性約束和操作細節。

自動櫃員機上的補償問題

以自動櫃員機(ATM)的設計來說,強一致性看似符合邏輯的選擇,但現實情況是可用性遠比一致性重要。理由很簡單:高可用性意味著高收入。不管怎麼樣,討論如何補償分割槽期間被破壞的不變性約束,ATM的設計很適合作為例子。

ATM的基本操作是存款、取款、檢視餘額。關鍵的不變性約束是餘額應大於或等於零。因為只有取款操作會觸犯這項不變性約束,也就只有取款操作將受到特別對待,其他兩種操作隨時都可以執行。

ATM系統設計師可以選擇在分割槽期間禁止取款操作,因為在那段時間裡沒辦法知道真實的餘額,當然這樣會損害可用性。現代ATM的做法正相反,在stand-in模式下(即分割槽模式),ATM限制淨取款額不得高於k,比如k為$200。低於限額的時候,取款完全正常;當超過限額的時候,系統拒絕取款操作。這樣,ATM成功將可用性限制在一個合理的水平上,既允許取款操作,又限制了風險。

分割槽結束的時候,必須有一些措施來恢復一致性和補償分割槽期間系統所造成的錯誤。狀態的恢復比較簡單,因為操作都是符合交換率的,補償就要分幾種情況去考慮。最後的餘額低於零違反了不變性約束。由於ATM已經把錢吐出去了,錯誤成了外部實在。銀行的補償辦法是收取透支費並指望顧客償還。因為風險已經受到限制,問題並不嚴重。還有一種情況是分割槽期間的某一刻餘額已經小於零(但ATM不知道),此時一筆存款重新將餘額變為正的。銀行可以追溯產生透支費,也可以因為顧客已經繳付而忽略該違反情況。

總而言之,因為通訊延遲的存在,銀行系統不依靠一致性來保證正確性,而更多地依靠審計和補償。“空頭支票詐騙”也是類似的例子,顧客趕在多家分行對賬之前分別取出錢來然後逃跑。透支的錯誤過後才會被發現,對錯誤的補償也許體現為法律行動的形式。

致謝

感謝Mike Dahlin、Hank Korth、Marc Shapiro、Justin Sheehy、Amin Vahdat、Ben Zhao以及IEEE Computer Society的志願者們,感謝他們對本文的有益反饋。

作者簡介

Eric Brewer是University of California, Berkeley的電腦科學教授,在Google擔任基礎設施方面的VP。他的研究興趣包括雲端計算、可伸縮的伺服器、感測器網路,還有適合發展中地區應用的技術。他還幫助建立了美國聯邦政府的入口網站USA.gov。Brewer從MIT獲得電子工程和電腦科學的博士學位。他是National Academy of Engineering的院士。聯絡方式:[email protected]