分佈式系統與網絡分區

在OpenStack中,數據庫是主要系統“狀態”的主要來源。大部分Core Projects都使用傳統關係型數據庫作爲系統數據和狀態的存儲,另外如Ceilometer使用了MongoDB,還有其他Incubator Projects使用了Redis作爲隊列或者狀態存儲。數據庫給OpenStack提供了狀態組件並把狀態的“共享”問題交給了數據庫,因此解決OpenStack的擴展問題實際上就是解決使用的數據庫本身的擴展問題。比如OpenStack HA Solution最令人頭疼的就是傳統關係數據庫或者其他數據存儲的擴展問題,數據庫擴展問題的根源是其本身不支持分佈式和良好的擴展性,而這個根源又會衍生出分佈式系統最大的噩夢–“網絡分區”。

下面會分析”網絡分區“給數據庫擴展帶來的問題,同時在OpenStack組件中如何規避和解決。

分佈式系統的基本: 數據一致性

現代軟件系統由一系列“組件”通過異步、不可靠的網絡相互溝通構建。理解一個可信賴的分佈式系統需要對網絡本身的分析,而“狀態”共享就是一個最重要的問題。

舉一個例子,當你發表一篇博文後,你可能想知道在你點擊“發佈”操作之後:
1. 從現在開始會對所有人可見
2. 從現在開始會對你的連接可見,其他人會延遲
3. 你也可能暫時不可見,但是未來會可見
4. 現在可見或者不可見:發生錯誤
5. …… etc

不同的分佈式系統會有對一致性和持久性相互影響的權衡和決定,比如Dynamo類系統通過NRW來指定一致性,如果N=3, W=2,R=1,你將會得到:
1. 可能不會馬上看到更新
2. 更新數據會在一個節點失敗後存活

如果你像Zookeeper來寫入,那會得到一個強一致性保證:寫操作會對所有人可見,比如這個寫操作在一半以下的節點失敗後仍然能夠保證。如果你像MySQL寫入,取決於你的事物一致性級別,你的寫操作一致性會對所有人、你可見或者最終一致性。

網絡分區

分佈式通常假設網絡是異步的,意味着網絡可能會導致任意的重複、丟失、延遲或者亂序的節點間消息傳遞。在實際中,TCP狀態機會保證節點間消息傳遞的不丟失、不重複、時序。但是,在Socket級別上,節點接發消息會阻塞,超時等等。

檢測到網絡失敗是困難,因爲我們唯一能跟得到其他節點狀態的信息就是通過網絡來得到,延遲跟網絡失敗也無從區分。這裏就會產生一個基本的網絡分區問題:高延遲可以考慮作爲失敗。當分區產生後,我們沒有渠道去瞭解到其他節點到底發生了什麼事: 它們是否還存活?或者已經crash?是否有收到消息?是否正在嘗試迴應。當網絡最終恢復後,我們需要重新建立連接然後嘗試解決在不一致狀態時的不一致。

很多系統在解決分區時會進入一個特殊的降級操作模式。CAP理論也告訴我們要麼得到一致性要麼高可用性,但是很少有數據庫系統能夠達到CAP理論的極限,多數只是丟失數據。

接下來的內容會介紹一些分佈式系統是如何在網絡失敗後進行相關行爲。

實例1:傳統數據庫與2PC

傳統的SQL數據庫如MySQL、Postgresql都提供一系列不同的一致性級別,然後通常都只能向一個primary寫入,我們可以把這些數據庫認爲是CP系統(CAP理論),如果分區發生,整個系統會不可用(因爲ACID)。

