這可能是我看過最通俗也是最深刻的CAP理論

數據庫 文章 數學 軟件 市場營銷 51CTO傳媒 2019-06-10

本文的原作者是 Martin Kleppmann, 著有《Designing Data-Intensive Applications》一書,無論是這本書還是這篇文章,都能站在一個獨特的視角去闡釋那些可能被大多數人誤解的理念,讓讀者醍醐灌頂。


這可能是我看過最通俗也是最深刻的CAP理論


在此之前我就隱約對文中提到的一些 CAP 誤解嗤之以鼻,這篇文章讓我更加確信了之前零碎的認知,不誇張地講,這應該是我看過的最通俗也是最深刻的 CAP 科普文。

在 Jeff Hodges 精彩的博客文章給年輕人關於分佈式系統的筆記中,他建議我們用 CAP 定理來評論系統。

很多人都聽取了這個建議,描述他們的系統為"CP" (有一致性但在網絡分區的時候不可用),“AP”(可用但是在網絡分區的時候不一致) 或者有時候 "CA" (說明"我還沒有讀過 Coda 的五年前的文章")。

我同意 Jeff 的所有觀點,唯獨他關於 CAP 定理的觀點,我必須表示不同意。

CAP 定理本身太簡單化而且被廣泛的誤解,以至於在描述系統上沒有太多用處。

因此我請求我們不要再引用 CAP 定理,不要再討論 CAP 定理。取而代之,我們應該用更精確的術語來理解我們系統的權衡。

PS:沒錯,我意識到很諷刺的是我不希望別人再討論這個話題,但我卻正在分享一篇關於這個話題的博客文章。

但是至少這樣以後別人問我為什麼不喜歡討論 CAP 定理的時候,我可以把這篇文章的鏈接給他。還有,抱歉這篇文章有些吐槽,但是至少這個吐槽有文獻引用。

CAP 用的是非常精確的定義

如果你想引用 CAP 作為一個定理(而不是一個模糊的,用來做數據庫市場營銷的概念),你需要用非常精確的定義。

數學要求精確,只有當你的用詞和定理的證明中的定義是一樣的時候,這個證明才有意義。


這可能是我看過最通俗也是最深刻的CAP理論


CAP 的證明用的是非常具體的定義:

  • 一致性(Consistency):在 CAP 中是可線性化的意思(linearizability)。而這個是非常特殊(而且非常強)的一致性。

尤其是雖然 ACID 中的 C 也是一致性(Consistency),但是和這裡的一致性沒有任何關係。我會在後面解釋可線性化是什麼意思。

  • 可用性(Availability):在 CAP 中是定義為"每一個請求(request)如果被一個工作中的[數據庫]節點收到,那一定要返回[非錯誤]的結果"。

注意到,這裡一部分節點可以處理這個請求是不充分的。任意一個工作中的節點都要可以處理這個請求。所以很多自稱"高度可用"的系統通常並沒有滿足這裡的可用性的定義。

  • 分區容錯(Partition Tolerance):基本上就是說通信是在異步的網絡中。信息是可能延遲送達或者被丟失的。互聯網還有我們所有的數據中心都有這個屬性。所以我們在這件事上並沒有選擇。

還有就是注意到 CAP 並沒有描述任意一個老的系統,而是一個非常特殊的系統:

  • CAP 系統的模型是一個只能讀寫單個數據的寄存器。這就是全部。CAP 沒有提到任何關於關係到多個事物(Object)的事務(Transaction)。

他們根本就不在這個定理的範圍之內,除非你可以把這些問題約化到一個單個寄存器的問題。

  • CAP 定理只考慮了網絡分區這一種故障情況(比如節點們還在運行,但是他們之間的網絡已經不工作了)。這種故障絕對會發生,但是這不是唯一會出故障的地方。

節點可以整個崩潰(Crash)或者重啟,你可能沒有足夠的磁盤空間,你可能會遇到一個軟件故障(Bug),等等。

在建分佈式系統的時候,你需要考慮到更多得多的問題。如果太關注 CAP 就容易導致忽略了其他重要的問題。

  • 還有 CAP 根本沒有提到延遲(Latency)。而常常人們其實對關心延遲比可用性更多。

