淺析資料一致性

朱小廝發表於2016-08-04

什麼是資料一致性?

在資料有多分副本的情況下,如果網路、伺服器或者軟體出現故障,會導致部分副本寫入成功,部分副本寫入失敗。這就造成各個副本之間的資料不一致,資料內容衝突。 實踐中,導致資料不一致的情況有很多種,表現樣式也多種多樣,比如資料更新返回操作失敗,事實上資料在儲存伺服器已經更新成功。

CAP定理

CAP定理是2000年,由 Eric Brewer 提出來的。Brewer認為在分散式的環境下設計和部署系統時,有3個核心的需求,以一種特殊的關係存在。這裡的分散式系統說的是在物理上分佈的系統,比如我們常見的web系統。

這3個核心的需求是:Consistency,Availability和Partition Tolerance,賦予了該理論另外一個名字 - CAP。

Consistency:一致性,這個和資料庫ACID的一致性類似,但這裡關注的所有資料節點上的資料一致性和正確性,而資料庫的ACID關注的是在在一個事務內,對資料的一些約束。系統在執行過某項操作後仍然處於一致的狀態。在分散式系統中,更新操作執行成功後所有的使用者都應該讀取到最新值。

Availability:可用性,每一個操作總是能夠在一定時間內返回結果。需要注意“一定時間”和“返回結果”。“一定時間”是指,系統結果必須在給定時間內返回。“返回結果”是指系統返回操作成功或失敗的結果。

Partition Tolerance:分割槽容忍性,是否可以對資料進行分割槽。這是考慮到效能和可伸縮性。

CAP定理認為,一個提供資料服務的儲存系統無法同事滿足資料一致性、資料可用性、分割槽容忍性。

為什麼不能完全保證這個三點了,個人覺得主要是因為一旦進行分割槽了,就說明了必須節點之間必須進行通訊,涉及到通訊,就無法確保在有限的時間內完成指定的行文,如果要求兩個操作之間要完整的進行,因為涉及到通訊,肯定存在某一個時刻只完成一部分的業務操作,在通訊完成的這一段時間內,資料就是不一致性的。如果要求保證一致性,那麼就必須在通訊完成這一段時間內保護資料,使得任何訪問這些資料的操作不可用。

如果想保證一致性和可用性,那麼資料就不能夠分割槽。一個簡單的理解就是所有的資料就必須存放在一個資料庫裡面,不能進行資料庫拆分。這個對於大資料量,高併發的網際網路應用來說,是不可接受的。

在大型網站應用中,資料規模總是快速擴張的,因此可伸縮性即分割槽容忍性必不可少,規模變大以後,機器數量也會變得龐大,這是網路和伺服器故障會頻繁出現,要想保證應用可用,就必須保證分散式處理系統的高可用性。所以在大型網站中,通常會選擇強化分散式儲存系統的可用性(A)和伸縮性(P),在某種程度上放棄一致性(C)。一般來說,資料不一致通常出現在系統高併發寫操作或者叢集狀態不穩(故障恢復、叢集擴容等)的情況下,應用系統需要對分散式資料處理系統的資料不一致性有所瞭解並進行某種意義上的補償和糾錯,以避免出現應用系統資料不正確。


資料一致性模型

一些分散式系統通過複製資料來提高系統的可靠性和容錯性,並且將資料的不同的副本存放在不同的機器,由於維護資料副本的一致性代價高,因此許多系統採用弱一致性來提高效能,一些不同的一致性模型也相繼被提出。

  1. 強一致性: 要求無論更新操作實在哪一個副本執行,之後所有的讀操作都要能獲得最新的資料。
  2. 弱一致性:使用者讀到某一操作對系統特定資料的更新需要一段時間,我們稱這段時間為“不一致性視窗”。
  3. 最終一致性:是弱一致性的一種特例,保證使用者最終能夠讀取到某操作對系統特定資料的更新。

資料一致性實現技術

Quorum系統NRW策略

這個協議有三個關鍵字N、R、W。

  • N代表資料所具有的副本數。
  • R表示完成讀操作所需要讀取的最小副本數,即一次讀操作所需要參與的最小節點數目。
  • W表示完成寫操作所需要寫入的最小副本數,即一次寫操作所需要參與的最小節點數目。

該策略中,只需要保證R+W>N,就可以保證強一致性。

