好吧,其實這節是解讀一個MySQL的一個事件捕獲工具——MySQL replication。MySQL的主從同步就是通過向Binlog寫入事件,由主庫向從庫傳遞,來完成的。那麼如果非MySQL的應用也想從主庫做同步,就需要自行解決獲取事件和解析的問題。這個工具就是做這件事的,其中有個例子就是把數據庫事件寫入Lucenet索引。
項目主頁詳見:https://launchpad.net/mysql-replication-listener。全C++風格編寫,網絡傳輸使用的Boost的asio庫,所有數據寫入和讀取基本上都採用流式完成。看着很不爽,幾次想改成傳統的結構體封裝內存結構的方式,自認爲對MySQL的Binlog文檔已經讀了很多,但是提到細節的理解還是不夠深刻,所以一直沒有完成。雖然文檔和MySQL源碼是瞭解協議的最好方式,可是前者內容較多,每次都是抓不到重點,後者嘗試着讀過幾天,(昨天才發現mysqlbinlog也能模擬爲從庫方式向主庫同步,獲取時間信息,要是之前就發現就好了,還是自己能力不夠啊!)和別的模塊耦合比較嚴重,有種牽一髮而動全身的感覺(這個成語用在這裏是不對的,有種迅雷不及掩耳盜鈴兒響叮噹之勢),真正應用到自己寫的應用還是感覺有難度。所以還是先從這個開源小工具入手吧。
這個故事,就先從程序啓動開始。
工具以庫的方式提供給使用者,接口是Binlog類。首先需要創建這樣的對象,在這時根據參數確定是解析本地的Binlog文件還是以找主庫去同步去。這裏我們只討論後者,因此Binarary_log_driver使用的是Binlog_tcp_driver。
創建過後需要調用connect方法。確定數據庫的標識爲username password ip:port。因此這幾個參數都要傳進去(端口可以不指定,MySQL默認是3306,要和主庫一致)。這個方法執行三個步驟:鑑權--確定首次讀取的偏移--開始循環讀取。
每個數據包,頭部都由固定的4個字節開始。前三個字節是數據包長度,採用小端序,第四個字節是包的序號,一般是自增。第四個字節同包的正文(文檔中稱其爲payload),一起算到包的總長度中去,而前三個字節不算在內。後文介紹的包格式,沒有包括固定的這前4個字節。
鑑權(sync_connect_and_authenticate)
MySQL的鑑權分爲Plain text和SSL兩種,對於局域網的話,前者就可以了,實現也比較簡單。整個流程可以用下圖來表示。
認證協議這塊還要多說兩句。歷史上,MySQL有兩種鑑權協議。4.0以及以前的版本使用Old Password Authentication。
從MySQL4.1開始,使用 Secure Password Authentication
作爲密碼的簽名方式。舊的加密採用8Byte的密鑰,簽名結果也是採用8Byte。這種加密強度很差,其後更換了加密方式,即20byte的密鑰,20Byte的結果。
由於使用的MySQL是5.5+,這裏忽略舊的加密方式以及加密切換,也不使用鑑權相關的plug-in,只介紹官方的方式。
①以io_service結構體建立到MySQL的tcp連接,主要是通過域名解析ip,逐個嘗試每個ip,直到連接建立好。
②讀取MySQL發來的握手包,版本一般是V10。格式如下,
1 [0a] protocol version string[NUL] server version 4 connection id string[8] auth-plugin-data-part-1 1 [00] filler 2 capability flags (lower 2 bytes) if more data in the packet: 1 character set 2 status flags 2 capability flags (upper 2 bytes) if capabilities & CLIENT_PLUGIN_AUTH { 1 length of auth-plugin-data } else { 1 [00] } string[10] reserved (all [00]) if capabilities & CLIENT_SECURE_CONNECTION { string[$len] auth-plugin-data-part-2 ($len=MAX(13, length of auth-plugin-data - 8)) if capabilities & CLIENT_PLUGIN_AUTH { string[NUL] auth-plugin name }
整個反序列化解析的過程在函數proto_get_handshake_package裏面,需要注意的是,這裏的capability flags非常有用,此後的解析都需要他。特別是其中的CLIENT_PROTOCOL_41,非常重要。其他位可以參見https://dev.mysql.com/doc/internals/en/capability-flags.html。可以說,MySQL4.1是新舊協議上的重大分水嶺。
③發起認證(authenticate)
類似的,認證包也有衆多格式。對於5.5+的MySQL而言,一般是採用Handshake41版,格式如下。
4 capability flags, CLIENT_PROTOCOL_41 always set 4 max-packet size 1 character set string[23] reserved (all [0]) string[NUL] username if capabilities & CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA { lenenc-int length of auth-response string[n] auth-response } else if capabilities & CLIENT_SECURE_CONNECTION { 1 length of auth-response string[n] auth-response } else { string[NUL] auth-response } if capabilities & CLIENT_CONNECT_WITH_DB { string[NUL] database } if capabilities & CLIENT_PLUGIN_AUTH { string[NUL] auth plugin name } if capabilities & CLIENT_CONNECT_ATTRS { lenenc-int length of all key-values lenenc-str key lenenc-str value if-more data in 'length of all key-values', more keys and value pairs }
最終的密碼是將裸密碼經過三次SHA-1計算而得,Key用到了先前的兩個scramble_buff。prot_client_flags由客戶端自行選擇,爲了實現簡單,最好不要啓用SSL、壓縮、分段事件等複雜協議。最大報文長度傳0xffffff。
讀服務端傳來的認證結果。
這裏涉及MySQL包的主庫通用返回類型,主要是三種:OK、ERROR、EOF。對應的格式分別是:
OK
1 [00] the OK header lenenc-int affected rows lenenc-int last-insert-id if capabilities & CLIENT_PROTOCOL_41 { 2 status_flags 2 warnings } elseif capabilities & CLIENT_TRANSACTIONS { 2 status_flags } string[EOF] info
ERROR
1 [ff] the ERR header 2 error code if capabilities & CLIENT_PROTOCOL_41 { string[1] '#' the sql-state marker string[5] sql-state } string[EOF] error-message
EOF
1 [fe] the EOF header if capabilities & CLIENT_PROTOCOL_41 { 2 warning count 2 status flags }
④向主庫註冊爲從庫
1 [14] COM_REGISTER_SLAVE 4 server-id 1 slaves hostname length string[$len] slaves hostname 1 slaves user len string[$len] slaves user 1 slaves password len string[$len] slaves password 2 slaves mysql-port 4 replication rank 4 master-id
這裏使用的命令是COM_REGISTER_SLAVE。MySQL暴露了一系列的COM_命令,其中有一部分稱之爲TEXT_PROTOCOL。使用這個COM_REGISTER_SLAVE要注意,任何從庫的ID都不要相同,否則會出現同步問題(因爲我們將不是用GTID的同步方式)。裏面有個很奇怪的字段叫replication rank,不知道是做什麼的,目前的MySQL只是簡單的忽略這個字段,所以傳什麼都OK。
隨後依舊是需要向③一樣接收並解析OK包。