干货 | 携程异地多活-MySQL实时双向(多向)复制实践
作者簡介
Roy,攜程軟件技術(shù)專家,負(fù)責(zé)MySQL雙向同步DRC和數(shù)據(jù)庫訪問中間件DAL的開發(fā)演進(jìn),對分布式系統(tǒng)高可用設(shè)計、數(shù)據(jù)一致性領(lǐng)域感興趣。
一、前言
攜程內(nèi)部MySQL部署采用多機(jī)房部署,機(jī)房A部署一主一從,機(jī)房B部署一從,作為DR(Disaster Recovery)切換使用。當(dāng)前部署下,機(jī)房B部署的應(yīng)用需要跨機(jī)房進(jìn)行寫操作;當(dāng)機(jī)房A出現(xiàn)故障時,DBA需要手動對數(shù)據(jù)庫進(jìn)行DR切換。
為了做到真正的數(shù)據(jù)異地多活,實(shí)現(xiàn)MySQL同機(jī)房就近讀寫,機(jī)房故障時無需進(jìn)行數(shù)據(jù)庫DR操作,只進(jìn)行流量切換,就需要引入數(shù)據(jù)實(shí)時雙向(多向)復(fù)制組件。
?
二、DRC 介紹
DRC(Data Replicate Center)是攜程框架架構(gòu)研發(fā)部推出的用于數(shù)據(jù)雙向或多向復(fù)制的數(shù)據(jù)庫中間件,在公司G2(高品質(zhì)Great Service、全球化Globalization)戰(zhàn)略的背景下,服務(wù)于異地多活項(xiàng)目,賦予了業(yè)務(wù)全球化的部署能力。
?
三、DRC 架構(gòu)設(shè)計
DRC采用服務(wù)端集中化設(shè)計,配合另一數(shù)據(jù)庫訪問中間件DAL(Data Access Layer)的本地讀寫功能,實(shí)現(xiàn)數(shù)據(jù)就近訪問。
模塊介紹
-
Replicator Container
Replicator Container 實(shí)現(xiàn)對 Replicator 實(shí)例的管理,一個 Replicator 實(shí)例表示對一個MySQL集群的復(fù)制單元,Instance將自己偽裝為MySQL的Slave,實(shí)現(xiàn)Binlog的拉取和本地存儲。
-
Applier Container
Applier Container實(shí)現(xiàn)對Applier 實(shí)例的管理,一個Applier 實(shí)例連接到一個Replicator 實(shí)例,實(shí)現(xiàn)對Replicator 實(shí)例本地存儲Binlog的拉取,進(jìn)而解析出SQL語句并應(yīng)用到目標(biāo)MySQL,從而實(shí)現(xiàn)數(shù)據(jù)的復(fù)制。
-
Cluster Manager
Cluster Manager負(fù)責(zé)集群高可用切換,包括由于MySQL主從切換導(dǎo)致的Replicator 實(shí)例和Applier 實(shí)例重啟,以及Replicator 實(shí)例與Applier 實(shí)例自身主從切換引起的新實(shí)例啟動通知。
-
Console
Console提供UI操作、外部系統(tǒng)交互API以及監(jiān)控告警。
?
四、DRC 詳細(xì)設(shè)計
4.1 接入DB規(guī)范
DRC的核心指標(biāo)包括復(fù)制延遲和數(shù)據(jù)一致性。
為了實(shí)現(xiàn)數(shù)據(jù)復(fù)制的低延遲,Applier能夠快速應(yīng)用SQL,就需要每個表至少包含主鍵或者唯一鍵,加速執(zhí)行效率;同時在保證數(shù)據(jù)準(zhǔn)確的前提下,SQL應(yīng)該盡量并行復(fù)制,需要MySQL開啟從5.7.22版本引入的Writeset功能。
為了保證數(shù)據(jù)復(fù)制的準(zhǔn)確性,在主備切換時Replicator仍能準(zhǔn)確定位Binlog位點(diǎn),需要MySQL開啟GTID;當(dāng)數(shù)據(jù)復(fù)制發(fā)生沖突時,為了具備自動解決沖突的能力,需要表包含時間戳列,并精確到毫秒。
這就需要接入DRC的MySQL數(shù)據(jù)庫滿足:
1)5.7.22及以上版本;
2)Master上開啟Writeset并行復(fù)制;
3)MySQL開啟GTID;
4)每個表包含時間戳列,精確到毫秒;
5)每個表至少包含主鍵或者唯一鍵。
DRC的復(fù)制依賴GTID(Global Transaction ID),這里先簡單介紹一下GTID的概念。MySQL 5.6.5版本新增了一種基于GTID的復(fù)制方式,強(qiáng)化了數(shù)據(jù)庫的主備一致性,故障恢復(fù)以及容錯能力,取代傳統(tǒng)的基于file和position主從復(fù)制,使得在MySQL主備切換時,仍能準(zhǔn)確定位到Binlog位點(diǎn)。
GTID的格式形如:source_id:transaction_id,其中source_id表示MySQL服務(wù)器的uuid,transaction_id是在事務(wù)提交的時候系統(tǒng)順序分配的一個序列號。
4.2 Binlog 復(fù)制
單向復(fù)制鏈路包含拉取Binlog并持久化到本地磁盤的Replicator,和請求Binlog且并行應(yīng)用到目標(biāo)MySQL的Applier。整個鏈路涉及的I/O操作包括網(wǎng)絡(luò)傳輸和磁盤讀寫。
4.2.1 低復(fù)制延遲
為了降低復(fù)制延遲,就要求復(fù)制鏈路中每一環(huán)都盡可能高效。網(wǎng)絡(luò)層通信模型使用異步I/O;系統(tǒng)層盡可能使用操作系統(tǒng)提供的Zero Copy和Page Cache;應(yīng)用層提高數(shù)據(jù)處理并行度以及降低系統(tǒng)不可用時間。
監(jiān)控顯示生產(chǎn)環(huán)境業(yè)務(wù)雙向復(fù)制延遲999線 < 1s。下面就介紹一下DRC在降低復(fù)制延遲方面所做的性能優(yōu)化工作。
1)網(wǎng)絡(luò)層
Replicator采用GTID復(fù)制方式,實(shí)現(xiàn)了MySQL復(fù)制協(xié)議,偽裝成源MySQL的Slave拉取Binlog。網(wǎng)絡(luò)層通信組件采用攜程開源組件XPipe(https://github.com/ctripcorp/x-pipe),實(shí)現(xiàn)網(wǎng)絡(luò)交互異步化。
2)系統(tǒng)層
接收Binlog時,從數(shù)據(jù)流中解析出不同類型的Event,直接保存在堆外內(nèi)存。每個Event需要經(jīng)過一組過濾器,進(jìn)而決定是否需要落盤持久化。對于Heartbeat類型的Event需要過濾丟棄;針對某些不需要進(jìn)行數(shù)據(jù)同步的庫和表,需要丟棄相應(yīng)Event,減少存儲量和傳輸量;對于需要持久化的Event,直接將堆外內(nèi)存中的數(shù)據(jù)寫入文件Page Cache并定時刷入磁盤,減少數(shù)據(jù)復(fù)制和IO操作,降低處理耗時,提升Replicator拉取效率。
發(fā)送Binlog時,當(dāng)Applier進(jìn)度落后Replicator,需要從磁盤讀取,這時只解析gtid_event事件,其他需要發(fā)送的事件直接從磁盤讀取到堆外內(nèi)存進(jìn)行發(fā)送,減少數(shù)據(jù)復(fù)制。
3)應(yīng)用層
Applier借鑒原生MySQL基于Writeset的并行復(fù)制,內(nèi)嵌了基于水位的并行算法,高效的將SQL應(yīng)用到目標(biāo)數(shù)據(jù)庫。
除去正常復(fù)制之外,為了降低系統(tǒng)的不可用時間,就需要系統(tǒng)在異常情況下,盡快恢復(fù)正常功能。比如斷網(wǎng)恢復(fù)時,為了避免一端使用老連接,就需要對連接進(jìn)行空閑檢測;為了應(yīng)對斷網(wǎng)導(dǎo)致數(shù)據(jù)堆積出現(xiàn)流量突增,就需要對流量進(jìn)行控制。
4)空閑檢測
Replicator與MySQL、Applier和Replicator通過Netty進(jìn)行數(shù)據(jù)傳輸,當(dāng)網(wǎng)絡(luò)出現(xiàn)故障,可能一端仍然使用老連接進(jìn)行通信,會導(dǎo)致數(shù)據(jù)復(fù)制出現(xiàn)中斷。
針對網(wǎng)絡(luò)故障,Replicator對MySQL添加了讀空閑檢測,啟動時設(shè)置MySQL空閑時間隔10s發(fā)送一次heartbeat_event,如果30s沒有收到MySQL任何事件,則認(rèn)為MySQL出現(xiàn)問題,發(fā)起重連。
Replicator對Applier設(shè)置了寫空閑檢測,當(dāng)沒有Event需要發(fā)送給Applier時,間隔10s發(fā)送一次heartbeat_event,如果發(fā)送失敗,則認(rèn)為Applier出現(xiàn)問題,斷開連接。
Applier對Replicator設(shè)置了讀空閑檢測,如果30s沒有收到Replicator任何事件,則認(rèn)為Replicator出現(xiàn)問題,發(fā)起重連。
5)流量控制
設(shè)計上Replicator Container使用物理機(jī),其中會運(yùn)行若干Replicator實(shí)例,Applier Container使用虛擬機(jī),這樣會造成發(fā)送和消費(fèi)的速率不匹配。尤其當(dāng)Applier由于某種原因出現(xiàn)故障后,在Replicator端堆積大量未消費(fèi)的Event,重啟后如果堆積的Event全部發(fā)送過來,可能會直接打垮Applier,這樣就需要在Replicator實(shí)例上對Applier進(jìn)行限流。
Replicator發(fā)送端使用Netty提供的WRITE_BUFFER_WATER_MARK高低水位的變化來控制流控的開關(guān),進(jìn)而動態(tài)調(diào)整發(fā)送速率,整形平滑流量。
4.2.2 數(shù)據(jù)一致性
為了保證數(shù)據(jù)的一致,就需要滿足:
1)數(shù)據(jù)拉取時保證時序;
2)數(shù)據(jù)拉取不能遺漏,SQL應(yīng)用時不重,或者即使重復(fù),要保證冪等操作,保證At Least Once;
3)數(shù)據(jù)沖突時,能正確處理,保證數(shù)據(jù)最終一致。
下面就看下DRC是如何保證以上3個要求。
1)時序保證
本地磁盤保存Binlog采用原生的存儲協(xié)議,Replicator順序處理接收到每一個Event事件。存儲協(xié)議兼容MySQL原生的mysqlbinlog命令,其中根據(jù)DRC自身的需要,保存了自定義的一些輔助事件,比如DDL事件,表結(jié)構(gòu)事件。消費(fèi)時順序發(fā)送Binlog文件中的事件給Applier。
2)At Least Once
為了實(shí)現(xiàn)At Least Once,需要解決3個子問題:
1)Replicator或者Applier重啟時,如何保證請求的GTID set準(zhǔn)確體現(xiàn)目前的消費(fèi)偏移?
2)雙向(多向)復(fù)制如何解決循環(huán)復(fù)制?
3)Applier由于異常重復(fù)拉取時,如何保證冪等?
下面逐一介紹每個子問題的解決方案。
斷點(diǎn)重續(xù)
當(dāng)Replicator重啟時,會從本地磁盤中恢復(fù)已經(jīng)拉取過的GTID set:
1)定位重啟前使用的最后一個Binlog文件;
2)解析出previous_gtids_event;
3)遍歷該文件的所有g(shù)tid_event,與previous_gtids_event解析出的GTID set取并集。
恢復(fù)過程中,會校驗(yàn)文件的正確性,對于沒有以xid_event結(jié)束的事務(wù),Replicator會對文件進(jìn)行截斷,對應(yīng)的gtid事務(wù)會重新請求。
當(dāng)Applier重啟時,Cluster Manager會從目標(biāo)數(shù)據(jù)庫中查詢出當(dāng)前已經(jīng)執(zhí)行過的GTID set發(fā)送給Applier,Applier帶著該參數(shù)向Replicator發(fā)送Binlog拉取請求。Replicator收到請求中的GTID set,從本地磁盤中定位出第一個需要發(fā)送的Event所在的Binlog文件,依次遍歷該文件中的每一個Event,針對gtid_event事件取出其中的gtid,判斷該gtid對應(yīng)的事務(wù)是否包含在GTID set中,如果包含其中,則表示Applier已經(jīng)消費(fèi)過,無需發(fā)送,否則通過堆外內(nèi)存直接將Event發(fā)送給Applier。
循環(huán)復(fù)制
單向復(fù)制時,經(jīng)過DRC復(fù)制到對端的SQL在執(zhí)行后,同樣會落到MySQL的Binlog中,這樣在雙向(多向)復(fù)制結(jié)構(gòu)中,對端的Replicator Instance在拉取到該條Binlog后如果繼續(xù)復(fù)制,就會出現(xiàn)循環(huán)復(fù)制的問題。
針對循環(huán)復(fù)制,業(yè)內(nèi)可選的解決方案是在Binlog事務(wù)開頭插入一條寫操作,標(biāo)識出該條事務(wù)是DRC復(fù)制過來,而不是真實(shí)業(yè)務(wù)寫入,這樣對端Replicator發(fā)現(xiàn)一個事務(wù)開頭包含DRC特殊標(biāo)記時,就不會繼續(xù)復(fù)制該事務(wù)。
分析MySQL自身主從復(fù)制,Slave在收到Master同步過來的Binlog時,通過set gtid_next將該事務(wù)的GTID設(shè)置為同步過來的gtid_event中的GTID,這樣就實(shí)現(xiàn)了主從GTID set的一致性。
如果將Replicator拉取Binlog類比為Slave的I/O線程,磁盤文件類比為Relay log,Applier類比為Slave的SQL線程,那么Applier是可以采用同樣的方式,使用set gtid_next設(shè)置經(jīng)過DRC復(fù)制到對端事務(wù)的GTID,這樣源和目標(biāo)數(shù)據(jù)庫的GTID set會保持一致,更重要的是可以標(biāo)識出該事務(wù)是經(jīng)DRC復(fù)制過來的。這也是DRC最終采用的破解循環(huán)復(fù)制的方案。
如下雙向復(fù)制結(jié)構(gòu),Replicator Instance1只會同步源MySQL集群uuidSet1中的服務(wù)器產(chǎn)生事務(wù),Replicator Instance2只會同步目標(biāo)MySQL集群uuidSet2中的服務(wù)器產(chǎn)生事務(wù)。如果業(yè)務(wù)在源MySQL集群寫入一條數(shù)據(jù),Replicator Instance1從gtid_event中的GTID解析出uuid屬于uuidSet1,那么會持久化到磁盤并發(fā)送給Applier Instance1,Applier Instance1接收到事務(wù)中包含的所有Event后,執(zhí)行set gtid_next=GTID,然后通過JDBC將SQL寫入目標(biāo)MySQL,完成單向復(fù)制;Replicator Instance2接收到gtid_event后,同樣解析出GTID,但是uuid并不屬于uuidSet2,這樣該條事務(wù)就會被過濾,從而避免的循環(huán)復(fù)制。
冪等
Applier如果重復(fù)接收到相同GTID的事務(wù),由于MySQL會記錄已經(jīng)執(zhí)行的GTID set,如果該GTID已經(jīng)被執(zhí)行,則會自動忽略,這樣即使Applier重復(fù)應(yīng)用同一條事務(wù),也不會對業(yè)務(wù)產(chǎn)生影響。
小結(jié)
從上面可以看到,在保證數(shù)據(jù)一致性時,GTID不論是在Replicator和Applier重啟后Binlog位點(diǎn)定位,標(biāo)識Binlog來源避免循環(huán)復(fù)制,還是Applier重復(fù)應(yīng)用時冪等實(shí)現(xiàn),都起到了至關(guān)重要的作用。
3)沖突解決
設(shè)計上,首先要避免沖突的出現(xiàn):
1)接入Set化的業(yè)務(wù)在流量入口處就會根據(jù)uid進(jìn)行分流,同一個用戶的流量進(jìn)入同一個機(jī)房;數(shù)據(jù)接入層中間件DAL同樣會采用local-2-local的路由策略。這樣同一條記錄在2個機(jī)房同時被修改的情況很少發(fā)生;
2)對于使用自增ID的業(yè)務(wù),通過不同機(jī)房設(shè)置不同的自增ID規(guī)則,或者采用分布式全局ID生成方案,避免雙向復(fù)制后數(shù)據(jù)沖突。
如果數(shù)據(jù)確實(shí)出現(xiàn)了沖突,2個機(jī)房對同一條數(shù)據(jù)進(jìn)行的修改,這時需要根據(jù)沖突處理策略進(jìn)行處理:
1)Applier根據(jù)默認(rèn)的沖突處理策略進(jìn)行處理,接入DRC的表都有一個精確到毫秒自動更新的時間戳,沖突時時間戳靠后的會被采用,進(jìn)而實(shí)現(xiàn)數(shù)據(jù)的一致;
2)沖突的SQL會被監(jiān)控記錄,連同數(shù)據(jù)庫中的原始數(shù)據(jù)同時提供給用戶,進(jìn)而自助決定是否需要進(jìn)行覆蓋。
4.3 DDL 支持
DDL操作會引起表結(jié)構(gòu)的變更,在復(fù)制鏈路中Applier需要表結(jié)構(gòu)信息解析對應(yīng)時刻的Binlog Event,當(dāng)Applier消費(fèi)速率落后Replicator的發(fā)送速率時,就需要?dú)v史版本的表結(jié)構(gòu)信息才能夠正確解析Binlog Event。這就引入了表結(jié)構(gòu)設(shè)計第一個問題:歷史版本如何存儲?
為了存儲表結(jié)構(gòu),勢必首先要獲得表結(jié)構(gòu),如果從源MySQL直接抓取表結(jié)構(gòu),由于Binlog是異步發(fā)送,就導(dǎo)致抓取到DDL的Binlog時刻,與MySQL上表結(jié)構(gòu)未必能夠一一對應(yīng),從而引起Applier解析出現(xiàn)問題,進(jìn)而導(dǎo)致數(shù)據(jù)不一致。這就引入表結(jié)構(gòu)設(shè)計第二個問題:表結(jié)構(gòu)從何處抓取?
業(yè)界通用的解決方案是基于獨(dú)立的第3方數(shù)據(jù)庫進(jìn)行表結(jié)構(gòu)單獨(dú)存儲管理。數(shù)據(jù)庫本身就是存儲工具,Snapshot表和DDL表分別保存表結(jié)構(gòu)快照和DDL變更記錄,這樣任意時刻的表結(jié)構(gòu)等于Snapshot及其后DDL變更集合,則第一個表結(jié)構(gòu)存儲問題順其自然得以解決;獨(dú)立數(shù)據(jù)庫鏡像一份源數(shù)據(jù)庫的庫表結(jié)構(gòu),每次從Binlog接收到DDL Event后,將解析出的DDL語句直接應(yīng)用到鏡像數(shù)據(jù)庫,隨即抓取相應(yīng)表結(jié)構(gòu)即可,這樣就解決了第二個表結(jié)構(gòu)從何處抓取的問題。
獨(dú)立數(shù)據(jù)庫解決方案的缺點(diǎn)是引入外部依賴,降低了系統(tǒng)的可用性,提高了運(yùn)維成本。
4.3.1 表結(jié)構(gòu)存儲和計算
針對DDL功能中問題一:
從數(shù)據(jù)庫中查詢Snapshot和DDL記錄的好處是時間順序容易確定,能夠簡單準(zhǔn)確的恢復(fù)表結(jié)構(gòu)。那么是否有其他存儲介質(zhì),在保存表結(jié)構(gòu)快照和DDL操作的同時,能夠保證時序呢?有,保存Binlog的文件就具有這種特性,DRC采用了這種基于Binlog的表結(jié)構(gòu)文件存儲方案。
針對DDL功能中問題二:
鏡像數(shù)據(jù)庫是為了實(shí)時計算出DDL變更后最新的表結(jié)構(gòu)信息,在存儲不使用獨(dú)立部署的數(shù)據(jù)庫后,DRC引入嵌入式輕量數(shù)據(jù)庫,降低外部依賴和系統(tǒng)運(yùn)維成本。
這樣整體的設(shè)計方案如下圖所示:
Binlog文件頭會保存自定義表結(jié)構(gòu)快照事件,當(dāng)從接收的Event事件檢測到DDL后,保存為自定義的DDL事件。這樣當(dāng)Applier連接上Replicator后,總是會根據(jù)GTID set定位到需要的第一個歷史版本表結(jié)構(gòu)所在的文件,從而實(shí)時恢復(fù)表結(jié)構(gòu)歷史,用于后續(xù)Binlog Event的解析。
我們將數(shù)據(jù)庫最小依賴打成獨(dú)立的Jar包服務(wù),每個Replicator實(shí)例啟動時,會一并啟動一個獨(dú)立的嵌入式數(shù)據(jù)庫,在恢復(fù)GTID set的同時,根據(jù)表結(jié)構(gòu)快照事件和DDL事件重建嵌入式數(shù)據(jù)庫中表結(jié)構(gòu)。
4.3.2 DDL 入口
攜程內(nèi)部發(fā)布DDL是通過gh-ost進(jìn)行變更,gh-ost會在影子表中執(zhí)行DDL操作,等影子表中數(shù)據(jù)同步完成后,業(yè)務(wù)低峰期進(jìn)行原表和影子表的切換。
針對gh-ost,需要追蹤gh-ost變更過程中內(nèi)部形如_xxx_gho的表的DDL所有操作,最終執(zhí)行切換時檢測出rename操作,保存對應(yīng)表結(jié)構(gòu)最新信息發(fā)送給Applier即可。
同時針對數(shù)據(jù)庫直接進(jìn)行的DDL操作,直接檢測出DDL類型的Event即可。
4.3.3 DDL 異常處理
對于接入DRC的數(shù)據(jù)庫,當(dāng)在進(jìn)行DDL變更時,可能會出現(xiàn)兩邊數(shù)據(jù)庫變更不同步,單側(cè)進(jìn)行了DDL變更,另一側(cè)未進(jìn)行變更。針對新增列這種場景,Applier在保證數(shù)據(jù)一致的前提下,對新增列的值進(jìn)行比較,如果Binlog中解析出的值和該列的默認(rèn)值一致,則會剔除該列,繼續(xù)數(shù)據(jù)復(fù)制。這樣在另一側(cè)補(bǔ)上DDL變更后,兩側(cè)的數(shù)據(jù)最終仍然一致。
4.4 監(jiān)控告警
DRC核心指標(biāo)包括復(fù)制延遲和數(shù)據(jù)一致性。除此之外我們還提供BU、應(yīng)用和IDC維度的監(jiān)控:
1)流量和TPS監(jiān)控告警;
2)BU、應(yīng)用和IDC維度的監(jiān)控告警;
3)DDL變更監(jiān)控;
4)表結(jié)構(gòu)一致性監(jiān)控告警;
5)數(shù)據(jù)沖突監(jiān)控;
6)GTID set GAP監(jiān)控。
?
五、總結(jié)
本次分享圍繞DRC的核心指標(biāo)復(fù)制延遲和數(shù)據(jù)一致性,介紹了復(fù)制過程中對性能的優(yōu)化以及各種場景如何保證數(shù)據(jù)的一致性。針對DDL,分別支持gh-ost和直接DDL操作,實(shí)現(xiàn)在線表結(jié)構(gòu)變更不影響數(shù)據(jù)復(fù)制。
后續(xù)DRC的工作會集中在高可用、海外支持上以及外圍設(shè)施的建設(shè)上,為攜程的國際化戰(zhàn)略提供數(shù)據(jù)層面的支撐。
總結(jié)
以上是生活随笔為你收集整理的干货 | 携程异地多活-MySQL实时双向(多向)复制实践的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 美团配送A/B评估体系建设与实践
- 下一篇: 软件吃软件,编程工作会越来越多吗?