《設計數據密集型應用/DDIA》精要翻譯(四) :副本機制

從這一章開始,我們就正式從單機應用轉向了分佈式應用的旅程!

ps: 其實DDIA這書我1月份已經看完了,只不過那會兒實在沒有心力去翻譯。前段時間太太太忙了,對幾千萬日活的系統做了技術棧遷移、跨洲數據中心無縫平移。後續也會寫文章來分享我們是怎麼做的,歡迎持續關注。

副本機制的意思是,在多臺通過網絡互連的機器上保存同一份數據的多份拷貝。我們爲什麼需要副本:

  • 讓用戶在地理上離數據更近,從而降低延遲
  • 在一部分節點宕機的時候,系統仍能繼續工作(即提高可用性)
  • 擴大機器數量,從而可以支撐更高的讀請求量

1.Leaders和Followers

常見的一種副本模式:leader-based replication, 工作原理如下圖所示:

1.副本中的一個被指定爲leader,客戶端的寫請求必須發給leader處理
2.別的副本被稱作followers, 當leader把新數據寫到本地存儲時,它也會把數據變更以log的形式發給它所有的followers。 每個follower從leader那裏拿到log之後,把變更應用到本地存儲。
3.客戶端的讀請求可以由leader或者followers來處理。

這種模式有很多系統在用,比如PostgreSQL, MySQL, Oracle Data Guard, MongoDB, RethinkDB, Espresso ,Kafka, RabbitMQ。

1.1 同步和異步複製

同步複製: leader接受到寫請求後,需要等待follower的確認。好處是提高了duration。
異步: 不需要等待。好處是提高了響應速度。 目前被廣泛應用。

在下圖中,follower1的複製是同步的,follower2的複製是異步的。

半同步的配置: 一部分同步,一部分異步。

1.2 增加新的follower

  1. 在某個時間點,獲取leader數據庫的快照。有時候需要用到第三方工具,比innobackupex for MySQL
  2. 把這份快照拷貝到新的follower機器上
  3. follower連接到leader,請求在快照時間點之後所有的數據變更。這個快照時間點在MySQL中叫做binlog coordinates
  4. 當follower處理完所有積壓的數據變更之後,我們稱之爲“caught up”。這個follower就可以繼續處理來自leader的變更了。

1.3 處理節點宕機

Follower宕機: Catch-up恢復

如果follower掛了或是與leader間的網絡連接中斷,那麼可以在恢復之後,向主庫請求從中斷點開始之後所有的變更即可。

Leader宕機: 故障切換

故障切換: 把一個follower提升爲新的leader,重新配置客戶端,將它們的寫操作發送給新的leader,其他follower開始拉取來自新leader的變更。

故障切換可以手動也可以自動完成。自動的步驟一般如下:

  1. 如果leader沒有通過health check,可以認爲它掛了(宕機or網絡中斷)
  2. 通過選舉機制來指定新的leader

故障切換是一件非常麻煩的事情:

  • 如果是異步複製,可能會丟數據
  • 腦裂: 多個節點都認爲自己是leader。

1.4 Replication Logs的實現

基於語句的複製

比如對於mysql而言, 用delete之類的語句。
但現在在默認情況下,如果語句中存在任何不確定性(比如調用函數NOW()、自增列、有副作用(比如觸發器和存儲過程)),MySQL會切換到基於行的複製(見下文)。

Write-ahead log (WAL)

非常底層,WAL會記錄哪些磁盤塊中的哪些字節發生了更改。

邏輯日誌複製(基於行的)

以行爲粒度記錄變更:

  • 對於插入的行,日誌包含所有列的新值。
  • 對於刪除的行,日誌包含主鍵,如果表沒有主鍵,需要記錄所有列的舊值。
  • 對於更新的行,日誌包含足夠的信息來唯一標識更新的行,以及所有列的新值。

基於觸發器

比如Canal之類的。

2. Replication Lag造成的問題及解決方案

如果客戶端從異步複製的follower那裏讀取,它可能會拿到已經過時的數據。如果停止向leader寫入並等待足夠時間,follower最終會追上leader,這叫做最終一致性。

