分散式資料中的坑(一)Master-Slave架構

lyowish發表於2019-03-16

前言

Master-slave 架構可以說是最常用的架構,關係型資料庫諸如:mysql,postgreSql,oracle,Nosql諸如:MongoDb,訊息佇列諸如:Kafka,RabbitMQ等都使用了這種架構,本文將先簡要介紹此種架構並介紹高可用Master-slave架構中的一些坑,以及應對之策。

Master-Slave架構的原理

分散式資料中的坑(一)Master-Slave架構
如上圖所示,Master-slave可以說是最常見的架構:

  • 副本之一會被指定為Leader,或者是主庫,寫入請求會傳送給主庫,主庫會將資料寫入自己的本地
  • 每當主庫寫完後,會將變更的日誌傳送從庫,從庫按照主庫更新的順序應用所有寫入
  • 客戶端可以從任何庫讀取資料

同步複製和非同步複製

分散式資料中的坑(一)Master-Slave架構
如圖中所示,Follower 1是同步複製,Follower 2是非同步複製。 同步複製的優點是,從庫可以保證與主庫一致的最新副本,如果主庫掛了,可以從從庫找到一致的資料。缺點是,如果從庫掛了,沒有響應,那麼所有的寫入操作都將無法處理,直到從庫恢復為止。

所以通常情況下,這種架構一般都是用完全非同步的方式進行同步,這種情況下,如果主庫失效,那麼所有還沒有被複制的資料就可能會被丟失;但是如果從庫失效,主庫依然可以繼續處理寫入操作。

主從複製的底層實現

基於語句

主庫會記錄每個請求的sql,並且將這些sql:insert select update等傳送至從庫執行,雖然聽上去沒什麼問題,但是:

  • 有一些非確定性的函式會在每個副本上產生不一樣的結果,比如now(),rand()這種
  • 如果有自增列或者語句有依賴資料庫現有的資料,那麼必須保證從庫執行的順序與主庫相同,否則會有不同的結果
  • 另外,一些儲存過程,觸發器的執行,如果有問題的話,他會影響到所有的從庫

這種複製方法現在基本不太使用了。

基於預寫日誌

之前說過,主庫在處理寫請求時,都會先寫WAL(Write Ahead Log),日誌的結構非常底層,包含了寫入時需要追加的資料序列:磁碟塊中哪些資料發生了更改。這就意味著它會比資料庫儲存結構緊密相關,甚至有可能資料庫只因為版本不一致而導致沒辦法複製。

對於運維來說,這點就很不友好了。如果複製協議對於版本不匹配的話,通常情況下需要停機才可以升級。

基於行的邏輯日誌

另一種方法是使用另一種日誌,只是這種日誌不再於底層儲存耦合,比如Mysql的binlog。它一般是以行為密度來描述寫入的操作:

  • 對於新插入的行,日誌包含所有列的新值
  • 對於刪除的行,日誌包含唯一主鍵來標誌已刪除的行
  • 對於更新的行,日誌同樣包含唯一定位到這條記錄的資訊,來記錄新資料

這種日誌廣泛應用,它包含的資訊與底層完全解耦,甚至可以基於它複製不同資料庫的資料。

Master-Slave架構如何實現高可用

增加設定新的從庫

如果我們需要新增新的副本,如何保證新的從庫擁有與主庫完全一致的資料呢?因為客戶端在不斷的向主庫寫入資料,最簡單的辦法,我們可以禁止主庫的寫入,然後使用日誌進行同步;但這會違揹我們高可用的原則。我們一般使用如下方法:

  • 大多數的資料庫都有一致性快照的功能,我們可以獲取它
  • 接著講快照複製到新的從庫節點
  • 從庫複製所有快照時間點之後的資料根據日誌(mysql中的binlog)追趕主庫

從庫掛了怎麼辦

首先每個從庫的硬碟上肯定也會記錄所有從主庫收到的資料庫的變更,如果從庫掛了,等他恢復的時候可以從日誌中知道發生故障之前最後處理的一個事物,接著連線主庫,請求拉取所有之後它斷片之後資料變更,之後追趕上主庫即可

主庫掛了怎麼辦

主庫掛了需要fail over(故障轉移):需要將一個新的從庫提升為主庫,並且重新配置應用客戶端,將所有寫操作傳送到新的主庫。通常有如下幾個步驟:

1、確認主庫失效。現實生活中有很多原因導致失效:崩潰、停電、網路卡以及機器被修空調的師傅搬走等原因。沒有萬無一失的方法,大部分系統採用簡單的超時來確定。

2、選一個新的主庫。主庫的選舉通常是以擁有著主庫最新資料的那個從庫為準,具體的演算法可以是paxos raft等共識演算法。

3、重新配置路由,寫請求傳送到新的主庫上;並且如果老領導回來了,需要避免“腦裂”的情況,讓老領導下臺成從庫。

