知道微服務架構下如何做數據複製嗎?

關注Java後端技術棧

回覆“面試”獲取最新資料

回覆“加羣”邀您進技術交流羣

前言

一個高可用的分佈式系統,底層的存儲也是需要高性能、高可用的。上一篇介紹了一些數據存儲產品,如果數據存儲服務都是單庫的,那麼在唯一的單庫發生故障時,將導致上層的微服務系統也都無法正常運行。爲了加強可用性,我們需要更多的數據庫節點,一個節點掛了,可以快速切換到可用的節點提供服務。有更多的節點提供服務可以帶來很多好處,如高可用、高性能,基於地理分佈的數據中心可以提升用戶訪問速度。

爲了提供更好的性能和可用性,也需要解決一些複雜的問題,尤其是多 Node 的數據複製問題。第一篇文章中有介紹到網絡延遲的問題,在多節點間進行數據複製必然會遇到網絡延遲,也需要能處理節點上下線、一致性問題。本篇內容將介紹分佈式存儲系統數據複製的問題和解決方案,以及以 MySQL 爲例介紹一下主從複製模型,最後延伸介紹一下多數據中心使用的主主複製。

複製的模型

數據庫的複製主要是爲了讓多個數據庫實例的節點都保存同一份數據,數據的傳輸有同步和異步兩種方式。假設我們有三個數據庫節點,通過下圖示意可以看到兩種方式的不同。

同步數據複製

同步的數據複製一般是阻塞的,保證了所有的 Node 都保持最新的數據。不過同步複製的方案一般不會被實踐使用,同步的方式對性能的犧牲是比較大的,而且可用性低,如果要保證強一致性,則會犧牲可用性,一個節點的故障,會導致集羣的同步任務阻塞。

異步的數據複製

異步的數據複製常用於主-從(有時也叫 Master-Slaves,Leaders-Followers)複製。客戶端確認數據保存到主庫之後即可返回,對於其他從庫的節點是異步的方式同步的。異步複製縮小了響應時間,提高了服務系統的吞吐量,一致性方面保證最終一致性

爲了提高可用性,對於有多個從庫的數據複製,可以同步異步結合。主庫同步給一個從庫,然後直接返回,跟主庫實時同步的從庫再跟其他從庫間進行異步複製,這樣可以保證當主庫掛掉時,有一個從庫可以保證有最新的數據。如果這個同步的從庫掛了,可以將同步任務切換到其他正常運行的從庫。這種同步保證了至少兩個 Node 有最新的數據,進一步增強了一致性。雖然增強一致性聽起來很美好,但大多數的數據庫集羣還是以“全異步”實現數據複製。在大的分佈式集羣的環境下,尤其是多數據中心,雖然異步複製有“弱持久化”的風險,但是可用性強,避免了同步複製阻塞在故障節點,而且對於網絡延遲敏感型的系統來說,同步複製對系統的性能將會造成更大影響。

數據複製實現

無論是同步或是異步,不同節點的數據複製需要一些“媒介”,因爲數據庫複製不會像應用服務,提供個接口調用來進行同步異步。比較廣泛使用的實現都是基於主庫提供的複製日誌(Replication Log),一般複製日誌都是以二進制數據存儲,有的文獻也會叫二進制日誌。

實現方式

日誌複製的方式每個數據庫可能會稍微有差別,主要的實現方式有以下幾種:

(1)基於語句的複製(Statement-based Replication)

這種比較好理解,就是主庫關於寫入的語句,update、insert、delete 等都在持久化後同步到複製日誌裏,然後將語句執行的請求發送給其他從庫按序執行,這種基於語句執行一般會引發一些問題,比如當執行語句中有調用本地資源的函數是,如 NOW(),通過第一篇文章可以瞭解到,時鐘同步的問題,各個節點時間各不相同,所以會導致各個節點執行後時間列(如 utc_create)的數據不一致,MySQL 5.7.7 版本之前的 binlog 默認使用的是基於語句的複製。