複製延遲導致的一些問題和解決方法:

2.1 讀已之寫

如果用戶上傳了一些數據,但是讀請求打在了還沒同步變更的那個follower上,這個用戶大概率會罵一句傻逼。

這種情況我們就不能用最終一致性,可以用讀寫一致性,read-after-write consistency / read-your-writes consistency。

具體怎麼做呢:

  • 從主庫讀用戶可能修改過的東西,比如用戶拉自己的profile頁時,都走主庫,拉別人的就可以走從庫
  • 但是如果大部分內容都可能由用戶來編輯,就不能用上面的方案了。這時可以用別的方法:記錄上次變更的時間,如果與當前時間的差小於某個閾值,就走主庫

2.2 單調讀

從異步複製的follower讀數據會碰到另一個問題是:moving backward in time,也就是時光倒流問題。。

比如用戶看別人的主頁,第一次從一個延遲小的從庫,第二次讀一個延遲大的從庫,會發現剛剛刷到的動態又消失了。

解決這種問題的方法是單調讀: 單調讀比強一致性弱,單比最終一致性強。 單調讀保證如果一個用戶順序進行多次讀,那麼後續的讀取不會讀到比前面的讀取更老的數據。

實現方法:保證每個用戶總是讀同一個副本,比如根據userid來做hash(但是如果那個副本掛了,做了重新路由之後,這種保證又被打破了)。

一致前綴讀

在分區/分片數據庫中,還有一個特殊問題: 如果某個分區的複製速度比另一個慢,那麼同時讀取這倆分區的用戶可能得到順序錯亂的數據, 如下圖:

解決這個問題的常見思路是: 有因果關係的寫入都寫到同一個分區中。
(ps:比如在kafka中,如果要保證兩條消息的消費順序,那麼就要保證它們寫入了同一個partition )

3. 多主複製

前面我們討論的都是單一leader的複製架構,這也是比較常見的做法。除此之外,還有一些別的方案,比如多leader、無leader。

3.1 多主複製的使用場景

多數據中心

如果是單數據中心,沒有必要用多leader,平白無故提高複雜度。
但是如果是多數據中心,我們就可以用這種方案:
這裏寫圖片描述

好處:

  1. 就近寫入,提高性能
  2. 一個數據中心掛了不會導致整站掛掉

缺點:
會有寫入衝突,下文會介紹。

3.2 處理寫入衝突問題

如下圖,當多個用戶對同一個數據做修改時,如果用異步複製,可能會導致:數據都寫到了本地leader,但是在複製時發生了衝突。

同步/異步衝突檢測

上圖是異步檢測。
同步衝突檢測: 等到寫入被複制到所有的副本後才返回成功

避免衝突

特定用戶的寫入、或者同一份數據的寫入都寫到同一個數據中心

收斂到一致狀態

不管怎麼樣,數據庫最後必須處於一致狀態,即所有副本必須再副本複製完成後處於同一個值。

方法:

  • 給每個寫入一個唯一ID,比如時間戳之類的。最終值爲最後的寫入。 即Last Write Wins, LWW。
  • 記錄衝突,用程序或人工去解決。

4. 無主複製

現在基本不用了。 AWS內部的Dynamo系統在用。(注意並不是DynamoDB, DynamoDB是單一Leader架構)。

5.解決寫入衝突

5.1 LWW

上文已經介紹了

5.2 “happens-before”關係與併發

如果操作B依賴操作A,那麼A happens-before B。 如果A和B都沒有happens-before對方,那麼可以說他們是併發的。

我們可以用下面的方法來判斷兩個操作的關係:

這個算法的工作原理是:

  • 數據庫爲每個key創建版本號,每次變更都會增加版本號,把寫入的值和新版本號一起存儲
  • 客戶端讀取時,服務端返回key最新的版本號以及所有的值。客戶端在寫入前必須先讀取
  • 客戶端寫入時,要帶上之前讀取的版本號,並且把之前讀到的值與新的值做一個merge操作
  • 服務端收到寫請求後,可以覆蓋比這個版本號小的所有的值,但是必須保留比這個版本號大的所有的值。(因爲它們是併發操作)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章