mysql replication 协议_深入解析MySQL replication协议
WHY
最開始的時(shí)候,go-mysql只是簡(jiǎn)單的抽象mixer的代碼,提供一個(gè)基本的mysql driver以及proxy framework,但做到后面,筆者突然覺得,既然研究了這么久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來區(qū)別標(biāo)示不同的server實(shí)例,所以這里我們偽造的slave也需要一個(gè)唯一的server id。
Binlog dump
最開始的時(shí)候,MySQL只支持一種binlog dump方式,也就是指定binlog filename + position,向master發(fā)送COM_BINLOG_DUMP命令。在發(fā)送dump命令的時(shí)候,我們可以指定flag為BINLOG_DUMP_NON_BLOCK,這樣master在沒有可發(fā)送的binlog event之后,就會(huì)返回一個(gè)EOF package。不過通常對(duì)于slave來說,一直把連接掛著可能更好,這樣能更及時(shí)收到新產(chǎn)生的binlog event。
在MySQL 5.6之后,支持了另一種dump方式,也就是GTID dump,通過發(fā)送COM_BINLOG_DUMP_GTID命令實(shí)現(xiàn),需要帶上的是相應(yīng)的GTID信息,不過筆者覺得,如果只是單純的實(shí)現(xiàn)一個(gè)能同步 binlog的工具,使用最原始的binlog filename + position就夠了,畢竟我們不是MySQL,解析GTID還是稍顯麻煩的。這里,順帶吐槽一下MySQL internal文檔,里面關(guān)于GTID encode的格式說明竟然是錯(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了很久都沒發(fā)現(xiàn)為啥GTID dump一直出錯(cuò),直到筆者查看了MySQL的源碼。
MariaDB雖然也引入了GTID,但是并沒有提供一個(gè)類似MySQL的GTID dump命令,仍是使用的COM_BINLOG_DUMP命令,不過稍微需要額外設(shè)置一些session variable,譬如要設(shè)置slave_connect_state為當(dāng)前已經(jīng)完成的GTID,這樣master就能知道下一個(gè)event從哪里發(fā)送 了。
Binlog Event
對(duì)于一個(gè)binlog event來說,它分為三個(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解析,首先是不想寫過多的兼容代碼,另一個(gè)更主要的原因就在于現(xiàn)在幾乎都沒有人使用低版本的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用來標(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í)候,就需要它來判斷TableID的長(zhǎng)度了。這個(gè)后續(xù)在說明。
而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,用來告知slave下一個(gè)event所在位置,然后才跟著FORMAT_DESCRIPTION_EVENT。
其實(shí)我們可以看到,binlog event的格式很簡(jiǎn)單,文檔都有著詳細(xì)的說明。通常來說,我們僅僅需要關(guān)注幾種特定類型的event,所以只需要寫出這幾種event的解析代碼就可以了,剩下的完全可以跳過。
Row Based Replication
如果真要說處理binlog event有啥復(fù)雜的,那鐵定屬于row based replication相關(guān)的ROWS_EVENT了,對(duì)于一個(gè)ROWS_EVENT來說,它記錄了每一行數(shù)據(jù)的變化情況,而對(duì)于外部來說,是需要準(zhǔn)確的知道這一行數(shù)據(jù)到底如何變化的,所以我們需要獲取到該行每一列的值。而如何解析相關(guān)的數(shù)據(jù),是非常復(fù)雜的。筆者也是看了很久MySQL,MariaDB源碼,以及mysql-python-replication的實(shí)現(xiàn),才最終搞定了這個(gè)個(gè)人覺得最困難的部分。
在詳細(xì)說明ROWS_EVENT之前,我們先來看看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來判斷字節(jié)長(zhǎng)度,而post_header_len就是存放到FORMAT_DESCRIPTION_EVENT里面的。這里需要注意,雖然我們可以用table id來代表一個(gè)特定的table,但是因?yàn)閍lter table或者rotate binlog event等原因,master會(huì)改變某個(gè)table的table id,所以我們?cè)谕獠坎荒苁褂眠@個(gè)table id來索引某個(gè)table。
TABLE_MAP_EVENT最需要關(guān)注的就是里面的column meta信息,后續(xù)我們解析ROWS_EVENT的時(shí)候會(huì)根據(jù)這個(gè)來處理不同數(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是能保證一致的,所以我們也是通過這個(gè)來找到對(duì)應(yīng)的TABLE_MAP_EVENT。
為了節(jié)省空間,ROWS_EVENT里面對(duì)于各列狀態(tài)都是采用bitmap的方式來處理的。
首先我們需要得到columns present bitmap的數(shù)據(jù),這個(gè)值用來表示當(dāng)前列的一些狀態(tài),如果沒有設(shè)置,也就是某列對(duì)應(yīng)的bit為0,表明該ROWS_EVENT里面沒有該列的數(shù)據(jù),外部直接使用null代替就成了。
然后就是null bitmap,這個(gè)用來表明一行實(shí)際的數(shù)據(jù)里面有哪些列是null的,這里最坑爹的是null bitmap的計(jì)算方式并不是(num of columns+7)/8,也就是MySQL計(jì)算bitmap最通用的方式,而是通過columns present bitmap的bits set個(gè)數(shù)來計(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,就不需要額外空間存放了,不過話說真有必要這么省空間嗎?
null bitmap的計(jì)算需要通過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,則跳過用null表示,然后在看是否在null bitmap里面標(biāo)記了,如果為1,表明值為null,最后我們就開始解析真有有數(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ì)于一些類型,則沒有這么簡(jiǎn)單了。這時(shí)候就需要通過meta來輔助計(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)度。這里,筆者并沒有列出 MYSQL_TYPE_NEWDECIMAL,MYSQL_TYPE_TIME2等,因?yàn)樗鼈兊膶?shí)現(xiàn)實(shí)在是過于復(fù)雜,筆者幾乎對(duì)照著MySQL的源碼實(shí)現(xiàn)的。
搞定了這些,我們終于可以完整的解析一個(gè)ROWS_EVENT了,順帶說一下,python-mysql-replication里面minimal/noblob row image的支持,也是筆者提交的pull request,貌似是筆者第一次給其他開源項(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ǔ)。
話說,現(xiàn)在成果已經(jīng)顯現(xiàn),不然go-mysql-elasticsearch不可能如此快速實(shí)現(xiàn),后續(xù)筆者準(zhǔn)備基于此做一個(gè)更新cache的服務(wù),這樣我們的代碼里面就不會(huì)到處出現(xiàn)更新cache的代碼了。
來自siddon的個(gè)人博客
總結(jié)
以上是生活随笔為你收集整理的mysql replication 协议_深入解析MySQL replication协议的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: mysql中以下正确的sql是_总结My
- 下一篇: mysql slave 线程 简书_My