MySQLユーザーのためのMySQLプロトコル入門 #4
なんとなく気分で始めたMySQLプロトコル入門ですが今回は少し趣向をかえてbinlog formatについて書いてみたいと思います。
MySQLでのレプリケーションのメッセージ単位として使われているbinlogですが、そもそもbinlog(バイナリーログ)とはどういったものなのでしょうか?実際問題よくわからなくてもちょちょっと設定すれば素敵に動いてくれるのでわからなくても大丈夫っちゃー、大丈夫なんですがせっかく興味が湧いてしまった事ですし調べていきましょう。
http://dev.mysql.com/doc/internals/en/replication-protocol.html より引用してみます。
Replication uses binlogs to ship changes done on the master to the slave and can be written to Binlog File and sent over the network as Binlog Network Stream.
ええっと、要するにレプリケーションという仕組みはbinlogというものを使ってmasterでの変更をslaveに伝えられるもので、Binlog Network Streamというものでnetwork上を流れるモノのようです。
ううん、なんだか分かるような、わからないような。ちょっとややこしいですね。
今日のところはひとまずBinlog Fileというものがどういったものなのかについて見て行きましょう。
Binlog File
http://dev.mysql.com/doc/internals/en/binlog-file.html
BinlogファイルはBinlog File Headerで始まり、Binlog Eventが連続したものである。と書かれています。
そのまんますぎますが、図で書いてみるとこんな感じでしょうか。
Binlog File Headerとはどういうものか、というと
http://dev.mysql.com/doc/internals/en/binlog-file-header.html
0xfe, 0x62, 0x69, 0x6e (0xfe bin)の4byteから始まるファイルとかいてありますが単なるMagicですね。あまり重要ではないので次に進みましょう
Binlog Event Header
Binlog Eventが続いたもの……と書いてあるぐらいなのでここが一番重要なポイントっぽいですね。Binlog EventはMySQL Packetと同じようにHeader + Payload形式となっています。
http://dev.mysql.com/doc/internals/en/binlog-event-header.html から定義を引用します。
1 2 3 4 5 6 7 |
4 timestamp 1 event type 4 server-id 4 event-size if binlog-version > 1: 4 log pos 2 flags |
binlogのversionによって定義が分岐しますが、現状のMySQLではlog pos, flagsは必ずつくようになっています。今までのMySQLプロトコルをParseしていれば簡単にParse出来そうですね。
それでは早速Binlog Event Headerをparseするプログラムを書いてみましょう。
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 |
package main import ( "fmt" "encoding/binary" "encoding/hex" ) func main() { buffer := []byte{ 0x7e, 0x8d, 0x3b, 0x54, 0x0f, 0xe9, 0x03, 0x00, 0x00, 0x74, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x35, 0x2e, 0x36, 0x2e, 0x31, 0x35, 0x2d, 0x6c, 0x6f, 0x67, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7e, 0x8d, 0x3b, 0x54, 0x13, 0x38, 0x0d, 0x00, 0x08, 0x00, 0x12, 0x00, 0x04, 0x04, 0x04, 0x04, 0x12, 0x00, 0x00, 0x5c, 0x00, 0x04, 0x1a, 0x08, 0x00, 0x00, 0x00, 0x08, 0x08, 0x08, 0x02, 0x00, 0x00, 0x00, 0x0a, 0x0a, 0x0a, 0x19, 0x19, 0x00, 0x01, 0xfa, 0xd5, 0x17, 0x2f, } offset := 0 timestamp := binary.LittleEndian.Uint32(buffer[offset:offset+4]) offset += 4 event_type := buffer[offset] offset += 1 server_id := binary.LittleEndian.Uint32(buffer[offset:offset+4]) offset += 4 event_size := binary.LittleEndian.Uint32(buffer[offset:offset+4]) offset += 4 log_pos := binary.LittleEndian.Uint32(buffer[offset:offset+4]) flags := binary.LittleEndian.Uint16(buffer[offset:offset+2]) offset += 2 fmt.Printf("[timestamp]: %d\n", timestamp) fmt.Printf("[event_type]: %d\n", event_type) fmt.Printf("[server_id]: %d\n", server_id) fmt.Printf("[event_size]: %d\n", event_size) fmt.Printf("[log_pos]: %d\n", log_pos) fmt.Printf("[flags]: %d\n", flags) fmt.Printf("[payload]:\n%s", hex.Dump(buffer[offset:(offset+int(event_size)-19)])) } |
毎度の通り、PlayGroundで試せます。実行結果は下記の通りです。
http://play.golang.org/p/2QL3ZD7b29
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
[timestamp]: 1413188990 [event_type]: 15 [server_id]: 1001 [event_size]: 116 [log_pos]: 120 [flags]: 120 [payload]: 00000000 00 00 00 00 04 00 35 2e 36 2e 31 35 2d 6c 6f 67 |......5.6.15-log| 00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000030 00 00 00 00 00 00 00 00 7e 8d 3b 54 13 38 0d 00 |........~.;T.8..| 00000040 08 00 12 00 04 04 04 04 12 00 00 5c 00 04 1a 08 |...........\....| 00000050 00 00 00 08 08 08 02 00 00 00 0a 0a 0a 19 19 00 |................| 00000060 01 |.| |
Parse出来てそうですね!これで中身はともかく、Binlogが読み進められるようになりました。
Binlog Event Type
ひとまずBinlogのメッセージをParseできるようになりましたが、先ほどParseした0x0f(15)のEvent Typeとはなんだったのでしょうか?
全部のEventを解説していく結構な量になってしまうので
http://dev.mysql.com/doc/internals/en/binlog-event-type.html から私が興味有る部分だけ抜粋してみます。
1 2 3 |
0x02 QUERY_EVENT 0x04 ROTATE_EVENT 0x0f FORMAT_DESCRIPTION_EVENT |
と、これぐらいですかね。せっかくBinlogをparseするのですし自力でクエリが見れるようになりたいですよね(mysqlbinlogでqueryみれますが、まーそれはおいといて)。あとはFORMAT DESCRIPTION EVENTとROTATE EVENTぐらいがわかればとりあえず肝は分かりそうです。
それではFormat Description Eventを見ていきます
Format Description Event
http://dev.mysql.com/doc/internals/en/format-description-event.html Format Description Eventはbinlog version 4において最初のイベントで、binlog formatのバージョンや, binlogを作成したMySQL Serverのバージョン情報などが入っています。
細かい話は置いといて、まずはParseしてみましょう。
1 2 3 4 5 |
2 binlog-version string[50] mysql-server version 4 create timestamp 1 event header length string[p] event type header lengths |
event type header lengthsの項目だけが解釈がしづらい上に解説が少ないですね。
こういった説明が少ないのはMySQLに限らずよくあるパターンなんですが、いくつか実際のバイナリを眺めていればなんとなく理解しやすいものです。
ざっくりと説明するとこの項目はuint8の配列で各種Event Headerの大きさを明記だけのものです(配列のOffsetはEventType - 1という仮定になっています)
例えば先ほどの0x0f format descriptionの大きさがどれくらいか、という場合はEvent Type - 1のOffsetを見れば大きさが書いてある、ということになります。
Binlog HeaderをParseするプログラムにFormat Description EventをParseする機能を追加してみましょう。
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 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 |
package main import ( "fmt" "encoding/binary" "encoding/hex" ) type EventType int const ( UNKNOWN_EVENT EventType = 0x00 START_EVENT_V3 EventType = 0x01 QUERY_EVENT EventType = 0x02 STOP_EVENT EventType = 0x03 ROTATE_EVENT EventType = 0x04 INTVAR_EVENT EventType = 0x05 LOAD_EVENT EventType = 0x06 SLAVE_EVENT EventType = 0x07 CREATE_FILE_EVENT EventType = 0x08 APPEND_BLOCK_EVENT EventType = 0x09 EXEC_LOAD_EVENT EventType = 0x0a DELETE_FILE_EVENT EventType = 0x0b NEW_LOAD_EVENT EventType = 0x0c RAND_EVENT EventType = 0x0d USER_VAR_EVENT EventType = 0x0e FORMAT_DESCRIPTION_EVENT EventType = 0x0f XID_EVENT EventType = 0x10 BEGIN_LOAD_QUERY_EVENT EventType = 0x11 EXECUTE_LOAD_QUERY_EVENT EventType = 0x12 TABLE_MAP_EVENT EventType = 0x13 WRITE_ROWS_EVENTv0 EventType = 0x14 UPDATE_ROWS_EVENTv0 EventType = 0x15 DELETE_ROWS_EVENTv0 EventType = 0x16 WRITE_ROWS_EVENTv1 EventType = 0x17 UPDATE_ROWS_EVENTv1 EventType = 0x18 DELETE_ROWS_EVENTv1 EventType = 0x19 INCIDENT_EVENT EventType = 0x1a HEARTBEAT_EVENT EventType = 0x1b IGNORABLE_EVENT EventType = 0x1c ROWS_QUERY_EVENT EventType = 0x1d WRITE_ROWS_EVENTv2 EventType = 0x1e UPDATE_ROWS_EVENTv2 EventType = 0x1f DELETE_ROWS_EVENTv2 EventType = 0x20 GTID_EVENT EventType = 0x21 ANONYMOUS_GTID_EVENT EventType = 0x22 PREVIOUS_GTIDS_EVENT EventType = 0x23 ) func main() { buffer := []byte{ 0x7e, 0x8d, 0x3b, 0x54, 0x0f, 0xe9, 0x03, 0x00, 0x00, 0x74, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x35, 0x2e, 0x36, 0x2e, 0x31, 0x35, 0x2d, 0x6c, 0x6f, 0x67, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7e, 0x8d, 0x3b, 0x54, 0x13, 0x38, 0x0d, 0x00, 0x08, 0x00, 0x12, 0x00, 0x04, 0x04, 0x04, 0x04, 0x12, 0x00, 0x00, 0x5c, 0x00, 0x04, 0x1a, 0x08, 0x00, 0x00, 0x00, 0x08, 0x08, 0x08, 0x02, 0x00, 0x00, 0x00, 0x0a, 0x0a, 0x0a, 0x19, 0x19, 0x00, 0x01, 0xfa, 0xd5, 0x17, 0x2f, } offset := 0 timestamp := binary.LittleEndian.Uint32(buffer[offset:offset+4]) offset += 4 event_type := buffer[offset] offset += 1 server_id := binary.LittleEndian.Uint32(buffer[offset:offset+4]) offset += 4 event_size := binary.LittleEndian.Uint32(buffer[offset:offset+4]) offset += 4 log_pos := binary.LittleEndian.Uint32(buffer[offset:offset+4]) offset += 4 flags := binary.LittleEndian.Uint16(buffer[offset:offset+2]) offset += 2 fmt.Printf("[timestamp]: %d\n", timestamp) fmt.Printf("[event_type]: %d\n", event_type) fmt.Printf("[server_id]: %d\n", server_id) fmt.Printf("[event_size]: %d\n", event_size) fmt.Printf("[log_pos]: %d\n", log_pos) fmt.Printf("[flags]: %d\n", flags) fmt.Printf("[payload]:\n%s\n", hex.Dump(buffer[offset:(offset+int(event_size)-19)])) payload := buffer[offset:(offset+int(event_size)-19)] offset = 0 binlog_version := binary.LittleEndian.Uint16(payload[offset:offset+2]) offset += 2 server_version := payload[offset:offset+50] offset += 50 create_timestamp := binary.LittleEndian.Uint32(payload[offset:offset+4]) offset += 4 event_header_length := payload[offset] offset += 1 fmt.Printf("[binlog_version]: %d\n", binlog_version) fmt.Printf("[server_version]: %s\n", server_version) fmt.Printf("[create_timestamp]: %d\n", create_timestamp) fmt.Printf("[event_header_lengthphp]: %d\n", event_header_length) fmt.Printf("[lengths]:\n%s", hex.Dump(payload[offset:])) for i := 0; i+offset < len(payload) || i <= (int)(PREVIOUS_GTIDS_EVENT); i++ { fmt.Printf(" Event %d Length: %d\n", i + 1, payload[offset+i]) } } |
Format Description EventをParseすることで各イベントのサイズがわかるようになりました。
http://play.golang.org/p/m8cFKfsLIG
そいでは次すすみます。
Rotate Event
Replicationをしているとbinlogがrotationしていることに気がつくと思います。このRotate Eventでは主にbinlogのrotationを行う時にどうしたらよいか、ということが記されています。
http://dev.mysql.com/doc/internals/en/rotate-event.html から抜粋すると
1 2 |
8 position string[p] name of the next binlog |
binlog version 1は現状のversionではないので省いておきました。
uint64のpositionと次のbinlog fileの名前が入っているだけなのでparse簡単ですね。適当に書くとこんな感じでしょうか
1 2 3 |
position := binary.LittleEndian.Uint64(buffer[0:8]) name := buffer[8:] |
実際Rotate EventはRotation以外の用途にも使われたりするのですが、現状はとりあえずParseしてみる事が目的なのでまた後日にでも解説しようと思います
Query Event
さぁ、ようやくおまちかねのQuery Eventです。
http://dev.mysql.com/doc/internals/en/query-event.html
1 2 3 4 5 6 7 8 9 10 11 12 |
[Post Header] 4 slave_proxy_id 4 execution time 1 schema length 2 error-code if binlog-version ≥ 4: 2 status-vars length [Payload] string[$len] status-vars string[$len] schema 1 [00] string[EOF] query |
ここらへんまでやってきた皆さんであればもうサクサクとParseできるはずです。決して解説が面倒になってきたわけではありませんヨ!?
一箇所status-varsの部分だけ分かりづらいので補足しておきます。
status-varsは連続したbyteで表したkey valueでkeyが1byte, value sizeはkeyによって事前に定義された長さを使います。
例えばstatus-varsのoffset 0が0x00の場合はKeyがQ_FLAGS2_CODEとなり、Valueの長さは4byteとなります。
まー、ぶっちゃけこれらのstatus-varsの情報は自前でReplicationをしようとかいった場合以外には不要な情報ですし実装するのが結構面倒だったりするのでスルーしておきましょう。
終わりに
Binlog FileのParseを通してEventについて学ぶことが出来ました。
実際はmysqlbinlogコマンドを使うだけで十分なんですが、ファイルフォーマットまで詳しくなっているとbinlogが壊れた時もバイナリを眺めればどこが悪いのか想像がつくようになるので覚えておいて損はないですね。
今日説明した内容のサンプルを ここ においておきますので興味がある人は書いてみて答え合わせしてみたりしてください。
そうそう、こういった新しい事とか知らない事についてやらない理由とかわからない理由こねくり回すのはそれはそれで楽しいんですが、ぼくたち創造的な開発者ですし、ごたごたいってねーでコード書こうって姿勢って大事だったりします。最初は難しいかもしれないけどちょっとトライしたら絶対できるから。
こういった事は「やるかやったか」(やるかやらないか、じゃないよ)なのでどんどん書いて引き出し増やしていきましょ。
そんなことはさておき、次回はBinlog Eventの中でもRow Based Replication部分に焦点を当てて解説して行きたいと思います。年末進行&私がAdvent Calendarに参加しすぎてしまい次の記事まで期間があいてしまうかもしれませんが、いましばらくお待ちいただければと思います。
Treasure Data Advent Calendarへのお誘い
今年はhttp://qiita.com/advent-calendar/2014/td というのを主催しておりますので、みなさんも是非是非ご参加いただければと思います。私もGREEで実際の現場で使われている中からちょろちょろと事例をご紹介できるように調整すすめています。
TD使い始めの方へのtipsや実際の事例などの共有ができれば幸いです。
それでは