請不要再稱數(shù)據(jù)庫是CP或者AP
在 Jeff Hodges 精彩的博客文章《給年輕人關(guān)于分布式系統(tǒng)的筆記》中,他建議我們用CAP定理來評論系統(tǒng)。 很多人都聽取了這個(gè)建議,描述他們的系統(tǒng)為“CP”(有一致性但在網(wǎng)絡(luò)分區(qū)的時(shí)候不可用),“AP”(可用但是在網(wǎng)絡(luò)分區(qū)的時(shí)候不一致)或者有時(shí)候“CA”(說明“我還沒有讀過Coda的五年前的文章”)。
我同意Jeff的所有觀點(diǎn)。唯獨(dú)他關(guān)于CAP定理的觀點(diǎn),我必須表示不同意。CAP定理本身太簡單化而且被廣泛的誤解,以至于在描述系統(tǒng)上沒有太多用處。因此我請求我們不要再引用CAP定理,不要再討論CAP定理。取而代之,我們應(yīng)該用更精確的術(shù)語來理解我們系統(tǒng)的權(quán)衡。
(沒錯,我意識到很諷刺的是我不希望別人再討論這個(gè)話題,但我卻正在一篇關(guān)于這個(gè)話題的博客文章。但是至少這樣以后別人問我為什么不喜歡討論CAP定理的時(shí)候,我可以把這篇文章的鏈接給他。還有,抱歉這篇文章有些吐槽,但是至少這個(gè)吐槽有文獻(xiàn)引用。)
CAP用的是非常精確的定義
如果你想引用CAP作為一個(gè)定理(而不是一個(gè)模糊的,用來做數(shù)據(jù)庫市場營銷的概念),你需要用非常精確的定義。數(shù)學(xué)要求精確。只有當(dāng)你的用詞和定理的證明中的定義是一樣的時(shí)候,這個(gè)證明才有意義。CAP的證明用的是非常具體的定義。
- 一致性(Consistency)在CAP中是可線性化的意思(linearizability)。而這個(gè)是非常特殊(而且非常強(qiáng))的一致性。尤其是雖然ACID中的C也是一致性(Consistency),但是和這里的一致性沒有任何關(guān)系。我會在后面解釋可線性化是什么意思。
- 可用性(Availability)在CAP中是定義為“每一個(gè)請求(request)如果被一個(gè)工作中的[數(shù)據(jù)庫]節(jié)點(diǎn)收到,那一定要返回[非錯誤]的結(jié)果”。注意到,這里一部分節(jié)點(diǎn)可以處理這個(gè)請求是不充分的。任意一個(gè)工作中的節(jié)點(diǎn)都要可以處理這個(gè)請求。所以很多自稱“高度可用”的系統(tǒng)通常并沒有滿足這里的可用性的定義。
- 分區(qū)容錯(Partition Tolerance)基本上就是說通信是在異步的網(wǎng)絡(luò)中。信息是可能延遲送達(dá)或者被丟失的。互聯(lián)網(wǎng)還有我們所有的數(shù)據(jù)中心都有這個(gè)屬性。所以我們在這件事上并沒有選擇。
還有就是注意到CAP并沒有描述任意一個(gè)老的系統(tǒng),而是一個(gè)非常特殊的系統(tǒng):
- CAP系統(tǒng)的模型是一個(gè)只能讀寫單個(gè)數(shù)據(jù)的寄存器。這就是全部。CAP沒有提到任何關(guān)于關(guān)系到多個(gè)事物(object)的事務(wù)(transaction)。他們根本就不在這個(gè)定理的范圍之內(nèi),除非你可以把這些問題約化到一個(gè)單個(gè)寄存器的問題。
- CAP定理只考慮了網(wǎng)絡(luò)分區(qū)這一種故障情況(比如節(jié)點(diǎn)們還在運(yùn)行,但是他們之間的網(wǎng)絡(luò)已經(jīng)不工作了)。這種故障絕對會發(fā)生,但是這不是唯一會出故障的地方。節(jié)點(diǎn)可以整個(gè)崩潰(crash)或者重啟,你可能沒有足夠的磁盤空間,你可能會遇到一個(gè)軟件故障(bug),等等。在建分布式系統(tǒng)的時(shí)候,你需要考慮到更多得多的問題。如果太關(guān)注CAP就容易導(dǎo)致忽略了其他重要的問題。
- 還有CAP根本沒有提到延遲(latency)。而常常人們其實(shí)對關(guān)心延遲比可用性更多。事實(shí)上,滿足CAP可用性的系統(tǒng)可以花任意長的時(shí)間來回復(fù)一個(gè)請求,而且同時(shí)保持可用性這個(gè)屬性。我來冒險(xiǎn)說一句,我猜如果你的系統(tǒng)要花兩分鐘來加載一個(gè)頁面,你的用戶是不會稱它是“可用的”。
如果你的用詞是符合CAP證明中的精確定義的,那么它對你來說是適用的。但是如果你的一致性還有可用性是有其他意思的,那么你不能期待CAP對你還是適用的。當(dāng)然,這并不意味著你通過重新定義一些詞匯就可以做到一些不可能的事情!這只是說你不能靠CAP來給你提供指導(dǎo)方向,而且你不能通過CAP來為你的觀點(diǎn)來辯解。
如果CAP定理不適用,那么這就意味著你必須自己來考慮取舍。你必須根據(jù)你自己對一致性還有可用性的定義來思考這些屬性,而且你能證明自己的定理就更好了。但是請不要稱它為CAP定理,因?yàn)檫@個(gè)名字已經(jīng)被用了。
可線性化
如果你對可線性化不是很熟悉(也就是CAP中的一致性),那么讓我來簡短地解釋一下。正式的定義不是特別直觀,但是關(guān)鍵的思想用非正式的描述就是:
如果B操作在成功完成A操作之后,那么整個(gè)系統(tǒng)對B操作來說必須表現(xiàn)為A操作已經(jīng)完成了或者更新的狀態(tài)。
為了可以解釋的更清楚一些,讓我們來看一個(gè)例子。在這個(gè)例子中的系統(tǒng)并不是可線性化的。
看下面這個(gè)圖(我還沒有發(fā)行的書的預(yù)覽):
這張圖展示了Alice還有Bob,他們在同一個(gè)房間,都在看他們的手機(jī)查2014年世界杯的決賽結(jié)果。就在最終結(jié)果剛發(fā)布之后,Alice刷新了頁面,看到了宣布冠軍,而且很興奮地告訴了Bob。Bob馬上也重新加載了他手機(jī)上的頁面,但是他的請求被送到了一個(gè)數(shù)據(jù)庫的拷貝,還沒有拿到最新的數(shù)據(jù),結(jié)果他的手機(jī)上顯示決賽還正在進(jìn)行。
如果Alice和Bob同時(shí)刷新,拿到了不一樣的結(jié)果,并不會太讓人意外。因?yàn)樗麄儾恢谰唧w服務(wù)器到底是先處理了他們中哪一個(gè)請求。但是Bob知道他刷新頁面是在Alice告訴了他最終結(jié)果_之后_的。所以他預(yù)期他查詢的結(jié)果一定比Alice的更新。事實(shí)是,他卻拿到了舊的結(jié)果。這就違反了可線性化。
只有Bob通過另外一個(gè)溝通渠道從Alice那里知道了結(jié)果,Bob才能知道他的請求一定在Alice之后。如果Bob沒有從Alice那里聽到比賽已經(jīng)結(jié)束了,他就不會知道他看到的結(jié)果是舊的。
如果你在建一個(gè)數(shù)據(jù)庫,你不知道用戶們會有什么另外的溝通渠道。所以,如果你想提供可線性化(CAP的一致性),你就需要讓你的數(shù)據(jù)庫看起來就好像只有一個(gè)拷貝,雖然實(shí)際上可能有多個(gè)備份在多個(gè)地方。
這是一個(gè)非常昂貴的屬性,因?yàn)樗竽阕龊芏鄥f(xié)調(diào)工作。甚至你電腦上的CPU都不提供本地內(nèi)存的可線性化訪問!在現(xiàn)代的CPU上,你需要用memory barrier 指令來達(dá)到可線性化訪問。甚至測試一個(gè)系統(tǒng)是不是可線性化的也是很困難的。
CAP可用性
讓我們來簡短的討論一下為什么在網(wǎng)絡(luò)分區(qū)的情況下,我們要放棄可用性和一致性中的一個(gè)。
舉個(gè)例子,你的數(shù)據(jù)庫有兩個(gè)拷貝在兩個(gè)不同的數(shù)據(jù)中心。具體怎么做備份并不重要,可以是single-master,或者多個(gè)leader,或者基于quorum的備份(Dynamo使用的方式)。要求是當(dāng)數(shù)據(jù)被寫到一個(gè)數(shù)據(jù)中心的時(shí)候,他也一定要被寫到另一個(gè)數(shù)據(jù)中心。假設(shè)client只連接到其中一個(gè)數(shù)據(jù)中心,而且連接兩個(gè)數(shù)據(jù)中心的網(wǎng)絡(luò)故障了。
那么現(xiàn)在假設(shè)網(wǎng)絡(luò)中斷了,這就是我們所說的網(wǎng)絡(luò)分區(qū)的意思。接下來怎么樣呢?
顯然你有兩個(gè)選擇:
- 你的應(yīng)用還是被允許寫到數(shù)據(jù)庫,所以兩邊的數(shù)據(jù)庫還是完全可用的。但是一旦兩個(gè)數(shù)據(jù)庫之間的網(wǎng)絡(luò)中斷了,任何一個(gè)數(shù)據(jù)中心的寫操作就不會在另一個(gè)數(shù)據(jù)中心出現(xiàn)。這就違反了可線性化(用之前的例子,Alice可能鏈接到了一號數(shù)據(jù)中心,而Bob連接到了二號數(shù)據(jù)中心)。
- 如果你不想失去可線性化,你就必須保證你的讀寫操作都在同一個(gè)數(shù)據(jù)中心,你可能叫這它leader。另一個(gè)數(shù)據(jù)中心,因?yàn)榫W(wǎng)絡(luò)故障不能被更新,就必須停止接收讀寫操作,直到網(wǎng)絡(luò)恢復(fù),兩邊數(shù)據(jù)庫又同步了之后。所以雖然非leader的數(shù)據(jù)庫在正常運(yùn)行著,但是他卻不能處理請求,這就違反了CAP的可用性定義。
(而這個(gè),其實(shí)就是CAP定理的證明。這就是全部了。這里的例子用到了兩個(gè)數(shù)據(jù)中心,但是對于一個(gè)數(shù)據(jù)中心內(nèi)的網(wǎng)絡(luò)故障也是同樣適用的。我只是覺得用兩個(gè)數(shù)據(jù)中心這樣更容易考慮這個(gè)問題。)
注意到上面第二點(diǎn),就算它違反了CAP的可用性,但我們還是在成功地處理著請求。所以當(dāng)一個(gè)系統(tǒng)選擇了可線性化(也就是說不是CAP可用的),這并不一定意味著網(wǎng)絡(luò)分區(qū)一定會造成應(yīng)用停運(yùn)。如果你可以把用戶的流量轉(zhuǎn)移到leader數(shù)據(jù)庫,那么用戶根本就不會注意到任何問題。
實(shí)際應(yīng)用中的可用性和CAP可用性并不相同。你應(yīng)用的可用性多數(shù)是通過SLA來衡量的(比如99.9%的正確的請求一定要在一秒鐘之內(nèi)返回成功),但是一個(gè)系統(tǒng)無論是否滿足CAP可用性其實(shí)都可以滿足這樣的SLA。
實(shí)際操作中,跨多個(gè)數(shù)據(jù)中心的系統(tǒng)經(jīng)常是通過異步備份(asynchronous replication)的,所以不是可線性化的。但是做出這個(gè)選擇的原因經(jīng)常是因?yàn)檫h(yuǎn)距離網(wǎng)絡(luò)的延遲,而不是僅僅為了處理數(shù)據(jù)中心的網(wǎng)絡(luò)故障。
很多系統(tǒng)既不是可線性化的也不是CAP可用的
在CAP對可用性還有一致性嚴(yán)格的定義下,系統(tǒng)們表現(xiàn)怎么樣?
拿任意一個(gè)single master的有備份的數(shù)據(jù)庫作為一個(gè)例子。這也是標(biāo)準(zhǔn)的數(shù)據(jù)庫設(shè)置。在這種情況下,如果用戶不能訪問leader,就不能寫到數(shù)據(jù)庫。雖然他還能從follower那里讀到數(shù)據(jù),但是他不能寫任何數(shù)據(jù)就說明它不是CAP可用的。更不要說這種設(shè)置還常常聲稱自己是“高可用的(high availablity)”。
如果以上這種設(shè)置不是CAP可用的,那是不是就是說他滿足CP(一致)?等一下。如果你是從follower那里讀到的數(shù)據(jù),因?yàn)閭浞菔钱惒降?,所以你可能讀到舊的數(shù)據(jù)。所以你的讀操作不是可線性化的,所以不滿足CAP中的一致性。
而且支持snapshot isolation/MVCC的數(shù)據(jù)庫是故意做成不可線性化的。否則會降低數(shù)據(jù)庫的并發(fā)性。比如PostgreSQL的SSI提供的是可串行化而不是可線性化,Oracle兩者都不支持。僅僅因?yàn)閿?shù)據(jù)庫標(biāo)榜自己是ACID并不意味著它就滿足CAP中的一致性。
所以這些系統(tǒng)既不是CAP一致的,也不是CAP可用的。他們既不是CP也不是AP,他們只是P,不管這是什么意思。(是的,“三選二”也允許你只從三個(gè)中選一個(gè),甚至一個(gè)都不選!)
那NoSQL怎么樣的?拿MongoDB作為一個(gè)例子:每一個(gè)shard都只有一個(gè)leader(至少只要他不在split-brain的模式下,它應(yīng)該是這樣的),根據(jù)以上的論證,那就說明他不是CAP可用的。而且Kyle最近發(fā)現(xiàn),設(shè)置了最強(qiáng)的一致性,他還是允許非一致性的讀操作,所以它也不是CAP一致的。
那像Riak,Cassandra還有Voldemort這些聲稱是AP的高可用的Dynamo的繼承者們又怎么樣呢?這取決于你的設(shè)置。如果你接受讀寫只訪問一個(gè)拷貝(R=W=1),那么這確實(shí)是CAP可用的。但是如果你要求quorum讀寫(R+W>N),而且你有網(wǎng)絡(luò)分區(qū),那么那些被分在少部分節(jié)點(diǎn)的用戶就不能達(dá)到quorum,所以quorum操作不是CAP可用的(至少暫時(shí)是不可用的,直到你在少部分的分區(qū)內(nèi)加入了更多的節(jié)點(diǎn))。
你有時(shí)候會看到人們聲稱quorum讀寫可以保證可線性化,但是我覺得依賴這樣的聲明是不明智的。因?yàn)樵谝恍?fù)雜的情況下,read repair操作和sloppy quorum同時(shí)發(fā)生,就有可能會重寫已經(jīng)被刪除了的數(shù)據(jù)?;蛘弋?dāng)備份數(shù)(replicas)已經(jīng)低于原來的W值(違反了quorum的條件),或者當(dāng)備份數(shù)被加到了高于原來的N值(還是違反了quorum的條件),這些都可以導(dǎo)致不可線性化的訪問結(jié)果。
這些都不是差的系統(tǒng):他們在實(shí)際運(yùn)用中都很成功。但是目前為止,我們還是不能嚴(yán)格把他們分類為AP或者CP,要么是因?yàn)槿Q于具體的設(shè)定,或者是因?yàn)檫@個(gè)系統(tǒng)一致性和可用性都不滿足。
案例分析:ZooKeeper
那ZooKeeper又怎么樣呢?他用了consensus算法,所以人們一般認(rèn)為他是很清楚的選擇了一致性而放棄了可用性(也就是CP系統(tǒng))。
但是如果你閱讀ZooKeeper的文檔,他們很清楚的說了ZooKeeper的默認(rèn)設(shè)置不提供可線性化的讀操作。每一個(gè)連接到一個(gè)服務(wù)器的客戶端,當(dāng)你要讀的時(shí)候,即使別的節(jié)點(diǎn)有更新的數(shù)據(jù),你只能看到那個(gè)服務(wù)器本地的數(shù)據(jù)。這樣讀操作就比需要收集quorum或者訪問leader要更快。但這也說明ZooKeeper默認(rèn)不滿足CAP的一致性定義。
做可線性化的讀操作在ZooKeeper中是支持的。你需要在讀操作之前發(fā)一個(gè)sync命令。但這不是默認(rèn)的設(shè)置,因?yàn)檫@樣讀操作會更慢。人們有時(shí)候會用sync命令,但一般不會是所有的讀操作都用。
那ZooKeeper的可用性呢?他要求達(dá)到大多數(shù)quorum,來達(dá)到共識,才能處理一個(gè)寫操作。如果你有網(wǎng)絡(luò)分區(qū),一邊有大多數(shù)節(jié)點(diǎn),一邊有少部分節(jié)點(diǎn)。那么擁有大多數(shù)節(jié)點(diǎn)的分區(qū)還可以繼續(xù)工作,但是少部分節(jié)點(diǎn)的分區(qū)就算節(jié)點(diǎn)們都正常工作著,還是不能處理寫操作。所以ZooKeeper得寫操作在網(wǎng)絡(luò)分區(qū)的情況下,不滿足CAP的可用性(即使擁有大多數(shù)節(jié)點(diǎn)的分區(qū)還是可以處理寫操作的)。
更有意思的是,ZooKeeper 3.4.0還加入了一個(gè)只讀的模式。在這個(gè)模式下,少部分節(jié)點(diǎn)的分區(qū)還可以繼續(xù)處理讀操作 -- 不需要quorum! 這個(gè)讀操作是滿足CAP可用性的。所以ZooKeeper默認(rèn)設(shè)置既不是一致的(CP)也不是可用的(AP),只是“P”。但是你有選擇通過用sync命令來讓它成為CP。并且在正確的設(shè)置下,讀操作(不包括寫)其實(shí)是CAP可用的。
這讓人不是很舒服。如果就因?yàn)閆ooKeeper的默認(rèn)設(shè)置不是可線性化的就稱他為不一致,那就歪曲了他的功能。他其實(shí)可以提供非常強(qiáng)的一致性!他支持atomic broadcast(這個(gè)可以約化為共識問題)以及每個(gè)session的causal consistency -- 這比read your writes, monotonic reads還有consistent prefix reads在一起都要強(qiáng)。他的文檔上說ZooKeeper提供可串行化的一致性,但這其實(shí)是過于謙虛了,因?yàn)樗鋵?shí)可以提供更強(qiáng)的一致性。
根據(jù)ZooKeeper的例子,你就會發(fā)現(xiàn)就算這系統(tǒng)在網(wǎng)絡(luò)分區(qū)的時(shí)候既不是CP也不是AP(甚至在默認(rèn)設(shè)置下,就算沒有網(wǎng)絡(luò)分區(qū),也不是可線性化的),但他還是很合理的。(我猜ZK在Abadi的PACELC的框架下是PC/EL,但我不覺得這比CAP更有啟發(fā)性。)
CP/AP:一個(gè)偽二分法
事實(shí)上我們都沒有成功地把一個(gè)數(shù)據(jù)庫無歧義地分類為AP或者CP。這應(yīng)該告訴我們CP/AP根本就不是合適的用來描述系統(tǒng)的標(biāo)簽。
我相信我們應(yīng)該不要再把數(shù)據(jù)庫歸類為AP或者CP了,因?yàn)椋?/p>
- 在同一個(gè)軟件內(nèi),你可能有多個(gè)一致性屬性的選擇
- 很多系統(tǒng)在CAP的定義下,既不是一致也不可用。然而我從來沒有聽到別人稱這些系統(tǒng)為"P",可能是因?yàn)檫@樣不太好看。但這并不差,他很可能是完全合理的設(shè)計(jì),他只是不在CP/AP這兩個(gè)分類中。
- 雖然大部分軟件都不在CP/AP這兩類中,但人們還是強(qiáng)行把軟件分為這兩類。這就導(dǎo)致了,為了適用,不可避免地改變對“一致性”或者“可用性”的定義。不幸的是,如果用詞的定義改變了,CAP定理自己也不適用了,那CP/AP區(qū)分也就完全沒有意義了。
- 把系統(tǒng)分為這兩類,導(dǎo)致了很多細(xì)節(jié)被忽略。在考慮分布式系統(tǒng)設(shè)計(jì)的時(shí)候,會有很多關(guān)于容錯,延遲,簡單模型,運(yùn)行成本,等等的考慮。把那么多細(xì)節(jié)編碼到一個(gè)比特的信息,顯然是不可能的。比如說雖然ZooKeeper有一個(gè)AP的只讀模式,但這個(gè)模式也提供對所有寫操作的total ordering。這比Riak或者Cassandra這些AP系統(tǒng)提供的保障要強(qiáng)得多。所以簡單地把他們都?xì)w為AP一個(gè)類別就顯得很不合理。
- 甚至Eric Brewer承認(rèn)CAP是一個(gè)容易誤導(dǎo)人的而且過于簡化的模型。在2000年,CAP的意義在于讓大家開始討論關(guān)于分布式系統(tǒng)的取舍。他在這方面做得很好。但是他不是用來作為一個(gè)正式的突破性的結(jié)果,也不是一個(gè)嚴(yán)格的數(shù)據(jù)系統(tǒng)的分類方式。15年之后,我們已經(jīng)有了多得多的有不一樣一致性和容錯模型的系統(tǒng)。CAP已經(jīng)完成了他自己的使命,現(xiàn)在是時(shí)候不要在糾結(jié)了。
學(xué)會獨(dú)立思考
如果CP和AP用來描述和評論系統(tǒng)是不合適的,那么我們應(yīng)該用什么呢?我不認(rèn)為有一個(gè)唯一的答案。很多人花了很多心思考慮這些問題,也提出了術(shù)語和模型來幫助我們理解這些問題。想要學(xué)習(xí)這些思想,你就需要更深入自己閱讀文獻(xiàn)。
- 一個(gè)很好的起點(diǎn)就是Doug Terry的論文。其中他用棒球來解釋了各種不一樣的最終一致性??勺x性很強(qiáng),而且就算對像我這樣不是美國人而且完全不懂棒球也解釋的很清晰。
- 如果你對transaction的isolation模型有興趣(這和分布式系統(tǒng)的一致性不一樣,但是相關(guān)),我的小項(xiàng)目Hermitage你可以看一下。
- 這篇論文討論了分布式系統(tǒng)的一致性和transaction的isolation以及可用性之間的關(guān)系。(這篇論文也描述了不同一致性之間的分級。Kyle Kingsbury很喜歡給別人講這個(gè)。)
- 當(dāng)你讀到過這些了以后,你應(yīng)該已經(jīng)準(zhǔn)備好深入閱讀論文。我在這篇文章中加入了很多對文獻(xiàn)的引用。去看一下,很多專家已經(jīng)幫你把很多問題都已經(jīng)解決了。
- 作為最后的手段,如果你不想讀論文原文,我建議你看一下我的書。這本書用通俗易懂的方式總結(jié)了大多數(shù)重要的思想。(你看,我已經(jīng)竟可能的讓這篇文章看上去不是用來推銷我的書的。)
- 如果你想學(xué)跟多關(guān)于怎么正確使用ZooKeeper,F(xiàn)lavio Junqueira 還有 Benjamin Reed的書是非常不錯的。
不管你選擇哪一種學(xué)習(xí)方式,我都鼓勵你保持好奇心和耐心,因?yàn)檫@不是容易的學(xué)科。但是這是有回報(bào)的,因?yàn)槟銓W(xué)會如果考慮取舍,進(jìn)而搞清楚什么樣的架構(gòu)對于你的應(yīng)用是最合適的。但是不管你做什么,請不要再說CP還有AP了,因?yàn)楦静缓侠怼?/p>