千萬(wàn)級(jí)日訂單下,餓了么異地多活數(shù)據(jù)實(shí)施DRC的應(yīng)用實(shí)踐
今天,我主要分享餓了么多活的底層數(shù)據(jù)實(shí)施,和大家介紹在整個(gè)多活的設(shè)計(jì)和實(shí)施過(guò)程中,我們是怎么處理異地?cái)?shù)據(jù)同步的,而這個(gè)數(shù)據(jù)同步組件在我們公司內(nèi)部稱之為 DRC。
餓了么異地多活背景
在講 DRC 或者講數(shù)據(jù)復(fù)制之前,先跟大家回顧一下異地多活的背景。
去年,我們?cè)谧龆嗷钫{(diào)研的時(shí)候,整個(gè)公司所有的業(yè)務(wù)服務(wù)都是部署在北京機(jī)房,服務(wù)器大概有四千多臺(tái),災(zāi)備的機(jī)器是在云端,都是虛擬機(jī),大概有三千多臺(tái)。
當(dāng)時(shí),我們峰值的業(yè)務(wù)訂單數(shù)量已經(jīng)接近了千萬(wàn)級(jí)別,但是基本上北京機(jī)房(IDC)已經(jīng)無(wú)法再擴(kuò)容了,也就是說(shuō)我們沒有空余的機(jī)架,沒有辦法添加新的服務(wù)器了,必須要再建一個(gè)新的機(jī)房。
于是,我們?cè)谏虾P陆ㄒ粋€(gè)機(jī)房,在今年的 4 月份投入使用,所以在上海機(jī)房建成之后,異地多活項(xiàng)目能具備在生產(chǎn)環(huán)境上進(jìn)行灰度。
異地多活的底層數(shù)據(jù)同步實(shí)施
這是異地多活的底層數(shù)據(jù)同步實(shí)施的一個(gè)簡(jiǎn)單的概要圖,大家可以看到,我們有兩個(gè)機(jī)房,一個(gè)是北京機(jī)房,一個(gè)是上海機(jī)房。
在這個(gè)時(shí)候,我們期望目標(biāo)是北方所有的用戶請(qǐng)求、用戶流量全部進(jìn)入北京機(jī)房,南方所有的用戶請(qǐng)求、用戶流量進(jìn)入上海機(jī)房。
困難的地方是,這個(gè)用戶有可能今天在北方,明天在南方,因?yàn)樗诔霾睿€有就是存在一些區(qū)域在我們劃分南北 shard 的時(shí)候,它是在邊界上面的,這種情況會(huì)加劇同一個(gè)用戶流量在南北機(jī)房來(lái)回漂移的發(fā)生。
還有個(gè)情況,當(dāng)我們某個(gè)機(jī)房出現(xiàn)故障,如核心交換機(jī)壞掉導(dǎo)致整個(gè)機(jī)房服務(wù)不可用,我們希望可以把這個(gè)機(jī)房的所有流量快速切到另外的數(shù)據(jù)中心去,從而提高整個(gè)餓了么服務(wù)的高可用性。
以上所有的因素,都需要底層數(shù)據(jù)庫(kù)的數(shù)據(jù)之間是打通的。而今天我所要分享的 DRC 項(xiàng)目就是餓了么異地 MySQL 數(shù)據(jù)庫(kù)雙向復(fù)制的組件服務(wù),即上圖中紅色框標(biāo)記的部分。
異地多活對(duì)底層數(shù)據(jù)的要求
我們?cè)谇捌谡{(diào)研 DRC 實(shí)現(xiàn)的時(shí)候,主要總結(jié)了的三點(diǎn),而在后續(xù)的設(shè)計(jì)和實(shí)施當(dāng)中,基本上也是圍繞這三點(diǎn)來(lái)去解決問題:
- 我們覺得是延遲要低,當(dāng)時(shí)給自己定的目標(biāo)是秒級(jí)的,我們希望在北京機(jī)房或上海機(jī)房寫入的數(shù)據(jù),需要在 1 秒鐘之內(nèi)同步到上?;蛘弑本C(jī)房。整個(gè)延遲要小于 1 秒鐘。
- 我們要確保數(shù)據(jù)的一致性,數(shù)據(jù)是不能丟也不能錯(cuò)的,如果出現(xiàn)數(shù)據(jù)的不一致性,可能會(huì)給上層的業(yè)務(wù)服務(wù)、甚至給產(chǎn)品帶來(lái)災(zāi)難性的問題。
- 保證整個(gè)復(fù)制組件具備高吞吐處理能力,指的是它可以面對(duì)各種復(fù)雜的環(huán)境,比方說(shuō)業(yè)務(wù)正在進(jìn)行數(shù)據(jù)的批量操作、數(shù)據(jù)的維護(hù)、數(shù)據(jù)字典的變更情況。
這些會(huì)產(chǎn)生瞬間大量的變更數(shù)據(jù),DRC 需要面對(duì)這種情況,需要具備高吞吐能力去扛住這些情況。
數(shù)據(jù)低延遲和一致性之間,我們認(rèn)為主要從數(shù)據(jù)的并發(fā)復(fù)制這個(gè)策略上去解決,安全、可靠、高效的并發(fā)策略,才能保證數(shù)據(jù)是低延遲的復(fù)制,在大量數(shù)據(jù)需要復(fù)制時(shí),DRC 并發(fā)處理才能快速在短時(shí)間內(nèi)解決。
數(shù)據(jù)一致性,用戶的流量可能被路由到兩個(gè)機(jī)房的任何一個(gè)機(jī)房去,也就是說(shuō)同樣一條記錄可能在兩個(gè)機(jī)房中被同時(shí)更改,所以 DRC 需要做數(shù)據(jù)沖突處理,最終保持?jǐn)?shù)據(jù)一致性,也就是數(shù)據(jù)不能出錯(cuò)。
如果出現(xiàn)沖突且 DRC 自身無(wú)法自動(dòng)處理沖突,我們還提供了一套數(shù)據(jù)沖突訂正平臺(tái),會(huì)要求業(yè)務(wù)方一道來(lái)制定數(shù)據(jù)訂正規(guī)則。
高吞吐剛才已經(jīng)介紹了,正常情況用戶流量是平穩(wěn)的,DRC 是能應(yīng)對(duì)的,在 1 秒鐘之內(nèi)將數(shù)據(jù)快速?gòu)?fù)制到對(duì)端機(jī)房。
當(dāng) DBA 對(duì)數(shù)據(jù)庫(kù)數(shù)據(jù)進(jìn)行數(shù)據(jù)歸檔、大表 DDL 等操作時(shí),這些操作會(huì)在短時(shí)間內(nèi)快速產(chǎn)生大量的變更數(shù)據(jù)需要我們復(fù)制,這些數(shù)據(jù)可能遠(yuǎn)遠(yuǎn)超出了 DRC 的最大處理能力,最終會(huì)導(dǎo)致 DRC 復(fù)制出現(xiàn)延遲。
所以 DRC 與現(xiàn)有的 DBA 系統(tǒng)需要進(jìn)行交互,提供一種彈性的數(shù)據(jù)歸檔機(jī)制,如當(dāng) DRC 出現(xiàn)大的復(fù)制延遲時(shí),終止歸檔 JOB,控制每輪歸檔的數(shù)據(jù)規(guī)模。
如 DRC 識(shí)別屬于大表 DDL 產(chǎn)生的 binlog events,過(guò)濾掉這些 events,避免這些數(shù)據(jù)被傳輸?shù)狡渌麢C(jī)房,占用機(jī)房間帶寬資源。
以上是我們?cè)趯?shí)施異地多活的數(shù)據(jù)層雙向復(fù)制時(shí)對(duì) DRC 項(xiàng)目提出的主要要求。
數(shù)據(jù)集群規(guī)模(多活改造前)
這是我們?cè)谧龆嗷钪暗谋本?shù)據(jù)中心的數(shù)據(jù)規(guī)模,這個(gè)數(shù)據(jù)中心當(dāng)時(shí)有超過(guò) 250 套 MySQL 的集群,一千多臺(tái) MySQL 的實(shí)例,Redis 也超過(guò)四百個(gè)集群。
DRC 服務(wù)的目標(biāo)對(duì)象就是這 250 套 MySQL 集群,因?yàn)樵谡诮ㄔO(shè)的第二個(gè)數(shù)據(jù)中心里未來(lái)也會(huì)有對(duì)應(yīng)的 250 套 MySQL 集群,我們需要把兩個(gè)機(jī)房業(yè)務(wù)對(duì)等的集群進(jìn)行數(shù)據(jù)打通。
多活下 MySQL 的用途分類
我們按照業(yè)務(wù)的用途,給它劃分了多種 DB 服務(wù)類型。為什么要總結(jié)這個(gè)呢?因?yàn)橛幸恍╊愋?,我們是不需要?fù)制的,所以要甄別出來(lái),首先第一個(gè)多活 DB,我們認(rèn)為它的服務(wù)需要做多活的。
比方說(shuō)支付、訂單、下單,一個(gè)機(jī)房掛了,用戶流量切到另外新的機(jī)房,這些業(yè)務(wù)服務(wù)在新的機(jī)房是工作的。
我們把這些多活服務(wù)依賴的 DB 稱為多活 DB,我們優(yōu)先讓業(yè)務(wù)把 DB 改造成多活 DB,DRC 對(duì)多活 DB 進(jìn)行數(shù)據(jù)雙向復(fù)制,保障數(shù)據(jù)一致性。
多活 DB 的優(yōu)勢(shì)剛才已經(jīng)講了,如果機(jī)房出現(xiàn)故障、核心交換機(jī)出問題,整個(gè)機(jī)房垮了,運(yùn)維人員登不進(jìn)機(jī)房機(jī)器,那么我們可以在云端就把用戶流量切到其它的機(jī)房。
有些業(yè)務(wù)對(duì)數(shù)據(jù)有強(qiáng)一致性要求,后面我會(huì)講到其實(shí) DRC 是沒有辦法做到數(shù)據(jù)的強(qiáng)一致性要求的,它是有數(shù)據(jù)沖突發(fā)生的,需要引入數(shù)據(jù)訂正措施。
業(yè)務(wù)如果對(duì)數(shù)據(jù)有強(qiáng)一致性要求,比方說(shuō)用戶注冊(cè),要求用戶登錄名全局唯一(DB字段上可能加了唯一約束),兩個(gè)機(jī)房可能會(huì)在同一時(shí)間接收了相同用戶登錄名的注冊(cè)請(qǐng)求。
這種情況下,DRC 是無(wú)法自身解決掉這個(gè)沖突,而且業(yè)務(wù)方對(duì)這個(gè)結(jié)果也是無(wú)法接受的,這種 DB 我們會(huì)把它歸納到 GlobalDB 里面,它的特性是什么呢?
它的特性是單機(jī)房可寫,多機(jī)房可讀,因?yàn)槟阋WC數(shù)據(jù)的強(qiáng)一致性的話,必須讓所有機(jī)房的請(qǐng)求處理結(jié)果,最終寫到固定的一個(gè)機(jī)房中。
這種 DB 的上層業(yè)務(wù)服務(wù),在機(jī)房掛掉之后是有損的。比方說(shuō)機(jī)房掛了,用戶注冊(cè)功能可能就不能使用了。
最后一個(gè)非多活 DB,它是很少的,主要集中于一些后端的管理平臺(tái),這種項(xiàng)目本身基本上不是多活的,所以這種 DB 我們不動(dòng)它,還是采用原生的主備方式。
DRC 總體架構(gòu)設(shè)計(jì)
這是 DRC 復(fù)制組件的總體架構(gòu)設(shè)計(jì)。我們有一個(gè)組件叫 Replicator,它會(huì)從 MySQL 集群的 Master 上把 binlog 日志記錄抽取出來(lái),解析 binlog 記錄并轉(zhuǎn)換成我們自定義的數(shù)據(jù),存放到一個(gè)超大的 event buffer 里面,event buffer 支持 TB 級(jí)別的容量。
在目標(biāo)機(jī)房里,我們會(huì)部署一個(gè) Applier 服務(wù),這個(gè)服務(wù)啟一個(gè) TCP 長(zhǎng)連接到 Replicator 服務(wù),Replicator 會(huì)不斷的推送數(shù)據(jù)到 Applier,Applier 通過(guò) JDBC 最終把數(shù)據(jù)寫入到目標(biāo)數(shù)據(jù)庫(kù)。
我們會(huì)通過(guò)一個(gè) Console 控制節(jié)點(diǎn)來(lái)進(jìn)行配置管理、部署管理以及進(jìn)行各個(gè)組件的 HA 協(xié)調(diào)工作。
DRC Replicator Server
這是 DRC Replicator Server 組件比較細(xì)的結(jié)構(gòu)描述,主要是包含了一個(gè) MetaDB 模塊,MetaDB 主要用來(lái)解決歷史的 Binlog 的解析問題。
我們成功解析 Binlog 記錄之后,會(huì)把它轉(zhuǎn)換成我們自己定義的一種數(shù)據(jù)結(jié)構(gòu),這種結(jié)構(gòu)相對(duì)于原生的結(jié)構(gòu),Size 更小,MySQL binlog event 的定義在 Size 角度上考慮事實(shí)上已經(jīng)很極致了。
但是可以結(jié)合我們自己的特性,我們會(huì)把不需要的 event 全部過(guò)濾掉(如table_map_event),把可以忽略的數(shù)據(jù)全部忽略掉。我們比對(duì)的結(jié)果是需要復(fù)制的 event 數(shù)據(jù)只有原始數(shù)據(jù) Size 的 70%。
DRC Applier Server
往目標(biāo)的 MySQL 集群復(fù)制寫的時(shí)候,由 DRC Applier Server 負(fù)責(zé),它會(huì)建一個(gè)長(zhǎng)連接到 Replicator 上去,Replicator PUSH 數(shù)據(jù)給 Applier。
Applier 把數(shù)據(jù)拿到之后做事務(wù)的還原,最后通過(guò) JDBC 把事務(wù)重新寫到目標(biāo) DB 里面,寫的過(guò)程當(dāng)中,我們應(yīng)用了并發(fā)的策略。
并發(fā)策略在提供復(fù)制吞吐能力,降低復(fù)制延遲上起到?jīng)Q定的作用,還有冪等也是非常重要的,后面有很多運(yùn)維操作,還有一些 Failover 回退操作,會(huì)導(dǎo)致發(fā)生數(shù)據(jù)被重復(fù)處理的情況,冪等操作保障重復(fù)處理數(shù)據(jù)不會(huì)發(fā)生問題。
DRC 防止循環(huán)復(fù)制
在做復(fù)制的時(shí)候,大家肯定會(huì)碰到解決循環(huán)復(fù)制的問題。我們?cè)诳紤]這個(gè)問題的時(shí)候,查了很多資料,也問了很多一些做過(guò)類似項(xiàng)目的前輩,當(dāng)時(shí)我們認(rèn)為有兩大類辦法。
第一大類辦法一開始否決了,因?yàn)槲覀儗?duì) MySQL 的內(nèi)核原碼不熟悉,而且時(shí)間上也來(lái)不及,雖然我們知道通過(guò) MySQL 的內(nèi)核解決回路復(fù)制是最佳的、最優(yōu)的。
靠 DRC 自身解決這個(gè)問題,也有兩種辦法:
- 一種辦法是我們?cè)?Apply 數(shù)據(jù)到目標(biāo) DB 的時(shí)候把 binlog 關(guān)閉掉。
- 另外一種辦法就是寫目標(biāo) DB 的時(shí)候在事物中額外增加 checkpoint 表的數(shù)據(jù),用于記錄源 DB的server_id。
后來(lái)我們比較了一下,第一個(gè)辦法是比較簡(jiǎn)單,實(shí)現(xiàn)容易,但是因?yàn)?Binlog 記錄沒有產(chǎn)生,導(dǎo)致不支持級(jí)聯(lián)復(fù)制,也對(duì)后續(xù)的運(yùn)維帶來(lái)麻煩。
所以我們最后選擇的是第二個(gè)辦法,通過(guò)把事務(wù)往目標(biāo) DB 復(fù)制的時(shí)候,在事務(wù)中 hack 一條 checkpoint 的數(shù)據(jù)來(lái)標(biāo)識(shí)事務(wù)產(chǎn)生的原始 server,DRC 在解析 MySQL binlog 記錄時(shí)就能正確分辨出數(shù)據(jù)的真正來(lái)源。
DRC 數(shù)據(jù)一致性保障
在剛開始研發(fā)、設(shè)計(jì)的時(shí)候,數(shù)據(jù)一致性保障是我們很頭疼的問題。并不是在一開始就把所有的點(diǎn)都想全了,是在做的過(guò)程當(dāng)中出現(xiàn)了問題,一步步解決的,回顧一下,我們大概從三個(gè)方面去保證數(shù)據(jù)的一致性:
首先,因?yàn)閿?shù)據(jù)庫(kù)是多活的,我們必須從數(shù)據(jù)中心層面盡可能把數(shù)據(jù)沖突發(fā)生的概率降到最低,避免沖突,怎么避免呢?就是合理的流量切分,你可以按照用戶的維度,按照地域的維度,對(duì)流量進(jìn)行拆分。
剛才我們講的,北方用戶的所有數(shù)據(jù)在北京機(jī)房,這些北方用戶的下單、支付等的所有操作數(shù)據(jù)都是在北方機(jī)房產(chǎn)生的,所以用戶在同一個(gè)機(jī)房中發(fā)生的數(shù)據(jù)變更操作絕對(duì)是安全的。
我們最怕的是同一個(gè)數(shù)據(jù)同時(shí)或者是在相近的時(shí)間里同時(shí)在兩個(gè)機(jī)房被修改,我們怕的是這個(gè)問題,因?yàn)檫@種情況就會(huì)引發(fā)數(shù)據(jù)沖突。所以我們通過(guò)合理的流量切分,保證絕大部分時(shí)候數(shù)據(jù)是不會(huì)沖突的。
第二個(gè)我們認(rèn)為你要保障數(shù)據(jù)一致性,首先你要確保數(shù)據(jù)不丟,一旦發(fā)生可能數(shù)據(jù)丟失的情況,我們會(huì)做一個(gè)比較保險(xiǎn)的策略,就是把數(shù)據(jù)復(fù)制的時(shí)間位置回退,即使重復(fù)處理數(shù)據(jù),也避免丟數(shù)據(jù)的可能。
但是這個(gè)時(shí)候會(huì)帶來(lái)數(shù)據(jù)重復(fù)處理的問題,所以數(shù)據(jù)的冪等操作特別重要。
這些都是我們避免數(shù)據(jù)發(fā)生沖突的方法,那沖突實(shí)際上是不可避免的,沖突發(fā)生后,我們?cè)趺唇鉀Q?
最終采用的辦法是在數(shù)據(jù)庫(kù)表上隱含地加一個(gè)時(shí)間字段(數(shù)據(jù)最后更新時(shí)間),這個(gè)字段對(duì)業(yè)務(wù)是透明的,主要用來(lái)輔助 DRC 復(fù)制。
一旦數(shù)據(jù)發(fā)生沖突,DRC 復(fù)制組件可以通過(guò)這個(gè)時(shí)間來(lái)判斷兩個(gè)機(jī)房或者三個(gè)機(jī)房中的哪條數(shù)據(jù)是最后被更新的,最新優(yōu)先的原則,誰(shuí)最后的修改時(shí)間是最新的,就以它為準(zhǔn)。
DRC 數(shù)據(jù)復(fù)制低延遲保障
剛才我們講的是數(shù)據(jù)的一致性,還有一個(gè)點(diǎn)非常重要,就是數(shù)據(jù)復(fù)制的低延遲保障。我們現(xiàn)在延遲包括用戶高峰時(shí)間也是小于 1 秒的,只有在凌晨之后,各種歸檔、批量數(shù)據(jù)處理、DDL 變更等操作會(huì)導(dǎo)致 DRC 延遲出現(xiàn)毛刺和抖動(dòng)。
如果你的延遲很高的話,第一在做流量切換時(shí),因?yàn)檫\(yùn)維優(yōu)先保障產(chǎn)品服務(wù)的可用性,在不得以的情況會(huì)不考慮你的復(fù)制延遲,不會(huì)等數(shù)據(jù)復(fù)制追平之后再切流量,所以你的數(shù)據(jù)沖突的概率就變的很大。
為了保證復(fù)制低延遲,我們認(rèn)為主要策略、或者你在實(shí)施時(shí)主要的做法還是并發(fā),因?yàn)槟阒挥杏酶咝У陌踩牟l(fā)復(fù)制策略,服務(wù)才有足夠的吞吐處理能力,而不至于你的復(fù)制通道因?yàn)橛龅?ldquo;海量”數(shù)據(jù)而導(dǎo)致數(shù)據(jù)積壓,從而加劇了復(fù)制延遲的產(chǎn)生。
我們一開始采用的基于表級(jí)別的并發(fā),但是表級(jí)別的并發(fā)在很多情況下,并發(fā)策略沒辦法被有效的利用。
比方說(shuō)有的業(yè)務(wù)線的數(shù)據(jù)庫(kù)可能 90% 的數(shù)據(jù)集中在一張表或者是幾個(gè)表里面,而大部分表數(shù)據(jù)量很小,那基于表的并發(fā)策略就并發(fā)不起來(lái)了。我們現(xiàn)在跑的是基于行級(jí)別的并發(fā),這種并發(fā)它更能容忍和適應(yīng)很多場(chǎng)景。
DRC & MySQL Master切換
這個(gè)是 DRC 復(fù)制組件與 MySQL 集群的關(guān)系關(guān)聯(lián)圖,一旦 MySQL 集群里面的 Master 發(fā)生了主備切換,原來(lái)的 Master 掛了,DRC 怎么處理?
目前的解決方案是 DBA 系統(tǒng)的 MHA 工具會(huì)通知 DRC 控制中心,DRC 的控制中心會(huì)找到對(duì)應(yīng)的復(fù)制鏈路,然后把復(fù)制鏈路從老的 Master 切到新的 Master。
但是關(guān)鍵點(diǎn)是 MHA 在通知之前先把老的 Master 設(shè)置為不可寫,阻斷 DRC 可能往老的 Master 繼續(xù)寫數(shù)據(jù)。
DRC 線上運(yùn)行狀況(規(guī)模)
這個(gè)是我們 DRC 上線之后的運(yùn)行狀況。現(xiàn)在大概有將近 400 多條復(fù)制鏈路。這個(gè)復(fù)制鏈路是指單向的鏈路。我們提供的消息訂閱大概有 17 個(gè)業(yè)務(wù)方接入,每天產(chǎn)生超過(guò) 1 億條的消息。
DRC 線上運(yùn)行狀況(性能)
這是 DRC 線上運(yùn)行的一個(gè)性能監(jiān)控快照,我們可以看到,它是上午 11 點(diǎn)多到 12 點(diǎn)多的一個(gè)小時(shí)的性能,你會(huì)發(fā)現(xiàn)其實(shí)有一個(gè) DB 是有毛刺的,有一個(gè)復(fù)制鏈路有毛刺,復(fù)制延遲最高達(dá)到 4s,但是大部分的復(fù)制鏈路的延遲大概也是在 1 秒或 1 秒以下。
陳永庭
餓了么框架工具部高級(jí)架構(gòu)師
主要負(fù)責(zé) MySQL 異地雙向數(shù)據(jù)復(fù)制,支撐餓了么異地多活項(xiàng)目。曾就職于 WebEx、Cisco、騰訊等公司。