事實上,滿足 CAP 可用性的系統可以花任意長的時間來回復一個請求,而且同時保持可用性這個屬性。

我來冒險說一句,我猜如果你的系統要花兩分鐘來加載一個頁面,你的用戶是不會稱它是“可用的”。

如果你的用詞是符合 CAP 證明中的精確定義的,那麼它對你來說是適用的。但是如果你的一致性還有可用性是有其他意思的,那麼你不能期待 CAP 對你還是適用的。

當然,這並不意味著你通過重新定義一些詞彙就可以做到一些不可能的事情!這只是說你不能靠 CAP 來給你提供指導方向,而且你不能通過 CAP 來為你的觀點來辯解。

如果 CAP 定理不適用,那麼這就意味著你必須自己來考慮取捨。你必須根據你自己對一致性還有可用性的定義來思考這些屬性,而且你能證明自己的定理就更好了。但是請不要稱它為 CAP 定理,因為這個名字已經被用了。

可線性化

如果你對可線性化不是很熟悉(也就是 CAP 中的一致性),那麼讓我來簡短地解釋一下。

正式的定義不是特別直觀,但是關鍵的思想用非正式的描述就是:

如果 B 操作在成功完成 A 操作之後,那麼整個系統對 B 操作來說必須表現為 A 操作已經完成了或者更新的狀態。

為了可以解釋的更清楚一些,讓我們來看一個例子。在這個例子中的系統並不是可線性化的。

看下面這個圖:


這可能是我看過最通俗也是最深刻的CAP理論


這張圖展示了 Alice 還有 Bob, 他們在同一個房間,都在用他們的手機查詢 2014 年世界盃的決賽結果。

就在最終結果剛發佈之後,Alice 刷新了頁面,看到了宣佈冠軍的消息,而且很興奮地告訴了 Bob。

Bob 馬上也重新加載了他手機上的頁面,但是他的請求被送到了一個數據庫的拷貝,還沒有拿到最新的數據,結果他的手機上顯示決賽還正在進行。

如果 Alice 和 Bob 同時刷新,拿到了不一樣的結果,並不會太讓人意外。因為他們不知道具體服務器到底是先處理了他們中哪一個請求。

但是 Bob 知道他刷新頁面是在 Alice 告訴了他最終結果之後的。所以他預期他查詢的結果一定比 Alice 的更新。事實是,他卻拿到了舊的結果。這就違反了可線性化。

只有 Bob 通過另外一個溝通渠道從 Alice 那裡知道了結果, Bob 才能知道他的請求一定在 Alice 之後。

如果 Bob 沒有從 Alice 那裡聽到比賽已經結束了,他就不會知道他看到的結果是舊的。

如果你在建一個數據庫,你不知道用戶們會有什麼另外的溝通渠道。所以,如果你想提供可線性化(CAP 的一致性),你就需要讓你的數據庫看起來就好像只有一個拷貝,雖然實際上可能有多個備份在多個地方。

這是一個非常昂貴的屬性,因為它要求你做很多協調工作。甚至你電腦上的 CPU 都不提供本地內存的可線性化訪問!

在現代的 CPU 上,你需要用 Memory Barrier 指令來達到可線性化訪問。甚至測試一個系統是不是可線性化的也是很困難的。

CAP 可用性

讓我們來簡短的討論一下為什麼在網絡分區的情況下,我們要放棄可用性和一致性中的一個。

舉個例子,你的數據庫有兩個拷貝在兩個不同的數據中心。具體怎麼做備份並不重要,可以是 Single-Master,或者多個 Leader,或者基於 Quorum 的備份(Dynamo 使用的方式)。

要求是當數據被寫到一個數據中心的時候,他也一定要被寫到另一個數據中心。

假設 Client 只連接到其中一個數據中心,而且連接兩個數據中心的網絡故障了。

那麼現在假設網絡中斷了,這就是我們所說的網絡分區的意思。接下來怎麼樣呢?


