1. 程式人生 > >CAP理論與MongoDB一致性、可用性的一些思考

CAP理論與MongoDB一致性、可用性的一些思考

  大約在五六年前,第一次接觸到了當時已經是hot topic的NoSql。不過那個時候學的用的都是mysql,Nosql對於我而言還是新事物,並沒有真正使用,只是不明覺厲。但是印象深刻的是這麼一張圖片(後來google到圖片來自這裡):

  

    這張圖片是講資料庫(包括傳統的關係型資料庫和NOSQL)與CAP理論的關係。由於並NoSql並沒有實踐經驗,也沒有去深入瞭解,對於CAP理論更是一知半解。因此,為什麼某一款資料庫被劃分到哪一個陣營,並不清楚。     工作之後對MongoDB使用得比較多,有了一定的瞭解,前段時間又看到了這張圖,於是想搞清楚,MongoDB是不是真的屬於CP陣營,又是為什麼?懷疑這個問題的初衷是因為,MongoDB的經典(官方推薦)部署架構中都會使用replica set,而replica set通過冗餘和自動failover提供高可用性(Availability),那麼為什麼上圖中說MongoDB犧牲了Avalability呢?而我在MongoDB的官方文件中搜索“CAP”,並沒有搜尋到任何內容。於是我想自己搞清楚這個疑問,給自己一個答案。   本文先闡明什麼是CAP理論,以及關於CAP理論的一些文章,然後討論MongoDB在一致性與可用性之間的折中與權衡。

CAP理論

  對CAP理論我只知道這三個單詞的意思,其解釋也是來自網上的一些文章,並不一定準確。所以首先得追根溯源,搞清楚這個理論的起源和準確的解釋。我覺得最好的開始就是wikipedia,從上面可以看到比較準確的介紹,更為重要的是可以看到很多有用的連結,比如CAP理論的出處,發展演變過程。   CAP理論是說對於分散式資料儲存,最多隻能同時滿足一致性(C,Consistency)、可用性(A, Availability)、分割槽容錯性(P,Partition Tolerance)中的兩者。   一致性,是指對於每一次讀操作,要麼都能夠讀到最新寫入的資料,要麼錯誤。   可用性,是指對於每一次請求,都能夠得到一個及時的、非錯的響應,但是不保證請求的結果是基於最新寫入的資料。   分割槽容錯性,是指由於節點之間的網路問題,即使一些訊息對包或者延遲,整個系統能繼續提供服務(提供一致性或者可用性)。   一致性、可用性都是使用非常寬泛的術語,在不同的語義環境下具體所指是不一樣的,比如在
cap-twelve-years-later-how-the-rules-have-changed
一文中Brewer就指出“CAP中的一致性與ACID中的一致性並不是同一個問題”,因此後文中除非特別說明,所提到的一致性、可用性都是指在CAP理論中的定義。只有明確了大家都是在同樣的上下文環境,討論才有意義。   對於分散式系統,網路分割槽(network partition)這種情況是難以避免的,節點間的資料複製一定存在延遲,如果需要保證一致性(對所有讀請求都能夠讀到最新寫入的資料),那麼勢必在一定時間內是不可用的(不能讀取),即犧牲了可用性,反之亦然。   按照維基百科上的描述,CAP之間的相互關係大約起源於1998年,Brewer在2000年的PODC(
Symposium on Principles of Distributed Computing
)上展示了CAP猜想[3],在2002年,由另外兩名科學家Seth Gilbert、Nancy Lynch證明了Brewer的猜想,從而從猜想變成了定理[4]