例如:N=3,W=2,R=2,那麼表示系統中資料有3個不同的副本,當進行寫操作時,需要等待至少有2個副本完成了該寫作業系統才會返回執行成功的狀態,對於讀操作,系統有同樣的特性。由於R + W > N,因此該系統是可以保證強一致性的。
R + W> N會產生類似Quorum的效果。該模型中的讀(寫)延遲由最慢的R(W)副本決定,有時為了獲得較高的效能和較小的延遲,R和W的和可能小於N,這時系統不能保證讀操作能獲取最新的資料。

如果R + W > N,那麼分散式系統就會提供強一致性的保證,因為讀取資料的節點和被同步寫入的節點是有重疊的。在關係型資料管理系統中,如果N=2,可以設定為W=2,R=1,這是比較強的一致性約束,寫操作的效能比較低,因為系統需要2個節點上的資料都完成更新後才將確認結果返回給使用者。

如果R + W ≤ N,這時讀取和寫入操作是不重疊的,系統只能保證最終一致性,而副本達到一致的時間則依賴於系統非同步更新的實現方式,不一致性的時間段也就等於從更新開始到所有的節點都非同步完成更新之間的時間。

R和W的設定直接影響系統的效能、擴充套件性與一致性。如果W設定為1,則一個副本完成更改就可以返回給使用者,然後通過非同步的機制更新剩餘的N-W的副本;如果R設定為1,只要有一個副本被讀取就可以完成讀操作,R和W的值如較小會影響一致性,較大則會影響效能,因此對這兩個值的設定需要權衡。

下面為不同設定的幾種特殊情況:
1. 當W=1,R=N時,系統對寫操作有較高的要求,但讀操作會比較慢,若N個節點中有節點發生故障,那麼讀操作將不能完成。
2. 當R=1,W=N時,系統對讀操作有較高效能、高可用,但寫操作效能較低,用於需要大量讀操作的系統,若N個節點中有節點發生故障,那麼些操作將不能完成。
3. 當R=Q,W=Q(Q=N/2+1)時,系統在讀寫效能之間取得平衡,兼顧了效能和可用性。

兩階段提交演算法

在兩階段提交協議中,系統一般包含兩類機器(或節點):一類為協調者(coordinator),通常一個系統中只有一個;另一類為事務參與者(participants,cohorts或workers),一般包含多個,在資料儲存系統中可以理解為資料副本的個數。兩階段提交協議由兩個階段組成,在正常的執行下,這兩個階段的執行過程如下所述:

  • 階段1:請求階段(commit-request phase,或稱表決階段,voting phase)。
    在請求階段,協調者將通知事務參與者準備提交或取消事務,然後進入表決過程。在表決過程中,參與者將告知協調者自己的決策:同意(事務參與者本地作業執行成功)或取消(本地作業執行故障)。
  • 階段2:提交階段(commit phase)。
    在該階段,協調者將基於第一個階段的投票結果進行決策:提交或取消。當且僅當所有的參與者同意提交事務協調者才通知所有的參與者提交事務,否則協調者將通知所有的參與者取消事務。參與者在接收到協調者發來的訊息後將執行響應的操作。

舉個例子:A組織B、C和D三個人去爬長城:如果所有人都同意去爬長城,那麼活動將舉行;如果有一人不同意去爬長城,那麼活動將取消。用2PC演算法解決該問題的過程如下:

  1. 首先A將成為該活動的協調者,B、C和D將成為該活動的參與者。
  2. 階段1:A發郵件給B、C和D,提出下週三去爬山,問是否同意。那麼此時A需要等待B、C和D的郵件。B、C和D分別檢視自己的日程安排表。B、C發現自己在當日沒有活動安排,則發郵件告訴A它們同意下週三去爬長城。由於某種原因,D白天沒有檢視郵件。那麼此時A、B和C均需要等待。到晚上的時候,D發現了A的郵件,然後檢視日程安排,發現週三當天已經有別的安排,那麼D回覆A說活動取消吧。
  3. 階段2:此時A收到了所有活動參與者的郵件,並且A發現D下週三不能去爬山。那麼A將發郵件通知B、C和D,下週三爬長城活動取消。此時B、C回覆A“太可惜了”,D回覆A“不好意思”。至此該事務終止。

