MySQL 5.6.10 以下に存在する レプリケーションのバグと詳細について

こんにちは。初めて会社のブログを書きます。吉川です。

先日、月に一度行われている社内勉強会(hbstyle)にて、下記の発表をしました。

MySQL 5.6.10 以下に存在する レプリケーションのバグと原因特定について (Heartbeats 社内勉強会 2014/12/11)

この件に関して、社内・外から反響がありましたので、詳細についてご紹介します。

概要

以下の環境にて、MySQL のレプリケーションが正常に同期されない事象に遭遇しました。

  • 使用している MySQL のバージョンは 5.5.32
  • バイナリログのフォーマットには ROW を使用

現象としては以下の通りです。

  • MasterDB で特定のテーブルに書き込んだ更新系 DML が SlaveDB に反映されない
  • SlaveDB にて show slave status\G を実行した結果
    • Slave_(IO|SQL)_Running は Yes が表示される
    • Seconds_behind_master に 0 が表示される
    • Exec_Master_Log_Pos の値が MasterDB の Log position と一致する
  • Slave 側の Relay-log には MasterDB で実行した更新系 DML が記載されている
  • MasterDB, SlaveDB 共にエラーログには出力なし

なお、本バグは下記の通り既に報告されており、MySQl 5.6.11 以上では修正済みとなっています。

table_id is defined differently in sql/table.h vs sql/log_event.h

発生条件

以下の条件を全て満たした場合、本バグが発生します。

  • SlaveDB の MySQL のバージョンが 5.6.10 以下
  • バイナリログのフォーマットが ROW もしくは MIXED
  • MasterDB の table_id が UINT_MAX (64bit Linux 環境で 4294967295) を超えた場合

MasterDB の table_id が UINT_MAX を超えると、SlaveDB 側でオーバーフローが発生し、書き込み対象のテーブルを取得できず、テーブルへの書き込みに失敗する、という事象です。

table_id は MySQL を再起動しない限り増え続ける値のため、長期間に渡り MySQL を運用している環境にて、特に注意が必要だと考えられます。

table_id の確認方法

MasterDB のテーブルに割り当てられている table_id は、以下の手順で binlog より確認することが出来ます。


# binlog の decode
ionice -c3 mysqlbinlog /var/lib/mysql/mysql-bin.000001 -v --base64-output=DECODE-ROWS > /tmp/mysql-bin-000001.decode

# decode 済み binlog から table_map_event を探す
grep 'Table_map:' /tmp/mysql-bin-000001.decode

下記のような出力が得られます。


:
#141215 19:18:04 server id 1  end_log_pos 328       Table_map: `testdb`.`table1` mapped to number 33
#141216 15:47:44 server id 1  end_log_pos 512       Table_map: `testdb`.`table1` mapped to number 33
#141216 15:50:00 server id 1  end_log_pos 696       Table_map: `testdb`.`table1` mapped to number 33
#141216 16:01:23 server id 1  end_log_pos 1093      Table_map: `testdb`.`table2` mapped to number 35
#141216 16:01:49 server id 1  end_log_pos 1277      Table_map: `testdb`.`table2` mapped to number 35
#141216 16:02:52 server id 1  end_log_pos 1461      Table_map: `testdb`.`table2` mapped to number 35
#141216 16:33:54 server id 1  end_log_pos 1645      Table_map: `testdb`.`table2` mapped to number 35
#141216 18:36:04 server id 1  end_log_pos 2307      Table_map: `testdb`.`table1` mapped to number 109
#141216 18:36:06 server id 1  end_log_pos 2491      Table_map: `testdb`.`table1` mapped to number 109
#141216 18:36:07 server id 1  end_log_pos 2675      Table_map: `testdb`.`table1` mapped to number 109
#141216 18:36:08 server id 1  end_log_pos 2859      Table_map: `testdb`.`table1` mapped to number 109
#141216 18:36:09 server id 1  end_log_pos 3043      Table_map: `testdb`.`table1` mapped to number 109
#141216 18:38:43 server id 1  end_log_pos 3220      Table_map: `testdb`.`table1` mapped to number 109
#141216 18:40:47 server id 1  end_log_pos 3511      Table_map: `testdb`.`table3` mapped to number 119
:

このログは、テーブルに INSERT/DELETE/UPDATE 等の更新系 DML が発行された際に binlog に出力されるものです。 (※もし出力がない場合には、新しくテーブルを作成し、INSERT 文等を発行することで確認することが出来ます。)