CAP理論起源

  在Towards Robust Distributed Systems 中,CAP理論的提出者Brewer指出:在分散式系統中,計算是相對容易的,真正困難的是狀態的維護。那麼對於分散式儲存或者說資料共享系統,資料的一致性保證也是比較困難的。對於傳統的關係型資料庫,優先考慮的是一致性而不是可用性,因此提出了事務的ACID特性。而對於許多分散式儲存系統,則是更看重可用性而不是一致性,一致性通過BASE(Basically Available, Soft state, Eventual consistency)來保證。下面這張圖展示了ACID與BASE的區別:   

  簡而言之:BASE通過最終一致性來儘量保證服務的可用性。注意圖中最後一句話“But I think it‘s a spectrum”,就是說ACID BASE只是一個度的問題,並不是對立的兩個極端。

  

  如上圖所示,N1,N2兩個節點儲存同一份資料V,當前的狀態是V0。在節點N1上執行的是安全可靠的寫演算法A,在節點N2執行的是同樣可靠的讀演算法B,即N1節點負責寫操作,N2節點負責讀操作。N1節點寫入的資料也會自動向N2同步,同步的訊息稱之為M。如果N1,N2之間出現分割槽,那麼就沒法保證訊息M在一定的時間內到達N2。

  從事務的角度來看這各問題

  

   α這個事務由操作α1, α2組成,其中α1是寫資料,α2是讀資料。如果是單點,那麼很容易保證α2能讀到α1寫入的資料。如果是分散式的情況的情況,除非能控制 α2的發生時間,否則無法保證 α2能讀到 α1寫入的資料,但任何的控制(比如阻塞,資料集中化等)要麼破壞了分割槽容錯性,要麼損失了可用性。

  另外,這邊文章指出很多情況下 availability比consistency重要,比如對於facebook google這樣的網站,短暫的不可用就會帶來巨大的損失。

  2010年的這篇文章brewers-cap-theorem-on-distributed-systems/,用了三個例子來闡述CAP,分別是example1:單點的mysql;example2:兩個mysql,但不同的mysql儲存不同的資料子集(類似sharding);example3:兩個mysql,對A的一個insert操作,需要在B上執行成功才認為操作完成(類似複製集)。作者認為在example1和example2上 都能保證強一致性,但不能保證可用性;在example3這個例子,由於分割槽(partition)的存在,就需要在一致性與可用性之間權衡。

  於我看來,討論CAP理論最好是在“分散式儲存系統”這個大前提下,可用性也不是說整體服務的可用性,而是分散式系統中某個子節點的可用性。因此感覺上文的例子並不是很恰當。

CAP理論發展

  文章中,最主要的觀點是CAP理論並不是說三者不需選擇兩者。首先,雖然只要是分散式系統,就可能存在分割槽,但分割槽出現的概率是很小的(否則就需要去優化網路或者硬體),CAP在大多數時候允許完美的C和A;只有在分割槽存在的時間段內,才需要在C與A之間權衡。其次,一致性和可用性都是一個度的問題,不是0或者1的問題,可用性可以在0%到100%之間連續變化,一致性分為很多級別(比如在casandra,可以設定consistency level)。因此,當代CAP實踐的目標應該是針對具體的應用,在合理範圍內最大化資料一致性和可用性的效力。   文章中還指出,分割槽是一個相對的概念,當超過了預定的通訊時限,即系統如果不能在時限內達成資料一致性,就意味著發生了分割槽的情況,必須就當前操作在C和A之間做出選擇。
  從收入目標以及合約規定來講,系統可用性是首要目標,因而我們常規會使用快取或者事後校核更新日誌來優化系統的可用性。因此,當設計師選擇可用性的時候,因為需要在分割槽結束後恢復被破壞的不變性約。   實踐中,大部分團體認為(位於單一地點的)資料中心內部是沒有分割槽的,因此在單一資料中心之內可以選擇CA;CAP理論出現之前,系統都預設這樣的設計思路,包括傳統資料庫在內。   分割槽期間,獨立且能自我保證一致性的節點子集合可以繼續執行操作,只是無法保證全域性範圍的不變性約束不受破壞。資料分片(sharding)就是這樣的例子,設計師預先將資料劃分到不同的分割槽節點,分割槽期間單個數據分片多半可以繼續操作。相反,如果被分割槽的是內在關係密切的狀態,或者有某些全域性性的不變性約束非保持不可,那麼最好的情況是隻有分割槽一側可以進行操作,最壞情況是操作完全不能進行。

  上面摘錄中下選線部分跟MongoDB的sharding情況就很相似,MongoDB的sharded cluste模式下,shard之間在正常情況下,是無需相互通訊的。

  (1)可用性一般是在不同的機器之間通過資料的複製來實現

  (2)一致性需要在允許讀操作之間同時更新幾個節點

  (3)temporary partion,即幾點之間的通訊延遲是可能發生了,此時就需要在A 和 C之間權衡。但只有在發生分割槽的時候才需要考慮權衡。

  在分散式系統中,網路分割槽一定會發生,因此“it is really just A vs C!”