那麼傳統數據庫是不是真的是強一致性?它們都是使用2PC([1]策略來提交請求:
1. 客戶端commit
2. 服務器端寫操作然後迴應
3. 客戶端收到迴應完成提交

1在這三個步驟中,可能發生不一致的情況在於2與3之間,當服務器寫操作完成但是迴應沒有被客戶端收到,無論是超時或者網絡故障,客戶端這時會認爲這次操作沒有完成,而事實上數據庫已經寫入。這時就會產生不一致的行爲。也就是客戶端得到的錯誤並不能解釋到底服務器端有沒有寫入。

2PC不僅在傳統SQL數據庫被廣泛使用,也有大量用戶實現2PC在MongoDB之上來完成多鍵值事務操作。

那麼如何解決這個問題?首先必須接受這個問題,因爲網絡失敗地概率比較低,並且正好在服務器寫操作完成與客戶端得到迴應之間失敗。這使得受到影響的操作非常稀有,在大部分業務中,這個失敗是可接受到。相對的,如果你必須要強一致性的實施,那麼應該在業務中付諸行動,比如所有的事務寫操作都是冪等的,是可重入的。這樣當遇到網絡問題時,retry即可而不管到底寫操作有沒有完成。最後,一些數據庫可以得到事務ID,通過track事務ID你可以在網絡故障後重新評估事務是否完成,通過數據庫在網絡恢復後檢查其記錄的事物ID然後回滾相應事務。

我們在OpenStack的選擇就很有限,目前各個項目中並不是所有寫操作都是冪等的,不過幸運的是,OpenStack的數據在罕見的2PC協議特例中損失是能接受的。

實例2: Redis

Redis通常被視爲一個共享的heap,因爲它容易理解的一致性模型,很多用戶把Redis作爲消息隊列、鎖服務或者主要數據庫。Redis在一個server上運行實例視爲CP系統(CAP理論),因此一致性是它的主要目的。

2

Redis集羣通常是主備,primary node負責寫入和讀取,而slave node只是用來備份。當primary node失敗時,slave node有機會被提升爲primary node。但是因爲primary node和slave node之間是異步傳輸,因此slave node被提升爲primary node後會導致0~N秒的數據丟失。此時Redis的一致性已經被打破,Redis這個模式的集羣不是一個CP系統!

Redis有一個官方組件叫Sentinel(參考[Redis Sentinel],它是通過類似Quorum的方式來連接Sentinel instance,然後檢測Redis集羣的狀態,對故障的primary節點試用slave節點替換。Redis官方號稱這個是HA solution,通過Redis Sentinel來構建一個CP系統。

3

考慮Redis Sentinel在網絡分區時候的情況,這時Redis集羣被網絡分成兩部分,Redis Sentinel在的大區域可能會提升Slave node作爲primary node。如果這時候一直client在連接原來的primary node,這時會出現兩個primary node(split-brain problem,腦裂問題)!也就是說,Redis Sentinel並沒有阻止client連接Old primary node。在此時,已經連接到old primary node的client會寫入old primary node,新的client會寫入到new primary node。此時,CP系統已經完全癱瘓。雖然Redis集羣一直是保持運行的,但是因爲依賴於Quorum來提升slave節點,因此它也不會是AP系統。

4

如果使用Redis作爲Lock service,那麼這個問題會成爲致命問題。這會導致分區後同時可以有兩個client獲取同一個鎖併成功,lock service必須是嚴格的CP系統,像Zookeeper。

如果使用Redis作爲queue,那麼你需要接受一個item可能會被分發零次、一次或者兩次等等,大部分的分佈式隊列都保證最多隻分發一次或者最少分發一次,CP系統會提確切一次的分發然後帶來較高的延遲。你需要明確使用Redis作爲隊列服務的話必須要接受網絡分區後隊列服務可能導致的不穩定。

如果使用Redis作爲database,那麼可想而知,利用Redis Sentinel建立的database是不能稱爲database的。

最後,以目前的Redis來說,使用官方提供的組件它只能成爲Cache。構建一個分佈式的Redis前往[WheatRedis]

實例3: MongoDB

MongoDB採用類似於Redis的集羣方式,primary node作爲單點寫操作服務然後異步寫入replication nodes。但是MongoDB內建了primary選舉和複製狀態機,這使得primary node失敗後,整個MongoDB會進行交流然後選擇一個合適的slave node。然後MongoDB支持指定primary node可以確認slave node已經把寫操作寫入log或者真正寫入,也就是通過一定的性能損耗來換取更強的一致性當primary node失敗後。

那麼MongoDB是否可以認定爲是一個嚴格的CP系統?還是與Redis類似的問題,在網絡分區後,當primary node在小的分區裏,大的分區裏的node會選舉產生一個新的primary node,而此時在分區的時候,這兩個node是會同時存在的(這個沒有問題),然後當分區恢復後,小分區裏的old primarynode會把在腦裂期間的操作發送到new primary node,這時候可能會產生衝突!

5

那麼如何面對這個問題?接受它,首先這個衝突的概念像2PC一樣可以在client端解決,同時MongoDB目前有WriteConnern可以解決這個問題,但會造成巨大的性能影響。

實例4: Dynamo

Dynamo是在傳統的primary-slave模式遇到問題時候出現的紅寶書,借鑑Dynamo的產品在一段時間內出現的非常多。

之前提到的系統都是面向CP的,起碼是面向CP設計的。Amazon設計的Dynamo鮮明地面向AP。在Dynamo,它是天然地分區友好型,每一個node都是平等的,通過NWR來指定不同地一致性級別和可用性。這裏不會詳細闡述Dynaomo的原理([Dynamo](http://www.read.seas.harvard.edu/~kohler/class/cs239-w08/decandia07dynamo.pdf),每一個試圖瞭解分佈式系統的人都應該對Dynamo這篇論文非常熟悉,即使它面臨很多問題,但是論文中出現的對Dynamo設計的思考和變遷是寶貴的。

那麼當分區發生時,Dynamo發生了什麼?首先根據NWR的推薦設定(W+R>N),小區是不能得到新的寫操作,新的對象會寫在大區。然後在分區恢復後,小區的對象會滯後並與新的對象發生衝突。這裏的衝突解決策略非常多,如Cassandra使用的client timestamps,Riak的Vector clock,如果無法解決,衝突可能會硬性覆蓋或者推到業務代碼。

然後Dynamo本身沒有任何方法來判斷一個節點是否數據同步,也無法判斷,只能通過完全的數據比較,而這個過程是代價昂貴並且不靈活的。因此Dynamo提到說(W+R>N)可以達到強一致性是不可能的,故障節點只會是最終一致性。

因此,解決Dynamo的問題像前面一樣,接受它。首先你的數據可以設計成immutable,然後你的數據決定可以在罕見情況下丟棄或者變舊,再或者使用CRDTs來設計你的數據結構。無論如何,Dynamo始終是一個good idea並且它推動了分佈式設計的發展。

實例5: BigTable

上面提到的系統都是面向分佈式的,要麼AP要麼CP。那麼Bigtable是AC系統,雖然我們介紹的一直是分區問題,但是我們也需要考慮在中心化設計的Bigtable。無論是HBase還是HDFS都是這類設計,它們迴避了分區問題並且在單IDC下達到非常好的效果。這裏不會詳細討論中心化設計,因爲它根本就沒有考慮分區問題。

思考: 分佈式數據庫系統的設計

通過上述的分析可以瞭解到構建一個分佈式數據庫集羣的困難,無論是同步複製,異步複製,Quorum還是其他的,在網絡分區面前,任何掙扎都是無力的,網絡錯誤意味着”I don’t know” not “I failed”。

構建一個“正確的”分佈式數據庫系統通常在幾個方面達成意見:
1. 接受罕見的問題
2. 使用開源的軟件,分佈式系統會產生極大的“漩渦”在“理論正確的分佈式算法”和“實際使用的系統“。一個有Bug的系統但是正確的算法比一個錯誤的設計更能接受。
3. 利用問題進行正確的設計,如使用[CRDTs](http://pagesperso-systeme.lip6.fr/Marc.Shapiro/papers/RR-6956.pdf)
4. split-brain問題是分區的原罪,如何解決split-brain之後的遺產纔是正確的解決方案

小結

如何在OpenStack上做到HA是OpenStack官方和其他發行版公司都在努力的方向,而其中關鍵就在於數據存儲的HA和一致性,在這個方向上,我們通過對”網絡分區“這一關鍵問題的分析並在不同類型的數據庫上進行落地思考,可以得到如何在其上規避、解決和接受它。通過在OpenStack的產品上思考這些問題,我們可以在HA Solution上有更強健的基礎。

下面一篇會介紹在OpenStack項目的上下文中,各個不同數據庫的應用會帶來怎樣的問題和後果。


文章來源 : http://www.ustack.com/blog/distributedsystems_networksdivision/

發佈了23 篇原創文章 · 獲贊 6 · 訪問量 12萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章