兩階段提交演算法在分散式系統結合,可實現單使用者對檔案(物件)多個副本的修改,多副本資料的同步。其結合的原理如下:

  1. 客戶端(協調者)向所有的資料副本的儲存主機(參與者)傳送:修改具體的檔名、偏移量、資料和長度資訊,請求修改資料,該訊息是1階段的請求訊息。
  2. 儲存主機接收到請求後,備份修改前的資料以備回滾,修改檔案資料後,向客戶端回應修改成功的訊息。如果儲存主機由於某些原因(磁碟損壞、空間不足等)不能修改資料,回應修改失敗的訊息。
  3. 客戶端接收傳送出去的每一個訊息回應,如果儲存主機全部回應都修改成功,向每儲存主機傳送確認修改的提交訊息;如果存在儲存主機回應修改失敗,或者超時未回應,客戶端向所有儲存主機傳送取消修改的提交訊息。該訊息是2階段的提交訊息。
  4. 儲存主機接收到客戶端的提交訊息,如果是確認修改,則直接回應該提交OK訊息;如果是取消修改,則將修改資料還原為修改前,然後回應取消修改OK的訊息。
  5. 客戶端接收全部儲存主機的回應,整個操作成功。

在該過程中可能存在通訊失敗,例如網路中斷、主機當機等諸多的原因,對於未在演算法中定義的其它異常,都認為是提交失敗,都需要回滾,這是該演算法基於確定的通訊回覆實現的,在參與者的確定回覆(無論是回覆失敗還是回覆成功)之上執行邏輯處理,符合確定性的條件當然能夠獲得確定性的結果哲學原理。

缺點:單個A是個嚴重問題:沒有熱備機制,A節點當機了或者連結它的網路壞了會阻塞該事務;吞吐量不行,沒有充分發動更多A的力量,一旦某個A第一階段投了贊成票就得在它上面加獨佔鎖,其他事務不得接入,直到當前事務提交or回滾。

分散式鎖服務

分散式鎖是對資料被外界修改持保守態度,在整個資料處理過程中將資料處於鎖定狀態,在使用者修改資料的同時,其它使用者不允許修改。

採用分散式鎖服務實現資料一致性,是在操作目標之前先獲取操作許可,然後再執行操作,如果其他使用者同時嘗試操作該目標將被阻止,直到前一個使用者釋放許可後,其他使用者才能夠操作目標。分析這個過程,如果只有一個使用者操作目標,沒有多個使用者併發衝突,也申請了操作許可,造成了由於申請操作許可所帶來的資源使用消耗,浪費網路通訊和增加了延時。

採用分散式鎖實現多副本內容修改的一致性問題, 選擇控制內容顆粒度實現申請鎖服務。例如我們要保證一個檔案的多個副本修改一致, 可以對整個檔案修改設定一把鎖,修改時申請鎖,修改這個檔案的多個副本,確保多個副本修改的一致,修改完成後釋放鎖;也可以對檔案分段,或者是檔案中的單個位元組設定鎖, 實現更細顆粒度的鎖操作,減少衝突。

常用的鎖實現演算法有Lamport bakery algorithm (俗稱麵包店演算法), 還有Paxos演算法以及樂觀鎖。下面對其原理做簡單概述。

1. Lamport麵包店演算法

是解決多個執行緒併發訪問一個共享的單使用者資源的互斥問題的演算法。 由Leslie Lamport(英語:Leslie Lamport)發明。
這個演算法也可以稱為時間戳策略,或者叫做Lamport邏輯時鐘。

這裡先陳述一下這個邏輯時鐘的內容:

我們用分散式系統中的事件的先後關係,用“->”符號來表示,例如:若事件a發生在事件b之前,那麼a->b.

該關係需要滿足下列三個條件:

  1. 如果a和b是同一程式中的事件,a在b之前發生,則a->b
  2. 如果事件a是訊息傳送方,b是接收方,則a->b
  3. 對於事件a、b、c,如果有a->b,b->c,則有a->c

注意,對於任何一個事件a,a -> a都是不成立的,也就是說,關係->是反自反的。有了上面的定義,我們也可以定義出“併發”(concurrent)的概念了:

對於事件a、b,如果a -> b,b -> a兩個都不成立,那麼a和b就是併發的。

直觀上,上面的->關係非常好理解,即“xxx在xxx之前發生”。也就是說,一個系統在輸入I1下,如果有a->b,那麼對於這個系統的同一個輸入I1,無論重複執行多少次,a也始終發生在b之前;如果在輸入I1下a和b是併發的,則表示在同一個輸入I1下的不同執行中,a可能在b之前,也可能在b之後,也可能恰好同時發生。也就是,併發並不是指一定同時發生,而是表示一種不確定性。->和併發的概念,就是我們理解一個系統時最基礎的概念之一了。