(2)基於 WAL(Write-ahead log)

在第二篇文章中介紹了 WAL 一般用於 SSTables 結構的存儲服務。B-tree 結構的服務也一樣,所有“寫”操作需要先寫入到一個 write-ahead log 中,然後再執行 DB 的持久化。WAL 一般是以字節流存儲,並且順序存儲所有“寫”操作,關係型數據庫中 PostgreSQL 使用 WAL 進行數據複製和故障恢復。WAL 也有不足,在需要進行數據庫升級的時候,需要確保新版本的 DB 的複製協議能夠向下兼容,這樣就可以先升級從庫,再升級主庫來確保不停服務的滾動升級。如果 DB 不支持向下兼容的話,需要停止數據庫服務來升級,以及啓動後進行數據恢復。

(3)基於行的複製(Row-based Replication)

基於行的日誌屬於一種邏輯日誌,不依賴存儲引擎的版本,存儲引擎可以在不停服務情況下進行滾動升級,不同的節點也可以運行着不同的引擎版本。邏輯日誌中一般包含如下信息:

  • insert 語句所有插入的值信息

  • update 的主鍵以及更新(前)後的值

  • delete 包含唯一主鍵信息

當然基於 Row 複製也有一些缺點,比如對於批量更新操作,基於 Statement 的在日誌中只需要一句,在基於 Row 的日誌則需要很多行,佔用的日誌空間大,用來做備份、恢復的時間也會相對較長。所以基於 Row 的二進制日誌適用於小的事務的業務存儲。另外,Statement-based 和 WAL 的區別主要是寫入的時間的不同。WAL 是在語句執行前先寫入 log 確保了即使持久化執行失敗也可以用WAL進行恢復;Statement-based 是在執行本地持久化之後再同步到日誌中。

MySQL 的主從(Master-Slave)集羣複製

