攜程異地多活-MySQL實時雙向(多向)複製實踐

一、前言

攜程內部MySQL部署採用多機房部署,機房A部署一主一從,機房B部署一從,作爲DR(Disaster Recovery)切換使用。當前部署下,機房B部署的應用需要跨機房進行寫操作;當機房A出現故障時,DBA需要手動對數據庫進行DR切換。

爲了做到真正的數據異地多活,實現MySQL同機房就近讀寫,機房故障時無需進行數據庫DR操作,只進行流量切換,就需要引入數據實時雙向(多向)複製組件。

二、DRC 介紹

DRC(Data Replicate Center)是攜程框架架構研發部推出的用於數據雙向或多向複製的數據庫中間件,在公司G2(高品質Great Service、全球化Globalization)戰略的背景下,服務於異地多活項目,賦予了業務全球化的部署能力。

三、DRC 架構設計

DRC採用服務端集中化設計,配合另一數據庫訪問中間件DAL(Data Access Layer)的本地讀寫功能,實現數據就近訪問。

模塊介紹

  • Replicator Container

Replicator Container 實現對 Replicator 實例的管理,一個 Replicator 實例表示對一個MySQL集羣的複製單元,Instance將自己僞裝爲MySQL的Slave,實現Binlog的拉取和本地存儲。

  • Applier Container

Applier Container實現對Applier 實例的管理,一個Applier 實例連接到一個Replicator 實例,實現對Replicator 實例本地存儲Binlog的拉取,進而解析出SQL語句並應用到目標MySQL,從而實現數據的複製。

  • Cluster Manager

Cluster Manager負責集羣高可用切換,包括由於MySQL主從切換導致的Replicator 實例和Applier 實例重啓,以及Replicator 實例與Applier 實例自身主從切換引起的新實例啓動通知。

  • Console

Console提供UI操作、外部系統交互API以及監控告警。

四、DRC 詳細設計

4.1 接入DB規範

DRC的核心指標包括複製延遲和數據一致性。

爲了實現數據複製的低延遲,Applier能夠快速應用SQL,就需要每個表至少包含主鍵或者唯一鍵,加速執行效率;同時在保證數據準確的前提下,SQL應該儘量並行複製,需要MySQL開啓從5.7.22版本引入的Writeset功能。

爲了保證數據複製的準確性,在主備切換時Replicator仍能準確定位Binlog位點,需要MySQL開啓GTID;當數據複製發生衝突時,爲了具備自動解決衝突的能力,需要表包含時間戳列,並精確到毫秒。

這就需要接入DRC的MySQL數據庫滿足:

1)5.7.22及以上版本;

2)Master上開啓Writeset並行複製;

3)MySQL開啓GTID;

4)每個表包含時間戳列,精確到毫秒;

5)每個表至少包含主鍵或者唯一鍵。

DRC的複製依賴GTID(Global Transaction ID),這裏先簡單介紹一下GTID的概念。MySQL 5.6.5版本新增了一種基於GTID的複製方式,強化了數據庫的主備一致性,故障恢復以及容錯能力,取代傳統的基於file和position主從複製,使得在MySQL主備切換時,仍能準確定位到Binlog位點。

GTID的格式形如:source_id:transaction_id,其中source_id表示MySQL服務器的uuid,transaction_id是在事務提交的時候系統順序分配的一個序列號。

4.2 Binlog 複製

單向複製鏈路包含拉取Binlog並持久化到本地磁盤的Replicator,和請求Binlog且並行應用到目標MySQL的Applier。整個鏈路涉及的I/O操作包括網絡傳輸和磁盤讀寫。

4.2.1 低複製延遲

爲了降低複製延遲,就要求複製鏈路中每一環都儘可能高效。網絡層通信模型使用異步I/O;系統層儘可能使用操作系統提供的Zero Copy和Page Cache;應用層提高數據處理並行度以及降低系統不可用時間。

監控顯示生產環境業務雙向複製延遲999線 < 1s。下面就介紹一下DRC在降低複製延遲方面所做的性能優化工作。

1)網絡層

Replicator採用GTID複製方式,實現了MySQL複製協議,僞裝成源MySQL的Slave拉取Binlog。網絡層通信組件採用攜程開源組件XPipe(https://github.com/ctripcorp/x-pipe),實現網絡交互異步化。

2)系統層

