第5節 複製(replication)【設計數據密集型應用】

Replication(複製)

Replication就是把相同的數據複製到多個機器上。
有以下幾個緣由mysql

  • 讓數據的地理位置離用戶更近(以減小延遲)
  • 保證系統仍然能提供正常的服務即便某些機器宕機(提升可用性)
  • scale out(橫向擴展)機器能夠提供讀服務(提升吞吐量)

在本節咱們假設數據集足夠小,每一個機器均可以存儲整個數據集。git

若是你要複製的數據不隨着時間變化,replication就很簡單。你只需複製數據到每個節點,而後就作完了。可是現實卻不是這樣,當你複製數據的時候,源數據可能發生變化,replication的難點就在於此。github

接下來咱們會討論三個流行的算法來處理處理節點間數據的變化:single-leader, multi-leader, and leaderless replication 。幾乎全部分佈式數據庫都會用這些算法,每一個算法都有各自的優缺點。算法

Leaders and Followers(主從複製)

每一個節點保存數據庫的一個copy,這個節點叫作replica(複製品),當replica愈來愈多的時候問題就來了,怎麼保證數據都會到達每一個replica上。sql

每一次寫數據到數據庫,其餘的replicas都要寫這份數據,不然你們的數據就不一致了。數據庫

常見的解決方案是leader-based複製,又叫主從複製。緩存

  1. replicas之一被指定爲leader/master,當客戶端寫數據的時候,必需要發請求給leader節點,leader節點把新數據寫到本地。
  2. 其餘的replicas被叫作followers,不管何時當leader節點向本地寫數據的時候,他都會把數據變化發給其餘的followers,做爲replication log 或 change stream的一部分。每個followers從leader拿到log以後,按照log上的數據變化順序更新本身的copy。
  3. 當客戶端想要讀數據的時候,能夠從leader或followers節點讀,可是隻有leader能夠寫。

圖片描述

replication是許多關係數據庫的內置特性,好比:PostgreSQL (since version 9.0), MySQL, Oracle Data Guard [2], and SQL Server’s AlwaysOn Availability Groups [3].網絡

非關係數據庫也在用,好比:MongoDB, RethinkDB, and Espresso數據結構

leader-based replication不只僅用於數據庫,一些分佈式消息中間件也在用,好比:Kafka,RabbitMQ,另一些文件系統,replicated block devices好比DRBD也是相似的原理架構

Synchronous Versus Asynchronous Replication(同步vs異步)

複製系統中重要一點是使用同步複製仍是異步複製。在關係數數據庫中是能夠配置的,其餘系統通常是hardcode兩者之一。

想一想圖5-1發生了什麼,當網站的用戶更新本身的照片時,客戶端發送更新請求給leader節點,leader節點收到以後,數據變化轉發給follows,最終leader告訴客戶端更新成功。

圖5-2是系統中幾個組件的交流狀況:客戶端,leader,followers,時間從左到右,請求和響應用箭頭表示。

不難看出,follow1是同步複製,他複製完後會給leader響應,leader確認follower1複製完成後再告訴客戶端更新成功。follower2相反是異步的,leader不會等待follower2,圖中有明顯的延遲。

圖片描述

同步複製的優勢是follower節點都會確保有最新的copy。若是leader節點掛了,follower節點還有完整的數據能夠用。缺點是follower節點若是因爲某些緣由(網絡延遲,機器掛了等)不能響應,那麼leader節點就得傻等着,寫請求也不能處理。leader節點必須鎖住全部寫操做,直到同步複製響應或恢復正常。

因爲以上緣由,讓全部follower節點都同步複製是不現實的:任何一個節點中斷都會致使整個系統暫停。在實戰中,若是你在數據庫中啓動同步複製,通常是一個follower同步複製,其餘followers異步複製,若是同步複製的節點很慢或不可用,那麼會把另一個follower節點變成同步複製,從而這樣就保證了至少兩個節點擁有完整的copy:leader和同步複製的follower。這種配置被叫作semi-synchronous (半同步)
一般leader-based replication會被配置成徹底異步複製的。在這種狀況中,若是leader掛了或不能回覆,任何尚未來得及在followers上覆制的的「數據變化」都會丟失。也便是說,寫操做不保證持久化,即便已經被客戶端確認。然而徹底異步複製的好處是leader能夠繼續進行寫操做,即便全部followers節點掛了。