有了上面的概念,我們可以給系統引入時鐘了。這裡的時鐘就是lamport邏輯時鐘。一個時鐘,本質上是一個事件到實數(假設時間是連續的)的函式。這個函式將每個事件對映到一個數字,代表這個事件發生的時間。形式一點來說,對於每個程式Pi,都有一個時鐘Ci,這個時鐘將該程式中的事件a對映到Ci(a)。而整個系統的時鐘C=< C0, C1, …, Cn>,對於一個事件b,假設b屬於程式Pj,那麼C(b) =Cj(b)。

這裡插一句,從這個定義也可以看到大師對分散式系統的理解。分散式系統中不存在一個“全域性”的實體。在該系統中,每個程式都是一個相對獨立的實體,它們有自己的本地資訊(本地Knowledge)。而整個系統的資訊則是各個程式的資訊的一個聚合。
有了時鐘的一個“本質定義”還不夠,我們需要考慮,什麼樣的時鐘是一個有意義的,或者說正確的時鐘。其實,有了前文的->關係的定義,正確的時鐘應滿足的條件已經十分明顯了:

時鐘條件:對於任意兩個事件a,b,如果a -> b,那麼C(a) < C(b)。

注意,反過來講這個條件可不成立。如果我們要求反過來也成立,即“如果a -> b為假,那麼C(a) < C(b)也為假”,那就等於要求併發事件必須同時發生,這顯然是不合理的。
結合前文->關係的定義,我們可以把上面的條件細化成如下兩條:

  1. 如果a和b是程式Pi中的兩個事件,並且在Pi中,a在b之前發生,那麼Ci(a) < Ci(b);
  2. 如果a是Pi傳送訊息m,b是Pj接收訊息m,那麼Ci(a) < Cj(b);

上面就定義了合理的邏輯時鐘。顯然,一個系統可以有無數個合理的邏輯時鐘。實現邏輯時鐘也相對簡單,只要遵守兩條實現規則就可以了:

  1. 每個程式Pi在自己的任何兩個連續的事件之間增加Ci值;
  2. 如果事件a是Pi傳送訊息m,那麼在m中應該帶上時間戳Tm=Ci(a);如果b是程式Pj接收到訊息m,那麼,程式Pj應該設定Cj為大於max(Tm,Cj(b))。

有了上面邏輯時鐘的定義,我們現在可以為一個系統中所有的事件排一個全序,就是使用事件發生時的邏輯時鐘讀數進行排序,讀數小的在先。當然,此時可能會存在兩個事件同時發生的情況。如果要去除這種情況,方法也非常簡單:如果a在程式Pi中,b在程式Pj中,Ci(a) = Cj(b)且i < j,那麼a在b之前。形式化一點,我們可以把系統事件E上的全序關係“=>”定義為:
假設a是Pi中的事件,b是Pj中的事件,那麼:a => b當且僅當以下兩個條件之一成立:

  1. Ci(a) < Cj(b);
  2. Ci(a) = Cj(b) 且 i < j;

Lamport把上面這些數理邏輯時鐘的概念以非常直觀地類比為顧客去麵包店採購。麵包店只能接待一位顧客的採購。已知有n位顧客要進入麵包店採購,安排他們按照次序在前臺登記一個簽到號碼。該簽到號碼逐次加1。根據簽到號碼的由小到大的順序依次入店購貨。完成購買的顧客在前臺把其簽到號碼歸0. 如果完成購買的顧客要再次進店購買,就必須重新排隊。

這個類比中的顧客就相當於執行緒,而入店購貨就是進入臨界區獨佔訪問該共享資源。由於計算機實現的特點,存在兩個執行緒獲得相同的簽到號碼的情況,這是因為兩個執行緒幾乎同時申請排隊的簽到號碼,讀取已經發出去的簽到號碼情況,這兩個執行緒讀到的資料是完全一樣的,然後各自在讀到的資料上找到最大值,再加1作為自己的排隊簽到號碼。為此,該演算法規定如果兩個執行緒的排隊簽到號碼相等,則執行緒id號較小的具有優先權。