接收Binlog時,從數據流中解析出不同類型的Event,直接保存在堆外內存。每個Event需要經過一組過濾器,進而決定是否需要落盤持久化。對於Heartbeat類型的Event需要過濾丟棄;針對某些不需要進行數據同步的庫和表,需要丟棄相應Event,減少存儲量和傳輸量;對於需要持久化的Event,直接將堆外內存中的數據寫入文件Page Cache並定時刷入磁盤,減少數據複製和IO操作,降低處理耗時,提升Replicator拉取效率。

發送Binlog時,當Applier進度落後Replicator,需要從磁盤讀取,這時只解析gtid_event事件,其他需要發送的事件直接從磁盤讀取到堆外內存進行發送,減少數據複製。

3)應用層

Applier借鑑原生MySQL基於Writeset的並行複製,內嵌了基於水位的並行算法,高效的將SQL應用到目標數據庫。

除去正常複製之外,爲了降低系統的不可用時間,就需要系統在異常情況下,儘快恢復正常功能。比如斷網恢復時,爲了避免一端使用老連接,就需要對連接進行空閒檢測;爲了應對斷網導致數據堆積出現流量突增,就需要對流量進行控制。

4)空閒檢測

Replicator與MySQL、Applier和Replicator通過Netty進行數據傳輸,當網絡出現故障,可能一端仍然使用老連接進行通信,會導致數據複製出現中斷。

針對網絡故障,Replicator對MySQL添加了讀空閒檢測,啓動時設置MySQL空閒時間隔10s發送一次heartbeat_event,如果30s沒有收到MySQL任何事件,則認爲MySQL出現問題,發起重連。

Replicator對Applier設置了寫空閒檢測,當沒有Event需要發送給Applier時,間隔10s發送一次heartbeat_event,如果發送失敗,則認爲Applier出現問題,斷開連接。

Applier對Replicator設置了讀空閒檢測,如果30s沒有收到Replicator任何事件,則認爲Replicator出現問題,發起重連。

5)流量控制

設計上Replicator Container使用物理機,其中會運行若干Replicator實例,Applier Container使用虛擬機,這樣會造成發送和消費的速率不匹配。尤其當Applier由於某種原因出現故障後,在Replicator端堆積大量未消費的Event,重啓後如果堆積的Event全部發送過來,可能會直接打垮Applier,這樣就需要在Replicator實例上對Applier進行限流。

Replicator發送端使用Netty提供的WRITE_BUFFER_WATER_MARK高低水位的變化來控制流控的開關,進而動態調整發送速率,整形平滑流量。

4.2.2 數據一致性

爲了保證數據的一致,就需要滿足:

1)數據拉取時保證時序;

2)數據拉取不能遺漏,SQL應用時不重,或者即使重複,要保證冪等操作,保證At Least Once;

3)數據衝突時,能正確處理,保證數據最終一致。

下面就看下DRC是如何保證以上3個要求。

1)時序保證

本地磁盤保存Binlog採用原生的存儲協議,Replicator順序處理接收到每一個Event事件。存儲協議兼容MySQL原生的mysqlbinlog命令,其中根據DRC自身的需要,保存了自定義的一些輔助事件,比如DDL事件,表結構事件。消費時順序發送Binlog文件中的事件給Applier。

2)At Least Once

爲了實現At Least Once,需要解決3個子問題:

1)Replicator或者Applier重啓時,如何保證請求的GTID set準確體現目前的消費偏移?

2)雙向(多向)複製如何解決循環複製?

3)Applier由於異常重複拉取時,如何保證冪等?

下面逐一介紹每個子問題的解決方案。

斷點重續

當Replicator重啓時,會從本地磁盤中恢復已經拉取過的GTID set:

1)定位重啓前使用的最後一個Binlog文件;

2)解析出previous_gtids_event;

3)遍歷該文件的所有gtid_event,與previous_gtids_event解析出的GTID set取並集。

恢復過程中,會校驗文件的正確性,對於沒有以xid_event結束的事務,Replicator會對文件進行截斷,對應的gtid事務會重新請求。

