複製主要指經過互聯網在多臺機器上保存相同數據的副本。經過數據複製方案,人們一般但願達到如下目的:算法
每一個保存數據庫完整數據集的節點稱之爲副本。對於每一筆數據寫入,全部副本都須要隨之更新;不然,某些副本將出現不一致。最多見的解決方案是基於主節點的複製(也稱爲主動/被動,或主從複製)。主從複製的工做原理以下:數據庫
複製很是重要的一個設計選項是同步複製仍是異步複製。對於關係數據庫系統,同步或異步一般是一個可配置的選項。瀏覽器
以下圖,網站用戶須要更新首頁的頭像圖片。其基本流程是,客戶將更新請求發送給主節點,主節點接收到請求,接下來將數據更新轉發給從節點。最後,由主節點來通知客戶更新完成。安全
從節點1的複製是同步的,即主節點需等待直到從節點1確認完成了寫入,而後纔會向用戶報告完成,而且將最新的寫入對其餘客戶端可見。而從節點2的複製是異步的:主節點發送完消息以後當即返回,不用等待從節點2的完成確認。服務器
同步複製的優勢是,一旦向用戶確認,從節點能夠明確保證完成了與主節點的更新同步,數據已經處於最新版本。萬一主節點發生故障,老是能夠在從節點繼續訪問最新數據。缺點則是,若是同步的從節點沒法完成確認(例如因爲從節點發生崩潰,或者網絡故障,或任何其餘緣由),寫入就不能視爲成功。主節點會阻塞其後全部的寫操做,直到同步副本確認完成。網絡
所以,把全部從節點都配置爲同步複製有些不切實際。由於這樣的話,任何一個同步節點的中斷都會致使整個系統更新停滯不前。實踐中,若是數據庫啓用了同步複製,一般意味着其中某一個從節點是同步的,而其餘節點則是異步模式。萬一同步的從節點變得不可用或性能降低,則將另外一個異步的從節點提高爲同步模式。這樣能夠保證至少有兩個節點(即主節點和一個同步從節點)擁有最新的數據副本。這種配置有時也稱爲半同步。架構
主從複製還常常會被配置爲全異步模式。此時若是主節點發生失敗且不可恢復,則全部還沒有複製到從節點的寫請求都會丟失。這意味着即便向客戶端確認了寫操做,卻沒法保證數據的持久化。但全異步配置的優勢則是,無論從節點上數據多麼滯後,主節點老是能夠繼續響應寫請求,系統的吞吐性能更好。併發
若是須要增長新的從節點,如何確保新的從節點和主節點保持數據一致呢?邏輯上的主要操做步驟以下:運維
從節點的本地磁盤上都保存了副本收到的數據變動日誌。若是從節點發生崩潰,而後順利重啓,或者主從節點之間的網絡發生暫時中斷(閃斷),則恢復比較容易,根據副本的複製日誌,從節點能夠知道在發生故障以前所處理的最後一筆事務,而後鏈接到主節點,並請求自那筆事務以後中斷期間內全部的數據變動。在收到這些數據變動日誌以後,將其應用到本地來追趕主節點。以後就和正常狀況同樣持續接收來自主節點數據流的變化。異步
處理主節點故障的狀況則比較棘手:選擇某個從節點將其提高爲主節點;客戶端也須要更新,這樣以後的寫請求會發送給新的主節點,而後其餘從節點要接受來自新的主節點上的變動數據,這一過程稱之爲切換。
故障切換能夠手動進行,或者以自動方式進行。自動切換的步驟一般以下:
然而,上述切換過程依然充滿了不少變數:
對於這些問題沒有簡單的解決方案。所以,即便系統可能支持自動故障切換,有些運維團隊仍然更願意以手動方式來控制整個切換過程。
主節點記錄所執行的每一個寫請求(操做語句)並將該操做語句做爲日誌發送給從節點。對於關係數據庫,這意味着每一個INSERT、UPDATE或DELETE語句都會轉發給從節點,而且每一個從節點都會分析並執行這些SQL語句,如同它們是來自客戶端那樣。
但這種複製方式有一些不適用的場景:
不管是日誌結構的存儲引擎,仍是B-tree結構的存儲引擎,全部對數據庫寫入的字節序列都被記入日誌。所以可使用徹底相同的日誌在另外一個節點上構建副本:除了將日誌寫入磁盤以外,主節點還能夠經過網絡將其發送給從節點。
從節點收到日誌進行處理,創建和主節點內容徹底相同的數據副本。其主要缺點是日誌描述的數據結果很是底層:一個WAL包含了哪些磁盤塊的哪些字節發生改變,諸如此類的細節。這使得複製方案和存儲引擎緊密耦合。若是數據庫的存儲格式從一個版本改成另外一個版本,那麼系統一般沒法支持主從節點上運行不一樣版本的軟件。
另外一種方法是複製和存儲引擎採用不一樣的日誌格式,這樣複製與存儲邏輯剝離。這種複製日誌稱爲邏輯日誌,以區分物理存儲引擎的數據表示。
關係數據庫的邏輯日誌一般是指一系列記錄來描述數據錶行級別的寫請求:
若是一條事務涉及多行的修改,則會產生多個這樣的日誌記錄,並在後面跟着一條記錄,指出該事務已經提交。MySQL的二進制日誌binlog(當配置爲基於行的複製時)使用該方式。
因爲邏輯日誌與存儲引擎邏輯解耦,所以能夠更容易地保持向後兼容,從而使主從節點可以運行不一樣版本的軟件甚至是不一樣的存儲引擎。
主從複製要求全部寫請求都經由主節點,而任何副本只能接受只讀查詢。在這種擴展體系下,只需添加更多的從副本,就能夠提升讀請求的服務吞吐量。可是,這種方法實際上只能用於異步複製,若是試圖同步複製全部的從副本,則單個節點故障或網絡中斷將使整個系統沒法寫入。並且節點越多,發生故障的機率越高,因此徹底同步的配置現實中反而很是不可靠。
若是一個應用正好從一個異步的從節點讀取數據,而該副本落後於主節點,則應用可能會讀到過時的信息。這會致使數據庫中出現明顯的不一致,這種不一致只是一個暫時的狀態,若是中止寫數據庫,通過一段時間以後,從節點最終會遇上並與主節點保持一致。這種效應也被稱爲最終一致性。
總的來講,副本落後的程度理論上並無上限。正常狀況下,主節點和從節點上完成寫操做之間的時間延遲(複製滯後)可能不足1秒,這樣的滯後,在實踐中一般不會致使太大影響。可是,若是系統已接近設計上限,或者網絡存在問題,則滯後可能輕鬆增長到幾秒甚至幾分鐘不等。
許多應用讓用戶提交一些數據,接下來查看他們本身所提交的內容。提交新數據鬚髮送到主節點,可是當用戶讀取數據時,數據可能來自從節點。
對於異步複製存在這樣一個問題,如圖所示,用戶在寫入不久即查看數據,則新數據可能還沒有到達從節點。對用戶來說,看起來彷佛是剛剛提交的數據丟失了。
基於主從複製的系統該如何實現寫後讀一致性呢?有多種可行的方案,如下例舉一二:
若是同一用戶可能會從多個設備訪問數據,例如一個桌面Web瀏覽器和一個移動端的應用,狀況會變得更加複雜。此時,要提供跨設備的寫後讀一致性,即若是用戶在某個設備上輸入了一些信息而後在另外一臺設備上查看,也應該看到剛剛所輸入的內容。在這種狀況下,還有一些須要考慮的問題:
假定用戶從不一樣副本進行了屢次讀取,如圖所示,用戶刷新一個網頁,讀請求可能被隨機路由到某個從節點。用戶2345前後在兩個從節點上執行了兩次徹底相同的查詢(先是少許滯後的節點,而後是滯後很大的從節點),則頗有可能出現如下狀況。第一個查詢返回了最近用戶1234所添加的評論,但第二個查詢由於滯後的緣由,尚未收到更新於是返回結果是空。
實現單調讀的一種方式是,確保每一個用戶老是從固定的同一副本執行讀取(而不一樣的用戶能夠從不一樣的副本讀取)。例如,基於用戶ID的哈希的方法而不是隨機選擇副本。但若是該副本發生失效,則用戶的查詢必須從新路由到另外一個副本。
假如Poons先生與Cake夫人之間進行了以下對話:
Poons先生:Cake夫人,您能看到多遠的將來?
Cake夫人:一般約10s,Poons先生。
這兩句話之間存在因果關係:Cake夫人首先是聽到了Poons先生的問題,而後再去回答該問題。
不過,Cake夫人所說的話經歷了短暫的滯後到達該從節點,但Poons先生所說的經歷了更長的滯後纔到達。對於觀察者來講,彷佛在Poon先生提出問題以前,Cake夫人就開始了回答問題。
這是分區(分片)數據庫中出現的一個特殊問題。若是數據庫老是以相同的順序寫入,則讀取老是看到一致的序列,不會發生這種反常。然而,在許多分佈式數據庫中,不一樣的分區獨立運行,所以不存在全局寫入順序。這就致使當用戶從數據庫中讀數據時,可能會看到數據庫的某部分舊值和另外一部分新值。
一個解決方案是確保任何具備因果順序關係的寫入都交給一個分區來完成,但該方案真實實現效率會大打折扣。如今有一些新的算法來顯式地追蹤事件因果關係。
主從複製存在一個明顯的缺點:系統只有一個主節點,而全部寫入都必須經由主節點。若是因爲某種緣由,例如與主節點之間的網絡中斷而致使主節點沒法鏈接,主從複製方案就會影響全部的寫入操做。
對主從複製模型進行天然的擴展,則能夠配置多個主節點,每一個主節點均可以接受寫操做,後面複製的流程相似:處理寫的每一個主節點都必須將該數據更改轉發到全部其餘節點。這就是多主節點(也稱爲主-主,或主動/主動)複製。此時,每一個主節點還同時扮演其餘主節點的從節點。
爲了容忍整個數據中心級別故障或者更接近用戶,能夠把數據庫的副本橫跨多個數據中心。而若是使用常規的基於主從的複製模型,主節點勢必只能放在其中的某一個數據中心,而全部寫請求都必須通過該數據中心。
有了多主節點複製模型,則能夠在每一個數據中心都配置主節點,以下圖所示的基本架構。在每一個數據中心內,採用常規的主從複製方案;而在數據中心之間,由各個數據中心的主節點來負責同其餘數據中心的主節點進行數據的交換、更新。
在多數據中心環境下,部署單主節點的主從複製方案與多主複製方案之間的差別:
性能
對於主從複製,每一個寫請求都必須經由廣域網傳送至主節點所在的數據中心。這會大大增長寫入延遲,並基本偏離了採用多數據中心的初衷(即就近訪問)。而在多主節點模型中,每一個寫操做均可以在本地數據中心快速響應,而後採用異步複製方式將變化同步到其餘數據中心。
容忍數據中心失效
對於主從複製,若是主節點所在的數據中心發生故障,必須切換至另外一個數據中心,將其中的一個從節點被提高爲主節點。在多主節點模型中,每一個數據中心則能夠獨立於其餘數據中心繼續運行,發生故障的數據中心在恢復以後更新到最新狀態。
容忍網絡問題
數據中心之間的通訊一般經由廣域網,它每每不如數據中心內的本地網絡可靠。對於主從複製模型,因爲寫請求是同步操做,對數據中心之間的網絡性能和穩定性等更加依賴。多主節點模型則一般採用異步複製,能夠更好地容忍此類問題,例如臨時網絡閃斷不會妨礙寫請求最終成功。
儘管多主複製具備上述優點,但也存在一個很大的缺點:不一樣的數據中心可能會同時修改相同的數據,於是必須解決潛在的寫衝突。
另外一種多主複製比較適合的場景是,應用在與網絡斷開後還須要繼續工做。每一個設備都有一個充當主節點的本地數據庫,而後在全部設備之間採用異步方式同步這些多主節點上的副本,同步滯後多是幾小時或者數天,具體時間取決於設備什麼時候能夠再次聯網。
從架構層面來看,上述設置基本上等同於數據中心之間的多主複製,只不過是個極端狀況,即一個設備就是數據中心,並且它們之間的網絡鏈接很是不可靠。
咱們一般不會將協做編輯徹底等價於數據庫複製問題,但兩者確實有不少類似之處。當一個用戶編輯文檔時,所作的更改會當即應用到本地副本(Web瀏覽器或客戶端應用程序),而後異步複製到服務器以及編輯同一文檔的其餘用戶。
若是是主從複製數據庫,第二個寫請求要麼會被阻塞直到第一個寫完成,要麼被停止。然而在多主節點的複製模型下,這兩個寫請求都是成功的,而且只能在稍後的時間點上才能異步檢測到衝突,那時再要求用戶層來解決衝突爲時已晚。
理論上,也能夠作到同步衝突檢測,即等待寫請求完成對全部副本的同步,而後再通知用戶寫入成功。可是,這樣作將會失去多主節點的主要優點:容許每一個主節點獨立接受寫請求。若是確實想要同步方式衝突檢測,或許應該考慮採用單主節點的主從複製模型。
處理衝突最理想的策略是避免發生衝突,即若是應用層能夠保證對特定記錄的寫請求老是經過同一個主節點,這樣就不會發生寫衝突。現實中,因爲很多多主節點複製模型所實現的衝突解決方案存在瑕疵,所以,避免衝突反而成爲你們廣泛推薦的首選方案。
例如,一個應用系統中,用戶須要更新本身的數據,那麼咱們確保特定用戶的更新請求老是路由到特定的數據中心,並在該數據中心的主節點上進行讀/寫。不一樣的用戶則可能對應不一樣的主數據中心(例如根據用戶的地理位置來選擇)。從用戶的角度來看,這基本等價於主從複製模型。
可是,有時可能須要改變事先指定的主節點,例如因爲該數據中心發生故障,不得不將流量從新路由到其餘數據中心,或者是由於用戶已經漫遊到另外一個位置,於是更靠近新數據中心。此時,衝突避免方式再也不有效,必須有措施來處理同時寫入衝突的可能性。
若是每一個副本都只是按照它所看到寫入的順序執行,那麼數據庫最終將處於不一致狀態。這絕對是不可接受的,全部的複製模型至少應該確保數據在全部副本中最終狀態必定是一致的。所以,數據庫必須以一種收斂趨同的方式來解決衝突,實現收斂的衝突解決有如下可能的方式:
解決衝突最合適的方式可能仍是依靠應用層,因此大多數多主節點複製模型都有工具來讓用戶編寫應用代碼來解決衝突。能夠在寫入時或在讀取時執行這些代碼邏輯:
衝突解決一般用於單個行或文檔,而不是整個事務。若是有一個原子事務包含多個不一樣寫請求,每一個寫請求仍然是分開考慮來解決衝突。
什麼是衝突?有些衝突是顯而易見的。兩個寫操做同時修改同一個記錄中的同一個字段,並將其設置爲不一樣的值。毫無疑問,這就是一個衝突。
而其餘類型的衝突可能會很是微妙,更難以發現。例如一個會議室預訂系統,它主要記錄哪一個房間由哪一個人在哪一個時間段所預訂。這個應用程序須要確保每一個房間只能有一組人同時預約(即不得有相同房間的重複預訂)。若是爲同一個房間建立兩個不一樣的預訂,可能會發生衝突。儘管應用在預訂時會檢查房間是否可用,但若是兩個預訂是在兩個不一樣的主節點上進行,則仍是存在衝突的可能。
若是存在兩個以上的主節點,會有多個可能的同步拓撲結構,如圖所示:
最多見的拓撲結構是所有-至-所有,每一個主節點將其寫入同步到其餘全部主節點。而其餘一些拓撲結構也有廣泛使用,例如,默認狀況下MySQL只支持環形拓撲結構,其中的每一個節點接收來自前序節點的寫入,並將這些寫入(加上本身的寫入)轉發給後序節點。另外一種流行的拓撲是星形結構:一個指定的根節點將寫入轉發給全部其餘節點。星形拓撲還能夠推廣到樹狀結構。
在環形和星形拓撲中,寫請求須要經過多個節點才能到達全部的副本,即中間節點須要轉發從其餘節點收到的數據變動。爲防止無限循環,每一個節點須要賦予一個惟一的標識符,在複製日誌中的每一個寫請求都標記了已經過的節點標識符。若是某個節點收到了包含自身標識符的數據更改,代表該請求已經被處理過,所以會忽略此變動請求,避免重複轉發。
環形和星形拓撲的問題是,若是某一個節點發生了故障,在修復以前,會影響其餘節點之間複製日誌的轉發。能夠採用從新配置拓撲結構的方法暫時排除掉故障節點。在大多數部署中,這種從新配置必須手動完成。而對於連接更密集的拓撲(如所有到所有),消息能夠沿着不一樣的路徑傳播,避免了單點故障,於是有更好的容錯性。但另外一方面,全連接拓撲也存在一些自身的問題。主要是存在某些網絡鏈路比其餘鏈路更快的狀況(例如因爲不一樣網絡擁塞),從而致使複製日誌之間的覆蓋,以下圖所示。
一些數據存儲系統則採用了不一樣的設計思路:選擇放棄主節點,容許任何副本直接接受來自客戶端的寫請求。
對於某些無主節點系統實現,客戶端直接將其寫請求發送到多副本,而在其餘一些實現中,由一個協調者節點表明客戶端進行寫入,但與主節點的數據庫不一樣,協調者並不負責寫入順序的維護。
假設一個三副本數據庫,其中一個副本當前不可用。在基於主節點複製模型下,若是要繼續處理寫操做,則須要執行切換操做。
對於無主節點配置,則不存在這樣的切換操做。用戶將寫請求並行發送到三個副本,有兩個可用副本接受寫請求,而不可用的副本沒法處理該寫請求。若是假定三個副本中有兩個成功確認寫操做,用戶收到兩個確認的回覆以後,便可認爲寫入成功。客戶徹底能夠忽略其中一個副本沒法寫入的狀況。
失效的節點以後從新上線,而客戶端又開始從中讀取內容。因爲節點失效期間發生的任何寫入在該節點上都還沒有同步,所以讀取可能會獲得過時的數據。
爲了解決這個問題,當一個客戶端從數據庫中讀取數據時,它不是向一個副本發送請求,而是並行地發送到多個副本。客戶端可能會獲得不一樣節點的不一樣響應,包括某些節點的新值和某些節點的舊值。能夠採用版本號技術肯定哪一個值更新。
讀修復
當客戶端並行讀取多個副本時,能夠檢測到過時的返回值。用戶從副本3得到的是版本6,而從副本1和2獲得的是版本7。客戶端能夠判斷副本3是一個過時值,而後將新值寫入到該副本。這種方法主要適合那些被頻繁讀取的場景。
反嫡過程
一些數據存儲有後臺進程不斷查找副本之間數據的差別,將任何缺乏的數據從一個副本複製到另外一個副本。與基於主節點複製的複製日誌不一樣,此反嫡過程並不保證以特定的順序複製寫入,而且會引入明顯的同步滯後。
咱們知道,成功的寫操做要求三個副本中至少兩個完成,這意味着至多有一個副本可能包含舊值。所以,在讀取時須要至少向兩個副本發起讀請求,經過版本號能夠肯定必定至少有一個包含新值。若是第三個副本出現停機或響應緩慢,則讀取仍能夠繼續並返回最新值。
把上述道理推廣到通常狀況,若是有n個副本,寫入須要w個節點確認,讀取必須至少查詢r個節點,則只要 w + r > n,讀取的節點中必定會包含最新值。例如在前面的例子中,n = 3,w = 2,r = 2。知足上述這些r、w值的讀/寫操做稱之爲法定票數讀(或仲裁讀)或法定票數寫(或仲裁寫)。也能夠認爲r和w是用於斷定讀、寫是否有效的最低票數。
參數n、w和r一般是可配置的,一個常見的選擇是設置n爲某奇數(一般爲3或5),w = r = (n + 1) / 2(向上舍入)。也能夠根據本身的需求靈活調整這些配置。例如,對於讀多寫少的負載,設置w = n和r = 1比較合適,這樣讀取速度更快,可是一個失效的節點就會使得數據庫全部寫入因沒法完成quorum而失敗。
一般,設定r和w爲簡單多數(多於n / 2)節點,便可確保 w + r > n,且同時容忍多達n / 2個節點故障。可是,quorum不必定非得是多數,讀和寫的節點集中有一個重疊的節點纔是最關鍵的。
也能夠將w和r設置爲較小的數字,從而讓 w + r <= n。此時,讀取和寫入操做仍會被髮送到n個節點,但只需等待更少的節點回應便可返回。
因爲w和r配置的節點數較小,讀取請求當中可能剛好沒有包含新值的節點,所以最終可能會返回一個過時的舊值。好的一方面是,這種配置能夠得到更低的延遲和更高的可用性,例如網絡中斷,許多副本變得沒法訪問,相比而言有更高的機率繼續處理讀取和寫入。只有當可用的副本數已經低於w或r時,數據庫纔會變得沒法讀/寫,即處於不可用狀態。
即便在 w + r > n 的狀況下,也可能存在返回舊值的邊界條件。這主要取決於具體實現,可能的狀況包括:
建議最好不要把參數w和r視爲絕對的保證,而是一種靈活可調的讀取新值的機率。
這裏一般沒法獲得前面的「複製滯後問題」中所羅列的一致性保證,包括寫後讀、單調讀、前綴一致讀等,所以前面討論種種異常一樣會發生在這裏。若是確實須要更強的保證,須要考慮事務與共識問題。
quorum並不總如期待的那樣提供高容錯能力。一個網絡中斷能夠很容易切斷一個客戶端到多數數據庫節點的鏈接。儘管這些集羣節點是活着的,並且其餘客戶端也確實能夠正常鏈接,可是對於斷掉鏈接的客戶端來說,狀況無疑等價於集羣總體失效。這種狀況下,極可能沒法知足最低的w和r所要求的節點數,所以致使客戶端沒法知足quorum要求。
在一個大規模集羣中(節點數遠大於n個),客戶可能在網絡中斷期間還能鏈接到某些數據庫節點,但這些節點又不是可以知足數據仲裁的那些節點。此時,咱們是否應該接受該寫請求,只是將它們暫時寫入一些可訪問的節點中?(這些節點並不在n個節點集合中)。
這稱之爲寬鬆的仲裁:寫入和讀取仍然須要w和r個成功的響應,但包含了那些並不在先前指定的n個節點。
一旦網絡問題獲得解決,臨時節點須要把接收到的寫入所有發送到原始主節點上。這就是所謂的數據回傳。
能夠看出,sloppy quorum對於提升寫入可用性特別有用:要有任何w個節點可用,數據庫就能夠接受新的寫入。然而這意味着,即便知足 w + r > n,也不能保證在讀取某個鍵時,必定能讀到最新值,由於新值可能被臨時寫入n以外的某些節點且還沒有回傳過來。