把該演算法原理與分散式系統相結合,即可實現分步鎖。

注意這個系統中需要引入時鐘同步,博主的意見是可以採用SNTP實現時鐘同步(非權威,僅供參考)。

2.Paxos演算法

該演算法比較熱門,類似2pc演算法的升級版,在此不做贅述,可以自行搜尋相關資料。(博主會在之後整理列出)

需要注意的是這個演算法也是Leslie Lamport提出的,由此可見這位大師之牛逼!

Paxos演算法解決的問題是一個分散式系統如何就某個值(決議)達成一致。一個典型的場景是,在一個分散式資料庫系統中,如果各節點的初始狀態一致,每個節點都執行相同的操作序列,那麼他們最後能得到一個一致的狀態。為保證每個節點執行相同的命令序列,需要在每一條指令上執行一個“一致性演算法”以保證每個節點看到的指令一致。一個通用的一致性演算法可以應用在許多場景中,是分散式計算中的重要問題。節點通訊存在兩種模型:共享記憶體(Shared memory)和訊息傳遞(Messages passing)。Paxos演算法就是一種基於訊息傳遞模型的一致性演算法。BigTable使用一個分散式資料鎖服務Chubby,而Chubby使用Paxos演算法來保證備份的一致性。

不僅只用在分散式系統,凡是多個過程需要達成某種一致性的都可以用到Paxos 演算法。一致性方法可以通過共享記憶體(需要鎖)或者訊息傳遞實現,Paxos 演算法採用的是後者。下面是Paxos 演算法適用的幾種情況:一臺機器中多個程式/執行緒達成資料一致;分散式檔案系統或者分散式資料庫中多客戶端併發讀寫資料;分散式儲存中多個副本響應讀寫請求的一致性。

3. 採用樂觀鎖原理實現的同步

我們舉個例子說明該演算法的實現原理。如一個金融系統,當某個操作員讀取使用者的資料,並在讀出的使用者資料的基礎上進行修改時(如更改使用者帳戶餘額),如果採用前面的分散式鎖服務機制,也就意味著整個操作過程中(從操作員讀出資料、開始修改直至提交修改結果的全過程,甚至還包括操作員中途去煮咖啡的時間),資料庫記錄始終處於加鎖狀態,可以想見,如果面對幾百上千個併發,這樣的情況將導致怎樣的後果。

樂觀鎖機制在一定程度上解決了這個問題。樂觀鎖,大多是基於資料版本( Version)記錄機制實現。何謂資料版本?即為資料增加一個版本標識,在基於資料庫表的版本解決方案中,一般是通過為資料庫表增加一個 “version” 欄位來實現。讀取出資料時,將此版本號一同讀出,之後更新時,對此版本號加一。此時,將提交資料的版本資料與資料庫表對應記錄的當前版本資訊進行比對,如果提交的資料版本號大於資料庫表當前版本號,則予以更新,否則認為是過期資料。

對於上面修改使用者帳戶資訊的例子而言,假設資料庫中帳戶資訊表中有一個 version 欄位,當前值為 1 ;而當前帳戶餘額欄位( balance )為 $100 。

  1. 操作員 A 此時將其讀出(version=1 ),並從其帳戶餘額中扣除 50100-$50 )。
  2. 在操作員 A 操作的過程中,操作員B也讀入此使用者資訊( version=1 ),並從其帳戶餘額中扣除 20100-$20 )。
  3. 操作員 A 完成了修改工作,將資料版本號加一( version=2 ),連同帳戶扣除後餘額( balance=$50),提交至資料庫更新,此時由於提交資料版本大於資料庫記錄當前版本,資料被更新,資料庫記錄 version 更新為 2 。
  4. 操作員 B 完成了操作,也將版本號加一( version=2 )試圖向資料庫提交資料( balance=$80),但此時比對資料庫記錄版本時發現,操作員 B 提交的資料版本號為 2 ,資料庫記錄當前版本也為 2 ,不滿足 “提交版本必須大於記錄當前版本才能執行更新 “ 的樂觀鎖策略,因此,操作員 B 的提交被駁回。這樣,就避免了操作員 B 用基於version=1 的舊資料修改的結果覆蓋操作員A 的操作結果的可能。

打賞支援我寫出更多好文章,謝謝!

打賞作者

打賞支援我寫出更多好文章,謝謝!

任選一種支付方式

淺析資料一致性 淺析資料一致性

相關文章