深入解析MySQL replication協議
Why
最開始的時候,go-mysql只是簡單的抽象mixer的代碼,提供一個基本的mysql driver以及proxy framework,但做到后面,筆者突然覺得,既然研究了這么久mysql client/server protocol,干脆順帶把replication protocol也給弄明白算了。現在想想,幸好當初決定實現了replication的支持,不然后續go-mysql-elasticsearch這個自動同步MySQL到Elasticsearch的工具就不可能在短時間完成。
其實MySQL replication protocol很簡單,client向server發送一個MySQL binlog dump的命令,server就會源源不斷的給client發送一個接一個的binlog event了。
Register
首先,我們需要偽造一個slave,向master注冊,這樣master才會發送binlog event。注冊很簡單,就是向master發送COM_REGISTER_SLAVE命令,帶上slave相關信息。這里需要注意,因為在MySQL的 replication topology中,都需要使用一個唯一的server id來區別標示不同的server實例,所以這里我們偽造的slave也需要一個唯一的server id。
Binlog dump
最開始的時候,MySQL只支持一種binlog dump方式,也就是指定binlog filename + position,向master發送COM_BINLOG_DUMP命令。在發送dump命令的時候,我們可以指定flag為 BINLOG_DUMP_NON_BLOCK,這樣master在沒有可發送的binlog event之后,就會返回一個EOF package。不過通常對于slave來說,一直把連接掛著可能更好,這樣能更及時收到新產生的binlog event。
在MySQL 5.6之后,支持了另一種dump方式,也就是GTID dump,通過發送COM_BINLOG_DUMP_GTID命令實現,需要帶上的是相應的GTID信息,不過筆者覺得,如果只是單純的實現一個能同步 binlog的工具,使用最原始的binlog filename + position就夠了,畢竟我們不是MySQL,解析GTID還是稍顯麻煩的。這里,順帶吐槽一下MySQL internal文檔,里面關于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個字節。這個錯誤可以算是血的教訓,筆者當時debug了很久都沒發現為啥GTID dump一直出錯,直到筆者查看了MySQL的源碼。
MariaDB雖然也引入了GTID,但是并沒有提供一個類似MySQL的GTID dump命令,仍是使用的COM_BINLOG_DUMP命令,不過稍微需要額外設置一些session variable,譬如要設置slave_connect_state為當前已經完成的GTID,這樣master就能知道下一個event從哪里發送了。
Binlog Event
對于一個binlog event來說,它分為三個部分,header,post-header以及payload。但實際筆者在處理event的時候,把post-header和payload當成了一個整體body。
MySQL的binlog event有很多版本,但這里筆者只關心version 4的,也就是從MySQL 5.1.x之后支持的版本。而且筆者也只支持這個版本的event解析,首先是不想寫過多的兼容代碼,另一個更主要的原因就在于現在幾乎都沒有人使用低版本的MySQL了。
Binlog event的header格式如下:
4 timestamp 1 event type 4 server-id 4 event-size 4 log pos 2 flags
header的長度固定為19,event type用來標識這個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
我們需要關注的就是event type header length這個字段,它保存了不同event的post-header長度,通常我們都不需要關注這個值,但是在解析后面非常重要的 ROWS_EVENT的時候,就需要它來判斷TableID的長度了。這個后續在說明。
而binlog文件的結尾,通常(只要master不當機)就是ROTATE_EVENT或者STOP_EVENT。這里我們重點關注ROTATE_EVENT,格式如下:
Post-header 8 position Payload string[p] name of the next binlog
它里面其實就是標明下一個event所在的binlog filename和position。這里需要注意,當slave發送binlog dump之后,master首先會發送一個ROTATE_EVENT,用來告知slave下一個event所在位置,然后才跟著 FORMAT_DESCRIPTION_EVENT。
其實我們可以看到,binlog event的格式很簡單,文檔都有著詳細的說明。通常來說,我們僅僅需要關注幾種特定類型的event,所以只需要寫出這幾種event的解析代碼就可以了,剩下的完全可以跳過。
Row Based Replication
如果真要說處理binlog event有啥復雜的,那鐵定屬于row based replication相關的ROWS_EVENT了,對于一個ROWS_EVENT來說,它記錄了每一行數據的變化情況,而對于外部來說,是需要準確的知道這一行數據到底如何變化的,所以我們需要獲取到該行每一列的值。而如何解析相關的數據,是非常復雜的。筆者也是看了很久MySQL,MariaDB源碼,以及mysql-python-replication的實現,才最終搞定了這個個人覺得最困難的部分。
在詳細說明ROWS_EVENT之前,我們先來看看TABLE_MAP_EVENT,該event記錄的是某個table一些相關信息,格式如下:
post-header: if post_header_len == 6 { 4 table id } else { 6 table id } 2 flagspayload: 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</pre>
table id需要根據post_header_len來判斷字節長度,而post_header_len就是存放到 FORMAT_DESCRIPTION_EVENT里面的。這里需要注意,雖然我們可以用table id來代表一個特定的table,但是因為alter table或者rotate binlog event等原因,master會改變某個table的table id,所以我們在外部不能使用這個table id來索引某個table。
TABLE_MAP_EVENT最需要關注的就是里面的column meta信息,后續我們解析ROWS_EVENT的時候會根據這個來處理不同數據類型的數據。column def則定義了每個列的類型。
ROWS_EVENT包含了insert,update以及delete三種event,并且有v0,v1以及v2三個版本。
ROWS_EVENT的格式很復雜,如下:
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</pre>
ROWS_EVENT的table id跟TABLE_MAP_EVENT一樣,雖然table id可能變化,但是ROWS_EVENT和TABLE_MAP_EVENT的table id是能保證一致的,所以我們也是通過這個來找到對應的TABLE_MAP_EVENT。
為了節省空間,ROWS_EVENT里面對于各列狀態都是采用bitmap的方式來處理的。
首先我們需要得到columns present bitmap的數據,這個值用來表示當前列的一些狀態,如果沒有設置,也就是某列對應的bit為0,表明該ROWS_EVENT里面沒有該列的數據,外部直接使用null代替就成了。
然后就是null bitmap,這個用來表明一行實際的數據里面有哪些列是null的,這里最坑爹的是null bitmap的計算方式并不是(num of columns+7)/8,也就是MySQL計算bitmap最通用的方式,而是通過columns present bitmap的bits set個數來計算的,這個坑真的很大,為啥要這么設計,最主要的原因就在于MySQL 5.6之后binlog row image的格式增加了minimal和noblob,尤其是minimal,update的時候只會記錄相應更改字段的數據,譬如我一行有16列,那么用2個byte就能搞定null bitmap了,但是如果這時候只有第一列更新了數據,其實我們只需要使用1個byte就能記錄了,因為后面的鐵定全為0,就不需要額外空間存放了,不過話說真有必要這么省空間嗎?
null bitmap的計算需要通過columns present bitmap的bits set計算,bits set其實也很好理解,就是一個byte按照二進制展示的時候1的個數,譬如1的bits set就是1,而3的bits set就是2,而255的bits set就是8了。
好了,得到了present bitmap以及null bitmap之后,我們就能實際解析這行對應的列數據了,對于每一列,首先判斷是否present bitmap標記了,如果為0,則跳過用null表示,然后在看是否在null bitmap里面標記了,如果為1,表明值為null,最后我們就開始解析真有有數據的列了。
但是,因為我們得到的是一行數據的二進制流,我們怎么知道一列數據如何解析?這里,就要靠TABLE_MAP_EVENT里面的column def以及meta了。
column def定義了該列的數據類型,對于一些特定的類型,譬如MYSQL_TYPE_LONG, MYSQL_TYPE_TINY等,長度都是固定的,所以我們可以直接讀取對應的長度數據得到實際的值。但是對于一些類型,則沒有這么簡單了。這時候就需要通過meta來輔助計算了。
譬如對于MYSQL_TYPE_BLOB類型,meta為1表明是tiny blob,第一個字節就是blob的長度,2表明的是short blob,前兩個字節為blob的長度等,而對于MYSQL_TYPE_VARCHAR類型,meta則存儲的是string長度。這里,筆者并沒有列出 MYSQL_TYPE_NEWDECIMAL,MYSQL_TYPE_TIME2等,因為它們的實現實在是過于復雜,筆者幾乎對照著MySQL的源碼實現的。
搞定了這些,我們終于可以完整的解析一個ROWS_EVENT了,順帶說一下,python-mysql-replication里面minimal/noblob row image的支持,也是筆者提交的pull request,貌似是筆者第一次給其他開源項目做貢獻。
總結
實現MySQL replication protocol的解析真心是一件很有挑戰的事情,雖然辛苦,但是讓筆者更加深入的學習了MySQL的源碼,為后續筆者改進LedisDB的replication以及更深入的了解MySQL的replication打下了堅實的基礎。
話說,現在成果已經顯現,不然go-mysql-elasticsearch不可能如此快速實現,后續筆者準備基于此做一個更新cache的服務,這樣我們的代碼里面就不會到處出現更新cache的代碼了。
</div> 來自:http://siddontang.com/2015/02/02/mysql-replication-protocol/