當Applier重啓時,Cluster Manager會從目標數據庫中查詢出當前已經執行過的GTID set發送給Applier,Applier帶着該參數向Replicator發送Binlog拉取請求。Replicator收到請求中的GTID set,從本地磁盤中定位出第一個需要發送的Event所在的Binlog文件,依次遍歷該文件中的每一個Event,針對gtid_event事件取出其中的gtid,判斷該gtid對應的事務是否包含在GTID set中,如果包含其中,則表示Applier已經消費過,無需發送,否則通過堆外內存直接將Event發送給Applier。

循環複製

單向複製時,經過DRC複製到對端的SQL在執行後,同樣會落到MySQL的Binlog中,這樣在雙向(多向)複製結構中,對端的Replicator Instance在拉取到該條Binlog後如果繼續複製,就會出現循環複製的問題。

針對循環複製,業內可選的解決方案是在Binlog事務開頭插入一條寫操作,標識出該條事務是DRC複製過來,而不是真實業務寫入,這樣對端Replicator發現一個事務開頭包含DRC特殊標記時,就不會繼續複製該事務。

分析MySQL自身主從複製,Slave在收到Master同步過來的Binlog時,通過set gtid_next將該事務的GTID設置爲同步過來的gtid_event中的GTID,這樣就實現了主從GTID set的一致性。

如果將Replicator拉取Binlog類比爲Slave的I/O線程,磁盤文件類比爲Relay log,Applier類比爲Slave的SQL線程,那麼Applier是可以採用同樣的方式,使用set gtid_next設置經過DRC複製到對端事務的GTID,這樣源和目標數據庫的GTID set會保持一致,更重要的是可以標識出該事務是經DRC複製過來的。這也是DRC最終採用的破解循環複製的方案。

如下雙向複製結構,Replicator Instance1只會同步源MySQL集羣uuidSet1中的服務器產生事務,Replicator Instance2只會同步目標MySQL集羣uuidSet2中的服務器產生事務。如果業務在源MySQL集羣寫入一條數據,Replicator Instance1從gtid_event中的GTID解析出uuid屬於uuidSet1,那麼會持久化到磁盤併發送給Applier Instance1,Applier Instance1接收到事務中包含的所有Event後,執行set gtid_next=GTID,然後通過JDBC將SQL寫入目標MySQL,完成單向複製;Replicator Instance2接收到gtid_event後,同樣解析出GTID,但是uuid並不屬於uuidSet2,這樣該條事務就會被過濾,從而避免的循環複製。

冪等

Applier如果重複接收到相同GTID的事務,由於MySQL會記錄已經執行的GTID set,如果該GTID已經被執行,則會自動忽略,這樣即使Applier重複應用同一條事務,也不會對業務產生影響。

小結

從上面可以看到,在保證數據一致性時,GTID不論是在Replicator和Applier重啓後Binlog位點定位,標識Binlog來源避免循環複製,還是Applier重複應用時冪等實現,都起到了至關重要的作用。

3)衝突解決

設計上,首先要避免衝突的出現:

1)接入Set化的業務在流量入口處就會根據uid進行分流,同一個用戶的流量進入同一個機房;數據接入層中間件DAL同樣會採用local-2-local的路由策略。這樣同一條記錄在2個機房同時被修改的情況很少發生;

2)對於使用自增ID的業務,通過不同機房設置不同的自增ID規則,或者採用分佈式全局ID生成方案,避免雙向複製後數據衝突。

如果數據確實出現了衝突,2個機房對同一條數據進行的修改,這時需要根據衝突處理策略進行處理:

1)Applier根據默認的衝突處理策略進行處理,接入DRC的表都有一個精確到毫秒自動更新的時間戳,衝突時時間戳靠後的會被採用,進而實現數據的一致;

2)衝突的SQL會被監控記錄,連同數據庫中的原始數據同時提供給用戶,進而自助決定是否需要進行覆蓋。

4.3 DDL 支持

DDL操作會引起表結構的變更,在複製鏈路中Applier需要表結構信息解析對應時刻的Binlog Event,當Applier消費速率落後Replicator的發送速率時,就需要歷史版本的表結構信息才能夠正確解析Binlog Event。這就引入了表結構設計第一個問題:歷史版本如何存儲?

爲了存儲表結構,勢必首先要獲得表結構,如果從源MySQL直接抓取表結構,由於Binlog是異步發送,就導致抓取到DDL的Binlog時刻,與MySQL上表結構未必能夠一一對應,從而引起Applier解析出現問題,進而導致數據不一致。這就引入表結構設計第二個問題:表結構從何處抓取?