failOver同樣會有很多問題:

  • 如果是非同步複製,那麼難以避免會有資料的丟失,這些資料寫入的丟失往往會被直接忽略
  • 寫入的丟失還可能會有潛在問題,因為資料庫的資料可能會與外部儲存(redis)想結合,進行業務上的控制或者快取。Github曾經有這個故障,一個過時的從庫被提升為主庫,表採用自增ID列,因為丟失了資料,新主庫的計數器落後於老主庫的計數器,所以新主庫分配了一些已經被老主庫分配掉了的ID作為主鍵,而這些主鍵又在redis中當快取使用,導致了隱私資料的洩露。
  • 腦裂的問題:老領導恢復過來發現自己還是領導,那麼這個叢集中就有兩個leader,兩個leader的話會產生一系列的衝突,例如下:

分散式資料中的坑(一)Master-Slave架構
這個問題我們下一節解決它

  • 主庫失效的超時時間應該如何配置?太長的話,意味著更多的資料可能被丟失,太短的話有可能出現不必要的故障轉移(比如只是單純的系統負載比較高或者網路有延遲)

複製延遲的問題

大部分web應用都是讀多寫少,所以我們通常採用多副本非同步複製的架構,基於非同步架構,可能會導致資料庫從庫和主庫的明顯不一致,但是經過一段時間後,他們最終會是一致的。

正常情況下,複製延遲大概是幾分之一秒,但是在極端的情況下(網路延遲,機器高負荷)延遲可能達到幾秒甚至幾分鐘。

所以這是我們在實際中會遇到的真實問題,瞭解這些問題之後可以幫助我們更好的設計業務。問題體現在下面幾個方面:

讀己之寫

分散式資料中的坑(一)Master-Slave架構
如上圖所示,使用者剛提交的資料就查不到了。 我們需要保證讀寫一致性。

讀寫一致性的意思就是保證使用者可以讀到自己的寫,但是不保證其它使用者也可以及時的讀到;其它使用者可能需要延遲才可以讀取到。保證讀寫一致性的話,有下面幾個方向:

  • 基於業務,從主庫讀。舉個例子,微信的個人資料通常只能由你自己編輯,那麼我們就可以在業務上控制:從主庫讀取自己的檔案,從從庫讀取其他人的檔案
  • 如果業務上的資料還是由大多數人編輯呢?這種情況我們可以追蹤末次更新的時間,從末次更新時間一分鐘內的讀取都從主庫讀。
  • 客戶端在每次查詢時帶上一個時間戳(最好是邏輯時鐘或者是同步的系統時鐘,時鐘裡面也有一些坑後續會講到),從庫收到請求發現自己的資料還不夠新的話,將請求redirect給主庫或者其他從庫查詢

還有一種情況,比如使用者有電腦和app端進行查詢,電腦更新了資訊如何保證手機上可以及時的檢視到,如何保證讀寫一致性呢?有多臺裝置的話你無法記錄末次更新的時間戳(因為手機不可能知道電腦末次操作的時間),不同裝置的時間本身也是不可靠的,這種情況,可以根據userID進行雜湊,保證同一個使用者永遠會落到一個Datacenter上的主庫。

單調讀

單調讀的意思是時光倒流:

分散式資料中的坑(一)Master-Slave架構

使用者2345前一秒還可以查詢出結果,後一秒資料就沒有了。單調讀是這種異常的保證機制,我們需要保證同一個使用者的查詢請求總是被落到同一個follower上,比如說可以根據userid取模,雜湊到固定的機器上。

一致字首度

分散式資料中的坑(一)Master-Slave架構
如上圖所示,所有問的問題儲存在分割槽1,回答的答案儲存在分割槽2,Poons問了一個問題,Cake回答,明明是先問問題再回答,但是從觀察者的角度來看,時間順序卻已經錯亂了。

實際上資料庫的操作可以分成因果操作和併發操作兩種型別,併發操作可以理解為A發起set A操作,B發起set B操作,這兩個操作是併發的沒有先後因果關係的,資料庫對於這種操作只需要確認發生的順序就可以確定最終的值;對於因果操作:A發起insert 666,B再發起update 666,這兩個操作是有依賴關係的,A成功了B才可以成功。如果資料庫先接受到B的請求,那麼久發生衝突了。 這樣的操作叫做因果操作。(下一章會詳細討論)

回到我們的問題,如果要解決這類問題,首先我們需要一個演算法分辨出那些操作是併發的,那些操作是因果的;對於這種問問題再回答的典型的因果型別的操作,我們應該儘量讓他們分配到同一個partition之內,確保有因果關係的寫入到寫到同一個分割槽。

總結

本文討論了典型的master-salve架構中常見的問題和解決的辦法,某些概念還沒有進行深入研究,後續介紹多主、無主架構時再說明。

根據CAP原則,最終一致性是無可避免的,但是我們可以做一些基於業務的特殊處理,在保證高可用的同時,儘量去保證資料的一致性。

相關文章