MySQLエラー調査: Table definition has changed, please retry transaction

はじめに

こんにちは、id:shallow1729です。小ネタですが、先日”ERROR 1412 (HY000): Table definition has changed, please retry transaction”というエラーに出会ったのでそれの調査をします。MySQL8.0.31をベースにします。

エラーの意味

まずエラーメッセージで調べると以下の公式ドキュメントが出てきました。

https://dev.mysql.com/doc/refman/8.0/en/innodb-consistent-read.html

Consistent read does not work over ALTER TABLE operations that make a temporary copy of the original table and delete the original table when the temporary copy is built. When you reissue a consistent read within a transaction, rows in the new table are not visible because those rows did not exist when the transaction's snapshot was taken. In this case, the transaction returns an error: ER_TABLE_DEF_CHANGED, “Table definition has changed, please retry transaction”.

まずconsistent readですが、ここではMVCCを使っている場合の話をしています。MVCCの詳細は過去に書いたことあるのでそちらを参照いただけたらと思います*1が、ざっくりとはデータの変更があった場合は変更前のバージョンのログを残しておき、各トランザクションに開始時のタイムスタンプを持たせる事で自身の開始時のバージョンを見に行くようにする仕組みです。MySQLのデフォルトのREPEATABLE READだとトランザクション開始後に読んだデータを他のトランザクションによって変更がコミットされた後にもう一度読みに行っても一回目に読んだ時と同じ結果を見れる(一貫性のある読み取りができる)のですが、これはMVCCの仕組みのおかげです。

なのですが、DDLについてはMVCCでのバージョン管理の外で行われるのでALTER TABLEしてしまうと過去のバージョンを参照できないのでconsistent readを提供できないからエラーにするという挙動のようです。もう一回実行すれば大丈夫だよというのもまあそうって感じですね。

ちなみにドキュメントの説明はかなりALGORYTHM=COPYっぽい感じですがALGORYTHM=INSTANTでも再現できます。

再現方法

session1: BEGIN;
session1: SELECT * FROM NOT_TARGET_TABLE;
session2: ALTER TABLE TARGET_TABLE ADD hoge int;
session1: SELECT * FROM TARGET_TABLE;

SELECT * FROM NOT_TARGET_TABLE;を入れてるのはデフォルトではMySQLのMVCCのタイムスタンプの払い出しのタイミングが一つ目のクエリのタイミングだからです。

詳細な動作の調査

とりあえずドキュメントのおかげでエラーを吐かせたモチベーションは理解できたのですが、本番環境で無停止でDDLオペレーションをする時にこのエラーによりリクエストが失敗するのはどの程度起こるかなどの見積もりは欲しいので、そういった事を考えるために詳細な発生条件を考えます。

このエラーが発生した時クエリはテーブルにアクセスはできるのか

実際にはこのエラーは一分程度まばらに発生したのですが、この時に気になったのが、これは一分間そのテーブルへのアクセスができない状態だったのか、そうでない一部なのかという点でした。

テーブルにアクセスできない可能性が気になったのは、メタデータロックというMySQLDDLで気を遣わないといけないことがよく知られた事があるからです。メタデータロックはテーブル定義に対して取るロックで、そのテーブルのレコードの読み書きやDDLが行われる時にそのトランザクションで取得されます。そして、このトランザクションは、トランザクションの終了時(commit/rollback時)に解放されます。基本的にレコードの読み書きの際はテーブル定義は参照のみなのでshared lockで構わないのですが、DDLはテーブル定義の書き込みなのでexclusive lockでないといけません。で、このロックの取得のやり方が、ナイーブに現状取られているshared lockを全部待って、exclusive lockを取るスレッドの後のshared lockを取りたいスレッドを全部待たせるという仕組みなので、DDLメタデータを取るタイミングで他のトランザクションがそのテーブルのレコードをreadしてcommitもrollbackもせずにいたりすると、DDLの後ろのトランザクションはshared lockも取れないのでそのテーブルに読み取りも書き込みもできなくなってしまいます。

今回の”Table definition has changed”はどういうケースで起きるかというと、テーブル定義が変更されるより先にトランザクションが開始されているが、テーブル定義の変更後にそのトランザクションがテーブル定義変更対象のテーブルにアクセスする場合に起こるという感じです。もしテーブル定義変更前に対象のテーブルにアクセスがあった場合はメタデータロックを先にそのトランザクションが取っているのでこのエラーは出ないはずです。また、テーブル定義変更後のトランザクションは問題なくテーブルにアクセスができます。

なので、このエラーがDDL実行から少し経ってから発生しても、タイミング的にDDLを跨いでるトランザクションのみが対象なのでエラーは一部のトランザクションに絞られると言えます。ただ、そんな長時間のトランザクションがあると、トランザクションのクエリの実行順序が違えばメタデータロック待ちになったと思うので運がよかったと考えるのがいいのかなと思いました。

「テーブル定義が変更される」とは

先ほどなんとなく出した「テーブル定義が変更される」という言葉ですが、ALGORITHM=INSTANTの場合はDDLが一瞬で終わるのでDDLの実行とほぼ同じ意味と考えてよさそうですが、ALGORITHM=COPYの場合は新しいテーブル定義でデータをコピーして最後にコピー先を新しいテーブルとする感じの挙動なので長時間かかる事もあり、いつの事かが気になります。

これは最初に引用したドキュメントからも察する事ができますが、コピーが完了して最後にコピー先を新しいテーブルとするタイミングのようです。ALGORITHM=COPYの時に最後のテーブルを切り替える直前でsleepを入れたり*2して確認しました。(大きいテーブル用意して試す方が確実ですが...)

余談ですが「テーブル定義が変更される」タイミングでメタデータロックを取るのでメタデータロック待ちが発生したり、それ対策でDDLのセッションでlock_wait_timeoutを短く取ってるとここでDDLが失敗してがんばったコピーが無駄になったりする事もあるようです。*3

トランザクション分離レベルによる挙動の違い

このエラーはconsisten readができなくなる事に起因するエラーなのでtx_isolation=”READ-COMMITTED”なら問題が起きない事が期待できます。実際最初のサンプルでsession1でSET TRANSACTION ISOLATION LEVEL READ-COMMITTED;を先にしてから実行するとエラーは発生しませんでした。これはREAD-COMMITTEDなのでDDLがcommitされてたら新しいバージョンを読めば問題ないという事ですね。

最後に

以上です。REPEATABLE READで使うならロングトランザクションを避けましょう、みたいな結論にしかならないかもですが勉強になりました。