接著上一篇的內容,我們繼續來梳理分散式系統之中的副本機制與副本一致。上文我們聊到了在可用性與一致性之間的一個折中的一致性等級: 最終一致性 。我們順著上篇的內容,由用戶來分析一致性等級。 1. 客戶端的困擾 上篇文章我們提到了數據系統常用的模型,當提交新數據時,必須將它發送給Leader節點,但是當 ...
接著上一篇的內容,我們繼續來梳理分散式系統之中的副本機制與副本一致。上文我們聊到了在可用性與一致性之間的一個折中的一致性等級:最終一致性。我們順著上篇的內容,由用戶來分析一致性等級。
1. 客戶端的困擾
上篇文章我們提到了數據系統常用的模型,當提交新數據時,必須將它發送給Leader節點,但是當用戶查詢數據時,可以從一個Follower節點讀取該數據。
這樣的模型使十分適合Web應用的讀多寫少的特點。
讀寫一致性
但是倘若Leader與Follower之間以非同步的方式複製的話,會存在一些問題。如下圖所示:如果用戶數據剛剛寫入,而新的數據可能尚未達到Follower節點的副本。在用戶的角度,他們提交的數據看起來似乎丟失了。
在這種情況下,我們需要讀寫一致性。對於用戶來說,總是能看到它們最新更新的數據。而其他用戶的更新可能需要一定的時間之後才可見。現在新的問題來了,我們如何實現Leader-Follower機制下的讀寫一致性呢?
這裡有一個最簡單粗暴的規則是:用戶可以選擇總是從Leader節點那裡讀取自己寫入的數據,然後選擇自從Follower節點處讀取其他用戶寫入的數據。(註:這裡的技巧十分巧妙,十分適合在多用戶下的隔離,但是僅僅適用於每個用戶都僅僅修改自己數據的場景。)
所以更好的方式是時間戳機制,客戶端可以通過記錄最近一次寫入的時間戳,然後數據系統需要確保為該用戶提供的任何讀取的副本至少在該時間戳之後更新。如果一個副本還沒有達到最新的時間戳,則該讀取需要由另一個副本處理,或者等待可本節點的副本跟進到滿足要求的時間戳。時間戳可以是邏輯時間戳(表示寫入順序的命令,如日誌序列號)或實際系統時鐘(強依賴系統時鐘的話,需要處理時鐘回撥等問題,十分麻煩~~~)。
單調讀一致性
解決了讀寫一致性,我們再來看看下麵的這個場景:
因為用戶可以從多個不同的副本進行多次讀取,則可能發生這種情況。如上圖所示,用戶2345進行了兩次相同的查詢,第一次訪問了Follower1節點,第二次查詢訪問了Follower2節點。第一次查詢返回一個最近由用戶1234添加的註釋,但是第二次查詢並沒有上次查詢的註釋了,因為滯後的Follower還沒有同步到之前的寫入註釋的操作。如果用戶2345第一次看到用戶1234的註釋出現,然後再次查詢它時卻消失了,這對用戶2345來說是非常令人困惑的。
單調讀一致性來是保證這種異常不會發送。當客戶讀到的數據,保證不會看到一個舊的數據。要滿足單調讀一致性。實現單調讀取的一種方法是確保每個用戶總是從同一副本中讀取(不同的用戶可以從不同的副本讀取)。例如,可以根據用戶ID散列選擇副本,而不是隨機選擇。
多數據中心下的交叉設備讀:
在多個數據中心的環境下,問題會變的更加複雜。任何需要由Leader節點服務的請求都必須路由轉發到包含Leader的數據中心。當同一用戶從多個設備訪問服務時,另一個複雜的問題出現了,例如桌面Web瀏覽器和移動應用程式。在這種情況下,您可能希望在讀寫一致性的基礎之上提供交叉設備讀:如果用戶輸入某個設備上的一些信息,然後在另一個設備上查看,則應該看到他們剛剛輸入的信息。如果需要提供交叉設備讀,記錄用戶上次更新的時間戳是十分困難的,因為一個設備不知道其他設備上發生了什麼更新。假如你的副本分佈在不同的數據中心,也不能保證不同設備的連接將被路由引導到同一數據中心。
小結:當使用一個最終一致性的數據系統時,如果複製延遲增加到幾分鐘甚至幾小時,就需要考慮應用程式的行為。如果答案是“沒有問題”,那太好了。但如果應用程式對一致性敏感,則應用程式需要提供額外的處理邏輯來處理特殊的場景,如對某些特殊的讀取操作,可以限定只對Leader節點執行某些類型的讀取。但是,在應用程式代碼中處理這些問題會很複雜,很容易出錯。事務確保了許多一致性模型,使應用程式更簡單。然而,在向分散式的環境之中,許多數據系統放棄了對事務的支持,因為事務會大大降低分散式環境之中系統的性能與可用性。所以,最終一致性能夠使用的場景有限,我們還是要按需選擇,避免踩坑。
2. 多Leader機制
在多數據中心的環境下,如果僅僅只有一個Leader,所以每次寫操作都必須訪問同一個數據中心,這將會導致延遲大大提高。所以我們可以考慮多Leader的機制。在多Leader機制可以在每個數據中心中設置一個Leader。下圖展示了多Leader機制的結構:
在數據中心內部,保持前文提到的Leader-Follower機制。而跨數據中心的Leader之間通過衝突協調進行數據同步。我們來梳理一下多Leader機制的一些特點
- 性能
在多Leader機制中,每個寫操作可以在本地數據中心進行處理,再非同步複製到其他的數據中心。因此可以大大降低跨數據中心的網路延遲,性能表現顯然會更好。
- Leader失效
在單Leader的機制里,如果數據中心失效,則故障轉移可以使另一個數據中心中的Follower成為Leader。而多Leader機制,每個數據中心可以獨立於其他數據中心繼續運行。
- 網路的延遲與故障
數據中心之間的通信通常依托於公共互聯網,它相比數據中心內的本地網路更加不可靠。顯然具有非同步複製特性的多Leader機制可以更好地容忍跨數據中心通信的延遲與故障。
寫衝突
雖然多Leader機制具備了很多優勢,它也有一個大缺點是:相同的數據可以在兩個不同的數據中心,一旦數據同時被修改就必須要有機制來解決寫衝突的問題。如下圖所示,考慮一個同時由兩個用戶編輯的wiki頁面。User1將頁面標題從A改為B,User2同時將標題從A改為C。每個用戶的更改都分別成功提交給了Leader1與Leader2。當進行非同步複製時,系統會檢測到衝突:
在一個單Leader的數據系統之中,User2要麼阻塞,等待第一次寫入完成,要麼中止第二個寫事務,迫使User2重試寫入。而在多Leader機制之中,兩個寫入操作都是成功的,並且衝突只是在稍後的某個時間點非同步檢測到的。有什麼辦法可以解決這樣的問題呢?
避免衝突
避免衝突:如果應用程式可以確保某個特定記錄的所有寫入都由同一個Leader處理,那麼衝突就不會發生。由於多Leader機制處理衝突十分複雜,避免衝突是經常推薦的方法。(在用戶可以編輯自己的數據的應用程式中,可以確保特定用戶的請求總是路由到同一個數據中心,並使用該數據中心中的Leader處理讀寫請求。不過這隻是一種鴕鳥策略,用戶地理位置的轉移,或者是路由系統的更新,衝突協調仍然不可避免。)收斂到一致狀態
在單Leader的機制中:如果對同一個欄位有多個更新,最後一個寫入確定欄位的最終值。。而在多Leader的機制中,沒有定義的寫入順序,因此不清楚最終值應該是什麼。所以數據系統必須以收斂的方式解決衝突,這意味著當所有更改都被覆制時,所有副本必須到達相同的最終值。可以為每個寫操作分配一個唯一的ID(例如,一個時間戳,一個長的隨機數,一個UUID或散列的鍵和值),最高的ID值認為是最終值,這種技術被稱為Last Write Win(LWW)。(強依賴系統時間又會造成很多問題,唉,這真的很煩)自定義衝突消解的邏輯
最合適的解決衝突的方法可能取決於應用程式,該代碼可以在寫或讀時執行:一旦數據系統檢測到複製更改日誌中的衝突,它就調用衝突處理程式。或是在應用程式讀取的階段檢測到衝突時,會將這些數據的多個版本將返回應用程式。應用程式可以提示用戶或自動解決衝突,並將結果寫入資料庫。(Cassandra與CouchDB就是採取了這種機制)
多Leader機制的複製拓撲
兩個Leader進行同步時,拓撲結構十分簡單。但是一旦擴展到4,5個Leader,之後多個Leader之間的同步結構又應該是怎麼樣的呢?(雖然在實踐中,很少採用這樣的架構)
最一般的拓撲結構是圖(c),其中每個節點都將其寫入傳遞給所有的節點。而(a)或(b)採用了環形或星型的結構來減少網路的流量。在環形和星形拓撲中,在到達所有副本之前,寫入可能需要經過幾個節點。因此,節點需要轉發它們從其他節點接收到的數據更改。為了防止無限複製迴圈,每個節點都被賦予唯一的標識符,並且在複製日誌中,每個寫入都用它經過的所有節點的標識符標記。當一個節點接收一個帶有自己標識符的數據更改時,該數據更改將被忽略,因為節點知道它已經被處理了。
環形和星形結構存在的一個問題是,如果有一個節點失效,會中斷其他節點之間的同步消息流,而因為它不允許消息沿著不同的路徑傳播,造成了單點故障。但是All pass的結構也會帶來一些新的問題,由於網路擁塞的原因,各個節點的信息接收順序不一致,如下圖所示:
Client A將行插入到一個Leader 1的表,和Client B在Leader 3之中進行更新。而Leader 2收到了不同順序的寫操作:update操作出現在了insert操作之前。為了正確地排列這些事件,我們可以使用一種稱為多版本向量控制(MVCC)的技術。至於什麼是MVVC,我們下一篇繼續來梳理~~( 不是我故意賣關子啊,只是怕寫的太長你們懶得看~~~)