深入解析MySQL replication協(xié)議

Why

最開(kāi)始的時(shí)候灰瞻,go-mysql只是簡(jiǎn)單的抽象mixer的代碼猿诸,提供一個(gè)基本的mysql driver以及proxy framework饼暑,但做到后面稳析,筆者突然覺(jué)得,既然研究了這么久mysql client/server protocol弓叛,干脆順帶把replication protocol也給弄明白算了≌镁樱現(xiàn)在想想,幸好當(dāng)初決定實(shí)現(xiàn)了replication的支持撰筷,不然后續(xù)go-mysql-elasticsearch這個(gè)自動(dòng)同步MySQL到Elasticsearch的工具就不可能在短時(shí)間完成陈惰。

其實(shí)MySQL replication protocol很簡(jiǎn)單,client向server發(fā)送一個(gè)MySQL binlog dump的命令毕籽,server就會(huì)源源不斷的給client發(fā)送一個(gè)接一個(gè)的binlog event了抬闯。

Register

首先,我們需要偽造一個(gè)slave关筒,向master注冊(cè)溶握,這樣master才會(huì)發(fā)送binlog event。注冊(cè)很簡(jiǎn)單蒸播,就是向master發(fā)送COM_REGISTER_SLAVE命令睡榆,帶上slave相關(guān)信息萍肆。這里需要注意,因?yàn)樵贛ySQL的replication topology中胀屿,都需要使用一個(gè)唯一的server id來(lái)區(qū)別標(biāo)示不同的server實(shí)例塘揣,所以這里我們偽造的slave也需要一個(gè)唯一的server id。

Binlog dump

最開(kāi)始的時(shí)候宿崭,MySQL只支持一種binlog dump方式亲铡,也就是指定binlog filename + position,向master發(fā)送COM_BINLOG_DUMP命令葡兑。在發(fā)送dump命令的時(shí)候奖蔓,我們可以指定flag為BINLOG_DUMP_NON_BLOCK,這樣master在沒(méi)有可發(fā)送的binlog event之后铁孵,就會(huì)返回一個(gè)EOF package锭硼。不過(guò)通常對(duì)于slave來(lái)說(shuō),一直把連接掛著可能更好蜕劝,這樣能更及時(shí)收到新產(chǎn)生的binlog event檀头。

在MySQL 5.6之后,支持了另一種dump方式岖沛,也就是GTID dump暑始,通過(guò)發(fā)送COM_BINLOG_DUMP_GTID命令實(shí)現(xiàn),需要帶上的是相應(yīng)的GTID信息婴削,不過(guò)筆者覺(jué)得廊镜,如果只是單純的實(shí)現(xiàn)一個(gè)能同步binlog的工具,使用最原始的binlog filename + position就夠了唉俗,畢竟我們不是MySQL嗤朴,解析GTID還是稍顯麻煩的。這里虫溜,順帶吐槽一下MySQL internal文檔雹姊,里面關(guān)于GTID encode的格式說(shuō)明竟然是錯(cuò)誤的,文檔格式如下:

