MySQL で super_read_only=on なのに GTID が割り当てられる statement などについて

こんにちわ。せじまです。

2025-10-21 にリリースされた MySQL 9.5.0、個人的には久しぶりに Innovation Release だなと思わされる変更が入ってました。

To enable enhanced replication capabilities by default, the default values of gtid_mode has been changed to ON. Additionally, the default value of enforce_gtid_consistency has been changed from ON.

ついに、GTIDがデフォルトで有効化されることになりました。

みなさん、GTID使ってますか? Bug #88720でAffects Meのボタンは押しましたか? MySQL 8.4.1 で fix されたBug #35665076の commit log は読まれましたか?

Bug #88720もBug #35665076もご存知の方は、できればご同僚やご友人の方々にもお誘い合わせの上、Bug #88720でAffects Meをお願いいたします。

そうでない方は、これからこれらのバグなどについて解説しますので、読み終わったらできればBug #88720でAffects Meのご協力をお願いいたします。

でははじめます。

はじめに

MySQLでGTIDを使ったreplicationを行うとき、super_read_onlyオプションを有効にすることが良いとされています。(もう十年以上前の記事になりますが)Percona Database Performance Blogのsuper_read_only and GTID replicationの時点ですでに言及されています。

MySQLのreplicationが開始できないケースの一つとして、次のようなものがあります。

これを防ぐための有効な手段として、 SOURCE_AUTO_POSITION=1 で replication する際は replica で super_read_only=on にすることが望ましいと考えられます。

では、 replica で super_read_only=on にすることで何を防げるのかを見ていきましょう。

ER_SOURCE_HAS_PURGED_REQUIRED_GTIDSが発生する状況

MySQL 8.0公式ドキュメントの17.1.3.3 GTID 自動配置で、ER_MASTER_HAS_PURGED_REQUIRED_GTIDS(ER_SOURCE_HAS_PURGED_REQUIRED_GTIDS)が発生するケースについて言及されているのですが、OSSなので生成AIに聞いたりドキュメントを読んだりするよりも、ソースコード読んだほうがより理解できるケースがしばしばあります。なのでソースコードを読んでいきます。

CHANGE REPLICATION SOURCE TO ... SOURCE_AUTO_POSITION=1; START REPLICA; すると、MySQLのプロトコル的には replica から COM_BINLOG_DUMP_GTID が投げられるのですが、COM_BINLOG_DUMP_GTID 投げる際、 replica からは gtid_executed を source に送信しています。

https://github.com/mysql/mysql-server/blob/mysql-8.0.44/sql/rpl_replica.cc#L4188-L4215

source は replica から COM_BINLOG_DUMP_GTID を受け取ると、BinlogSenderクラスのオブジェクトを生成するのですが

https://github.com/mysql/mysql-server/blob/mysql-8.0.44/sql/rpl_source.cc#L985-L1043
https://github.com/mysql/mysql-server/blob/mysql-8.0.44/sql/rpl_source.cc#L1054-L1059

BinlogSenderクラスのコンストラクタを見ると、replica から渡された gtid_executed は、exclude_gtids という変数名になっています。
https://github.com/mysql/mysql-server/blob/mysql-8.0.44/sql/rpl_binlog_sender.cc#L234-L241

Binlog_sender::run() → Binlog_sender::init() → Binlog_sender::check_start_file() と見ていきますと

https://github.com/mysql/mysql-server/blob/mysql-8.0.44/sql/rpl_binlog_sender.cc#L379-L382
https://github.com/mysql/mysql-server/blob/mysql-8.0.44/sql/rpl_binlog_sender.cc#L257-L298
https://github.com/mysql/mysql-server/blob/mysql-8.0.44/sql/rpl_binlog_sender.cc#L854

Binlog_sender::check_start_file() でどの binary log から送信するべきか判定しているのですが

i.e., throw error if slave's GTID executed set is not a superset of Master's purged GTID set.

slave(replica) の gtid_executed が master(source) の gtid_purged のスーパーセットでなければ、 master は error を返すといった意のコメントがあります。

https://github.com/mysql/mysql-server/blob/mysql-8.0.44/sql/rpl_binlog_sender.cc#L899-L930

実態は

なので、サブセットでないかどうかを見ているわけですね。

replica の gtid_executed に含まれない GTID、replica が実行していない GTID の binlog event をsourceが持っていたならば、その GTID がどれくらい古くても、 replica に送る必要があります。そうしなければ、 source と replica は同期が取れていることを保証できません。(大変ざっくりした例をあげると、source上でどれほど昔に発行されたINSERTであっても、それがreplicaで実行されていなければ、sourceとreplicaのテーブルの状態は一致しないですよね)
このように考えていくと、replica の gtid_executed は、言い換えると、 source から送信すべき binlog event から除外できる GTID の集合とも言えます。(それでBinlogSenderのソースコード中では exclude_gtids という変数名が用いられているのではないかと思われます。)