這可能是我看過最通俗也是最深刻的CAP理論


顯然你有兩個選擇:

  • 你的應用還是被允許寫到數據庫,所以兩邊的數據庫還是完全可用的。但是一旦兩個數據庫之間的網絡中斷了,任何一個數據中心的寫操作就不會在另一個數據中心出現。

這就違反了可線性化(用之前的例子,Alice 可能鏈接到了一號數據中心,而 Bob 連接到了二號數據中心)。

  • 如果你不想失去可線性化,你就必須保證你的讀寫操作都在同一個數據中心,你可能叫它 Leader。

另一個數據中心,因為網絡故障不能被更新,就必須停止接收讀寫操作,直到網絡恢復,兩邊數據庫又同步了之後。

所以雖然非 Leader 的數據庫在正常運行著,但是他卻不能處理請求,這就違反了 CAP 的可用性定義。

而這個,其實就是 CAP 定理的證明。這就是全部了。這裡的例子用到了兩個數據中心,但是對於一個數據中心內的網絡故障也是同樣適用的。我只是覺得用兩個數據中心這樣更容易考慮這個問題。

注意到上面第二點,就算它違反了 CAP 的可用性,但我們還是在成功地處理著請求。

所以當一個系統選擇了可線性化(也就是說不是 CAP 可用的),這並不一定意味著網絡分區一定會造成應用停運。

如果你可以把用戶的流量轉移到 Leader 數據庫,那麼用戶根本就不會注意到任何問題。

實際應用中的可用性和 CAP 可用性並不相同。你應用的可用性多數是通過 SLA 來衡量的(比如 99.9% 的正確的請求一定要在一秒鐘之內返回成功)。

但是一個系統無論是否滿足 CAP 可用性其實都可以滿足這樣的 SLA。實際操作中,跨多個數據中心的系統經常是通過異步備份(Asynchronous Replication)的,所以不是可線性化的。

但是做出這個選擇的原因經常是因為遠距離網絡的延遲,而不是僅僅為了處理數據中心的網絡故障。

很多系統既不是可線性化的也不是 CAP 可用的

在 CAP 對可用性還有一致性嚴格的定義下,系統們表現怎麼樣?

拿任意一個 Single Master 的有備份的數據庫作為一個例子。這也是標準的數據庫設置。

在這種情況下,如果用戶不能訪問 Leader,就不能寫到數據庫。雖然他還能從 Follower 那裡讀到數據,但是他不能寫任何數據就說明它不是 CAP 可用的。更不要說這種設置還常常聲稱自己是“高可用的(High Availablity)”。

如果以上這種設置不是 CAP 可用的,那是不是就是說他滿足 CP(一致)?

等一下,如果你是從 Follower 那裡讀到的數據,因為備份是異步的,所以你可能讀到舊的數據。所以你的讀操作不是可線性化的,所以不滿足 CAP 中的一致性。

而且支持 Snapshot Isolation/MVCC 的數據庫是故意做成不可線性化的。否則會降低數據庫的併發性。

比如 PostgreSQL 的 SSI 提供的是可串行化而不是可線性化,Oracle 兩者都不支持。僅僅因為數據庫標榜自己是 ACID 並不意味著它就滿足 CAP 中的一致性。

所以這些系統既不是 CAP 一致的,也不是 CAP 可用的。他們既不是 CP 也不是 AP,他們只是 P,不管這是什麼意思。(是的,“三選二”也允許你只從三個中選一個,甚至一個都不選!)

那 NoSQL 怎麼樣的?拿 MongoDB 作為一個例子:每一個 Shard 都只有一個 Leader(至少只要他不在 split-brain 的模式下,它應該是這樣的),根據以上的論證,那就說明他不是 CAP 可用的。

而且 Kyle 最近發現,設置了最強的一致性,他還是允許非一致性的讀操作,所以它也不是 CAP 一致的。

那像 Riak,Cassandra 還有 Voldemort 這些聲稱是 AP 的高可用的 Dynamo 的繼承者們又怎麼樣呢?