弱持久看起來是個很差的妥協,可是asynchronous replication卻被普遍應用,尤爲存在不少followers或者分佈在不一樣的地理位置。

Setting Up New Followers(設置新follower)

有時你須要添加新的followers節點,要麼增長replicas的數量,要麼替換失敗的節點。問題來了,你怎麼確保新的follower能獲得準確的leader數據的copy呢?

簡單的copy是遠遠不夠的,由於客戶端在持續的向數據庫寫數據,數據一直在不斷的變化,因此在不用的時間點複製數據會產生不一樣的版本。這樣的結果是沒有意義的。

添加新follower節點過程大體以下:

  1. 在某個時間點拍一個數據庫的快照
  2. 把快照複製到新follower節點上
  3. follower節點鏈接leader節點,而後請求全部從拍快照起開始的全部數據變化。快照應該與leader的replication log中的精確位置相關聯。這個「位置」有不少名字,在PostgreSQL叫log sequence number,在MySQL中叫binlog coordinates
  4. 當follower處理完從拍快照開始積壓的數據變化後,咱們就說它跟上了。而後follower就能夠繼續處理來自leader數據變化了。

添加新follower在不一樣數據庫中的實現各有不一樣。一些數據庫是全自動實現,一些須要手動實現。

Handling Node Outages處理節點故障

系統中任何節點均可能掛掉,可能因爲意外,也極可能因爲維護重啓。在重啓的同時又不影響系統正常提供服務對於運維來講是頗有利的。咱們的目標是保持整個系統運行,儘管有某些節點掛掉,同時將單點故障帶來的影響降到最低。

那麼咱們怎麼用leader-based replication實現高可用呢?

Follower failure: Catch-up recovery (follower節點故障:catch-up恢復)
在本地磁盤上,每一個follower節點都保存一個log文件,用來記錄來自leader的數據變化。若是follower節點掛了或者重啓,或者leader和follower節點之間的網絡中斷了。follower節點能很快恢復,由於log記錄了故障發生前的最後一次transaction。當follower連上leader以後,就能夠請求獲得因爲故障而缺失的數據變化,把缺失補回來以後,就和leader一致了,而後就能夠繼續接受leader的數據變化流了。

Leader failure: Failover (leader節點故障:故障轉移)

處理leader節點故障更棘手:一個follower節點要被晉升爲leader,客戶端要被從新配置,而後把寫請求發給新leader。其餘的followers開始接受新leader的數據變化。這個過程叫作故障轉移

故障轉移能夠手動操做也能夠自動完成。一個自動的故障轉移流程包括如下幾步:

  1. 確認leader掛了。死機,斷電,網絡問題均可能是緣由。目前沒有一個完美的方法能夠探測究竟是怎麼掛的。因此大部分系統都用timeout:節點之間互相發消息,若是某個節點在必定時間內沒有響應,咱們就假設這個節點掛了。
  2. 選新leader。選新leader能夠經過一個選舉流程(大部分replicas選舉的),也能夠被一個之前選舉的controller節點來指定。最佳候選人是和leader數據最接近的那個(最小化數據丟失)。讓全部節點都贊成以個新leader是consensus問題,之後會詳細討論。
  3. 從新配置系統去用新leader。客戶端須要發送新的寫請求到新leader。若是老leader回來後,其餘節點可能忘了老leader已經下臺了,可能還會把老leader當作leader。因此係統須要確保老leader變成follower並且可以識別新的leader。