4                n_sids
  for n_sids {
string[16]       SID
8                n_intervals
    for n_intervals {
8                start (signed)
8                end (signed)
    }

但實(shí)際坑爹的是n_sids的長(zhǎng)度是8個(gè)字節(jié)衡楞。這個(gè)錯(cuò)誤可以算是血的教訓(xùn)吱雏,筆者當(dāng)時(shí)debug了很久都沒(méi)發(fā)現(xiàn)為啥GTID dump一直出錯(cuò),直到筆者查看了MySQL的源碼瘾境。

MariaDB雖然也引入了GTID歧杏,但是并沒(méi)有提供一個(gè)類似MySQL的GTID dump命令,仍是使用的COM_BINLOG_DUMP命令迷守,不過(guò)稍微需要額外設(shè)置一些session variable犬绒,譬如要設(shè)置slave_connect_state為當(dāng)前已經(jīng)完成的GTID,這樣master就能知道下一個(gè)event從哪里發(fā)送了兑凿。

Binlog Event

對(duì)于一個(gè)binlog event來(lái)說(shuō)懂更,它分為三個(gè)部分眨业,header急膀,post-header以及payload沮协。但實(shí)際筆者在處理event的時(shí)候,把post-header和payload當(dāng)成了一個(gè)整體body卓嫂。

MySQL的binlog event有很多版本慷暂,但這里筆者只關(guān)心version 4的,也就是從MySQL 5.1.x之后支持的版本晨雳。而且筆者也只支持這個(gè)版本的event解析行瑞,首先是不想寫過(guò)多的兼容代碼,另一個(gè)更主要的原因就在于現(xiàn)在幾乎都沒(méi)有人使用低版本的MySQL了餐禁。

Binlog event的header格式如下:

4              timestamp
1              event type
4              server-id
4              event-size
4              log pos
2              flags

header的長(zhǎng)度固定為19血久,event type用來(lái)標(biāo)識(shí)這個(gè)event的類型,event size則是該event包括header的整體長(zhǎng)度帮非,而log pos則是下一個(gè)event所在的位置氧吐。

在v4版本的binlog文件中,第一個(gè)event就是FORMAT_DESCRIPTION_EVENT末盔,格式為:

2                binlog-version
string[50]       mysql-server version
4                create timestamp
1                event header length
string[p]        event type header lengths

我們需要關(guān)注的就是event type header length這個(gè)字段筑舅,它保存了不同event的post-header長(zhǎng)度,通常我們都不需要關(guān)注這個(gè)值陨舱,但是在解析后面非常重要的ROWS_EVENT的時(shí)候翠拣,就需要它來(lái)判斷TableID的長(zhǎng)度了。這個(gè)后續(xù)在說(shuō)明游盲。

而binlog文件的結(jié)尾误墓,通常(只要master不當(dāng)機(jī))就是ROTATE_EVENT或者STOP_EVENT。這里我們重點(diǎn)關(guān)注ROTATE_EVENT益缎,格式如下:

Post-header
8              position
Payload
string[p]      name of the next binlog

它里面其實(shí)就是標(biāo)明下一個(gè)event所在的binlog filename和position谜慌。這里需要注意,當(dāng)slave發(fā)送binlog dump之后链峭,master首先會(huì)發(fā)送一個(gè)ROTATE_EVENT畦娄,用來(lái)告知slave下一個(gè)event所在位置,然后才跟著FORMAT_DESCRIPTION_EVENT弊仪。

其實(shí)我們可以看到熙卡,binlog event的格式很簡(jiǎn)單,文檔都有著詳細(xì)的說(shuō)明励饵。通常來(lái)說(shuō)店雅,我們僅僅需要關(guān)注幾種特定類型的event,所以只需要寫出這幾種event的解析代碼就可以了筑公,剩下的完全可以跳過(guò)。

Row Based Replication

如果真要說(shuō)處理binlog event有啥復(fù)雜的表窘,那鐵定屬于row based replication相關(guān)的ROWS_EVENT了,對(duì)于一個(gè)ROWS_EVENT來(lái)說(shuō)甜滨,它記錄了每一行數(shù)據(jù)的變化情況乐严,而對(duì)于外部來(lái)說(shuō),是需要準(zhǔn)確的知道這一行數(shù)據(jù)到底如何變化的衣摩,所以我們需要獲取到該行每一列的值昂验。而如何解析相關(guān)的數(shù)據(jù),是非常復(fù)雜的艾扮。筆者也是看了很久MySQL既琴,MariaDB源碼,以及mysql-python-replication的實(shí)現(xiàn)泡嘴,才最終搞定了這個(gè)個(gè)人覺(jué)得最困難的部分甫恩。

在詳細(xì)說(shuō)明ROWS_EVENT之前,我們先來(lái)看看TABLE_MAP_EVENT酌予,該event記錄的是某個(gè)table一些相關(guān)信息磺箕,格式如下:

post-header:
    if post_header_len == 6 {
  4              table id
    } else {
  6              table id
    }
  2              flags

payload:
  1              schema name length
  string         schema name
  1              [00]
  1              table name length
  string         table name
  1              [00]
  lenenc-int     column-count
  string.var_len [length=$column-count] column-def
  lenenc-str     column-meta-def
  n              NULL-bitmask, length: (column-count + 8) / 7

table id需要根據(jù)post_header_len來(lái)判斷字節(jié)長(zhǎng)度,而post_header_len就是存放到FORMAT_DESCRIPTION_EVENT里面的霎终。這里需要注意滞磺,雖然我們可以用table id來(lái)代表一個(gè)特定的table,但是因?yàn)閍lter table或者rotate binlog event等原因莱褒,master會(huì)改變某個(gè)table的table id击困,所以我們?cè)谕獠坎荒苁褂眠@個(gè)table id來(lái)索引某個(gè)table。

