初探 MySQL 的 Binlog
花瓣網的搜索架構需要重構,尤其是在索引建立或者更新層面。
目前的一個架構導致的結果就是時間越久,數據本體與搜索引擎索引中的數據越不同步,相差甚大。
新的一個架構打算從 MySQL 的 Binlog 中讀取數據更新、刪除、新增等歷史記錄,並把相應信息提取出來丟到隊列中慢慢去同步。
所以我就在這裏小小去了解一下 Binlog。
準備工作
什麼是 Binlog
MySQL Server 有四種類型的日誌——Error Log、General Query Log、Binary Log 和 Slow Query Log。
第一個是錯誤日誌,記錄 mysqld 的一些錯誤。第二個是一般查詢日誌,記錄 mysqld 正在做的事情,比如客戶端的連接和斷開、來自客戶端每條 Sql Statement 記錄信息;如果你想準確知道客戶端到底傳了什麼瞎 [嗶嗶] 玩意兒給服務端,這個日誌就非常管用了,不過它非常影響性能。第四個是慢查詢日誌,記錄一些查詢比較慢的 SQL 語句——這種日誌非常常用,主要是給開發者調優用的。
剩下的第三種就是 Binlog 了,包含了一些事件,這些事件描述了數據庫的改動,如建表、數據改動等,也包括一些潛在改動,比如?DELETE FROM ran WHERE bing = luan,然而一條數據都沒被刪掉的這種情況。除非使用 Row-based logging,否則會包含所有改動數據的 SQL Statement。
那麼 Binlog 就有了兩個重要的用途——複製和恢復。比如主從表的複製,和備份恢復什麼的。
啓用 Binlog
通常情況 MySQL 是默認關閉 Binlog 的,所以你得配置一下以啓用它。
啓用的過程就是修改配置文件?my.cnf?了。
至於?my.cnf?位置請自行尋找。例如通過 OSX 的?brew?安裝的?mysql?默認配置目錄通常在
/usr/local/Cellar/mysql/$VERSION/support-files/my-default.cnf
這個時候需要將它拷貝到?/etc/my.cnf?下面。
詳見 <StackOverflow - MySQL ‘my.cnf’ location?>。
緊接著配置?log-bin?和?log-bin-index?的值,如果沒有則自行加上去。
| 1 2 | log-bin=master-bin log-bin-index=master-bin.index |
這裏的?log-bin?是指以後生成各 Binlog 文件的前綴,比如上述使用?master-bin,那麼文件就將會是?master-bin.000001、master-bin.000002?等。而這裏的?log-bin-index?則指 binlog index 文件的名稱,這裏我們設置爲?master-bin.index。
如果上述工作做完之後重啓 MySQL 服務,你可以進入你的 MySQL CLI 驗證一下是否真的啓用了。
| 1 | $ mysql -u $USERNAME ... |
然後在終端裏面輸入下面一句 SQL 語句:
| 1 | SHOW VARIABLES LIKE '%log_bin%'; |
如果結果裏面出來這樣類似的話就表示成功了:
| 1 2 3 4 5 6 7 8 9 10 11 | +---------------------------------+---------------------------------------+ | Variable_name | Value | +---------------------------------+---------------------------------------+ | log_bin | ON | | log_bin_basename | /usr/local/var/mysql/master-bin | | log_bin_index | /usr/local/var/mysql/master-bin.index | | log_bin_trust_function_creators | OFF | | log_bin_use_v1_row_events | OFF | | sql_log_bin | ON | +---------------------------------+---------------------------------------+ 6 rows in set (0.00 sec) |
更多的一些相關配置可以參考這篇《MySQL 的 binary log 初探》。
隨便玩玩
然後你就可以隨便去執行一些數據變動的 SQL 語句了。當你執行了一堆語句之後就可以看到你的 Binlog 裏面有內容了。
如上表所示,log_bin_basename?的值是?/usr/local/var/mysql/master-bin?就是 Binlog 的基礎文件名了。
那我們進去看,比如我的這邊就有這麼幾個文件:
很容易發現,裏面有?master-bin.index?和?master-bin.000001?兩個文件,這兩個文件在上文中有提到過了。
我們打開那個?master-bin.index?文件,會發現這個索引文件就是一個普通的文本文件,然後列舉了各 binlog 的文件名。而?master-bin.000001?文件就是一堆亂碼了——畢竟人家是二進制文件。
結構解析
索引文件
索引文件就是上文中的?master-bin.index?文件,是一個普通的文本文件,以換行爲間隔,一行一個文件名。比如它可能是:
| 1 2 3 | master-bin.000001 master-bin.000002 master-bin.000003 |
然後對應的每行文件就是一個 Binlog 實體文件了。
Binlog 文件
Binlog 的文件結構大致由如下幾個方面組成。
文件頭
文件頭由一個四字節 Magic Number,其值爲?1852400382,在內存中就是?"\xfe\x62\x69\x6e",參考 MySQL 源碼的?log_event.h,也就是?'\0xfe' 'b' 'i' 'n'。
與平常二進制一樣,通常都有一個 Magic Number 進行文件識別,如果 Magic Number 不吻合上述的值那麼這個文件就不是一個正常的 Binlog。
事件
在文件頭之後,跟隨的是一個一個事件依次排列。每個事件都由一個事件頭和事件體組成。
事件頭裏面的內容包含了這個事件的類型(如新增、刪除等)、事件執行時間以及是哪個服務器執行的事件等信息。
第一個事件是一個事件描述符,描述了這個 Binlog 文件格式的版本。接下去的一堆事件將會按照第一個事件描述符所描述的結構版本進行解讀。最後一個事件是一個銜接事件,指定了下一個 Binlog 文件名——有點類似於鏈表裏面的?next?指針。
根據《High-Level Binary Log Structure and Contents》所述,不同版本的 Binlog 格式不一定一樣,所以也沒有一個定性。在我寫這篇文章的時候,目前有三種版本的格式。
- v1,用於 MySQL 3.2.3
- v3,用於 MySQL 4.0.2 以及 4.1.0
- v4,用於 MySQL 5.0 以及更高版本
實際上還有一個 v2 版本,不過只在早期 4.0.x 的 MySQL 版本中使用過,但是 v2 已經過於陳舊並且不再被 MySQL 官方支持了。
通常我們現在用的 MySQL 都是在 5.0 以上的了,所以就略過 v1 ~ v3 版本的 Binlog,如果需要了解 v1 ~ v3 版本的 Binlog 可以自行前往上述的《High-level…》文章查看。
事件頭
一個事件頭有 19 字節,依次排列爲四字節的時間戳、一字節的當前事件類型、四字節的服務端 ID、四字節的當前事件長度描述、四字節的下個事件位置(方便跳轉)以及兩字節的標識。
用 ASCII Diagram 表示如下:
| 1 2 3 4 | +---------+---------+---------+------------+-------------+-------+ |timestamp|type code|server_id|event_length|next_position|flags | |4 bytes |1 byte |4 bytes |4 bytes |4 bytes |2 bytes| +---------+---------+---------+------------+-------------+-------+ |
也可以字節編造一個結構體來解讀這個頭:
| 1 2 3 4 5 6 7 8 9 | struct BinlogEventHeader { int timestamp; char type_code; int server_id; int event_length; int next_position; char flags[2]; }; |
如果你要直接用這個結構體來讀取數據的話,需要加點手腳。
因爲默認情況下 GCC 或者 G++ 編譯器會對結構體進行字節對齊,這樣讀進來的數據就不對了,因爲 Binlog 並不是對齊的。爲了統一我們需要取消這個結構體的字節對齊,一個方法是使用?#pragma pack(n),一個方法是使用?__attribute__((__packed__)),還有一種情況是在編譯器編譯的時候強制把所有的結構體對其取消,即在編譯的時候使用?fpack-struct?參數,如:
| 1 | $ g++ temp.cpp -o a -fpack-struct=1 |
根據上述的結構我們可以明確得到各變量在結構體裏面的偏移量,所以在 MySQL 源碼裏面(libbinlogevents/include/binlog_event.h)有下面幾個常量以快速標記偏移:
| 1 2 3 4 5 | #define EVENT_TYPE_OFFSET 4 #define SERVER_ID_OFFSET 5 #define EVENT_LEN_OFFSET 9 #define LOG_POS_OFFSET 13 #define FLAGS_OFFSET 17 |
而具體有哪些事件則在?libbinlogevents/include/binlog_event.h#L245?裏面被定義。如有個?FORMAT_DESCRIPTION_EVENT?事件的?type_code?是 15、UPDATE_ROWS_EVENT?的?type_code?是 31。
還有那個?next_position,在 v4 版本中代表從 Binlog 一開始到下一個事件開始的偏移量,比如到第一個事件的?next_position?就是 4,因爲文件頭有一個字節的長度。然後接下去對於事件 n 和事件 n + 1 來說,他們有這樣的關係:
next_position(n + 1) = next_position(n) + event_length(n)
關於 flags 暫時不需要了解太多,如果真的想了解的話可以看看 MySQL 的相關官方文檔。
事件體
事實上在 Binlog 事件中應該是有三個部分組成,header、post-header?和?payload,不過通常情況下我們把?post-header?和?payload?都歸結爲事件體,實際上這個?post-header?裏面放的是一些定長的數據,只不過有時候我們不需要特別地關心。想要深入瞭解可以去查看 MySQL 的官方文檔。
所以實際上一個真正的事件體由兩部分組成,用 ASCII Diagram 表示就像這樣:
| 1 2 3 4 5 | +=====================================+ | event | fixed part (post-header) | | data +----------------------------+ | | variable part (payload) | +=====================================+ |
而這個?post-header?對於不同類型的事件來說長度是不一樣的,同種類型來說是一樣的,而這個長度的預先規定將會在一個“格式描述事件”中定好。
格式描述事件
在上文我們有提到過,在 Magic Number 之後跟著的是一個格式描述事件(Format Description Event),其實這只是在 v4 版本中的稱呼,在以前的版本里面叫起始事件(Start Event)。
在 v4 版本中這個事件的結構如下面的 ASCII Diagram 所示。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | +=====================================+ | event | timestamp 0 : 4 | | header +----------------------------+ | | type_code 4 : 1 | = FORMAT_DESCRIPTION_EVENT = 15 | +----------------------------+ | | server_id 5 : 4 | | +----------------------------+ | | event_length 9 : 4 | >= 91 | +----------------------------+ | | next_position 13 : 4 | | +----------------------------+ | | flags 17 : 2 | +=====================================+ | event | binlog_version 19 : 2 | = 4 | data +----------------------------+ | | server_version 21 : 50 | | +----------------------------+ | | create_timestamp 71 : 4 | | +----------------------------+ | | header_length 75 : 1 | | +----------------------------+ | | post-header 76 : n | = array of n bytes, one byte per event | | lengths for all | type that the server knows about | | event types | +=====================================+ |
這個事件的?type_code?是 15,然後?event_length?是大於等於 91 的值的,這個主要取決於所有事件類型數。
因爲從第 76 字節開始後面的二進制就代表一個字節類型的數組了,一個字節代表一個事件類型的?post-header?長度,即每個事件類型固定數據的長度。
那麼按照上述的一些線索來看,我們能非常快地寫出一個簡單的解讀 Binlog 格式描述事件的代碼。
如上文所述,如果需要正常解讀 Binlog 文件的話,下面的代碼編譯時候需要加上?-fpack-struct=1?這個參數。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | #include <cstdio> #include <cstdlib> struct BinlogEventHeader { int timestamp; unsigned char type_code; int server_id; int event_length; int next_position; short flags; }; int main() { FILE* fp = fopen("/usr/local/var/mysql/master-bin.000001", "rb"); int magic_number; fread(&magic_number, 4, 1, fp); printf("%d - %s\n", magic_number, (char*)(&magic_number)); struct BinlogEventHeader format_description_event_header; fread(&format_description_event_header, 19, 1, fp); printf("BinlogEventHeader\n{\n"); printf(" timestamp: %d\n", format_description_event_header.timestamp); printf(" type_code: %d\n", format_description_event_header.type_code); printf(" server_id: %d\n", format_description_event_header.server_id); printf(" event_length: %d\n", format_description_event_header.event_length); printf(" next_position: %d\n", format_description_event_header.next_position); printf(" flags[]: %d\n}\n", format_description_event_header.flags); short binlog_version; fread(&binlog_version, 2, 1, fp); printf("binlog_version: %d\n", binlog_version); char server_version[51]; fread(server_version, 50, 1, fp); server_version[50] = '\0'; printf("server_version: %s\n", server_version); int create_timestamp; fread(&create_timestamp, 4, 1, fp); printf("create_timestamp: %d\n", create_timestamp); char header_length; fread(&header_length, 1, 1, fp); printf("header_length: %d\n", header_length); int type_count = format_description_event_header.event_length - 76; unsigned char post_header_length[type_count]; fread(post_header_length, 1, type_count, fp); for(int i = 0; i < type_count; i++) { printf(" - type %d: %d\n", i + 1, post_header_length[i]); } return 0; } |
這個時候你得到的結果有可能就是這樣的了:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | 1852400382 - �binpz� BinlogEventHeader { timestamp: 1439186734 type_code: 15 server_id: 1 event_length: 116 next_position: 120 flags[]: 1 } binlog_version: 4 server_version: 5.6.24-log create_timestamp: 1439186734 header_length: 19 - type 1: 56 - type 2: 13 - type 3: 0 - type 4: 8 - type 5: 0 - type 6: 18 - ... |
一共會輸出 40 種類型(從 1 到 40),如官方文檔所說,這個數組從?START_EVENT_V3?事件開始(type_code?是 1)。
跳轉事件
跳轉事件即?ROTATE_EVENT,其?type_code?是 4,其?post-header?長度爲 8。
當一個 Binlog 文件大小已經差不多要分割了,它就會在末尾被寫入一個?ROTATE_EVENT——用於指出這個 Binlog 的下一個文件。
它的?post-header?是 8 字節的一個東西,內容通常就是一個整數?4,用於表示下一個 Binlog 文件中的第一個事件起始偏移量。我們從上文就能得出在一般情況下這個數字只可能是四,就偏移了一個魔法數字。當然我們講的是在 v4 這個 Binlog 版本下的情況。
然後在?payload?位置是一個字符串,即下一個 Binlog 文件的文件名。
各種不同的事件體
由於篇幅原因這裏就不詳細舉例其它普通的不同事件體了,具體的詳解在?MySQL 文檔中一樣有介紹,用到什麼類型的事件體就可以自己去查詢。
小結
本文大概介紹了 Binlog 的一些情況,以及 Binlog 的內部二進制解析結構。方便大家造輪子用——不然老用別人的輪子,只知其然而不知其所以然多沒勁。
好了要下班了,就寫到這裏過吧。
參考
總結
以上是生活随笔為你收集整理的初探 MySQL 的 Binlog的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: MySQL误操作后如何快速恢复数据
- 下一篇: JAVA虚拟机关闭钩子(Shutdown