上記ログの末尾に書いてある数字が table_id となり、Table_map: ... の後に続いているのが当該の id が割り振られているテーブル名となります。

例えば、Log position 328 の段階では、testdb.table1 に 33 という table_id が割り当てられています。

割り当てられてる table_id が UINT_MAX を超えた場合、当該テーブルへの書き込みは SlaveDB に反映されません。

table_id の割り当てタイミング

table_id の割り当ては、table_definition_cache に乗っていないテーブルのオープン時に行われます。

具体的には、show open tables; 句を発行した際に一覧に出ないテーブルをオープンした際です。 例としては、下記の操作が考えられます。

  • CREATE TABLE にて新たなテーブルを作成し書き込みを実施
  • FLUSH TABLES にてテーブルを閉じた後に読み込みを実施
  • ソート時などのバッファ溢れにより一時テーブルを作成し書き込みを実施

table_id は MySQL の起動時から、割り当てられる度に 1 ずつ上昇していく値となります。 具体的には、last_table_id という値で管理されています。

また、MasterDB と SlaveDB では異なる last_table_id を保持しているため、 単純に Master から Slave へスイッチオーバしても table_id をリセットすることは出来ません。

取りうる回避策

MySQL 5.6.11 以降へのアップデート

もちろんこれが適切な改善策です。 ちなみに、MySQL 5.6 以降では information_schema に INNODB_SYS_TABLES というテーブルがあり、 その中の table_id というカラムから、テーブルの ID を取得できるようになっています。

MasterDB 再起動

table_id の値は、MySQL を再起動するまでリセットされません。 そのため、table_id が UINT_MAX を超える前に MasterDB の MySQL を再起動する必要があります。

SlaveDB 再起動 + スイッチオーバ

冗長構成ならこちらの方法が現実的かなと思います。 単純に SlaveDB にスイッチオーバするだけだと、スイッチオーバした際に、 SlaveDB が保持している table_id から増加していくため、 table_id が大きく減る保証はありません。

事前に SlaveDB を再起動しておくことで、last_table_id をリセットすることが出来るので、 SlaveDB を再起動し、MasterDB から SlaveDB にスイッチオーバすることで、 table_id が UINT_MAX を超えることを防ぐことが出来ます。

table_id の急激な増加を防ぐために

table_id は前述の通り、新たにテーブルをオープンした際に割り当てられます。 特に table_id が増加しやすい原因として、以下の 3 つが考えられます。

  • 各種バッファサイズが小さすぎる
  • テーブル数に対して table_definition_cache が小さすぎる
  • FLUSH TABLES を実行しすぎている

各種バッファサイズが小さすぎる

各種バッファサイズが小さすぎる場合、ソートなどの操作により一時テーブルが生成されます。 table_id は一時テーブルにも割り当てられるため、頻繁に一時テーブルが作成されるような環境だと、 table_id が不必要に増加してしまう原因となります。 ソート時に一時テーブルが頻繁に作られてしまうような環境では、 sort_buffer_size 等のパラメータを調整する必要があるかもしれません。

テーブル数に対して table_definition_cache が小さすぎる

table_definition_cache の値がテーブル数に対して小さすぎる場合、 頻繁に table_definition_cache からの追い出しが発生し、 次にオープンするタイミングで、table_id が増加します。 多くのテーブルをそれぞれ頻繁に使用するような場合は、 パラメータを調整する必要があるかもしれません。

FLUSH TABLES を実行しすぎている

FLUSH TABLES 句は、全てのテーブルを一度閉じます。 次にテーブルを開くタイミングで、table_id が新たに割り当てられることになるため、 FLUSH TABLES 句を頻繁に使用した場合、table_id が急激に増加することになります。

まとめ

MySQL 5.6.10 以下に存在するレプリケーションのバグについて現象を説明しました。 table_id は普通に MySQL を使用していても増え続ける値であり、皆さんの環境でもいつこのバグが発生するとも限りません。

また、RDS で発生したという報告が挙がっていたり、MariaDB にも当該コード部分がそのままであったりするので、 気になる方は確認してみるのが良いかと思います。

この件に関しては引き続き対応や調査を進めますので、また何か公開できる事があればブログに書くかもしれません。 その際はまたよろしくお願いします。