2PC(二相コミット)完全ガイド:仕組み・障害時の挙動・実装と代替手法

概要:2PC(二相コミット)とは何か

二相コミット(Two-Phase Commit、以降2PC)は、分散トランザクションにおいて参加ノード間で原子性(all-or-nothing)を保証するための古典的な分散合意プロトコルです。1つのトランザクションが複数のデータベースやリソースマネージャ(例:DB、メッセージキュー、ファイルストレージ)にまたがる場合に、すべての参加者が同じ最終決定(コミットまたはアボート)を行うことを目的とします。

2PCは名前のとおり「準備(prepare/vote)フェーズ」と「コミット(commit/abort)フェーズ」の2段階で構成されます。シンプルで理解しやすい一方、ネットワークやノードの障害時にブロッキング(参加者が不確定状態で待ち続ける)するという欠点があります。

プロトコルの基本フロー(標準2PC)

  • 役割:コーディネータ(coordinator)と複数の参加者(participant/resource manager)。コーディネータはトランザクションのオーケストレーションを行う。

  • Phase 1 — Prepare(投票):

    • コーディネータが「Prepare(準備要求)」メッセージを全参加者に送る。
    • 各参加者はローカルでトランザクションを実行し、コミット可能かを判断する。コミット可能なら「PREPARED」(またはvote-commit)を返す前に、必ずその状態を安定記憶(stable storage、ログ)に書き込んで永続化する。アボートが必要なら「vote-abort」を返す。
  • Phase 2 — Commit/Abort(決定伝達):

    • コーディネータは全員がvote-commitを返した場合に限り、決定を「COMMIT」としてログに永続化し、全員に「COMMIT」メッセージを送る。1つでもvote-abortがあれば、ログに「ABORT」を書き、全員に「ABORT」を送る。
    • 参加者は決定メッセージを受け取ったら、それをログに書き込み、ローカルで確定(コミットまたはロールバック)を行い、通常はACKを返信する。

安全性と永続化の重要性

2PCの安全性(原子性)を保証するためには、各ノードが重要な状態遷移を行う際に“強制書き込み(forced write)”でログに永続化することが必須です。具体的には参加者は「PREPARED」前に永続ログへ書き、コーディネータは最終決定(COMMIT/ABORT)を送る前にその決定を永続化します。これにより障害後の再起動時にログから状態を復元し、正しい回復手続きを取れます。

障害シナリオと回復挙動

  • 参加者が投票前にクラッシュ:参加者がPrepareメッセージを受け取る前にクラッシュすれば、タイムアウトでコーディネータはアボートを決定する(通常は参加者からの応答がない=アボート)。

  • 参加者がPREPARED状態でクラッシュ:参加者はPREPAREDをログに書いて応答を返した後にクラッシュすると、回復時にログを見て自分がPREPARED状態であったことを認識する。だが最終決定(COMMIT/ABORT)を受け取っていないため不確定(uncertain)な状態にある。参加者はコーディネータへ問い合わせるか、回復用のプロトコル(タイムアウト後の問い合わせや再試行)を使って決定を取得する必要がある。コーディネータが不明な場合、参加者は待機(blocking)することが多い。

  • コーディネータが決定前にクラッシュ:コーディネータが参加者からの票を集めたが、決定を書き込む前にクラッシュした場合、参加者は誰も最終決定を知らないため多くがPREPAREDで停止する。コーディネータが復旧するまで運用は停止・ブロックする可能性がある。回復時にコーディネータはログを元に再送を行い、決定を確定させる。

  • コーディネータが決定を送信した後にクラッシュ:コーディネータがCOMMITを送ったがクラッシュしてACKを待てない場合、参加者はCOMMITを受け取っているためローカルでコミットを行う。コーディネータは後でACKを受け取れなくても参加者側の状態に矛盾は生じない(安全性は保たれる)。

  • ネットワーク分断・パーティション:ネットワークの分断によりメッセージが遅延・喪失すると、タイムアウトによりアボートするか待つかの判断が必要になる。2PC自体はパーティション耐性が強いわけではなく、分断中に多数の参加者がPREPAREDでブロックするリスクがある。

ブロッキング問題と制限