MongoDB與CAP        

        在《通過一步步建立sharded cluster來認識MongoDB》一文中,對MongoDB的特性做了一些介紹,包括高效能、高可用、可擴充套件(水平伸縮),其中,MongoDB的高可用性依賴於replica set的複製與自動failover。對MongoDB資料庫的使用有三種模式:standalone,replica set, shareded cluster,在前文中詳細介紹了shared cluster的搭建過程。

  standalone就是單個mongod,應用程式直接連線到這個Mongod,在這種情況下無分割槽容錯性可言,也一定是強一致性的。對於sharded cluster,每一個shard也都推薦是一個replica set。MongoDB中的shards維護的是獨立的資料子集,因此shards之間出現了分割槽影響不大(在chunk遷移的過程可能還是有影響),因此也主要考慮的是shard內部replica set的分割槽影響。所以,本文中討論MongoDB的一致性、可用性問題,針對的也是MongoDB的replica set。

  對於replica set,只有一個primary節點,接受寫請求和讀請求,其他的secondary節點接受讀請求。這是一個單寫、多讀的情況,比多讀、多寫的情況還是簡化了許多。後文為了討論,也是假設replica set由三個基點組成,一個primary,兩個secondary,且所有節點都持久化資料(data-bearing)

  MongoDB關於一致性、可用性的權衡,取決於三者:write-concern、read-concern、read-preference。下面主要是MongoDB3.2版本的情況,因為read-concern是在MongoDB3.2版本中才引入的。

write-concern:

  write concern表示對於寫操作,MongoDB在什麼情況下給予客戶端響應。包括下面三個欄位:

  { w: <value>, j: <boolean>, wtimeout: <number> }

  w: 表示當寫請求在value個MongoDB例項處理之後才向客戶端返回。取值範圍:

    1:預設值,表示資料寫入到standalone的MongoDB或者replica set的primary之後返回

    0:不用寫入就直接向客戶端返回,效能高,但可能丟資料。不過可以配合j:True來增加資料的可永續性(durability)

    >1: 只有在replica set環境下才有用,如果value大於的replica set中節點的數目,那麼可能導致阻塞

    ‘majority’: 當資料寫入到replica set的大多數節點之後向客戶端返回,對於這種情況,一般是配合read-concern使用:

    After the write operation returns with a w: "majority" acknowledgement to the client, the client can read the result of that write with a "majority" readConcern

  j:表示當寫請求在寫入journal之後才向客戶端返回,預設為False。兩點注意:

    如果在對於未開啟journaling的MongoDB例項使用j:True,會報錯

    在MongoDB3.2及之後,對於w>1, 需要所有例項都寫到journal之後才返回

  wtimeout:表示寫入的超時時間,即在指定的時間(number),如果還不能向客戶端返回(w大於1的情況),那麼返回錯誤

    預設為0,相當於沒有設定該選項

  

read-reference:

  在前文已經講解過,一個replica set由一個primary和多個secondary組成。primary接受寫操作,因此資料一定是最新的,secondary通過oplog來同步寫操作,因此資料有一定的延遲。對於時效性不是很敏感的查詢業務,可以從secondary節點查詢,以減輕叢集的壓力。

  

  MongoDB指出在不同的情況下選用不同的read-reference,非常靈活。MongoDB driver支援一下幾種read-reference:

  primary:預設模式,一切讀操作都路由到replica set的primary節點

  primaryPreferred:正常情況下都是路由到primary節點,只有當primary節點不可用(failover)的時候,才路由到secondary節點。

  secondary:一切讀操作都路由到replica set的secondary節點

  secondaryPreferred:正常情況下都是路由到secondary節點,只有當secondary節點不可用的時候,才路由到primary節點。

  nearest:從延時最小的節點讀取資料,不管是primary還是secondary。對於分散式應用且MongoDB是多資料中心部署,nearest能保證最好的data locality。

  如果使用secondary或者secondaryPreferred,那麼需要意識到:

  (1) 因為延時,讀取到的資料可能不是最新的,而且不同的secondary返回的資料還可能不一樣;

  (2) 對於預設開啟了balancer的sharded collection,由於還未結束或者異常終止的chunk遷移,secondary返回的可能是有缺失或者多餘的資料

  (3) 在有多個secondary節點的情況下,選擇哪一個secondary節點呢,簡單來說是“closest”即平均延時最小的節點,具體參加Server Selection Algorithm 