故障轉移充滿各類容易出錯的點:

  • 若是採用異步複製,老leader掛了,新leader可能尚未接收到全部來自老leader寫請求。若是新leader上臺後,老leader又加入了集羣,會發生什麼?新leader可能既接受老leader的寫請求又接受客戶端的寫請求,這樣就會形成衝突。常見的方案是拋棄老leader的尚未複製的寫請求。這樣會違反客戶對數據持久化的指望。
  • 若是數據庫以外的存儲系統須要和數據庫中的內容須要協調,那麼直接拋棄寫請求是很是危險的。好比github的一次事故,一個過期(落後於當前leader)的follower被選爲新leader,數據庫用的是自動增加主鍵,過期的follower因爲落後於老leader而重用了部分已經用過的主鍵(老leader分配的),這些重用的主鍵Redis也在用,因此就致使了數據庫和Redis的數據不一致(一些私有數據被暴露給錯誤的用戶)。
  • 在某些場景,可能出現兩個節點都認爲本身是leader,這種狀況叫作split brain,並且是很是危險的。由於兩個節點同時接受寫,沒有進程來解決衝突,數據極可能丟失或崩潰。一些系統有本身的機制去關閉一個節點,可是這種機制設計很差的話,就容易關閉兩個節點。
  • timeout多久合適(在leader宣佈死亡以前)?timeout時間長,節點故障恢復時間就長。timeout時間短,就會形成不少沒必要要的failover。好比一個加載高峯致使響應時間變長,網絡小故障致使數據包延誤。若是系統面臨着高負載或者惡略的網絡環境,沒必要要的failover會使整個情況更糟糕。

目前沒有容易的方案來解決這種問題,因此不少運維團隊寧願選擇手動failover,即便系統支持自動failover。

節點失敗,網絡不可靠,圍繞數據複製一致性的權衡,持久化,可用性,延遲是分佈式中的基本問題,咱們會在後面章節詳細討論。

Implementation of Replication Logs

基於leader的replication在後臺是怎麼運行的?在實戰中有幾個不一樣的方法在使用,讓咱們一個個看。

Statement-based replication (基於語句的複製)
最簡單的案例,leader記錄下每一個寫請求(statement語句),把寫請求語句發給followers。對於關係數據庫,就是把INSERT, UPDATE, or DELETE 語句發給followers,而後follower解析執行SQL語句。

這種方法看起來不錯,可是在複製過程當中可能失敗:

  • 調用不肯定函數的語句,好比調用了NOW(),RAND()的語句在不一樣節點會產生不一樣的值。
  • 若是語句用的是自增加的列,或者他們基於數據庫已有的數據(e.g., UPDATE ... WHERE <some condition>),在每一個節點上他們必須按照特定的順序執行,不然會有不一樣的效果。當有併發transaction同時執行時,這會成爲限制。
  • 語句有反作用(e.g., triggers, stored procedures, user-defined functions) 在每一個節點上會產生不一樣的反作用,除非反作用是徹底肯定的。

我也能夠繞過這些問題,好比在語句被記錄下的時候,把不肯定的函數調用全換成肯定的返回值。這樣每一個follower都會獲得相同的值。然而,現實場景會有大量的邊界案例,因此其餘的replication方法會被預先考慮。

Mysql5.1版本以前採用的是基於語句的replication。今天仍在應用,由於他很緊湊。在有不肯定的語句中,Mysql默認轉換成基於行的replication。

Write-ahead log (WAL) shipping(預寫日誌運送)
第三節咱們討論了,存儲引擎怎麼在磁盤上存儲數據。咱們發現每次寫都會追加到到一個日誌中:

  • 在日誌結構的存儲引擎中(see 「SSTables and LSM-Trees」 on page 76),日誌是主要的存儲的地方。日誌segment在後臺進行壓縮和垃圾回收。
  • 在B-Tree結構的存儲引擎中(see 「B-Trees」 on page 79),它重寫每一個獨立的磁盤塊,每一次修改首先寫到預寫日誌中,這樣節點掛了以後,索引就能從日誌中恢復,從而數據一致。

不管哪一種狀況,日誌是一個只能追加的字節序列,它包括了全部的寫操做。咱們可使用徹底相同的日誌在另一個節點上去生成一份replica。除了把日誌寫到磁盤上,leader也會把經過網絡把日誌發到其餘follower上。當follower處理日誌的時候,它會生成一份和leader相同的數據結構。

