アーキテクチャ●●●●○

サーガ

複数サービスにまたがる処理を、各ステップの補償処理で整合させる分散トランザクションのパターン。

概要

サーガは、複数のサービスにまたがる一連の処理を、全体としてつじつまが合った状態に保つための分散トランザクションのパターンです。「注文を受け付ける」「在庫を引き当てる」「決済する」「配送を手配する」といった、それぞれ別のサービスが受け持つステップを、途中で失敗しても矛盾が残らないように順に進めていく仕組み、と言い換えられます。

ふつうのデータベーストランザクションなら「全部成功か、全部なかったことにする」を1つのコミットで保証できます。ところが処理が複数のサービス・複数のデータベースに分散していると、その一括のロールバックが効きません。そこでサーガは、失敗したときに「それまでの成功を打ち消す逆向きの処理(補償トランザクション)」を実行することで、全体の整合を取り戻します。マイクロサービスアーキテクチャで業務処理を組み立てるときの、事実上の定番パターンです。

なぜ生まれたか

モノリスで1つのデータベースにすべてのテーブルが同居していた時代は、業務処理がどれだけ複雑でも、最後に1回コミットすれば「注文・在庫・決済」がまとめて確定し、途中で例外が起きればまとめてロールバックできました。ACIDのうちの原子性(Atomicity)が、サービスの外側からタダで手に入っていたわけです。

サービスを機能ごとに分割し、それぞれが自分のデータベースを持つマイクロサービス構成になると、この前提が崩れます。1つのビジネス処理が複数のデータベースへの書き込みに分かれ、それらを1つのトランザクションで束ねられなくなるのです。かつては複数リソースを1つのコミットに束ねる2相コミット(2PC)という手法もありましたが、全参加者がコミット確定まで資源をロックし合うため、1つでも遅い・落ちた相手がいると全体が待たされ、可用性とスケーラビリティを大きく損ないます。分散環境では現実的でないことが多いのです。

そこで発想を変え、「1つの大きなトランザクションを分割不能に保つ」のではなく、「小さなローカルトランザクションを連ねて、失敗したら補償で巻き戻す」ことで整合させる — このアプローチがサーガです。原型は1987年の論文で提示され、後にマイクロサービス時代の代表的パターンとして再発見されました。

詳細

ローカルトランザクションと補償の連鎖

サーガは、業務全体を「各サービスが自分のDBだけで完結できる小さなトランザクション(ローカルトランザクション)」の列に分解します。ステップが順に成功していけばそのまま完了です。問題はどこかで失敗したときで、サーガはそこから逆向きに、すでに成功した各ステップに対応する補償トランザクションを実行して打ち消していきます。

重要なのは、補償は「なかったことにする」ロールバックとは違い、「打ち消すための新しい操作」だという点です。すでにコミットされ、他から見えてしまった結果を消せはしないので、たとえば「決済した」を「返金する」で、「在庫を引き当てた」を「引き当てを解放する」で相殺します。次の図は、決済で失敗したときに手前の在庫引き当てを補償で戻す流れです。

配送決済在庫注文配送決済在庫注文ここから逆向きに補償を実行在庫を引き当て引き当て成功決済を実行残高不足で失敗補償 引き当てを解放解放完了注文をキャンセル状態にする

図のように、決済が失敗した時点でサーガは配送へは進まず、代わりに手前で成功していた在庫の引き当てを補償で解放し、注文自体をキャンセル状態に落とします。結果として「在庫は減っているのに決済も配送もされていない」といった中途半端な状態が残りません。

オーケストレーションとコレオグラフィ

サーガの進行を「誰が指揮するか」で、大きく2つの実装スタイルに分かれます。1つはオーケストレーション型で、中央にサーガの進行を管理する調整役(オーケストレーター)を置き、その1か所が「次は在庫、次は決済」と各サービスに指示し、失敗を検知したら補償を発火します。流れが1か所に集約されて追いやすい反面、調整役が肥大化しやすく、単一障害点にもなり得ます。

もう1つはコレオグラフィ型で、中央の指揮者を置かず、各サービスが自分の完了をメッセージキューにイベントとして流し、それを購読した次のサービスが自律的に動きます。各サービスの結合が緩く拡張しやすい一方、全体のフローがコードのどこにも書かれず、処理の連鎖が分散して見通しづらくなります。どこで詰まったかを追うには分散トレーシングのような可観測性の仕組みがほぼ必須です。

設計上の落とし穴

サーガを使ううえで避けて通れないのが、「一時的に矛盾した状態が外から見えてしまう」という性質です。全ステップが揃うまでの間、注文は確定済みだが決済はまだ、という中間状態が存在します。これは結果整合性の世界であり、強い一貫性を前提にしたUIや業務ルールはそのままでは成り立ちません。

補償が「必ず後で成功する」ことも前提になります。ネットワーク断や相手サービスのダウンで補償が届かなければ、リトライで再送する必要があり、そのとき同じ補償が二重に効かないよう冪等性を持たせる設計が不可欠です。さらに、原理的に補償できない操作(送ってしまったメール、出庫してしまった現物など)もあります。こうした操作は、実際に確定させる前に「仮予約」しておき、サーガが最後まで成功したときだけ本確定する、といった設計で不可逆な副作用を後ろに追いやるのが定石です。各ステップの状態を後から追跡・再生できるよう、進行そのものをイベント列として記録するイベントソーシングと組み合わせるのも相性のよい選択です。