TABLE_MAP_EVENT最需要關(guān)注的就是里面的column meta信息广凸,后續(xù)我們解析ROWS_EVENT的時(shí)候會(huì)根據(jù)這個(gè)來(lái)處理不同數(shù)據(jù)類型的數(shù)據(jù)阅茶。column def則定義了每個(gè)列的類型。

ROWS_EVENT包含了insert谅海,update以及delete三種event脸哀,并且有v0,v1以及v2三個(gè)版本扭吁。

ROWS_EVENT的格式很復(fù)雜撞蜂,如下:

header:
  if post_header_len == 6 {
4                    table id
  } else {
6                    table id
  }
2                    flags
  if version == 2 {
2                    extra-data-length
string.var_len       extra-data
  }

body:
lenenc_int           number of columns
string.var_len       columns-present-bitmap1, length: (num of columns+7)/8
  if UPDATE_ROWS_EVENTv1 or v2 {
string.var_len       columns-present-bitmap2, length: (num of columns+7)/8
  }

rows:
string.var_len       nul-bitmap, length (bits set in 'columns-present-bitmap1'+7)/8
string.var_len       value of each field as defined in table-map
  if UPDATE_ROWS_EVENTv1 or v2 {
string.var_len       nul-bitmap, length (bits set in 'columns-present-bitmap2'+7)/8
string.var_len       value of each field as defined in table-map
  }
  ... repeat rows until event-end

ROWS_EVENT的table id跟TABLE_MAP_EVENT一樣,雖然table id可能變化侥袜,但是ROWS_EVENT和TABLE_MAP_EVENT的table id是能保證一致的蝌诡,所以我們也是通過(guò)這個(gè)來(lái)找到對(duì)應(yīng)的TABLE_MAP_EVENT。

為了節(jié)省空間枫吧,ROWS_EVENT里面對(duì)于各列狀態(tài)都是采用bitmap的方式來(lái)處理的浦旱。

首先我們需要得到columns present bitmap的數(shù)據(jù),這個(gè)值用來(lái)表示當(dāng)前列的一些狀態(tài)九杂,如果沒(méi)有設(shè)置颁湖,也就是某列對(duì)應(yīng)的bit為0宣蠕,表明該ROWS_EVENT里面沒(méi)有該列的數(shù)據(jù),外部直接使用null代替就成了甥捺。