這取決於你的設置。如果你接受讀寫只訪問一個拷貝(R=W=1),那麼這確實是 CAP 可用的。

但是如果你要求 Quorum 讀寫(R+W>N),而且你有網絡分區,那麼那些被分在少部分節點的用戶就不能達到 Quorum。

所以 Quorum 操作不是 CAP 可用的(至少暫時是不可用的,直到你在少部分的分區內加入了更多的節點)。

你有時候會看到人們聲稱 Quorum 讀寫可以保證可線性化,但是我覺得依賴這樣的聲明是不明智的。

因為在一些複雜的情況下,Read Repair 操作和 Sloppy Quorum 同時發生,就有可能會重寫已經被刪除了的數據。

或者當備份數(Replicas)已經低於原來的 W 值(違反了 Quorum 的條件),或者當備份數被加到了高於原來的 N 值(還是違反了 Quorum 的條件),這些都可以導致不可線性化的訪問結果。

這些都不是差的系統:他們在實際運用中都很成功。但是目前為止,我們還是不能嚴格把他們分類為 AP 或者 CP,要麼是因為取決於具體的設定,或者是因為這個系統一致性和可用性都不滿足。

案例分析:ZooKeeper

那 ZooKeeper 又怎麼樣呢?他用了 Consensus 算法,所以人們一般認為他是很清楚的選擇了一致性而放棄了可用性(也就是 CP 系統)。

但是如果你閱讀 ZooKeeper 的文檔,他們很清楚的說了 ZooKeeper 的默認設置不提供可線性化的讀操作。

每一個連接到一個服務器的客戶端,當你要讀的時候,即使別的節點有更新的數據,你只能看到那個服務器本地的數據。

這樣讀操作就比需要收集 Quorum 或者訪問 Leader 要更快。但這也說明 ZooKeeper 默認不滿足 CAP 的一致性定義。

做可線性化的讀操作在 ZooKeeper 中是支持的。你需要在讀操作之前發一個 Sync 命令。

但這不是默認的設置,因為這樣讀操作會更慢。人們有時候會用 Sync 命令,但一般不會是所有的讀操作都用。

那 ZooKeeper 的可用性呢?他要求達到大多數 Quorum,來達到共識,才能處理一個寫操作。

如果你有網絡分區,一邊有大多數節點,一邊有少部分節點。那麼擁有大多數節點的分區還可以繼續工作,但是少部分節點的分區就算節點們都正常工作著,還是不能處理寫操作。

所以 ZooKeeper 的寫操作在網絡分區的情況下,不滿足 CAP 的可用性(即使擁有大多數節點的分區還是可以處理寫操作的)。

更有意思的是,ZooKeeper 3.4.0 還加入了一個只讀的模式。在這個模式下,少部分節點的分區還可以繼續處理讀操作,不需要 Quorum!

這個讀操作是滿足 CAP 可用性的。所以 ZooKeeper 默認設置既不是一致的(CP)也不是可用的(AP),只是"P"。

但是你有選擇通過用 Sync 命令來讓它成為 CP。並且在正確的設置下,讀操作(不包括寫)其實是 CAP 可用的。

這讓人不是很舒服。如果就因為 ZooKeeper 的默認設置不是可線性化的就稱他為不一致,那就歪曲了他的功能。

他其實可以提供非常強的一致性!他支持 Atomic Broadcast(這個可以約化為共識問題)以及每個 Session 的 Causal Consistency。

這比 read your writes,monotonic reads 還有 consistent prefix reads 在一起都要強。

他的文檔上說 ZooKeeper 提供可串行化的一致性,但這其實是過於謙虛了,因為他其實可以提供更強的一致性。

根據 ZooKeeper 的例子,你就會發現就算這系統在網絡分區的時候既不是 CP 也不是 AP(甚至在默認設置下,就算沒有網絡分區,也不是可線性化的),但他還是很合理的。

我猜 ZK 在 Abadi 的 PACELC 的框架下是 PC/EL,但我不覺得這比 CAP 更有啟發性。

CP/AP:一個偽二分法

