バイナリログからデータを吸い出したいケース
MySQL の障害復旧というと、まずバックアップからの復元(PITR)が思い付きます。 しかし実際の運用では、データベース全体を過去の時点へ戻したいとは限りません。
例えば、アプリケーションのバグなどで、レコードごとに異なるタイミングで誤った更新が行われることがあります。このような場合、レコードごとに異なる時点のデータが必要になります。
このような場合に役立つのがバイナリログ( binlog )です。 バイナリログ には INSERT、UPDATE、DELETE といった変更履歴が記録されているため、
- どのレコードが
- いつ
- どのように変更されたか
を後から追跡できます。
バイナリログの形式
binlog はバイナリ形式で保存されているため、そのままでは内容を読むことができません。
内容を確認するには、MySQL に付属している mysqlbinlog コマンドを利用します。binlog を見やすく表示するには、--base64-output=DECODE-ROWS と --verbose オプションを利用します。
$ mysqlbinlog --base64-output=DECODE-ROWS --verbose mysql-bin.000001
例えば次のような UPDATE が実行されたとします。
UPDATE t1 SET v = 'new value' WHERE pk = 1;
すると binlog には次のような内容が記録されます*1。
### UPDATE `sample`.`t1` ### WHERE ### @1=1 ### @2='old value' ### SET ### @1=1 ### @2='new value'
更新前の値(WHERE)と更新後の値(SET)が確認できるため、レコードがどのように変更されたかを追跡できます。
少量の調査であれば mysqlbinlog だけでも十分ですが、変更履歴を機械的に処理したい場合には扱いづらさを感じます。
そこで今回は Maxwell を利用して、binlog を JSON 形式へ変換してみます。
Maxwell を試す
Maxwell は Zendesk が開発している MySQL 向けの CDC(Change Data Capture)ツールです。MySQL のレプリカとして接続し、binlog を読み取りながら変更内容を JSON 形式で出力することができます。
bin/maxwell \ --host=localhost \ --user=maxwell \ --password=xxxx \ --producer=stdout
起動すると Maxwell は MySQL に接続し、binlog の読み取りを開始します。
例えば次の UPDATE を実行します。
UPDATE t1 SET v = 'new value' WHERE pk = 1;
すると Maxwell は次のような JSON を出力します。
{
"database": "sample",
"table": "t1",
"type": "update",
"ts": 1749600000,
"data": {
"pk": 1,
"v": "new value"
},
"old": {
"v": "old value"
}
}
mysqlbinlog の出力と比べると、かなり扱いやすくなっています。
Maxwell のオプション
Maxwell にはフィルタ機能もあります。例えば次のようにオプションを設定すると、sample.t1 テーブルの変更だけを取得できます。
--filter=include:sample.t1
Maxwell はデフォルトでは起動時点の最新の binlog 位置から処理を開始します。
一方で、過去の binlog を解析したい場合は --init_position を指定できます。
bin/maxwell \ --host=localhost \ --user=maxwell \ --password=xxxx \ --producer=stdout \ --init_position=mysql-bin.000123:456789
これは今回のような調査用途では非常に便利です。 問題が発生した時刻以降の binlog を指定することで、必要な範囲だけを JSON として取得できます。
Maxwell のメタデータ
Maxwell は binlog を読み取る際に、内部的なメタデータを MySQL 上に保存します。 初回起動時には maxwell スキーマが作成され、その中にいくつかの管理テーブルが作成されます。
特に重要なのが次の2つです。
| テーブル | 用途 |
|---|---|
| positions | 現在の binlog の読み取り位置 |
| schemas | Maxwell が認識しているスキーマ情報 |
mysql> show tables from maxwell; +-------------------+ | Tables_in_maxwell | +-------------------+ | bootstrap | | columns | | databases | | heartbeats | | positions | | schemas | | tables | +-------------------+ 7 rows in set (0.00 sec)
positions テーブルには、Maxwell が最後に処理した binlog のファイル名やポジションが保存されます。 そのため、一度停止した後に再起動しても、前回の続きから処理を再開できます。
mysql> select * from positions \G
*************************** 1. row ***************************
server_id: 1
binlog_file: mysql-bin.000003
binlog_position: 1059
gtid_set: NULL
client_id: maxwell
heartbeat_at: NULL
last_heartbeat_read: 0
1 row in set (0.00 sec)
一方、schemas テーブルにはテーブル定義の履歴が保存されます。
mysql> select * from `schemas` \G
*************************** 1. row ***************************
id: 1
binlog_file: mysql-bin.000003
binlog_position: 4
last_heartbeat_read: 0
gtid_set: NULL
base_schema_id: NULL
deltas: NULL
server_id: 1
position_sha: 44e4a4c80596f267066c22b1110298c5d1a4e06c
charset: utf8mb4
version: 4
deleted: 0
*************************** 2. row ***************************
id: 2
binlog_file: mysql-bin.000004
binlog_position: 235
last_heartbeat_read: 0
gtid_set: NULL
base_schema_id: 1
deltas: [{"type":"table-alter","database":"d","table":"t1","old":{"database":"d","charset":"utf8mb4","table":"t1","columns":[{"type":"int","name":"pk","signed":true},{"type":"varchar","name":"v","charset":"utf8mb4"},{"type":"datetime","name":"updated","column-length":0}],"primary-key":["pk"]},"def":{"database":"d","charset":"utf8mb4","table":"t1_old","columns":[{"type":"int","name":"pk","signed":true},{"type":"varchar","name":"v","charset":"utf8mb4"},{"type":"datetime","name":"updated","column-length":0}],"primary-key":["pk"]}}]
server_id: 1
position_sha: b1aa767914be6aea39d331882bee9aba9d39c90f
charset: utf8mb4
version: 4
deleted: 0
Maxwell のスキーマトラッキング
Maxwell 内部でテーブル定義を管理しており、CREATE TABLE, ALTER TABLE といった DDL を追跡しながら動作しています。
mysqlbinlog コマンド の出力では、
### UPDATE `sample`.`t1` ### WHERE ### @1=1 ### @2='old value' ### SET ### @1=1 ### @2='new value'
のように、カラム名ではなく @1 や @2 といった形式で値が出力されます。
一方で、Maxwell はテーブル定義を保持することで、
{
"pk": 1,
"v": "new value"
}
のように実際のカラム名を含めた、わかりやすいJSON を生成しています。
スキーマトラッキングによる制約
--init_position で開始時点を指定できると書きましたが、実際このオプションを使用すると、以下のようなエラーが発生しました。
2026-06-12 07:11:17 ERROR AbstractSchemaStore - Error on bin log position Position[BinlogPosition[mysql-bin.000004:974], lastHeartbeat=0] com.zendesk.maxwell.schema.ddl.InvalidSchemaError: Unexpectedly asked to create existing table t1_histories
Maxwell は起動時にデータベースのスキーマ(テーブル定義)を取得し、その情報を利用して binlog を解釈しています。
例えば、Maxwell が接続した時点で t1_histories テーブルが存在しているとします。
t1_histories テーブルがすでに存在している状態で、過去の binlog に含まれるCREAT TABLE t1_histories 〜 を評価することでこのようなエラーになります。
Maxwell は、 「現在のスキーマの状態」と「binlog を読み始める時点のスキーマの状態」が一致していることを前提としているようでした。
そのため、過去の任意の時点から binlog を解析したい場合は、binlog だけでなくデータベースのスキーマもその時点の状態に合わせる必要があります。 今回の用途では、バックアップからデータベースを復元し、PITR によって解析対象の時点まで戻した環境に対して Maxwell を実行する必要があります。
*1:binlog_format=ROW, binlog_row_image=FULL を前提としています










