マイグレーション — 流した瞬間に、書き込みが全部待つ
列を1つ足すだけの変更が、本番で11分サイトを止める。行数ではなく、同時に走っているトランザクションの長さが効く。
この記事の進み方
What — 流した瞬間に何が起きるか
ALTER TABLE は、実行したその瞬間から終わるまでの間、テーブルの形を触ります。読み書きと完全に同時にはできないので、どこかで必ずロックが要ります。
- 1ALTER がロックを要求するテーブルの形を触る合図軽い
- 2先に走っているトランザクションを待つ集計バッチ、長いレポート、放置された接続中
- 3後続の読み書きが ALTER の後ろに並ぶここで事故になる重い
- 4アプリのコネクションが尽きる返らないリクエストが接続を握り続ける重い
- 5ALTER が終わり、一斉に流れる復旧は一瞬ではない重い
長いトランザクションが無ければ、同じ ALTER が数秒で終わる。検証環境と本番で結果が変わるのは、たいていここ。
止まる原因は ALTER そのものではなく、ALTER が待たされている間に後ろへ積み上がる列。
- DDL
- Data Definition Language。テーブルの形を変える操作。CREATE / ALTER / DROP。
- メタデータロック
- テーブルの定義を守るためのロック。クエリの実行中はずっと保持される。
- オンライン DDL
- 処理中も読み書きを通す方式。開始と終了の一瞬だけはロックが要ることが多い。
- バックフィル
- 既存の行に値を埋める処理。行数に比例して時間がかかる。
変更の種類で、危険度がまったく違う
「スキーマ変更」とひとまとめにできません。
いちばん安全。それでもロックはゼロではない
ALTER TABLE members ADD COLUMN plan varchar(20)- 既存行の書き換え
- 不要
- 所要時間
- 行数にほぼ依存しない
- 危険の源
- 開始時の短いロックだけ
- 切り戻し
- 列を消せば戻る
- –それでも長いトランザクションの後ろで待てば、同じ事故になる
- –古いコードは新しい列を知らないが、NULL 可なら書き込みは通る
- –拡張 → 移行 → 縮小(ops/ci-cd)の「拡張」にあたる
行数だけで判断しない。同じ「列の追加」でも、既定値と NOT NULL の有無で別物になる。
検証環境で3秒だった ALTER が、本番で11分かかりました。最も疑うべきものはどれですか?
Why — なぜ止まるのか
テーブルの定義は、実行中のクエリが前提にしているものです。SELECT の途中で列が消えたら、その SELECT は自分が何を読んでいるのか分からなくなる。だから DB は、クエリが走っている間そのテーブルの定義を固定します。これがメタデータロックです。
ALTER は定義を変えるので、誰も定義を握っていない瞬間を待つ必要があります。
「オンライン DDL」が解決したもの、していないもの
昔は ALTER の間ずっとテーブル全体が止まりました。いまは多くの DB が、処理の本体を読み書きと並行して進められます。これがオンライン DDL です。
ただし**「一瞬もロックしない」ではありません。** 開始時に定義を確定し、終了時に差し替えるので、その前後に短い排他ロックが要ります。
短ければ問題ない、と思えます。問題はその短いロックが取れないときです。
12:00:00 集計バッチがトランザクションを開始(12分かかる)
12:00:30 ALTER が短いロックを要求 → バッチ待ちで待機
12:00:31 注文の INSERT → ALTER の後ろで待機 ← ここから事故
12:00:32 ログインの SELECT → その後ろで待機
... 以降すべて待機
12:12:00 バッチ終了 → ALTER 実行(3秒)→ 溜まった分が一斉に流れる
ロック待ちは先着順です。 ALTER が待たされている間、そのテーブルに触るすべてのクエリが ALTER の後ろに並びます。読み取りも止まります。
つまり、止めたのは ALTER ではありません。ALTER が待たされたことで、その後ろが詰まったのです。
レプリカでもう一度走る
プライマリで終わっても、変更はレプリカに伝わってそこでも実行されます。構成によっては、レプリカがその間ずっと遅れます。
読み取りをレプリカに逃がしている場合、プライマリは無事なのに読み取りだけ古いという状態になります。「マイグレーションは終わったのに、画面に反映されない」の正体はこれであることが多い。
オンライン DDL を使えば、書き込みを止めずにスキーマを変更できます。それでも事故が起きうるのはなぜですか?
演習 — まず自分で判断する
解説を読む前に、まず自分で判断してみてください。ここで一度詰まっておくと、 次の節の判断軸が「なるほど」ではなく「そう来たか」に変わります。
この条件で、マイグレーションの計画をどう立てますか?
- 会員テーブルは800万行。注文・ログイン・プロフィール更新がすべて触る
- 追加したいのは plan という列。値は 'free' か 'paid' で、必ず入る
- 毎時0分から10分ほど、集計バッチが同じテーブルを読んでいる
- 深夜でも注文は毎時30件ある。止められる時間は無い
- 読み取りの一部はレプリカに逃がしている
- · 列を追加する操作そのものの重さと、待たされる可能性
- · NOT NULL をいつ付けるか
- · レプリカが遅れている間、画面に何が見えるか
「列を足すだけなのに、なぜサイトが止まったのか」を、DB に詳しくない同僚に説明してください
- 相手はロックという言葉を聞いたことがある程度
- 検証環境では3秒で終わったことを知っている
- 責任追及ではなく、次にどうするかを決めたい場面
読み終わりましたか?
読了にすると、これを前提とする記事がロードマップで開放されます。