然后就是null bitmap抢蚀,這個(gè)用來(lái)表明一行實(shí)際的數(shù)據(jù)里面有哪些列是null的,這里最坑爹的是null bitmap的計(jì)算方式并不是(num of columns+7)/8涎永,也就是MySQL計(jì)算bitmap最通用的方式思币,而是通過(guò)columns present bitmap的bits set個(gè)數(shù)來(lái)計(jì)算的,這個(gè)坑真的很大羡微,為啥要這么設(shè)計(jì),最主要的原因就在于MySQL 5.6之后binlog row image的格式增加了minimal和noblob惶我,尤其是minimal妈倔,update的時(shí)候只會(huì)記錄相應(yīng)更改字段的數(shù)據(jù),譬如我一行有16列绸贡,那么用2個(gè)byte就能搞定null bitmap了盯蝴,但是如果這時(shí)候只有第一列更新了數(shù)據(jù),其實(shí)我們只需要使用1個(gè)byte就能記錄了听怕,因?yàn)楹竺娴蔫F定全為0捧挺,就不需要額外空間存放了,不過(guò)話說(shuō)真有必要這么省空間嗎尿瞭?

null bitmap的計(jì)算需要通過(guò)columns present bitmap的bits set計(jì)算闽烙,bits set其實(shí)也很好理解,就是一個(gè)byte按照二進(jìn)制展示的時(shí)候1的個(gè)數(shù)声搁,譬如1的bits set就是1黑竞,而3的bits set就是2,而255的bits set就是8了疏旨。

好了很魂,得到了present bitmap以及null bitmap之后,我們就能實(shí)際解析這行對(duì)應(yīng)的列數(shù)據(jù)了檐涝,對(duì)于每一列遏匆,首先判斷是否present bitmap標(biāo)記了,如果為0谁榜,則跳過(guò)用null表示幅聘,然后在看是否在null bitmap里面標(biāo)記了,如果為1惰爬,表明值為null喊暖,最后我們就開(kāi)始解析真有有數(shù)據(jù)的列了。

但是撕瞧,因?yàn)槲覀兊玫降氖且恍袛?shù)據(jù)的二進(jìn)制流陵叽,我們?cè)趺粗酪涣袛?shù)據(jù)如何解析狞尔?這里,就要靠TABLE_MAP_EVENT里面的column def以及meta了巩掺。

column def定義了該列的數(shù)據(jù)類型偏序,對(duì)于一些特定的類型,譬如MYSQL_TYPE_LONG, MYSQL_TYPE_TINY等胖替,長(zhǎng)度都是固定的研儒,所以我們可以直接讀取對(duì)應(yīng)的長(zhǎng)度數(shù)據(jù)得到實(shí)際的值。但是對(duì)于一些類型独令,則沒(méi)有這么簡(jiǎn)單了端朵。這時(shí)候就需要通過(guò)meta來(lái)輔助計(jì)算了。

譬如對(duì)于MYSQL_TYPE_BLOB類型燃箭,meta為1表明是tiny blob冲呢,第一個(gè)字節(jié)就是blob的長(zhǎng)度,2表明的是short blob招狸,前兩個(gè)字節(jié)為blob的長(zhǎng)度等敬拓,而對(duì)于MYSQL_TYPE_VARCHAR類型,meta則存儲(chǔ)的是string長(zhǎng)度裙戏。這里乘凸,筆者并沒(méi)有列出MYSQL_TYPE_NEWDECIMAL,MYSQL_TYPE_TIME2等累榜,因?yàn)樗鼈兊膶?shí)現(xiàn)實(shí)在是過(guò)于復(fù)雜营勤,筆者幾乎對(duì)照著MySQL的源碼實(shí)現(xiàn)的。

搞定了這些信柿,我們終于可以完整的解析一個(gè)ROWS_EVENT了冀偶,順帶說(shuō)一下,python-mysql-replication里面minimal/noblob row image的支持渔嚷,也是筆者提交的pull request进鸠,貌似是筆者第一次給其他開(kāi)源項(xiàng)目做貢獻(xiàn)。

總結(jié)

實(shí)現(xiàn)MySQL replication protocol的解析真心是一件很有挑戰(zhàn)的事情形病,雖然辛苦客年,但是讓筆者更加深入的學(xué)習(xí)了MySQL的源碼,為后續(xù)筆者改進(jìn)LedisDB的replication以及更深入的了解MySQL的replication打下了堅(jiān)實(shí)的基礎(chǔ)漠吻。

