0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

concurrent index × 同時 migrate が引き起こす本番デッドロック

0
Posted at

前提

bin/docker-entrypoint は起動時に db:prepareされる

# If running the rails server then create or migrate existing database
if [ "${1}" == "bin/rails" ] && [ "${2}" == "server" ]; then
  ./bin/rails db:prepare
fi

exec "${@}"
  • rails server 起動時に db:prepare
  • 本番では 複数コンテナが同時起動
  • 結果として 複数プロセスが同時に migrate

大規模サービスでは、前段に migrate worker を置く設計ですが、ここでは Rails new の状態です。
例えば GitLab では migrate を 明確に別フェーズに分離しています。
https://docs.gitlab.com/development/migration_style_guide/?utm_source=chatgpt.com

Rails は advisory lock を使い、同時migrateを防いでいる

Rails(ActiveRecord)は、同時に複数の migration が走らないように PostgreSQL の advisory lock(アドバイザリロック) を使って migration 実行全体を排他します。
https://apidock.com/rails/v6.0.0/ActiveRecord/Migrator/with_advisory_lock?utm_source=chatgpt.com

ActiveRecord::Migrator#with_advisory_lock でロック取得を行い、ロックが取れない場合は ConcurrentMigrationError を投げる実装になっている。
https://apidock.com/rails/v5.2.3/ActiveRecord/Migrator/with_advisory_lock?utm_source=chatgpt.com

Rails の advisory lock は、CONCURRENTLY の内部ロックを制御しない

Rails の advisory lock は 「migration 全体を直列化する」ための論理ロックであり、

CREATE INDEX CONCURRENTLY の内部フェーズ

PostgreSQL が取得する relation lock(例:ShareUpdateExclusiveLock)

「他トランザクションの終了待ち」などのDB内部待ち

までは 制御できません。

つまり advisory lock は、

✅ 「migrate プロセス同士の排他」

❌ 「DB が内部で取るロックや待ちの安全性保証」

ではない、という整理になります。
https://www.postgresql.org/docs/current/explicit-locking.html?utm_source=chatgpt.com

CONCURRENTLY が “並列実行される状況” ではエラーになり得る

CREATE INDEX CONCURRENTLY は「書き込みを止めにくい」一方で、内部的には複数フェーズを持ち、他トランザクションとの待ち合わせやロック取得が発生します。
そのため、複数プロセスが同時に DDL を走らせる状況では、デッドロックが起き得ます。

process競合によるエラー例:

Process 60356 waits for ShareLock on virtual transaction
Process 60363 waits for ShareUpdateExclusiveLock on relation
ERROR: deadlock detected

このパターンは実際に「CREATE INDEX CONCURRENTLY 同士」でも発生し得ることが報告されています。

Rails + Docker 環境で、rails server 起動時に db:prepare(≒ db:migrate)を実行していると、
本番デプロイ時に複数コンテナが同時起動 → 同時 migrate という構図が生まれます。

その状態で add_index algorithm: :concurrently を含む migration があると、
テーブルが空でも deadlock が発生することがあります。


問題の本質は「データ量」ではなく「同時実行」

今回の事故の原因は、

  • ❌ テーブルが重いから
  • ❌ レコードが多いから

ではありません。

本質はこれです。

  • ✅ 複数プロセスが
  • ✅ 同時に
  • ✅ DDL(特に index 作成)を実行すること

CREATE INDEX CONCURRENTLY は何をしているか(データ0件でも)

CREATE INDEX CONCURRENTLY は、
テーブルが空でも必ず複数フェーズで動作します。

概念的には以下の流れです。

  1. index 定義を system catalog に登録
  2. ShareUpdateExclusiveLock を取得
  3. 他トランザクションの終了を待つ
  4. テーブル全体を再スキャン(※ 0件でも発生)
  5. index を有効化

重要な点

  • データ件数に関係なく
    • ロック取得
    • トランザクション待ち
    • catalog 更新
      が発生する
  • 「空テーブル = ノーロック」ではない

なぜ「0件なのに0.001秒以上かかる」のか

レコード0件なのに index に0.001秒以上秒かかるのはおかしい?

おかしくありません。

理由:

  • CONCURRENTLY
    • 他トランザクションの終了待ちを含む
    • DDL 同士でも待ち合う
  • production では
    • 常時接続が張られている
    • idle transaction が残りやすい
  • その結果
    • データが無くても普通に秒単位で待つ

速さはデータ量ではなく、周辺トランザクションに支配される


同時 migrate が走ると何が起きるか

構成:

  • コンテナA:CREATE INDEX CONCURRENTLY
  • コンテナB:別 migration / 同じ migration

発生する状況:

  • A は B のトランザクション終了を待つ
  • B は A が取得した relation lock を待つ
  • 相互待ちが成立
  • PostgreSQL が deadlock と判定

実際に出るエラー例:

"""
Process 60356 waits for ShareLock on virtual transaction
Process 60363 waits for ShareUpdateExclusiveLock on relation
ERROR: deadlock detected
"""

これは データ量では絶対に起きません
同時 DDL 実行が原因です。


「concurrently が無ければ問題ない?」への正確な答え

結論

半分 Yes、半分 No

Yes 寄りの理由

  • CONCURRENTLY
    • フェーズが多い
    • 待ちが長い
    • DDL 同士が絡みやすい
  • deadlock が 顕在化しやすい

No 寄りの理由

  • concurrently が無くても
    • 同時に add_column
    • 同時に create_table
    • 同時に add_index
      は普通に衝突する
  • deadlock にならなくても
    • 長時間ロック
    • 起動待ち
    • タイムアウト
      という形で事故る

根本原因は concurrently ではない。
「同時に migrate が走る設計」そのもの。


Rails は advisory lock を使っている(が、万能ではない)

Rails の migration は、
PostgreSQL の advisory lock を使って
同時 migrate を防ごうとします。

参考リンク:

ただし、以下の条件では崩れることがあります。

  • PgBouncer の transaction pooling
  • 接続方式・構成差
  • migrate 以外の DDL が別経路で走る
  • 異常終了後のロック待ち

そのため、

Rails がロックしてるから大丈夫

運用前提としては危険


まとめ

  • 問題は データ量ではなく同時実行
  • CREATE INDEX CONCURRENTLY
    • データ0件でもロックと待ちが発生する
  • concurrently は事故の 引き金
  • 根本は 同時 migrate 設計

「空だから大丈夫」は開発環境の感覚。
本番は、偶然が設計の穴を正確に殴ってくる。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?