業界通用的解決方案是基於獨立的第3方數據庫進行表結構單獨存儲管理。數據庫本身就是存儲工具,Snapshot表和DDL表分別保存表結構快照和DDL變更記錄,這樣任意時刻的表結構等於Snapshot及其後DDL變更集合,則第一個表結構存儲問題順其自然得以解決;獨立數據庫鏡像一份源數據庫的庫表結構,每次從Binlog接收到DDL Event後,將解析出的DDL語句直接應用到鏡像數據庫,隨即抓取相應表結構即可,這樣就解決了第二個表結構從何處抓取的問題。

獨立數據庫解決方案的缺點是引入外部依賴,降低了系統的可用性,提高了運維成本。

4.3.1 表結構存儲和計算

針對DDL功能中問題一:

從數據庫中查詢Snapshot和DDL記錄的好處是時間順序容易確定,能夠簡單準確的恢復表結構。那麼是否有其他存儲介質,在保存表結構快照和DDL操作的同時,能夠保證時序呢?有,保存Binlog的文件就具有這種特性,DRC採用了這種基於Binlog的表結構文件存儲方案。

針對DDL功能中問題二:

鏡像數據庫是爲了實時計算出DDL變更後最新的表結構信息,在存儲不使用獨立部署的數據庫後,DRC引入嵌入式輕量數據庫,降低外部依賴和系統運維成本。

這樣整體的設計方案如下圖所示:

Binlog文件頭會保存自定義表結構快照事件,當從接收的Event事件檢測到DDL後,保存爲自定義的DDL事件。這樣當Applier連接上Replicator後,總是會根據GTID set定位到需要的第一個歷史版本表結構所在的文件,從而實時恢復表結構歷史,用於後續Binlog Event的解析。

我們將數據庫最小依賴打成獨立的Jar包服務,每個Replicator實例啓動時,會一併啓動一個獨立的嵌入式數據庫,在恢復GTID set的同時,根據表結構快照事件和DDL事件重建嵌入式數據庫中表結構。

4.3.2 DDL 入口

攜程內部發布DDL是通過gh-ost進行變更,gh-ost會在影子表中執行DDL操作,等影子表中數據同步完成後,業務低峯期進行原表和影子表的切換。

針對gh-ost,需要追蹤gh-ost變更過程中內部形如_xxx_gho的表的DDL所有操作,最終執行切換時檢測出rename操作,保存對應表結構最新信息發送給Applier即可。

同時針對數據庫直接進行的DDL操作,直接檢測出DDL類型的Event即可。

4.3.3 DDL 異常處理

對於接入DRC的數據庫,當在進行DDL變更時,可能會出現兩邊數據庫變更不同步,單側進行了DDL變更,另一側未進行變更。針對新增列這種場景,Applier在保證數據一致的前提下,對新增列的值進行比較,如果Binlog中解析出的值和該列的默認值一致,則會剔除該列,繼續數據複製。這樣在另一側補上DDL變更後,兩側的數據最終仍然一致。

4.4 監控告警

DRC核心指標包括複製延遲和數據一致性。除此之外我們還提供BU、應用和IDC維度的監控:

1)流量和TPS監控告警;

2)BU、應用和IDC維度的監控告警;

3)DDL變更監控;

4)表結構一致性監控告警;

5)數據衝突監控;

6)GTID set GAP監控。

五、總結

本次分享圍繞DRC的核心指標複製延遲和數據一致性,介紹了複製過程中對性能的優化以及各種場景如何保證數據的一致性。針對DDL,分別支持gh-ost和直接DDL操作,實現在線表結構變更不影響數據複製。

後續DRC的工作會集中在高可用、海外支持上以及外圍設施的建設上,爲攜程的國際化戰略提供數據層面的支撐。

作者介紹

Roy,攜程軟件技術專家,負責MySQL雙向同步DRC和數據庫訪問中間件DAL的開發演進,對分佈式系統高可用設計、數據一致性領域感興趣。

本文轉載自公衆號攜程技術(ID:ctriptech)。

原文鏈接

https://mp.weixin.qq.com/s/_CYE1MGUeyFCn5lpcdklDg

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