關係型數據庫,以 MySQL 爲例,提供的數據複製是基於二進制日誌(binlog)的異步複製實現,MySQL 提供了三種模式:Statement-based、Row-based(V5.1 版本之後提供)及 Mixed。Mixed 即同時有 Statement-based 和 Row-based 的混合模式,Mixed 默認使用 Statement 記錄,當有一些 Statement 無法準確處理的函數時,會使用 Row 格式。MySQL 的 5.7.7 之後默認的 binlog 實現是基於 Row 格式的複製日誌。具體使用哪種格式記錄,需要結合服務的特點,兩種日誌對比的細節可以看 [Statement-Based 和 Row-Based 複製的利弊:

https://dev.mysql.com/doc/refman/5.7/en/replication-sbr-rbr.html

這篇文章。

MySQL 的複製任務主要是由以下三個線程完成。

(1)Master 的 Binlog dump 線程

Master 運行的線程,負責把 binlog 內容發送給 slave,如果是有 N 個在連接的 slaves,則會創建 N 個 binlog dump 線程分別處理複製任務。

(2)Slave 的 I/O 線程

在 Slave 上運行的線程,負責連接 Master 並且請求 Master 將更新的 binlog 記錄發送過來,然後讀取 Master 的 binlog dump 線程發送來的數據,複製到本地的一箇中繼日誌(Relay Log)中。

(3)Slave 的 SQL 線程

Slave 創建的 SQL 線程用於讀取 Relay log 中的記錄,以及執行其中的事件任務。

下圖簡單示意了 MySQL 的主從複製流程:

對於 Binlog 的一些優化參數可以詳細查看 Binlog options,在 5.6 之後版本新增的 binlog_ row_image= minimal 參數可以讓 binlog 的 Update 只記錄影響後的行,一定程度也優化了 binlog 文件的大小。

在管理集羣時,如果想查看當前延遲信息,可以通過 show slave statusshow master status 命令查看,主要觀察 Slave 中的 Slave_IO_State 以及 Read_Master_Log_Pos(當前同步到的主庫的 binlog 偏移量),對比 Master 的 binlog 偏移量信息,即可知道大概有多少 I/O 延遲。

分佈式讀寫一致性問題

基於日誌的異步複製提升了數據存儲寫數據的吞吐量,但在分佈式的存儲集羣中,可能在同一時間點上,由於網絡延遲等各種原因,不同數據節點讀到的數據不同。所以儘管在單服務實例中的 InnoDB 支持事務的 ACID,但是在分佈式集羣中,使用異步複製的 ACID 也不再是絕對的。基於 Binlog 的實時複製提供的集羣一致性是“最終一致性”,最終一致性簡單可以理解爲,如果不再往主庫中寫入數據,那麼過一段時間,所有的數據庫節點的數據將完全一致。

主從讀一致性

對於主從集羣,如果寫主庫、讀從庫,因爲網絡延遲所以一般無法讀到最新的數據,可以通過一定方式,將一部分讀請求到主庫,如:

  • 通過業務場景區分,將修改數據的用戶的讀請求落在主庫,比如個人中心,當讀自己的個人中心時讀主庫,當讀其他人的個人中心時讀從庫。

  • 通過時間區分,比如最近 N 分鐘更新的數據從主庫讀,一定時間之前的數據讀從庫,不過需要不斷監控從庫的同步進度,是否可以控制在這段時間之內。

  • 如果是跨地域的多數據中心,需要儘量將用戶讀寫請求路由到同一數據中心的主庫。

還有另外一種方式,不從主庫讀數據,但要保證一個用戶只從一個從庫讀。如果用戶兩次同樣的請求落到兩個從庫,可能會因爲數據沒有同步,讀到不同版本的數據。爲了保證讀取一致,可以通過用戶 ID 來路由到一個從庫,所有讀請求都到這個從庫,如果這個從庫故障了,再重新路由到另外一個從庫繼續單調讀取。

多主複製

主從複製的集羣,寫操作集中在主庫,所以可以很好地處理併發寫。如果是多主複製(Master-Master)或者多數據中心,則情況會更爲複雜,比如多個 Master 都可以接受寫操作時,需要能夠處理寫入衝突。

一般在單個數據中心的集羣中很少會採用多主,因爲多主會增加複雜度,也確實沒有必要,但是對於多數據中心,一般都是需要每個數據中心至少有一臺主庫。設想一下如果北京和上海分別有個數據中心,但是主庫在上海,北京從庫需要異步複製數據,跨地域的網絡會導致很高的數據同步延遲。如果上海庫的主庫掛了,作爲容災要處理的 Leader 選舉也會更加複雜,並且 Leader 選舉期間,北京從庫們也只能等着,無法接受數據寫入。所以,更多的情況下,多數據中心都會選擇每個數據中心分別有一個主庫,每個數據中心自身內部是主從複製結構。

多主複製的問題

多主複製雖然好,但是卻比一主多從的結構複雜,有很多問題需要解決,問題主要源於多個主庫都可以寫入,而多個主庫的數據源是一樣的,並且跨集羣存在不可預估的網絡延遲,比如自增主鍵的衝突、寫入數據的衝突、數據庫唯一約束的衝突等。

衝突處理

下面介紹一些主主複製的衝突處理方案。

(1)同步寫

多主的寫入其實跟應用系統的併發很類似,一種簡單的處理方式就是同步,如上文的“同步模型”,用戶的寫入操作,需要等待所有的主庫都同步好之後再返回給客戶端,這種方式一般不會被採用,因爲這還不如用一主多從的同步複製。

(2)避免衝突

從業務場景思考,可以一定程度避免衝突,比如同一個維度的數據只在一箇中心寫入,例如“用戶的暱稱”必須是唯一的,解決方式可以是用戶編輯、新增暱稱的業務請求,只往一個數據中心寫。但這種方式也會增加複雜度,並且在一些流量高峯的場景,沒有辦法讓其他數據中心通過分擔寫請求來削峯。

(3)衝突檢測-處理

類似 LWW(Last Write Win)的方式,給每個寫操作一個唯一的 ID,如使用時間戳 timestamp,總是接受時間戳更大的值的寫入,MySQL 的 NDB 集羣 提供了內置的基於 LWW 的衝突解決,但 LWW 有數據丟失的風險。也有一種方式是記錄衝突的信息,並且將衝突稍後返回給應用去解決。

對於衝突解決,還有一種方案是存儲服務只進行衝突檢測,然後將衝突信息交給應用端去處理。

  • 寫檢測:寫檢測是通過檢測複製日誌中的數據衝突,然後會調起一個後臺線程來處理衝突信息,一般這種衝突不會反饋給用戶,可以應用端實現一些衝突解決接口。

  • 讀檢測:讀檢測一般是寫數據的衝突已經存儲了,在數據被讀取的時候,不同的數據的版本會一起返回給應用,然後應用可以自己選擇或者反饋給用戶來選擇想提交的數據。

MySQL NDB 也支持讀衝突檢測,其檢測到讀取數據衝突時(比如一個節點要更新一行被另外一個集羣節點刪除的行數據),通過設置一個讀排他鎖,所有關於這個含有衝突數據的讀取都會被記錄(Logged)到一個異常表中。當然 NDB 也提供了一些 API 讓應用層自己實現一些衝突處理方法。

多主複製的拓撲模型

目前很多數據庫會自帶多主複製的集羣模型,如 MySQL 的 NDB 集羣,也有一些實現多主複製的外部工具,比如 MySQL 的多數據中心集羣複製工具 Tungsten。多主複製的模型跟主從的區別就是多個主庫都可以接受寫入請求,而原來主從同步的模型結構不變。

在多主複製的網絡拓撲結構裏,比較普遍的是一下幾種:

  • 環形:MySQL 集羣默認是環形,一個主庫處理寫數據後依次(單向傳遞)發送給其他 Leader 進行數據複製。因爲有序,且在複製日誌中會記錄所有節點的 ID (每個節點的唯一標識),可以防止重複處理。

  • 星形:一般基於一個 Root 點的同步,比如 Tungsten 的 HA 集羣實現。

  • All-to-All:每個主庫都會把寫入的消息傳輸給其他所有的主庫。

三種方式各有利弊,環形和星星比較簡單,不過在一個主庫處理失敗時,有可能需要人工介入處理。All-to-All 的模型可以避免單點故障,但是延時比較嚴重,會有寫衝突導致一些一致性問題,不過事件傳輸的順序也可以通過一些基於寫衝突場景的處理方案來處理:如基於物理時間的 LWW、Happens before 關係、基於邏輯時鐘的版本向量(Version Vectors)等。

小結

在分佈式環境下,爲了提高性能以及可擴展性,大規模集羣需要進行數據的複製,本文主要介紹了分佈式的數據庫的增量的數據複製的一些模型和方案。對於數據的備份、恢復也是一個比較大的話題,不同的數據庫提供了不同的 dump 工具,這裏就不詳細介紹了。對於分佈式的複製,會存在一些複雜的問題,主從同步的一致性問題、主主複製的寫衝突問題。最終解決方案需要根據具體的領域模型做判斷,思考和分析的粒度越細,就會越清晰地知道該怎樣去處理分佈式的問題。下一篇內容還是從分佈式的存儲出發,介紹另一個維度的話題——分區。

資料

  • http://book.mixu.net/distsys/replication.html

  • https://dev.mysql.com/doc/refman/5.7/en/replication-implementation-details.html

  • https://dev.mysql.com/doc/refman/5.7/en/replication-sbr-rbr.html

  • https://dev.mysql.com/doc/refman/5.7/en/replication-options-binary-log.html

  • https://dev.mysql.com/doc/refman/5.7/en/mysql-cluster-replication-conflict-resolution.html

 


微服務架構下數據如何存儲?有考慮過嗎?

微服務架構下如何做數據分區呢?

聊聊【微服務架構】下【分佈式系統的問題】

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章