請不要再稱數據庫是CP或者AP
在 Jeff Hodges 精彩的博客文章《給年輕人關于分布式系統的筆記》中,他建議我們用CAP定理來評論系統。 很多人都聽取了這個建議,描述他們的系統為“CP”(有一致性但在網絡分區的時候不可用),“AP”(可用但是在網絡分區的時候不一致)或者有時候“CA”(說明“我還沒有讀過Coda的五年前的文章”)。
我同意Jeff的所有觀點。唯獨他關于CAP定理的觀點,我必須表示不同意。CAP定理本身太簡單化而且被廣泛的誤解,以至于在描述系統上沒有太多用處。因此我請求我們不要再引用CAP定理,不要再討論CAP定理。取而代之,我們應該用更精確的術語來理解我們系統的權衡。
(沒錯,我意識到很諷刺的是我不希望別人再討論這個話題,但我卻正在一篇關于這個話題的博客文章。但是至少這樣以后別人問我為什么不喜歡討論CAP定理的時候,我可以把這篇文章的鏈接給他。還有,抱歉這篇文章有些吐槽,但是至少這個吐槽有文獻引用。)
CAP用的是非常精確的定義
如果你想引用CAP作為一個定理(而不是一個模糊的,用來做數據庫市場營銷的概念),你需要用非常精確的定義。數學要求精確。只有當你的用詞和定理的證明中的定義是一樣的時候,這個證明才有意義。CAP的證明用的是非常具體的定義。
- 一致性(Consistency)在CAP中是可線性化的意思(linearizability)。而這個是非常特殊(而且非常強)的一致性。尤其是雖然ACID中的C也是一致性(Consistency),但是和這里的一致性沒有任何關系。我會在后面解釋可線性化是什么意思。
- 可用性(Availability)在CAP中是定義為“每一個請求(request)如果被一個工作中的[數據庫]節點收到,那一定要返回[非錯誤]的結果”。注意到,這里一部分節點可以處理這個請求是不充分的。任意一個工作中的節點都要可以處理這個請求。所以很多自稱“高度可用”的系統通常并沒有滿足這里的可用性的定義。
- 分區容錯(Partition Tolerance)基本上就是說通信是在異步的網絡中。信息是可能延遲送達或者被丟失的?;ヂ摼W還有我們所有的數據中心都有這個屬性。所以我們在這件事上并沒有選擇。
還有就是注意到CAP并沒有描述任意一個老的系統,而是一個非常特殊的系統:
- CAP系統的模型是一個只能讀寫單個數據的寄存器。這就是全部。CAP沒有提到任何關于關系到多個事物(object)的事務(transaction)。他們根本就不在這個定理的范圍之內,除非你可以把這些問題約化到一個單個寄存器的問題。
- CAP定理只考慮了網絡分區這一種故障情況(比如節點們還在運行,但是他們之間的網絡已經不工作了)。這種故障絕對會發生,但是這不是唯一會出故障的地方。節點可以整個崩潰(crash)或者重啟,你可能沒有足夠的磁盤空間,你可能會遇到一個軟件故障(bug),等等。在建分布式系統的時候,你需要考慮到更多得多的問題。如果太關注CAP就容易導致忽略了其他重要的問題。
- 還有CAP根本沒有提到延遲(latency)。而常常人們其實對關心延遲比可用性更多。事實上,滿足CAP可用性的系統可以花任意長的時間來回復一個請求,而且同時保持可用性這個屬性。我來冒險說一句,我猜如果你的系統要花兩分鐘來加載一個頁面,你的用戶是不會稱它是“可用的”。
如果你的用詞是符合CAP證明中的精確定義的,那么它對你來說是適用的。但是如果你的一致性還有可用性是有其他意思的,那么你不能期待CAP對你還是適用的。當然,這并不意味著你通過重新定義一些詞匯就可以做到一些不可能的事情!這只是說你不能靠CAP來給你提供指導方向,而且你不能通過CAP來為你的觀點來辯解。
如果CAP定理不適用,那么這就意味著你必須自己來考慮取舍。你必須根據你自己對一致性還有可用性的定義來思考這些屬性,而且你能證明自己的定理就更好了。但是請不要稱它為CAP定理,因為這個名字已經被用了。
可線性化
如果你對可線性化不是很熟悉(也就是CAP中的一致性),那么讓我來簡短地解釋一下。正式的定義不是特別直觀,但是關鍵的思想用非正式的描述就是:
如果B操作在成功完成A操作之后,那么整個系統對B操作來說必須表現為A操作已經完成了或者更新的狀態。
為了可以解釋的更清楚一些,讓我們來看一個例子。在這個例子中的系統并不是可線性化的。
看下面這個圖(我還沒有發行的書的預覽):
這張圖展示了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只連接到其中一個數據中心,而且連接兩個數據中心的網絡故障了。
那么現在假設網絡中斷了,這就是我們所說的網絡分區的意思。接下來怎么樣呢?
顯然你有兩個選擇:
- 你的應用還是被允許寫到數據庫,所以兩邊的數據庫還是完全可用的。但是一旦兩個數據庫之間的網絡中斷了,任何一個數據中心的寫操作就不會在另一個數據中心出現。這就違反了可線性化(用之前的例子,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的論文。其中他用棒球來解釋了各種不一樣的最終一致性??勺x性很強,而且就算對像我這樣不是美國人而且完全不懂棒球也解釋的很清晰。
- 如果你對transaction的isolation模型有興趣(這和分布式系統的一致性不一樣,但是相關),我的小項目Hermitage你可以看一下。
- 這篇論文討論了分布式系統的一致性和transaction的isolation以及可用性之間的關系。(這篇論文也描述了不同一致性之間的分級。Kyle Kingsbury很喜歡給別人講這個。)
- 當你讀到過這些了以后,你應該已經準備好深入閱讀論文。我在這篇文章中加入了很多對文獻的引用。去看一下,很多專家已經幫你把很多問題都已經解決了。
- 作為最后的手段,如果你不想讀論文原文,我建議你看一下我的書。這本書用通俗易懂的方式總結了大多數重要的思想。(你看,我已經竟可能的讓這篇文章看上去不是用來推銷我的書的。)
- 如果你想學跟多關于怎么正確使用ZooKeeper,Flavio Junqueira 還有 Benjamin Reed的書是非常不錯的。
不管你選擇哪一種學習方式,我都鼓勵你保持好奇心和耐心,因為這不是容易的學科。但是這是有回報的,因為你學會如果考慮取舍,進而搞清楚什么樣的架構對于你的應用是最合適的。但是不管你做什么,請不要再說CP還有AP了,因為根本不合理。