MVCC による PostgreSQL の並列性
最終更新日 2022年12月28日(水)
Postgres の大きなセールスポイントの 1 つは、並列性を処理する仕組みです。ルールはシンプルです。読み取りは決して書き込みをブロックせず、その逆も同様です。Postgres では、多版型同時実行制御 (Multi Version Concurrency Control) と呼ばれるメカニズムを介してこれを実現します。この手法は Postgres に特有のものではなく、Oracle、Berkeley DB、CouchDB、その他多数のデータベースが何らかの形で MVCC を実装しています。Postgres における MVCC の実装の仕組みを理解することは、並列性の高いアプリを PostgreSQL で設計するにあたって重要です。これは実際には、困難な問題に対する洗練されたシンプルなソリューションです。
MVCC の仕組み
Postgres では、すべてのトランザクションが XID と呼ばれるトランザクション ID を取得します。これには、挿入、更新、削除のように 1 つのステートメントから成るトランザクションに加えて、ステートメントのグループを BEGIN
- COMMIT
で明示的にラップしたものが含まれます。トランザクションが開始すると、Postgres は XID をインクリメントし、現在のトランザクションにその XID を割り当てます。Postgres はシステムのすべての行のトランザクション情報も保存し、この情報を利用してトランザクションからの行の可視性が決定されます。
たとえば、行を挿入すると、Postgres によって XID が行に保存され、xmin
という名前が付けられます。コミット済みで、現在のトランザクションの XID よりも xmin
が小さいすべての行がトランザクションから可視になります。つまり、トランザクションを開始して行を挿入したとしても、そのトランザクションが COMMIT
しない限り、その行は他のトランザクションから可視になりません。そのトランザクションがコミットし、他のトランザクションが作成されると、後者は xmin < XID
の条件を満たしており、行を作成したトランザクションは完了しているので、新しい行は後者に対して可視になります。
DELETE
と UPDATE
でも同様のメカニズムが発生しますが、これらのケースに限り、Postgres は可視性を決定するために xmax
の値を各行に保存します。次の図は、行の挿入と読み取りを行う 2 つの並列トランザクションと、トランザクションの分離に MVCC がどのような役割を果たすか示しています。
以下の図で前提としているのは次の DDL です。
CREATE TABLE numbers (value int);
日常の運用の中で xmin
や xmax
の値を目にすることはありませんが、これらの値は Postgres に問い合わせればすぐに確認できます。
SELECT *, xmin, xmax FROM numbers;
現在のトランザクションの XID も取得できます。
SELECT txid_current();
簡単です。
さて、読者の皆さんはきっとこう思っているでしょう。「2 つのトランザクションが同じ行を同時に更新したらどうなるのか?」ここでトランザクション分離レベルの出番です。Postgres では基本的に、この状況への対処方法を制御するための 2 つのモデルをサポートしています。デフォルトの READ COMMITTED
は、最初のトランザクションが完了した後に行を読み取り、ステートメントを実行します。待機している間に行が変更された場合、基本的には最初からやり直します。たとえば、WHERE
句を伴う UPDATE
を実行した場合、最初のトランザクションのコミット後に WHERE
句が再実行され、WHERE
句の条件がまだ満たされていれば UPDATE
が実行されます。次に示す、行を変更する 2 つのトランザクションの例では、最初の UPDATE
が原因で、2 番目のトランザクションの WHERE
句が行を返しません。したがって、2 番目のトランザクションはどの行も一切更新しません。
この動作をさらにきめ細かく制御する必要がある場合、トランザクション分離レベルを SERIALIZABLE
に設定できます。この戦略では “変更しようとしている行が別のトランザクションによって変更されている場合は、試すこともしない” という方針を採るので、上記のシナリオは単に失敗し、Postgres の応答はエラーメッセージ ERROR: could not serialize access due to concurrent update
(同時更新が原因でアクセスをシリアル化できませんでした) になります。このエラーを処理して再試行するか、諦めた方が良いと判断してそうするかはアプリ次第です。
MVCC の欠点
MVCC とトランザクション分離が実際にどのように機能するかを理解したので、SERIALIZABLE
の分離レベルが適している問題を解決するために、別のツールを追加しました。MVCC の利点は明らかですが、いくつかの欠点もあります。
可視である行のセットはトランザクションごとに異なるため、Postgres では、古くなっている可能性があるレコードを維持する必要があります。UPDATE
が実際には新しい行を作成するのはこれが理由であり、DELETE
が実際には行を削除せず、行を削除済みとマークして XID 値を適切に設定するだけである理由も同じです。トランザクションが完了すると、将来のどのトランザクションからも可視になる可能性がない行がデータベースに残ることになります。これらはデッドロー (dead row) と呼ばれます。MVCC に由来するもう 1 つの問題として、トランザクション ID はただ大きくなり続けることしかできません。32 ビットであり、サポートできるのは 約 40 億トランザクション “のみ"です。最大値に達すると、XID は最小値に戻って (ラップアラウンドして) ゼロから再開します。突然、すべての行が将来のトランザクションにあるかのように見え、新しいトランザクションはそれらの行への可視性を得られなくなります。
デッドローとトランザクション XID ラップアラウンドの問題はどちらも VACUUM
によって解決されます。これは定期的なメンテナンスですが、幸いにも Postgres には、設定可能な頻度で実行される auto_vacuum デーモンが付属しています。デプロイが異なれば必要なバキューム頻度も異なるため、これを注視することは重要です。VACUUM
の実際の動作について詳しくは、Postgres のドキュメントを参照してください。Heroku での処理方法も参照してください。