最終的に、 source の gtid_executed で実行済みの GTID に紐づく binlog event を、すべて replica でも実行する必要があります。sourceからreplicaが必要とするGTID(に紐づくbinlog event)をすべて送信できるかの判定をするとき、 source は Gtid_state::get_lost_gtids() を呼んで、サブセットかどうか調べてます。 get_lost_gtids() は、 source の binlog から失われた GTID、これらの GTID を含む binlog はすでに purge されているということです。

ちなみに gtid_purged は、Gtid_state::get_lost_gtids() の値を参照しています。
https://github.com/mysql/mysql-server/blob/mysql-8.0.44/sql/sys_vars.h#L2640-L2698

というわけで、source の gtid_purged が replica の gtid_executed のサブセットになってなかったらエラーになるわけです。

最終的にMYSQL_BIN_LOG::report_missing_gtids() でER_SOURCE_HAS_PURGED_REQUIRED_GTIDSの errmsg を生成して返すことになります。
https://github.com/mysql/mysql-server/blob/mysql-8.0.44/sql/binlog.cc#L9213-L9253

ER_SOURCE_HAS_PURGED_REQUIRED_GTIDS を予防するための super_read_only=on

よって、最初にあげた

となるわけで、これを防ぐためには replica で super_read_only=on を設定することが有効な手段となります。

具体的にどのような効果があるかといいますと、次のような状況を防げます。

  1. 例えば、source に対して replica が2台ぶら下がっている構成があるとします。replicaはそれぞれnode_a,node_bとします。
  2. ある日node_aでsuper_read_only=offの状態でnode_aでのみADD INDEXなど実行した後、月日が流れADD INDEXしたときのbinlogがpurgeされたとします。
  3. sourceのインスタンスが落ちて node_aを新しいsourceとして昇格させるため、node_bで CHANGE REPLICATION SOURCE TO ... SOURCE_HOST=node_a, SOURCE_AUTO_POSITION=1; START REPLICA; します。
  4. node_aでのみADD INDEXしたときのbinlogがpurgeで失われ、ADD INDEXに関連したGTIDがnode_aのgtid_purgedに含まれている状態なので、新しいsource の gtid_purged が 新しいreplica の gtid_executed のサブセットにならない(ADD INDEXしたときのGTIDが新しいsourceであるnode_aのgtid_purgedにのみ含まれており、新しいreplicaのnode_bのgtid_executedに存在しない)ので、ER_SOURCE_HAS_PURGED_REQUIRED_GTIDSが発生します。

replicaがsuper_read_only=onであれば、2.の時点でADD INDEXできないので、こういった状況を防げるわけですね。

ER_SOURCE_HAS_PURGED_REQUIRED_GTIDSが実際に発生した際の error message の補足事項

ER_SOURCE_HAS_PURGED_REQUIRED_GTIDSは、非常に猛烈にわかりにくい部分がありますので補足します。

具体的に試せるよう、参考までに docker-compose.yml や初期化用のscriptを用意してみます。

これで

  1. docker-compose up -d する
  2. docker-compose exec mysql-replica mysql して USE test;CREATE TABLE test (id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, val INT NOT NULL);
  3. docker-compose exec mysql-replica mysql して FLUSH BINARY LOGS; PURGE BINARY LOGS BEFORE DATE_ADD(NOW(), INTERVAL 1 SECOND);
  4. docker-compose exec mysql-replica2 mysql して STOP REPLICA; CHANGE REPLICATION SOURCE TO SOURCE_HOST='mysql-replica', SOURCE_USER='repl', SOURCE_PASSWORD='pass', SOURCE_SSL=1, SOURCE_AUTO_POSITION=1; START REPLICA; すると、ER_SOURCE_HAS_PURGED_REQUIRED_GTIDS が返ってくる

といったことを確認できます。ただ

あれ?Error number: 1789じゃなくてError number: 1236なのです?と思われるでしょうが、よくよく読んでみると

'Cannot replicate because the source purged required binary logs. Replicate the missing transactions from elsewhere, or provision a new replica from backup. Consider increasing the source's binary log expiration period. The GTID set sent by the replica is 'e24b415e-f1d7-11f0-88eb-066225a28251:1-4', and the missing transactions are 'e2855d1a-f1d7-11f0-8b58-e6f3098eb441:1''

この部分がER_SOURCE_HAS_PURGED_REQUIRED_GTIDS

Message: Cannot replicate because the source purged required binary logs. Replicate the missing transactions from elsewhere, or provision a new replica from backup. Consider increasing the source's binary log expiration period. %s

なんですね。ER_SOURCE_HAS_PURGED_REQUIRED_GTIDSはER_SOURCE_FATAL_ERROR_READING_BINLOGでラップされて返ってきているというわけですね。