事實上我們都沒有成功地把一個數據庫無歧義地分類為 AP 或者 CP。這應該告訴我們 CP/AP 根本就不是合適的用來描述系統的標籤。

我相信我們應該不要再把數據庫歸類為 AP 或者 CP 了,因為:

在同一個軟件內,你可能有多個一致性屬性的選擇。

  • 很多系統在 CAP 的定義下,既不是一致也不可用。然而我從來沒有聽到別人稱這些系統為"P",可能是因為這樣不太好看。但這並不差,他很可能是完全合理的設計,他只是不在 CP/AP 這兩個分類中。
  • 雖然大部分軟件都不在 CP/AP 這兩類中,但人們還是強行把軟件分為這兩類。這就導致了,為了適用,不可避免地改變對“一致性”或者“可用性”的定義。

不幸的是,如果用詞的定義改變了,CAP 定理自己也不適用了,那 CP/AP 區分也就完全沒有意義了。

  • 把系統分為這兩類,導致了很多細節被忽略。在考慮分佈式系統設計的時候,會有很多關於容錯,延遲,簡單模型,運行成本,等等的考慮。把那麼多細節編碼到一個比特的信息,顯然是不可能的。

比如說雖然 ZooKeeper 有一個 AP 的只讀模式,但這個模式也提供對所有寫操作的 total ordering。

這比 Riak 或者 Cassandra 這些 AP 系統提供的保障要強得多。所以簡單地把他們都歸為 AP 一個類別就顯得很不合理。

  • 甚至 Eric Brewer 承認 CAP 是一個容易誤導人的而且過於簡化的模型。在 2000 年,CAP 的意義在於讓大家開始討論關於分佈式系統的取捨。

他在這方面做得很好,但是他不是用來作為一個正式的突破性的結果,也不是一個嚴格的數據系統的分類方式。

15 年之後,我們已經有了多得多的有不一樣一致性和容錯模型的系統。CAP 已經完成了他自己的使命,現在是時候不要在糾結了。

學會獨立思考

如果用 CP 和 AP 來描述和評論系統是不合適的,那麼我們應該用什麼呢?我不認為有一個唯一的答案。

很多人花了很多心思考慮這些問題,也提出了術語和模型來幫助我們理解這些問題。

想要學習這些思想,你就需要更深入自己閱讀文獻:

  • 一個很好的起點就是 Doug Terry 的論文。其中他用棒球來解釋了各種不一樣的最終一致性。可讀性很強,而且就算對像我這樣不是美國人而且完全不懂棒球也解釋的很清晰。
  • 如果你對 Transaction 的 Isolation 模型有興趣(這和分佈式系統的一致性不一樣,但是相關),我的小項目 Hermitage 你可以看一下。
  • 這篇論文討論了分佈式系統的一致性和 Transaction 的 Isolation 以及可用性之間的關係。(這篇論文也描述了不同一致性之間的分級。Kyle Kingsbury 很喜歡給別人講這個。)


這可能是我看過最通俗也是最深刻的CAP理論


  • 當你讀到過這些了以後,你應該已經準備好深入閱讀論文。我在這篇文章中加入了很多對文獻的引用。去看一下,很多專家已經幫你把很多問題都已經解決了。
  • 作為最後的手段,如果你不想讀論文原文,我建議你看一下我的書。這本書用通俗易懂的方式總結了大多數重要的思想。
  • 如果你想學更多關於怎麼正確使用 ZooKeeper,Flavio Junqueira 還有 Benjamin Reed 的書是非常不錯的。

不管你選擇哪一種學習方式,我都鼓勵你保持好奇心和耐心,因為這不是容易的學科。

但是這是有回報的,因為你學會如果考慮取捨,進而搞清楚什麼樣的架構對於你的應用是最合適的。

但是不管你做什麼,請不要再說 CP 還有 AP 了,因為根本不合理。

最後,謝謝 Kyle Kingsbury 還有 Camille Fournier 對於這篇文章初稿的評論。當然,所有的錯誤還有不受歡迎的觀點都是我本人的。

作者:Martin Kleppmann,Lu Pan譯來源:博客

相關推薦

推薦中...