社内向けに、バックエンド関連のニュースや業務で発生したQ&A、利用しているライブラリなどの情報を定期的に書いています。
MySQLでのmigration実行時、ALTER TABLEを発行中にDuplicate Entryになるという問題が報告されました。 しかし該当のレコードを検索してみても重複はなく(そもそもPrimary Keyがあるので当たり前ですが)、再実行すると別のレコードがDuplicate Entryでエラーになるという現象が起きていました。
このようなエラーは初めて遭遇しましたが、該当のテーブルが弊社のDBの中でもかなりレコード数が多いものでした。これに着目し、何かしら小さいテーブルでは発生しないが大きなテーブルのmigration時に発生する問題なのではないかというあたりをつけて調べてみたところ、以下のブログ記事がヒットしました。
https://jfg-mysql.blogspot.com/2021/11/duplicate-entry-in-alter-table-and-in-optimize-table.html
この記事とMySqLの Online DDL Limitaions を見ると、確かに該当の記述がありました。
インプレースのオンライン DDL 操作を実行する場合、ALTER TABLE ステートメントを実行するスレッドは、他の接続スレッドから同じテーブルに対して同時に実行された DML 操作のオンラインログを適用します。 これらの DML 操作が適用されると、重複したキーエントリのエラー (ERROR 1062 (23000): 重複したエントリ) が発生する可能性があります。
ということで、原因らしきものを特定することができました。
ブログに解説がある通り、これはALTER TABLEがパフォーマンスのためにin-placeなmigrationを行っているからであるので、それをやめさせればこの問題は回避可能です。
今回はALTER TALBEに ALGORITHM=COPY
を指定することで、この挙動を回避してmigrationを無事に実行することができました。
オンラインDDL操作の詳細については、例えば以下に記述があります。
https://dev.mysql.com/doc/refman/8.0/ja/innodb-online-ddl-operations.html
上記のブログにもありますが、これはエラーメッセージとしてはかなりわかりにくい部類のものだと思います。そもそもDuplicate Entryで調べても(当たり前ですが)実際にレコードが重複した時の話しか最初はヒットしなかったので、もうちょっとわかりやすいエラーメッセージだとありがたいなと思いました。
挙動としてはmigrationのパフォーマンスなどを考えるとなるべくin-placeにしたい気持ちはわかるものの、このように処理が並列で走った時に意図せぬ挙動になるため根本的に難しい問題なのかもしれないと思っています。
知らないと対処が難しいので、今回上記の事例で学びがあってよかったです。
弊社ではaws-cdkを用いてAWS上のリソースを管理しています。ECSのスケジューリングの設定で、例えば以下のような記述をすることがあると思います。
schedule: autoScaling.Schedule.cron({
hour: "0", // JST: 9
minute: "0",
}),
この指定によって毎日9時(JST)に処理が実行されます。
ところで、ここでminuteの指定をしないとどうなるでしょうか?
cron式は基本的に指定しないものは全てANYとして解釈されます。だからhourとminuteの指定だけで毎日実行になるわけですが、ここでminuteを忘れると毎分の実行となります。 これにより9:00から9:59まで計60回タスクが実行されるという事例が発生しました。
タスクが大量に回るだけで大したことはありませんでしたが、 cron({ hour: "0" })
という記述だとなんとなく9:00に回ってしまうような気がしてしまうので、minuteのことを忘れないように気をつける必要がありそうです。
リリースされてから少し経ちますが、弊社でも採用しているフレームワークのため紹介をします。
2.0へのマイグレーションガイドが以下にあります。
https://diesel.rs/guides/migration_guide.html
RustのORMだと他のライブラリでも同様の変更を行っているものがありました。所有権を要求するものは、connectionを複数回呼べるようにするために戻り値で再度コネクションを返すような型になっていることが多く、mutable referenceだとその必要がないので多くの場合はこちらの方がわかりやすくなると思われます。
ユーザーとしてはmutをつけて回る必要があることには注意です。
2.0のリリースに伴い、diesel_migrationはリライトされたようです。これによりembed_migrationsマクロなどが影響を受けますが、Diesel CLIだけを使っている環境であれば特に目立った変更はないと思われます。
Diesel CLIではmigrationファイル(up.sqlとdown.sql)を作成→migrationを実行してschema.rsを自動で更新という流れでしたが、どうもこれが逆でも動くようになったみたいです。
diesel migration generate my_first_migration --diff-schema --database-url DATABASE_URL
のように、diff-schema optionにより、schema.rsファイルと実際のDBの差分からmigrationファイルを生成することができます。
個人的に、この変更は今までのDieselのmigrationとはかなり趣の異なる機能であるため驚いています。 とはいえDEFAULT制約のようにSQLでないと表現できないものがある以上今までのmigrationファイル方式を捨てられるかというと現状だと難しそうです。
INSERT INTO ... ON DUPLICATE KEYS ...のクエリがMySQLでサポートされました。これにより、レコードが存在しなければcreate、存在すればupdateというクエリがかけるようになります。
弊社としてはかなり嬉しい変更です。今まではupsertを実現するために、簡易な方法としてreplace_intoを使っていましたが、これだと更新時に削除→挿入という順序になるために、外部キー制約があるテーブルではエラーになってしまうことなど不便な点があったため、こちらが改善できそうです。
DB間でデータを移すツールであるEmbulkの0.11が出ました。リリース直前に記事が出ていたのでそちらを紹介します。
翻訳記事が以下にあります。
GitHubのRelease Noteには(なぜか)あまり記述がありませんが、かなり大きな変更を含んでいそうです。
ダウンロードしてきた embulk-X.Y.Z.jar
に実行権限を与えて直接実行することはできなくなるよ、という旨のメッセージが表示されるようになりました。
しばらくは今のままでも動くようですが、早めに移行をしておく必要などがありそうです。
zennのINFOにも説明がありますが、シェルスクリプト上でさまざまな環境へ対応するのが難しいということで、しょうがないのかなと思います。 やはり一方で実行可能なバイナリを吐いて配る前提となっている言語やエコシステムのありがたさみたいなものを実感します。(これはこれでもちろん良し悪しもありますが)
Embulkをツールとして使っているケースではJRubyベースのpluginも使っていることが多いと思いますが、これらはJRubyやembulk.gemのセットアップなどをユーザーが行う必要が出てくるようです。
確かにJRubyのプラグインでバージョン問題などでコケたりする事例は何度か遭遇していることや、Embulk側もJRubyを積極的にサポートしない方針のようなのでこちらの方が今後のことを考えると問題は少なくなりそうです。 マイグレーションはそれなりに気合いが必要そうですが、上記のzennの記事に詳しい手順の説明があります。