LASSIC Media らしくメディア
二相コミットとは|分散トランザクションの整合
在庫を管理するデータベースと決済を処理するサービスを同時に更新する注文処理を考えます。片方だけ更新が成功し、もう片方が失敗すると、在庫は減ったのに支払いの記録が残らない、という食い違いが生じるのです。複数のデータベースやサービスにまたがる更新では、こうした「一部だけ成功する」不整合をどう防ぐかが、システム設計の判断どころになるでしょう。開発会社との要件定義や設計レビューでは、「複数のサービスをまたぐトランザクション」という表現を耳にすることがあります。具体的に何を確認すればよいのかを、この記事でつかめるようにします。
この調整の枠組みが、二相コミット(2フェーズコミット、2PC)です。なお、単一のデータベース内で完結するトランザクションの整合性については、ACID特性やトランザクション分離レベルの話が中心であり、本記事の範囲外とします。ここでは、複数の独立したノードにまたがる更新の「全員一致」をどう取るかという、分散トランザクションの調整プロトコルに絞ります。発注担当者やプロジェクトマネージャー向けに整理するのが、本稿の狙いです。
この不整合を放置すると、後になって数字が合わない原因を一件ずつ洗い出す調査対応が発生し、業務の現場に負担がかかります。仕組みを理解しておけば、開発会社との要件レビューで「どこまで整合性をそろえる必要があるか」を具体的に詰められるようになるでしょう。
目次
この記事のポイント
- 二相コミットは、複数のデータベースやサービスにまたがる更新の一致をそろえる調整手順です。
- コーディネータ役が参加者へprepareを求め、全員の応答がそろってからcommitまたはrollbackを指示します。
- コーディネータ停止時のブロッキング問題があり、サーガや結果整合性という代替も検討されています。
二相コミット(2PC)とは 複数ノード更新の一致をそろえる仕組み
二相コミット(2フェーズコミット、2PC)とは、複数のデータベースやサービスにまたがる更新を、全員がコミットするか全員が取り消すかのどちらかに揃えるための調整手順です。分散システム内の全ノードがトランザクションのコミットに合意するための分散アルゴリズムとして、Wikipedia日本語版でも紹介されています*1。
なぜこうした調整が要るのでしょうか。冒頭の注文処理の例のように、在庫の引き落としと決済の記録という別々の処理を、それぞれ独立したデータベースやサービスが担っているとします。片方だけ更新される事態を放置すると、業務データの整合性が崩れてしまいます。二相コミットは、この「一部だけ更新される」不整合を防ぐために生まれた調整の枠組みです。
あらためて範囲を確認しておきます。単一のデータベース内で完結するトランザクションのACID特性や分離レベルは、それ自体が別の論点であり、本記事では扱いません。ここで整理するのは、複数の独立した参加者をまたいだ更新について、全員の判断をそろえる仕組みそのものです。
参加者は、同じ種類のデータベース同士とは限りません。在庫管理にはPostgreSQL、決済管理には別の製品を使うといった、異なる種類のデータベースやメッセージキューが混在する環境も珍しくありません。そうした環境でも、二相コミットの手順そのものは変わらないのです。異なる製品同士を共通の手順でつなぐための標準仕様として、X/Open社が策定したXA仕様があり、MySQLなど複数のデータベース製品がこれを採用しています*3。
名称の「2フェーズ」は、この確認(prepare)と確定(commit・rollback)という2段階の手順に由来するものです。段階を分けずに全員へいきなりコミットを指示してしまうと、途中で一部の参加者だけが更新に失敗した場合、他の参加者との整合性が取れなくなってしまいます。確認の段階を挟むことで、全員が対応できると分かってから初めて確定に進む、という流れが成り立つわけです。
単一DBのトランザクションと何が違うか 参加者をまたぐ合意形成が要る
一台のデータベースで完結するトランザクションでは、コミットするかロールバックするかの判断を、そのデータベース自身が単独で下せます。処理の途中で障害が起きても、内部のログをもとに、自分だけの判断で後始末が完了するのです。
これに対して二相コミットが扱うのは、独立して動く複数の参加者(別々のデータベースやサービス)にまたがる更新です。各参加者は自分の処理しか把握できないため、全員が同じ結論にたどり着けるよう、外部の調整役が要ります。単一DBでは不要だった、この「複数の独立した判断をそろえる」手間こそが、分散トランザクション特有の難所といえるでしょう。
注文管理サービスと在庫管理データベースが別々に運用されているという状況を考えます。注文確定の処理中に在庫側だけが更新に失敗しても、注文管理サービス側はそのまま処理を終えてしまうことがあるのです。単一DBであれば、こうした片方だけの失敗はデータベース内部のロールバックで吸収されますが、参加者が分かれている場合はそうはいきません。両者の状態をそろえるための調整役が要るのは、このためです。
失敗時のやり直し(リトライ)の設計も、単一DBより複雑になりがちです。単一DBであれば、失敗した処理をそのまま再実行すればよい場面が多いでしょう。複数の参加者にまたがる更新では、どの参加者まで処理が進んだ状態で失敗したのかを踏まえて、リトライの手順を組み立てる必要があります。
検討すべき論点も、参加者の数に比例して増えていきます。どの参加者にどこまでの応答時間を許容するか、障害時に誰が復旧を主導するかといった運用面の取り決めも、単一DBでは意識せずに済んでいた事項でしょう。
下表に、両者の違いを整理しました。
| 観点 | 単一DBのトランザクション | 二相コミット(分散トランザクション) |
|---|---|---|
| 判断主体 | データベース自身が単独で判断 | コーディネータが全参加者の応答を集約して判断 |
| 参加者数 | 1つ | 2つ以上の独立したDB・サービス |
| 障害時の後始末 | 内部ログから自律的に復旧 | コーディネータ停止時は参加者が応答を待ち続けることがある |
| 関連する既存の論点 | ACID特性・トランザクション分離レベル | サーガ・結果整合性など分散システム特有の設計 |
| 失敗時のやり直し | 失敗した処理をそのまま再実行しやすい | どこまで進んで失敗したかを踏まえた手順が要る |
表からも分かるとおり、参加者数が増えるほど、単一DBでは意識する必要のなかった調整の手間が増えていきます。設計の初期段階で、更新の対象がいくつの独立したデータベース・サービスにまたがるのかを洗い出しておくと、後工程での手戻りを抑えられるでしょう。
仕組み:コーディネータのprepareと参加者の応答で成否を決める
二相コミットの調整役を担うのがコーディネータです。更新に加わる各データベースやサービスは参加者と呼ばれ、コーディネータからの問いかけに応じて振る舞います。名前のとおり、手順は2つの相(フェーズ)に分かれているのです*1。
結婚式の司会者が「異議のある方はいらっしゃいますか」と参列者全員に問いかける場面を考えてみましょう。誰からも異議が出なければ式を先へ進める、という進行に例えると分かりやすいはずです。一人でも異議を唱えれば、その場で先へ進めることはできなくなります。二相コミットのprepareと応答も、この「全員に確認してから先へ進む」という考え方に近いものといえるでしょう。
第1相はprepare(コミット要求)です。コーディネータは、全参加者に対して「この更新をコミットできるか」を問い合わせます。各参加者は自分の処理結果を永続的な場所に記録したうえで、コミット可能かどうかを応答するのです。
prepareに応答した参加者は、その時点で更新に使った資源(行ロックなど)を保持したまま、コーディネータからの最終指示を待ちます。応答した後は、自分の判断だけで更新を確定することも取り消すこともできない状態です。この「応答してから指示を待つ」という制約が、後述するブロッキング問題の土台になっています。
第2相は、応答の結果に応じた確定です。全参加者からコミット可能という応答がそろえば、コーディネータは全員にコミットを指示します。1つでもコミットできないという応答があれば、コーディネータは全員にロールバックを指示し、更新をまとめて取り消すのです。下の図に、この2つの相の流れをまとめました。
PostgreSQLのPREPARE TRANSACTIONコマンドは、この第1相の役割を担うSQL文です。実行するとトランザクションは現在のセッションから切り離され、状態がディスクに保存されます*2。MySQLのXAトランザクションでも、XA PREPAREで各ブランチを準備状態にします。全ブランチの結果がそろってからコミットまたはロールバックする、同じ二相の流れが採られているのです*3。
コーディネータ自身も、全参加者へ指示を出す前に、その決定内容を自らのログへ記録します。コーディネータが指示の途中で停止しても、復旧後にログを読み返せば、コミットとロールバックのどちらを指示しようとしていたかを再現できるのです。参加者から見ると、prepareに「コミット可能」と応答した後は、指示が届くまで身動きが取れない時間が生じます。その間、更新に関わる資源を確保し続けなければならない点が特徴といえるでしょう。この待ち時間の長さは、コーディネータの復旧にかかる時間に左右されます。
弱点:コーディネータ停止で参加者が待ち続けるブロッキング問題
二相コミットには、見過ごせない弱点もあります。よく知られているのが、ブロッキング問題です。
第2相の途中、つまりコーディネータがコミットまたはロールバックの指示を出す前後で、コーディネータ自体が停止してしまうケースを考えます。すでにprepare応答を返してコミット可能な状態になっている参加者は、最終指示が届くまで動けません。その更新を確定させることも、取り消すこともできないのです。コーディネータが復旧するまで、参加者は宙ぶらりんの状態で待ち続けることになります。
もう一つの弱点は、レイテンシの増加です。参加者全員への問い合わせと応答を待ってから確定する分、単一DBのトランザクションに比べて処理全体にかかる時間が伸びます。参加者の数が増えるほど、あるいはネットワークの距離が離れるほど、この待ち時間は積み上がっていくでしょう。参加者のどこか一台でも応答が遅れれば、更新全体がその参加者の速度に引きずられる点も見落とせません。とくに参加者が離れた拠点や海外のデータセンターにまたがる場合、ネットワークの往復にかかる時間そのものが無視できない負荷になります。
この弱点を緩和する工夫として、参加者側にタイムアウトを設け、一定時間コーディネータからの応答がない場合に運用担当者へ通知する仕組みが採られることもあるでしょう。ただし、参加者同士がそれぞれ別々の判断でタイムアウト時の確定・取消を決めてしまうと、参加者間で結論が割れる恐れがあります。自動での判断だけに委ねるのは避けるべきでしょう。コーディネータの単一障害点を減らす拡張の仕組みも研究されていますが、構成が複雑になる分、実務での採用は限られています。多くの現場では、次に見るサーガのような別の設計で弱点を回避する道を選んでいるのが実情でしょう。
代替の考え方:サーガと結果整合性で緩く合わせる選択肢
ブロッキング問題やレイテンシ増を避けたい場面では、二相コミットとは違う整合性の取り方が選ばれることもあります。とくにマイクロサービスの構成では、サーガ(補償トランザクション)や結果整合性という考え方が検討の対象になるでしょう。
サーガは、一連の更新を小さな処理の連なりとして進め、途中で失敗した場合は、それまでに終えた処理を打ち消す「補償処理」を後から実行するという考え方です。全参加者を一斉にロックして待たせる二相コミットとは異なり、各処理はそれぞれ独立して確定していきます。
結果整合性は、更新の直後は各参加者のデータが一時的にずれていてもよいとし、時間の経過とともに最終的な状態がそろえばよい、とする考え方です。即座の一致を求めない分、ブロッキングによる待ちが生じにくくなります。ただし、サーガの補償処理の設計や、結果整合性を前提にした画面・通知の作り方には、二相コミットとは異なる固有の難しさがあります。その詳細は、別のテーマとして扱うべき領域です。
サーガを採用する場合は、各処理をべき等(同じ処理を重ねて実行しても結果が変わらない性質)に設計しておく必要があります。補償処理を何度実行しても結果が変わらないようにするためです。べき等性を欠いた補償処理は、再試行のたびに二重取消のような新たな不整合を生みかねません。
二相コミットとサーガは、対立する二択というより、扱うデータの重要度に応じて使い分けるものと捉えるとよいでしょう。決済のように即座の一致がどうしても要る処理には、二相コミットを充てます。通知やログ更新のように多少の遅れが許容できる処理にはサーガを、というように処理単位で組み合わせる設計も見られます。どちらを選ぶ場合も、コーディネータの実装・監視か、補償処理の設計かという、何らかの構築コストは避けられないと考えておくとよいでしょう。判断に迷う場合は、更新ごとに求められる一致の速さを整理するところから始めると進めやすくなります。
発注・レビューで確認すべき点 要否の見極めと障害時の挙動確認
二相コミットは有効な調整の枠組みですが、導入すれば万事解決というわけではありません。発注や設計レビューの段階で、次の点を確認しておくとよいでしょう。とくに、複数の部署やベンダーが関わる更新では、判断基準をあらかじめすり合わせておくことが、後工程の手戻りを防ぐ助けになります。
本当に分散トランザクションが要るかを見極める
複数のデータベースやサービスをまたぐ更新に見えても、実は片方の処理を非同期のメッセージキューに任せられる、あるいは結果整合性で足りる場面は少なくありません。まずは、即座の一致がどうしても要る更新なのか、後から取り消し・再試行できる更新なのかを切り分けることが出発点になります。この切り分けは、開発が進んでからよりも要件定義の段階で行うほうが、後戻りの手間を小さく抑えられるでしょう。
単一サービスに寄せられないかを検討する
更新の対象を見直し、境界をまたがず1つのサービス・1つのデータベースの中で完結させられないか、設計段階で検討する価値があります。参加者の数を減らせれば、それだけ調整の複雑さとブロッキングのリスクを抑えられるからです。既存の業務境界を疑わずに複数サービスへ分けてしまうと、後から二相コミットのような重い調整が要る設計に陥りやすくなります。サービスの分割案が固まる前に、更新の単位をどこで区切るかをレビューしておくとよいでしょう。分割後に境界を引き直すのは、分割前に見直すよりもずっと骨が折れます。関係するチームが複数にまたがる場合は、なおさらでしょう。
障害時の挙動をテストで確認する
コーディネータや参加者の一部が止まった場合に、システムがどう振る舞うのかを、実装段階のテストで確かめておくことが欠かせません。とくに、コーディネータが第2相の途中で停止した状況を意図的に再現しておくとよいでしょう。参加者が待ち続ける時間や、復旧後の後始末の手順を確認しておくと、稼働後の想定外を減らせるはずです。テスト環境では起きにくい遅延やタイムアウトも、想定して検証項目に含めておく必要があります。
監視・アラートの仕組みまで含めて依頼する
二相コミットを採用する場合は、コーディネータや参加者が想定時間内に応答したかどうかを継続的に監視する仕組みが要ります。異常があれば運用担当者へ通知する仕組みも、設計に含めておく必要があるでしょう。障害発生時に人手での復旧が必要になる場面を見込み、復旧手順をあらかじめ文書化しておくことも欠かせません。運用開始後にこうした手順を後付けで整えるのは、想像以上に手間のかかる作業になりがちです。設計段階から運用担当者を交えて検討しておくと、後の負担を軽くできるでしょう。
まとめ
ここまで整理してきた内容を、あらためて要点として振り返ります。
- 二相コミットは、複数のデータベースやサービスにまたがる更新を、全員一致でコミットまたはロールバックする調整手順である。
- 単一DBのACIDや分離レベルとは異なり、独立した複数の参加者の判断をそろえる点に特有の難しさがある。
- コーディネータがprepareで応答を集め、全員OKならcommit、1件でもNGならrollbackを指示する2段階の流れをとる。
- コーディネータ停止時のブロッキング問題やレイテンシ増という弱点があり、サーガや結果整合性という代替も検討される。
- 発注・レビューでは、分散トランザクションの要否や単一サービスへの集約可否、障害時の挙動を確認しておきたい。
よくある質問
二相コミットと通常のデータベーストランザクションは、どこが違いますか。
いちばんの違いは、判断を下す主体の数です。通常のデータベーストランザクションは、1つのデータベースが単独でコミットとロールバックを判断します。二相コミットは、複数の独立した参加者の応答をコーディネータが集約してから判断するのです。単一DB内のACID特性や分離レベルとは別の、複数ノードをまたぐ合意形成の仕組みだと捉えるとよいでしょう。
ブロッキング問題とは、具体的にどのような状況ですか。
第2相の途中でコーディネータが停止し、コミットかロールバックかの最終指示が参加者へ届かなくなる状況を指します。すでにprepareへ応答済みの参加者は、コミット可能な状態のまま指示を待つほかなく、コーディネータが復旧するまで更新を確定も取消もできません。参加者数が多いシステムほど、この待ちが業務全体に響きやすくなるでしょう。設計段階でこの弱点を認識しておくことが、後の対策を検討する出発点になります。
マイクロサービスの構成でも、二相コミットを使うべきですか。
即座の一致がどうしても要る更新であれば選択肢になりますが、マイクロサービスではサーガ(補償トランザクション)や結果整合性を採用する例も見られるでしょう。二相コミットはブロッキング問題やレイテンシ増という弱点を伴うため、サービスの独立性を保ちたい構成では代替案とあわせて比較検討することをおすすめします。処理ごとに求められる一致の速さが異なる点を、判断の軸にするとよいでしょう。
二相コミットを使わずに、複数サービス間の整合性をとる手段はありますか。
サーガのように、一連の処理を小さな単位に分けて進めるという考え方が代表的です。失敗時は補償処理で打ち消し、更新直後の一時的なずれを許容して時間の経過とともにそろえる結果整合性も、選択肢の一つでしょう。どちらも二相コミットとは異なる設計上の難しさを伴うため、業務要件に照らして選ぶ必要があります。
PostgreSQLやMySQLで二相コミットを使うには、開発側でどんな対応が必要ですか。
PostgreSQLの場合、アプリケーション側でPREPARE TRANSACTIONを呼び出す処理を組み込みます*2。加えて、COMMIT PREPAREDとROLLBACK PREPAREDを状況に応じて呼び分ける必要があるのです。MySQLの場合は、XA START・XA END・XA PREPARE・XA COMMITといったXA構文を使います*3。これらを、トランザクションマネージャー役のアプリケーションから発行する構成になります。どちらも、外部のトランザクション管理の仕組みと組み合わせて使うことを前提にした機能です。自前で実装する前に、利用しているORMやミドルウェアがXAに対応しているかを確認しておくと、開発の手戻りを防ぎやすくなります。対応状況は製品によって差があるため、選定段階での確認が欠かせません。
著者:テレリモ総研編集部 鈴木 亮佑
LASSICでは、国内ニアショア開発体制を活かし、分散システムのトランザクション設計から実装、公開済みシステムの見直しまでを一貫して支援する体制です。要件定義の段階から実装、テスト、リリース後の運用・保守まで、工程を分けずに任せられる点も強みでしょう。分散環境でのデータ整合性の設計でお困りの際も、ご相談いただけます。
ご不明な点はお問い合わせフォームからもご連絡いただけます。