具体的には

https://github.com/mysql/mysql-server/blob/mysql-8.0.44/sql/rpl_binlog_sender.cc#L926-L928

https://github.com/mysql/mysql-server/blob/mysql-8.0.44/sql/rpl_binlog_sender.h#L434-L436

https://github.com/mysql/mysql-server/blob/mysql-8.0.44/sql/rpl_binlog_sender.h#L423-L428

set_fatal_error()で Error number: 1236 ER_SOURCE_FATAL_ERROR_READING_BINLOG を設定しているかと。

しかしsuper_read_only=onでも生成されるGTIDがある

ここからが本題です。

例えば、gtid_mode=onかつsuper_read_only=on に設定したMySQL8.0.44のreplicaで次のSQLやコマンドを実行した場合、GTIDが発行されるものはどれでしょうか?

  1. FLUSH ERROR LOGS;
  2. mysqladmin flush-logs slow
  3. mysqldump --single-transaction

答えは 1〜3 ぜんぶです。

「おいおいマジかよ?!」と思われましたか?初めて知ったとき、私はそう思いました。

read only な replicaでmysqldumpを叩くのは至極当然でしょうし、error logやslow query logをreplicaごとに個別にローテートしてもいいと思うじゃないですか。でも、これらをそのまま実行するとbinlogにFLUSHステートメントが出力され、それに伴うGTIDが生成されます。

FLUSH ステートメントには [NO_WRITE_TO_BINLOG | LOCAL] といったオプションがあるのですが、

注記
レプリカにレプリケートすると問題が発生するため、FLUSH LOGS, FLUSH BINARY LOGS, FLUSH TABLES WITH READ LOCK (テーブルリストの有無にかかわらず)、および FLUSH TABLES tbl_name ... FOR EXPORT はバイナリログに書き込まれません。

といったごく一部の例外を除き、 [NO_WRITE_TO_BINLOG | LOCAL] を指定しないとFLUSHステートメントはbinlogに吐かれてしまい、GTIDが生成されてしまうのです。

例えば MySQL 8.0.44 をローカルで docker で立ち上げて set global super_read_only=on; した後、 mysqldump --single-transaction すると FLUSH TABLES が発行され、GTIDが生成されます。

幸いにして、mysqldumpでGTIDが生成されてしまうのは Bug #35665076 として認められたようで、MySQL 8.4.1 で fix されました。8.4.1以降のMySQLに付属するmysqldumpを使えば、この問題は回避できます。

一方、FLUSH ERROR LOGS や mysqladmin flush-logs slow でGTIDが生成される問題については、

MySQL Bugs: #88720: Inconsistent and unsafe FLUSH behavior in terms of replication

で2017年にPerconaの方がバグレポートして以来そのままらしく、未だに修正されていません。

これはもうみんなで Affects Me して、MySQLの中の人に「困ってます!」と伝えるしかないじゃないですか。

知らず知らずのうちにreplicaで発行されたGTIDを見つけるためにはどうしたら?

このような話をすると、

「super_read_only=onにして安心していたけど、ウチのMySQLにも、知らないうちにreplicaで生成されたGTIDがあるかも…」

と思う方もいらっしゃるかもしれません。私は初めて知ったときそう思って、ざっくり調査するためのSQLを考えました。

至ってシンプルです。replicaで次のSQLを実行してみてください。

SELECT @@GLOBAL.gtid_executed LIKE CONCAT('%', @@server_uuid, '%');

公式ドキュメントの 17.1.3.1 GTID 形式および格納にあるように、デフォルトでは

GTID は座標のペアとして表現され、次に示すように、コロン文字 (:) で区切られます。

GTID = source_id:transaction_id

source_id は発生元サーバーを識別します。 通常、この目的にはソース server_uuid が使用されます。

server_uuidが含まれます。具体的には次のように

例えば、MySQLは UUID()関数でUUIDを生成できますが、UUIDを生成しつつLIKEの比較演算子で評価を行っても、@@GLOBAL.gtid_executed に該当するUUIDは存在しないので 0 が返ってきます。

replicaのgtid_executedの中に、source_idにreplicaのserver_uuidが含まれているものがあれば、そのGTIDはreplicaで作成されたものだろう、という話です。

おわりに

長らくMySQLのデフォルトであったGTIDを使わないreplicationであれば、このようなFLUSHステートメントなどに悩まされることもなかったかと思いますが、MySQL 9.5.0は流石Innovation Release、今までの前提が覆ってきました。MySQL 9.7.0 LTS(予定)でも引き続きgtid_mode=offやgtid_mode=off_permissiveが指定できるなら、引き続きGTIDを使わないでやっていけますが、こういったGTIDに関連する問題など今のうちから調べつつ、MySQLのデフォルトに準拠していけるよう、やっていきたいものです。