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

Why

最開始的時候,go-mysql只是簡單的抽象mixer的代碼,提供一個基本的mysql driver以及proxy framework,但做到后面,筆者突然覺得,既然研究了這么久mysql client/server protocol,干脆順帶把replication protocol也給弄明白算了。現(xiàn)在想想,幸好當(dāng)初決定實現(xiàn)了replication的支持,不然后續(xù)go-mysql-elasticsearch這個自動同步MySQL到Elasticsearch的工具就不可能在短時間完成。

其實MySQL replication protocol很簡單,client向server發(fā)送一個MySQL binlog dump的命令,server就會源源不斷的給client發(fā)送一個接一個的binlog event了。

Register

首先,我們需要偽造一個slave,向master注冊,這樣master才會發(fā)送binlog event。注冊很簡單,就是向master發(fā)送COM_REGISTER_SLAVE命令,帶上slave相關(guān)信息。這里需要注意,因為在MySQL的replication topology中,都需要使用一個唯一的server id來區(qū)別標(biāo)示不同的server實例,所以這里我們偽造的slave也需要一個唯一的server id。

Binlog dump

最開始的時候,MySQL只支持一種binlog dump方式,也就是指定binlog filename + position,向master發(fā)送COM_BINLOG_DUMP命令。在發(fā)送dump命令的時候,我們可以指定flag為BINLOG_DUMP_NON_BLOCK,這樣master在沒有可發(fā)送的binlog event之后,就會返回一個EOF package。不過通常對于slave來說,一直把連接掛著可能更好,這樣能更及時收到新產(chǎn)生的binlog event。

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

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

但實際坑爹的是n_sids的長度是8個字節(jié)。這個錯誤可以算是血的教訓(xùn),筆者當(dāng)時debug了很久都沒發(fā)現(xiàn)為啥GTID dump一直出錯,直到筆者查看了MySQL的源碼。

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

Binlog Event

對于一個binlog event來說,它分為三個部分,header,post-header以及payload。但實際筆者在處理event的時候,把post-header和payload當(dāng)成了一個整體body。

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

Binlog event的header格式如下:

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

header的長度固定為19,event type用來標(biāo)識這個event的類型,event size則是該event包括header的整體長度,而log pos則是下一個event所在的位置。

在v4版本的binlog文件中,第一個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這個字段,它保存了不同event的post-header長度,通常我們都不需要關(guān)注這個值,但是在解析后面非常重要的ROWS_EVENT的時候,就需要它來判斷TableID的長度了。這個后續(xù)在說明。

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

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

它里面其實就是標(biāo)明下一個event所在的binlog filename和position。這里需要注意,當(dāng)slave發(fā)送binlog dump之后,master首先會發(fā)送一個ROTATE_EVENT,用來告知slave下一個event所在位置,然后才跟著FORMAT_DESCRIPTION_EVENT。

其實我們可以看到,binlog event的格式很簡單,文檔都有著詳細(xì)的說明。通常來說,我們僅僅需要關(guān)注幾種特定類型的event,所以只需要寫出這幾種event的解析代碼就可以了,剩下的完全可以跳過。

Row Based Replication

如果真要說處理binlog event有啥復(fù)雜的,那鐵定屬于row based replication相關(guān)的ROWS_EVENT了,對于一個ROWS_EVENT來說,它記錄了每一行數(shù)據(jù)的變化情況,而對于外部來說,是需要準(zhǔn)確的知道這一行數(shù)據(jù)到底如何變化的,所以我們需要獲取到該行每一列的值。而如何解析相關(guān)的數(shù)據(jù),是非常復(fù)雜的。筆者也是看了很久MySQL,MariaDB源碼,以及mysql-python-replication的實現(xiàn),才最終搞定了這個個人覺得最困難的部分。

在詳細(xì)說明ROWS_EVENT之前,我們先來看看TABLE_MAP_EVENT,該event記錄的是某個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來判斷字節(jié)長度,而post_header_len就是存放到FORMAT_DESCRIPTION_EVENT里面的。這里需要注意,雖然我們可以用table id來代表一個特定的table,但是因為alter table或者rotate binlog event等原因,master會改變某個table的table id,所以我們在外部不能使用這個table id來索引某個table。

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

ROWS_EVENT包含了insert,update以及delete三種event,并且有v0,v1以及v2三個版本。

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是能保證一致的,所以我們也是通過這個來找到對應(yīng)的TABLE_MAP_EVENT。

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

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

然后就是null bitmap,這個用來表明一行實際的數(shù)據(jù)里面有哪些列是null的,這里最坑爹的是null bitmap的計算方式并不是(num of columns+7)/8,也就是MySQL計算bitmap最通用的方式,而是通過columns present bitmap的bits set個數(shù)來計算的,這個坑真的很大,為啥要這么設(shè)計,最主要的原因就在于MySQL 5.6之后binlog row image的格式增加了minimal和noblob,尤其是minimal,update的時候只會記錄相應(yīng)更改字段的數(shù)據(jù),譬如我一行有16列,那么用2個byte就能搞定null bitmap了,但是如果這時候只有第一列更新了數(shù)據(jù),其實我們只需要使用1個byte就能記錄了,因為后面的鐵定全為0,就不需要額外空間存放了,不過話說真有必要這么省空間嗎?

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

好了,得到了present bitmap以及null bitmap之后,我們就能實際解析這行對應(yīng)的列數(shù)據(jù)了,對于每一列,首先判斷是否present bitmap標(biāo)記了,如果為0,則跳過用null表示,然后在看是否在null bitmap里面標(biāo)記了,如果為1,表明值為null,最后我們就開始解析真有有數(shù)據(jù)的列了。

但是,因為我們得到的是一行數(shù)據(jù)的二進(jìn)制流,我們怎么知道一列數(shù)據(jù)如何解析?這里,就要靠TABLE_MAP_EVENT里面的column def以及meta了。

column def定義了該列的數(shù)據(jù)類型,對于一些特定的類型,譬如MYSQL_TYPE_LONG, MYSQL_TYPE_TINY等,長度都是固定的,所以我們可以直接讀取對應(yīng)的長度數(shù)據(jù)得到實際的值。但是對于一些類型,則沒有這么簡單了。這時候就需要通過meta來輔助計算了。

譬如對于MYSQL_TYPE_BLOB類型,meta為1表明是tiny blob,第一個字節(jié)就是blob的長度,2表明的是short blob,前兩個字節(jié)為blob的長度等,而對于MYSQL_TYPE_VARCHAR類型,meta則存儲的是string長度。這里,筆者并沒有列出MYSQL_TYPE_NEWDECIMAL,MYSQL_TYPE_TIME2等,因為它們的實現(xiàn)實在是過于復(fù)雜,筆者幾乎對照著MySQL的源碼實現(xiàn)的。

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

總結(jié)

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

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容