2PCの主要な欠点はブロッキング性(blocking)です。特にコーディネータがクラッシュして復旧不能(または長時間不在)になると、PREPARED状態の参加者はどのように振る舞って良いか分からず、ロックしたリソースを解放できないまま待ち続けることがあります。これによりシステムの可用性が低下します。

また、2PCはフォールトトレラントではあるものの、フォールトの種類により扱いが難しいケースがある(例:協調者のハード障害、永続ログの破損、ヒューリスティックな手動介入が必要なケース)。

最適化・バリエーション

  • Presumed Abort / Presumed Commit:ログ記録を減らすための最適化。たとえば「presumed abort」では決定が明示的にコミットされた場合を除きアボートを前提とすることで、通常のアボートケースのログ書き込みを減らす。

  • 1PC(Single-Phase Commit)最適化:参加者が1つしかないか、コーディネータがその参加者でもある場合に、2フェーズを1フェーズに短縮することが可能(ただし分散性がないケースに限る)。

  • 3PC(Three-Phase Commit):2PCのブロッキング問題を改善するために提案されたプロトコルで、非ブロッキング性を達成できるが、実装が複雑でネットワーク同期の性質に強く依存する。

  • 代替:分散合意アルゴリズム(Paxos, Raftなど):整合性と可用性のトレードオフを考慮した上で、分散ログによる状態機械レプリケーションを使うアプローチは、2PCでのブロッキングや複雑な回復手順を回避する設計が可能。分散トランザクションを完全に置き換えることは難しいが、グローバルな合意にPaxos/Raftを使い、各ノードのコミットを単一プロトコルに統合するパターンがある。

実装上の注意点とベストプラクティス

  • 永続ログの堅牢性:PREPAREDや最終決定を必ずディスク等の永続化層にフラッシュする。ログの欠落は整合性破壊に直結する。

  • タイムアウトと再試行戦略:適切なタイムアウト値を設計し、ネットワーク遅延と負荷を考慮して再試行・問い合わせを行う。タイムアウトが短すぎると誤検出で不要なアボートが発生する。

  • モニタリングと運用手順:PREPARED状態の長時間継続を検出する監視を用意し、コーディネータ障害時の手動介入や自動復旧の手順を定義する。XAなどの分散トランザクションではヒューリスティックな結果を避けるための運用が重要。

  • 代替設計の検討:極力分散トランザクションを避け、データをサービス単位で分割(データローカリティ)、補償トランザクション(SAGAパターン)やイベント駆動の整合性設計を検討することが、可用性とスケーラビリティを改善する現実的な対処法となる場合が多い。

実世界での利用例・規格

  • X/Open XA:分散トランザクション処理のための標準インターフェース。トランザクションマネージャ(TM)とリソースマネージャ(RM)間の2PC相互運用性を規定している。

  • データベース実装:PostgreSQLの"PREPARE TRANSACTION" / "COMMIT PREPARED"、MySQLのXAトランザクションなど、多くのRDBMSが2PCをサポートしている。これらは外部のトランザクションコーディネータと連携してグローバルトランザクションを実現する。

  • 分散ミドルウェア:JavaのJTA(Java Transaction API)や各種トランザクションモネージャ(例:Atomikos、Bitronix)などが2PCを実装し、複数のリソースにまたがるトランザクションを管理する。

いつ2PCを採用すべきか

強い原子性・同時性を要求するユースケース(金融決済、在庫の厳密な引当てなど)では2PCは有効です。ただし、その運用コストや可用性への影響を事前に評価する必要があります。多くのウェブスケールアプリケーションでは可用性とスケーラビリティを優先し、SAGAやイベントソーシング、補償トランザクションなどの代替設計を用いることが増えています。

まとめ

2PCは分散トランザクションにおける標準的かつ直感的なプロトコルで、正しく実装すれば原子性を保証します。しかし、コーディネータ単一障害点によるブロッキングや導入・運用の複雑さが欠点です。実装では永続化、ログ、タイムアウト、リカバリ手順を厳密に設計することが不可欠であり、要件に応じて3PCや分散合意(Paxos/Raft)、あるいは完全に異なる整合性モデル(SAGAなど)を検討するべきです。

参考文献