這種replication方法被應用在PostgreSQL和Oracle等數據庫。主要缺點是,日誌描述的數據很詳細,好比,一個WAL包含哪一個磁盤塊的哪一個字節被改變。這就致使了replication和存儲引擎牢牢耦合在一塊兒。好比數據庫的存儲格式從一個版本換成另外一個版本,通常leader和follower不能運行不一樣的版本,因此就不能順利的升級版本。

這看起來是個小的實現細節,可是對運維有很大的影響。若是replication協議容許follower運行比leader更高的版本,那麼能夠先升級全部的followers,而後進行failover,把某個已升級的follower選爲leader。若是replication協議不容許版本不匹配,WAL shipping一般是這樣的,這種狀況須要停機來升級。

Logical (row-based) log replication 基於邏輯日誌
WAL shipping中replication和存儲引擎使用一樣的日誌格式,這也是耦合的緣由。讓存儲引擎和replication用不一樣的日誌格式,就能夠解耦了。這種日誌叫邏輯日誌(logical log),區分於存儲引擎的物理數據表示。

對於關係數據庫,邏輯日誌是描述對數據庫表寫操做的序列記錄,粒度是行(row)。

  • 插入一行,邏輯日誌會包含全部列的值
  • 刪除一行,邏輯日誌會包含主鍵或惟一肯定一行的信息,若是沒有主鍵,會包含全部的舊值。
  • 更新一行,邏輯日誌會包含主鍵和須要更新的列。

一個修改好幾行的transaction會生成這樣的日誌記錄,緊接着是一條記錄代表transaction被提交。mysql的binlog(若是配置成row-based replication)就會用這種方法。

因爲邏輯日誌和存儲引擎內部解耦了,因此很容易向下兼容,這樣leader和follower就能夠用不一樣的版本,或者用不一樣的存儲引擎。

邏輯日誌也能夠很簡單的被外部的應用解析。若是你想把數據庫的內容發給外部的應用,這點事很是有用的。好比把數據給數據倉庫作離線分析,定製索引和緩存。這種技術叫change data capture,第11節接着討論。

Trigger-based replication 基於觸發器
以上討論的replication方法都是在數據庫內部實現的,沒有任何的應用層的代碼。在不少狀況下,你可能須要更靈活。好比,replicate數據庫數據的一部分,把數據從一個數據庫複製到另外一個數據庫,或者你須要添加解決衝突的邏輯代碼,那麼你就須要把replication挪到應用層。

一些工具,好比Oracle GoldenGate,應用可使用這些工具從日誌中讀取數據變化。另一個方法是用關係數據庫的特性:trigger和stored procedure。

trigger可讓你註冊本身的代碼,當數據變化時,代碼會運行。trigger也能夠把數據變化存到另外一張表,外部應用能夠讀着張表來獲取數據變化。Databus for Oracle和 Bucardo for Postgres就是這樣的。

Trigger-based replication比其餘replication方法有更大的開銷。對於素菊開內置的replication方法,更容易出問題和限制。而後這種方法因爲他的靈活性而很是有用。

Problems with Replication Lag 延遲帶來的問題

容忍單節點錯誤是使用replication的一個緣由,另外還有擴展性(增長節點從而處理更多的請求),延遲(根據地理位置來放置節點)。

Leader-based replication中,寫要經過leader來執行,讀能夠經過全部節點執行。在多讀少寫的場景,簡單的增長follower節點就能夠減小leader的負擔。而後這種read-scaling架構只適用於異步replication,若是使用同步replication,一個節點掛了,就會致使整部系統不可用。節點越多,越可能出現問題。因此徹底同步的replication是不可靠的。

不幸的是,當應用從一個異步的follower節點讀數據後,它可能獲得過期的數據,由於那個節點可能落後於leader節點。這就致使了明顯的數據不一致,由於全部的寫操做並無當即反應在followers節點上。這種不一致時暫時的,等你中止寫數據後,過一會,followers節點都跟上後,就和leader一致了。因此叫作最終一致。

相關文章
相關標籤/搜索