read-concern:

  read concern是在MongoDB3.2中才加入的新特性,表示對於replica set(包括sharded cluster中使用複製集的shard)返回什麼樣的資料。不同的儲存引擎對read-concern的支援情況也是不一樣的

  read concern有以下三個level:

  local:預設值,返回當前節點的最新資料,當前節點取決於read reference。

  majority:返回的是已經被確認寫入到多數節點的最新資料。該選項的使用需要以下條件: WiredTiger儲存引擎,且使用election protocol version 1;啟動MongoDB例項的時候指定 

  linearizable:3.4版本中引入,這裡略過了,感興趣的讀者參考文件。

  在文章中有這麼一句話:

Regardless of the read concern level, the most recent data on a node may not reflect the most recent version of the data in the system.

  就是說,即便使用了read concern:majority, 返回的也不一定是最新的資料,這個和NWR理論並不是一回事。究其根本原因,在於最終返回的數值只來源於一個MongoDB節點,該節點的選擇取決於read reference。

  在這篇文章中,對readconcern的引入的意義以及實現有詳細介紹,在這裡只引用核心部分:

readConcern 的初衷在於解決『髒讀』的問題,比如使用者從 MongoDB 的 primary 上讀取了某一條資料,但這條資料並沒有同步到大多數節點,然後 primary 就故障了,重新恢復後 這個primary 節點會將未同步到大多數節點的資料回滾掉,導致使用者讀到了『髒資料』。

當指定 readConcern 級別為 majority 時,能保證使用者讀到的資料『已經寫入到大多數節點』,而這樣的資料肯定不會發生回滾,避免了髒讀的問題。

 一致性 or 可用性?

  回顧一下CAP理論中對一致性 可用性的問題:
  一致性,是指對於每一次讀操作,要麼都能夠讀到最新寫入的資料,要麼錯誤。
  可用性,是指對於每一次請求,都能夠得到一個及時的、非錯的響應,但是不保證請求的結果是基於最新寫入的資料。

  前面也提到,本文對一致性 可用性的討論是基於replica set的,是否是shared cluster並不影響。另外,討論是基於單個客戶端的情況,如果是多個客戶端,似乎是隔離性的問題,不屬於CAP理論範疇。基於對write concern、read concern、read reference的理解,我們可以得出以下結論。

  • 預設情況(w:1、readconcern:local)如果read preference為primary,那麼是可以讀到最新的資料,強一致性;但如果此時primary故障,那麼這個時候會返回錯誤,可用性得不到保證
  • 預設情況(w:1、readconcern:local)如果read preference為secondary(secondaryPreferred、primaryPreferred),雖然可能讀到過時的資料,但能夠立刻得到資料,可用性比較好
  • writeconern:majority保證寫入的資料不會被回滾; readconcern:majority保證讀到的一定是不會被回滾的資料
  • 若(w:1、readconcern;majority)即使是從primary讀取,也不能保證一定返回最新的資料,因此是弱一致性
  • 若(w: majority、readcocern:majority),如果是從primary讀取,那麼一定能讀到最新的資料,且這個資料一定不會被回滾,但此時寫可用性就差一些;如果是從secondary讀取,不能保證讀到最新的資料,弱一致性。


  回過來來看,MongoDB所說的高可用性是更普世意義上的可用性:通過資料的複製和自動failover,即使發生物理故障,整個叢集還是能夠在短時間內回覆,繼續工作,何況恢復也是自動的。在這個意義上,確實是高可用的。