2023.01.11

Amazon Aurora MySQLでテーブル再構築を伴う操作をするとテーブルが見えなくなるっぽい

Amazon Aurora MySQLでテーブルの再構築を伴う操作を行うと,Readerインスタンスで瞬間的に対象のテーブルが見えなくなる場合があるので,オペレーションの実行タイミングに気をけましょう,というお話です。再構築を伴う操作には,一部のALTER TABLEやOPTIMIZE TABLEが含まれます。


こんにちは,S.T.です。Amazon Aurora MySQLで少し気になる現象を見かけたので紹介します。この現象を知っていれば回避できるので,クリティカルな影響があるということではないですが,将来直ってくれると嬉しいですね。

1.Amazon Aurora MySQLとは

Amazon Aurora MySQLはAWSで利用できるマネージドのRDBです。コンピューティングとストレージが分かれたアーキテクチャになっており,容易にスケールできます。Aurora MySQLという名前の通り,MySQLと互換性があります。記事執筆時点(2023/1)ではMySQL 5.7,8.0と互換性があるバージョンがリリースされています。

細かな仕様はドキュメントを見ていただくとして,今回の話題に関連しそうな特徴は以下のとおりです。
  • プライマリインスタンスに相当するWriter 1台と,複数台の読み込み専用のReaderで構成できる
  • ストレージが分散していて,Writer・Readerで共有している
  • テーブルの実体だけでなく,メタデータやバイナリログも同じストレージに載る
  • ページキャッシュが別プロセスになっている

2.気をつけないといけない現象

紹介した通り,Aurora MySQLは非常に良い製品です。ただし,オリジナルのMySQLのレプリケーション構成と同様の挙動を期待していると,思わぬ落とし穴にハマる場合があります。今回紹介する「テーブルの再構築を行う操作を行うとReader側でテーブルが見えなくなる」というのもその1つです。

具体的には,Writer側でOPTIMIZE TABLEを実行すると,OPTIMIZEが完了するまでReader側でそのTABLEをSELECTしたときに「Table テーブル名 doesn’t exist」が帰ってくる,という現象が発生します。試しに,下記のような条件で実験してみます。
  1. AWS RDS Aurora MySQL 3.02.2(MySQL 8.0.23互換)
  2. db.r6g.largeインスタンス(Writer・Reader同スペック)
  3. スキーマ名:test
  4. テーブル名:test
  5. カラム:id INT AUTO_INCREMENT PRIMERY KEY, c1 VARCHAR(255)
  6. レコード数:20万件(c1は20文字のランダム文字列)
このテーブルに,適当なEC2インスタンスからRubyスクリプト(mysql2を使用)を用いてReaderインスタンスに接続し,テーブルに継続的にアクセスします。今回は,0.5秒毎に「SELECT * FROM test.test LIMIT 1」を実行するようにしました。その状態で,Writerインスタンスで「OPTIMIZE TABLE test.test」を実行し,スクリプトの挙動を確認しつつ,別セッションから適当にテーブルの情報を表示したりテーブルにアクセスしたりして,何が起きるのかを確認してみます。

