そりゃそうやろ、って話なんだけど、備忘としてブログにしておくことにした。
背景
MySQLで大きなテーブルに対して特定のALTER TABLEを実行すると、テーブル全体のロックが発生し、アプリケーションの更新処理が停止してしまうことがあります。
そのため、僕の所属する部では Percona Toolkit に含まれる pt-online-schema-change(以下 pt-osc)を利用して、ダウンタイムを最小限にしたスキーマ変更を行なっています。
pt-oscは、既存テーブルに直接ALTERをかけるのではなく、以下のような手順で安全に構造変更を行います。
- 対象テーブル(例:
orders)をもとに、一時テーブル(例:_orders_new)を作成する _orders_newに対してALTER TABLEを実行する- Perlプロセスが元のテーブルのレコードを
_orders_newに少しずつコピーする - 元テーブル(
orders)にトリガーを設定し、変更を_orders_newにも反映させる(差分同期) - 同期が完了したら、以下のようにリネームを行う
RENAME TABLE orders TO _orders_old, _orders_new TO orders; _orders_oldと古いトリガーを削除して完了
この手順によって、アプリケーションからの読み書きの停止を抑えてテーブル構造を変更することができます。
今回の事象
あるテーブル(ここでは例として orders)に対して pt-osc を実行し、スキーマ変更を行いました。
pt-osc によるコピーと同期が完了し、リネームも無事成功しました。
この時点で残っているのは以下の2つです。
- 新しい
ordersテーブルに残っているトリガー - 古い
_orders_oldテーブル
次の手順として、プライマリ(マスター)側で orders に設定されていたトリガーを削除しました。
この DROP TRIGGER は即座に完了しましたが、その後、レプリケーション遅延 が発生しました。
レプリケーション側で何が起きていたか
レプリカ(スレーブ)側では、マスターの DROP TRIGGER ステートメントがSQLスレッドを通じて実行されます。
このタイミングで、レプリカの1台(仮に replica01)では以下のようなクエリがちょうど実行中でした。
SELECT
c.*
FROM
customers AS c
LEFT JOIN
orders AS o ON o.customer_id = c.id
...
このクエリは orders テーブルを参照しており、実行に30秒ほどかかっていました。
MySQLでは、テーブルを参照中のクエリが存在する場合、そのテーブルに対するDDL文(例:ALTER、DROP、RENAMEなど)は、メタデータロック(metadata lock, MDL)を取得できずに待たされます。
そのため、DROP TRIGGER がこのクエリの終了を待機していました。
さらに発生した影響
DROP TRIGGER が完了しない間、レプリケーションSQLスレッドは次のステートメントを実行できず待機状態となります。
つまり、INSERT / UPDATE / DELETE など後続のトランザクションも全てブロックされ、レプリケーション遅延が発生します。
このように、「テーブルを参照中のSELECTクエリが原因でDDLがブロックされ、結果としてレプリケーションが遅延する」という現象が今回の本質的な原因でした。
まとめ
振り返ると、pt-osc起動時の問題と同じ話なんだが、innodb buffer poolをはかしておけば、油断しても大丈夫って思って完全に油断していた。
replicaでも長時間実行されるようなクエリはない方がいいし、すぐに無くせないなら関連テーブルをpt-oscするときは気をつけないとなって思った。