話說(shuō)量瓜,現(xiàn)在成果已經(jīng)顯現(xiàn),不然go-mysql-elasticsearch不可能如此快速實(shí)現(xiàn)途乃,后續(xù)筆者準(zhǔn)備基于此做一個(gè)更新cache的服務(wù)绍傲,這樣我們的代碼里面就不會(huì)到處出現(xiàn)更新cache的代碼了。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市烫饼,隨后出現(xiàn)的幾起案子猎塞,更是在濱河造成了極大的恐慌,老刑警劉巖杠纵,帶你破解...
    沈念sama閱讀 206,311評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件荠耽,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡比藻,警方通過(guò)查閱死者的電腦和手機(jī)铝量,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)银亲,“玉大人慢叨,你說(shuō)我怎么就攤上這事∪盒祝” “怎么了插爹?”我有些...
    開(kāi)封第一講書人閱讀 152,671評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)请梢。 經(jīng)常有香客問(wèn)我,道長(zhǎng)力穗,這世上最難降的妖魔是什么毅弧? 我笑而不...
    開(kāi)封第一講書人閱讀 55,252評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮当窗,結(jié)果婚禮上够坐,老公的妹妹穿的比我還像新娘。我一直安慰自己崖面,他們只是感情好元咙,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著巫员,像睡著了一般庶香。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上简识,一...
    開(kāi)封第一講書人閱讀 49,031評(píng)論 1 285
  • 那天赶掖,我揣著相機(jī)與錄音,去河邊找鬼七扰。 笑死奢赂,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的颈走。 我是一名探鬼主播膳灶,決...
    沈念sama閱讀 38,340評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼立由!你這毒婦竟也來(lái)了轧钓?” 一聲冷哼從身側(cè)響起序厉,我...
    開(kāi)封第一講書人閱讀 36,973評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎聋迎,沒(méi)想到半個(gè)月后脂矫,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,466評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡霉晕,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評(píng)論 2 323
  • 正文 我和宋清朗相戀三年庭再,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片牺堰。...
    茶點(diǎn)故事閱讀 38,039評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡拄轻,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出伟葫,到底是詐尸還是另有隱情恨搓,我是刑警寧澤,帶...
    沈念sama閱讀 33,701評(píng)論 4 323
  • 正文 年R本政府宣布筏养,位于F島的核電站斧抱,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏渐溶。R本人自食惡果不足惜辉浦,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望茎辐。 院中可真熱鬧宪郊,春花似錦、人聲如沸拖陆。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 30,259評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)依啰。三九已至乎串,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間孔飒,已是汗流浹背灌闺。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留坏瞄,地道東北人桂对。 一個(gè)月前我還...
    沈念sama閱讀 45,497評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像鸠匀,于是被迫代替她去往敵國(guó)和親蕉斜。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容

  • binlog想必大家都不陌生,在主從復(fù)制或者某些情況下的數(shù)據(jù)恢復(fù)會(huì)用到宅此。由于binlog是二進(jìn)制數(shù)據(jù)机错,要查看一般都...
    __七把刀__閱讀 51,554評(píng)論 8 62
  • Mariadb GTID 全局事務(wù)ID(Global transaction ID,GTID)為每個(gè)Event G...
    心云間丶聆聽(tīng)閱讀 791評(píng)論 0 0
  • 什么時(shí)候變成一個(gè)淚點(diǎn)低的人 大概是從知道自己已經(jīng)稀里糊涂的過(guò)了人生的四分之一的時(shí)候 那天看著媽媽的自拍突然想到有一...
    陸叉叉閱讀 201評(píng)論 0 0
  • 風(fēng)嘶吼起來(lái)父腕,望不見(jiàn)邊的黃沙就呼嘯著被裹卷起來(lái)弱匪,在空氣中狂舞。粗糲的沙拍打著客棧的門板璧亮,卷起一層又一層的浪萧诫。 門板嘭...
    達(dá)道閱讀 435評(píng)論 1 1