まず,Rubyスクリプトの出力です。
/home/ec2-user/.gem/ruby/gems/mysql2-0.5.4/lib/mysql2/client.rb:148:in `_query': Table 'test.test' doesn't exist (Mysql2::Error)
テーブルtest.testが”見つからない”というエラーが発生しています。次に,別セッションからSHOW CREATE TABLEを実行してみます。
MySQL [(none)]> show create table test.test;
ERROR 1146 (42S02): Table 'test.test' doesn't exist
こちらも同様にテーブルが見つからない旨のエラーが戻ってきました。これは非常にドキッとする挙動です。もし,「本番データベースでアクセスが少ない時間帯にテーブルの再構築を伴うALTER TABLEを実行する」というオペレーションを行うと,アプリケーションがテーブルがテーブルが見つからない,というエラーを出し始めることになります。このエラーだけを見ると本当にテーブルが消えてしまったかのように見えるので,オペレーション中に間違えてDROP TABLEしてしまったのではないかと冷や汗をかいたり,最新のバックアップの日付を確認して復旧オペレーションの手順を確認したり,心身の健康に良くない影響が出るのは確実です。

もちろん,本当にテーブルが消えてしまったわけではありません。原因となったDDLが完了すると何事もなかったかのようにテーブルが見えるようになり,エラーが止まってアプリケーションも正常に動くようになります。OPTIMIZE TABLE以外にも,ALTER TABLEでテーブル再構築を伴う処理(カラムの削除など)を実行した場合も同様の挙動になります。他にも,15.12.1 Online DDL Operationsに記載されている「Rebuilds Table」にチェックが入っている操作でも同じ現象が発生する可能性が高いです(すべてを試したわけではないのですが)。

ただ,DDL実行中でも問題なく実行できる操作もあります。テーブルに対する操作とその結果は以下の通りです。
  1. SHOW CREATE TABLE:table dosn’t exist
  2. DESC:正常
  3. SELECT:table dosn’t exist
  4. INSERT:table dosn’t exist
  5. UPDATE:table dosn’t exist
  6. SHOW TABLES:対象テーブルは存在する
  7. INFORMATION_SCHEMA.TABLESを確認:対象テーブルは存在する
  8. SHOW TABLES:対象テーブルは存在する
テーブルの参照・更新はどちらもアウトです。しかし,SHOW TABLESやINFORMATION_SCHEMAを見ると,テーブルは存在していることが確認できます。

このような現象が発生するため,オンラインのDBの大きなテーブルに対してテーブル再構築を伴う操作を実行する場合は,注意が必要です。小さなテーブルであっても,もしかしたら短時間発生する可能性があるので,油断はできません(未検証)。幸いなことに,Writerインスタンスからは正常にテーブルが参照できるため,このオペレーションを実行するときだけアプリケーションがWriterを見るようにする,ということが可能であれば,この方法で切り抜けることができます。

少し調べてみると,似たような現象に遭遇した方がいるようです。数年間修正されていないことから,結構根深い問題なのかもしれません。ユーザとしてこの問題を根本解決する方法はないので,ひとまずは「気をつける」でやり過ごすしかありません。ただ,それだけで終わってしまっては少し寂しいので,もう少し深掘りしてみます。気になるのは,SHOW CREATE TABLEがアウトで,DESCは正常にテーブルの情報を返してくれる,という挙動です。せっかくなので,この動作を追いかけてみましょう。

3.MySQLの動作を見てみる

先に述べたように,Auroraはストレージ部分のアーキテクチャが特有のものになっています。オリジナルのMySQLが読んでいるファイルを確認することで,SHOW CREATE TABLEがアウトでDESCがセーフな理由がわかるかもしれません。

このような調査にはstraceが便利です。straceは指定したプロセスが呼び出したシステムコールを調べることができるツールです。mysqldにstraceをアタッチし,SHOW CREATE TABLEとDESCを実行したときに実行されるシステムコールを確認することで,アクセスしているファイルに違いがあるかどうかを確認することができます。以下のコマンドでstraceをアタッチしてmysqldを起動し,その後mysql-clientからSHOW CREATE TABLEとDESCを実行してみます。
strace -s2048 -y -f -o /tmp/strace_show.out mysqld
strace -s2048 -y -f -o /tmp/strace_desc.out mysqld
呼び出されたシステムコールは/tmp/strace_*.outに記録されています。当然ながら大量のシステムコールのログが記録されているので,ここから必要な情報を探し出します。mysql-clientから送られてきた接続情報やコマンドをrecvfromシステムコールで受信するため,ユーザ名やパスワード,コマンドもログに記録されます。また,mysql-clientに結果を送り返すためにsendtoシステムコールを使用しており,それもログに記録されます。これらの情報を頼りに,SHOW CREATE TABLE/DESCが実行されてから,結果を送り返すまでに呼び出されたシステムコールで関係ありそうなものを調べると,以下のようなものが出てきました。
# SHOW CREATE TABLE
pread64(15</var/lib/mysql/mysql.ibd>, ..., 16384, 344064)
pread64(15</var/lib/mysql/mysql.ibd>, ..., 16384, 360448)
pread64(15</var/lib/mysql/mysql.ibd>, ..., 16384, 376832)
# DESC
pread64(15</var/lib/mysql/mysql.ibd>, ..., 16384, 163840)
pread64(15</var/lib/mysql/mysql.ibd>, ..., 16384, 180224)
pread64はオフセット,バイト数を指定してファイルディスクリプタから読み出しを行うシステムコールです。straceに-yオプションを付与していたおかげで,ファイルディスクリプタ15が/var/lib/mysql/mysql.ibdであることがわかります。最後の引数がオフセットです。SHOW CREATE TABLEとDESCはどちらも,mysql.ibdを読んでいますが,読む位置は異なり,SHOW CREATE TABLEは少し多くのデータを必要としていることがわかりました。

mysql.ibdはデータディクショナリテーブルを格納するためのテーブルスペースファイルです。MySQL 8.0では,テーブル定義などのメタデータはデータディクショナリテーブルで管理されています。SHOW CREATE TABLEとDESCはテーブルの中身が格納されているファイルにはアクセスせずに,メタデータのみを読んで結果を返しているようです。

この結果をみると,(Aurora MySQLがMySQL 8.0と同様のメタデータ管理を行っていると仮定すると)mysql.ibdの読み出すオフセットによっておかしな結果が返ってくるという仮説がたてられます。しかし,正しい結果が得られるINFORMATION_SCHEMA.TABLESのSELECTをstraceで調べてみると,SHOW CREATE TABLEとDESCで参照していた5箇所をすべて読んでいました。INFORMATION_SCHEMA.TABLESが正しい結果を返すことから,SHOW CREATE TABLEが読んでいる領域だけおかしくなっている,ということはなさそうです。

メタデータがデータディクショナリテーブルにまとまったのはMySQL 8.0からで,MySQL 5.7以前のバージョンではテーブルの実体である.ibdファイルの隣に.frmファイルを置いて,そこにテーブルのメタデータを記録していました。念のため,5.7でもstraceで同様の検証を行いました。得られた結果は想像通りSHOW CREATE TABLEもDESCも以下のように.frmファイルの読み込みのみを行っており,実体となる.ibdファイルをはじめとする他のファイルは参照していませんでした。
read(46</var/lib/mysql/test/test.frm>, ...)
以上から,オリジナルのMySQLでは,SHOW CREATE TABLEとDESCで読み込むファイルに違いはないことと,読んでいるファイルのオフセットの違いによって問題が起きるというわけではなさそう,ということがわかりました。Auroraはアーキテクチャからもわかるように,このあたりの実装がオリジナルのMySQLとは異なっていて,そこで何かが起きている可能性が高そうです(想像ですが)。

4.MySQLの実装を見てみる

次に,MySQLの実装面からSHOW CREATE TABLEとDESCの違いを探ってみます。DESCはSHOW COLUMNSのシノニムなので,SHOW CREATE TABLEとSHOW COLUMNを比較します。今回はmysql-server 8.0.23のコードを参照します。

SHOW CREATE TABLEとSHOW COLUMNSの実装は,sql/sql_show.ccにあります。SHOW CREATE TABLEの処理の実体は,Sql_cmd_show_create_table::execute_inner関数,SHOW COLUMNSの処理の実体は,Sql_cmd_show::execute関数にあります。

SHOW COLUMNSはコマンドとクラス名が一致しませんが,これは親クラスの関数を使うためです。sql/sql_show.hを見ていくと,Sql_cmd_show_columnsクラスSql_cmd_show_table_baseクラスを継承しており,それがSql_cmd_showクラスを継承しています。

まず,SHOW COLUMNSから見ていきます。Sql_cmd_show::execute関数を見ると,Sql_cmd_select::execute関数,つまりSELECTと同じ処理を実行していることがわかります。これは,SHOW COLUMNSがINFORMATION_SCHEMAに対してクエリを実行するという実装になっているためです。INFORMATION_SCHEMAに対するクエリを組み立てる処理は,parse_tree_nodes.ccで呼び出されているbuild_show_columns_query関数にあります。INFORMATION_SCHEMA.COLUMNSを見慣れた名前のエイリアスを付けてSELECTしている様子が見てとれます。

INFORMATION_SCHEMAはディクショナリテーブルのViewとして実装されています。そして,ディクショナリテーブルはメモリ上にキャッシュされています(14.4 Dictionary Object Cache)。Viewの定義はsql/dd/impl/system_views/以下,その元ネタとなるテーブルはsql/dd/impl/tables/以下に実装があるようです(ここは深く追っていません)。

次に,SHOW CREATE TABLEを見ていきます。Sql_cmd_show_create_table::execute_inner関数は,権限のチェックをしたあとにmysqld_show_create関数を呼び出しています。そして,その中open_tables関数を呼び出しています。さらに,これを追いかけると,open_and_process_table関数に入り,open_table関数を呼び出しています。

open_table関数は,check_if_table_exists関数を用いてテーブルの存在確認を行います。この関数の内部では,dd::table_exists関数を呼び出しており,この処理を通して最終的にデータディクショナリへの問い合わせを行います。この問い合わせは,スキーマ名・テーブル名を指定してオブジェクトを取得する操作で実施しています。もしテーブルの存在チェックが通ればmysqld_show_create関数の後続処理でCREATE TABLE文が返却されるのですが,通らない場合はTable doesn’t existのエラーが発生するのだと思います(ここは追いかけていないのでちょっと怪しい)。

このように,両者は最終的にデータディクショナリへのアクセスを行いますが,Viewを通してテーブルとしてアクセスするのか,ディクショナリにオブジェクトが存在するのか直接問い合わせるのか,という違いがあります。この実装の差異が,今回紹介した挙動の差を生んでいるのではないでしょうか。

5.まとめ

今回はAmazon Aurora MySQLでテーブル再構築を伴うオペレーションを行うと,そのテーブルが一時的にReaderから見えなくなる現象を確認しました。

先に紹介したように,Auroraはストレージを共有するアーキテクチャだったり,キャッシュが別プロセスになっていたり,オリジナルのMySQLとは根幹となる部分が異なります。アプリケーションから見ると互換性のある動作でも,(当たり前ですが)細かい挙動を見ていくと今回の例のように異なる振る舞いになることもあります。Auroraの運用にはAuroraのノウハウが必要になるので,MySQLの知見があっても油断はできなさそうです。

次世代システム研究室では,グループ全体のインテグレーションを支援してくれるアーキテクトを募集しています。アプリケーション開発者の方,次世代システム研究室にご興味を持って頂ける方がいらっしゃいましたら,ぜひ募集職種一覧からご応募をお願いします。

参考

Amazon Aurora アーキテクチャ概要
AWS RDS Aurora(MySQL)での困った問題
15.12.1 Online DDL Operations
MySQL道普請便り 第93回MySQL8.0のデータディクショナリー
14.4 Dictionary Object Cache

  • Twitter
  • Facebook
  • はてなブックマークに追加

グループ研究開発本部の最新情報をTwitterで配信中です。ぜひフォローください。

 
  • AI研究開発室
  • 大阪研究開発グループ

関連記事