從這一章開始,我們就正式從單機應用轉向了分佈式應用的旅程!
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
- 在某個時間點,獲取leader數據庫的快照。有時候需要用到第三方工具,比innobackupex for MySQL
- 把這份快照拷貝到新的follower機器上
- follower連接到leader,請求在快照時間點之後所有的數據變更。這個快照時間點在MySQL中叫做binlog coordinates
- 當follower處理完所有積壓的數據變更之後,我們稱之爲“caught up”。這個follower就可以繼續處理來自leader的變更了。
1.3 處理節點宕機
Follower宕機: Catch-up恢復
如果follower掛了或是與leader間的網絡連接中斷,那麼可以在恢復之後,向主庫請求從中斷點開始之後所有的變更即可。
Leader宕機: 故障切換
故障切換: 把一個follower提升爲新的leader,重新配置客戶端,將它們的寫操作發送給新的leader,其他follower開始拉取來自新leader的變更。
故障切換可以手動也可以自動完成。自動的步驟一般如下:
- 如果leader沒有通過health check,可以認爲它掛了(宕機or網絡中斷)
- 通過選舉機制來指定新的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,平白無故提高複雜度。
但是如果是多數據中心,我們就可以用這種方案:
好處:
- 就近寫入,提高性能
- 一個數據中心掛了不會導致整站掛掉
缺點:
會有寫入衝突,下文會介紹。
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操作
- 服務端收到寫請求後,可以覆蓋比這個版本號小的所有的值,但是必須保留比這個版本號大的所有的值。(因爲它們是併發操作)