Saga パターンを理解する
1. はじめに
現代の Web アプリケーションやモバイルアプリケーションは、複数のマイクロサービスが連携して動作する分散システムとして構築されることが一般的になっています。このような分散環境において、最も重要な課題の一つがデータ整合性の維持です。単一のデータベースであれば、ACID トランザクションによって簡単に保証できるデータの一貫性が、複数のサービスにまたがる処理では格段に複雑になります。
従来の分散トランザクション手法である 2 フェーズコミット(2PC)は、パフォーマンスの低下や可用性の問題により、現代の高スケールなシステムには適していません。このような背景から注目されているのが、Saga パターンです。Saga パターンは、分散システムにおいてデータ整合性を保ちながら、システムの可用性とスケーラビリティを両立させる設計パターンです。
本記事では、Saga パターンの基本概念から実装方式、具体的な設計手法まで体系的に解説します。特に、分散システムの設計に携わるエンジニアや、マイクロサービスアーキテクチャの導入を検討している方を対象としており、分散システムとマイクロサービスの基本概念についてある程度の理解があることを前提としています。
解説では、オンライン注文システムを具体例として使用します。
このシステムは、下記の 5 つで構成されており、実際のビジネスアプリケーションでよく見られる複雑な処理フローを含んでいます。
- 注文管理サービス
- 在庫管理サービス
- 決済サービス
- 配送サービス
- メール送信サービス
この具体例を通じて、Saga パターンがどのように実世界の問題を解決するかをみていきましょう。
次章では、まず分散システムにおけるトランザクション管理の課題について詳しく見ていきます。
2. 分散システムにおけるトランザクションの課題
ACID トランザクションと単一データベースの世界
データベースにおけるトランザクションは、複数の操作を論理的に一つの単位としてまとめる仕組みです。ACID という 4 つの特性によって、データの整合性と信頼性が保証されています。
Atomicity(原子性) は、トランザクション内のすべての操作が成功するか、すべて失敗するかのいずれかであることを保証します。Consistency(一貫性) は、トランザクション実行前後でデータの整合性が維持されることを意味します。Isolation(分離性) は、複数のトランザクションが同時実行されても、互いに影響を与えないことを保証します。Durability(耐久性) は、コミットされたトランザクションの結果が永続的に保存されることを表します。
単一データベースを使用するモノリシックなアプリケーションでは、これらの特性を活用して簡単にデータ整合性を保つことができました。以下は従来の注文処理の流れです。
従来の注文処理では、注文作成、在庫更新、決済処理がすべて同一データベース内で実行されるため、何らかのエラーが発生した場合は簡単にロールバックできました。
マイクロサービス環境での複雑性
マイクロサービスアーキテクチャでは、各サービスが独自のデータベースを持つのが一般的です。この設計により、サービス間の結合度を下げ、独立したデプロイとスケーリングが可能になります。しかし、単一のビジネス処理で複数のサービスにまたがるデータ更新が必要になった場合、従来の ACID トランザクションでは対応できません。
オンライン注文システムの例で考えてみましょう。ユーザーが商品を注文する際、注文管理サービス、在庫管理サービス、決済サービス、配送サービス、メール送信サービスの 5 つのサービスでデータ更新が必要です。
この処理フローでは、各サービスが異なるデータベースを持っているため、単一の ACID トランザクションで全体を制御することができません。例えば、在庫予約と決済処理が成功しても、配送手配でエラーが発生した場合、すでに実行された処理をどのように取り消すかが問題となります。
2 フェーズコミット(2PC)の仕組み
従来、このような分散環境でのトランザクション問題を解決するために、2 フェーズコミット(2PC)という手法が使われてきました。2PC は、トランザクションコーディネーターが各参加サービスと協調してトランザクションを管理する仕組みです。
第 1 フェーズでは、コーディネーターが各参加サービスに対してトランザクションの準備ができているかを確認します。すべてのサービスが準備完了を応答した場合のみ、第 2 フェーズでコミット指示を送信します。いずれかのサービスが準備完了を応答しなかった場合は、全体をアボートします。
2PC の深刻な問題点
2PC は理論的には分散トランザクション問題を解決しますが、実際の運用では多くの問題があります。
最も深刻な問題は 可用性の低下 です。2PC では、すべての参加サービスが利用可能でなければトランザクションが完了しません。参加サービスが増えるほど、システム全体の可用性は低下します。例えば、各サービスの可用性が 99.5%の場合、3 つのサービスが参加する 2PC の全体可用性は約 98.5%まで低下します。
全サービスが利用可能である確率は
99.5% × 99.5% × 99.5%
= 0.995 × 0.995 × 0.995
= 98.5%
パフォーマンスの問題 も重要です。2PC では、第 1 フェーズでリソースがロックされ、第 2 フェーズの完了まで他のトランザクションがブロックされます。ネットワーク遅延が大きい分散環境では、このロック時間が長期間になり、システム全体のスループットが大幅に低下します。
さらに、単一障害点 の問題があります。コーディネーターが障害を起こすと、準備完了状態のサービスがリソースをロックしたまま待機状態になり、システム全体が停止する可能性があります。
現代システムでの不適合性
現代のクラウドネイティブなアプリケーションでは、2PC が適さない理由がさらに明確になっています。
多くの NoSQL データベース(MongoDB、Cassandra、DynamoDB など)や メッセージブローカー(Apache Kafka、Amazon SQS など)は 2PC をサポートしていません。これらの技術は、高可用性とスケーラビリティを重視した設計になっており、分散トランザクションよりも結果整合性を採用しています。
また、マイクロサービスの設計原則 とも相反します。各サービスが独立してデプロイ、スケール、障害対応できることがマイクロサービスの利点ですが、2PC では参加サービス間で強い結合が生まれ、この利点が失われます。
CAP 定理 の観点からも、現代のシステムは一貫性よりも可用性と分断耐性を重視する傾向があります。2PC は強い一貫性を提供しますが、可用性を犠牲にするため、多くの場合で適切な選択とは言えません。
これらの課題を解決する新しいアプローチとして、Saga パターンが注目されています。次章では、Saga パターンの基本概念について詳しく解説します。
3. Saga パターンの基本概念
Saga パターンの定義と起源
Saga パターンは、1987 年に Hector Garcia-Molina と Kenneth Salem によって発表された論文「Sagas」で初めて提唱された概念です。当初は単一データベース内での長時間実行トランザクション(Long-Lived Transaction)の問題を解決するために考案されましたが、現在では分散システムにおけるデータ整合性の維持手法として広く採用されています。
Saga パターンは、複数のサービスにまたがるビジネス処理を、一連のローカルトランザクションの連鎖として実装する設計パターン です。各ローカルトランザクションは単一のサービス内で実行され、従来の ACID トランザクションの特性を維持します。サービス間の調整は非同期メッセージングを通じて行われ、全体として一つのビジネス処理を構成します。
この手法により、分散システムにおいても 2 フェーズコミットの問題を回避しながら、データの整合性を保つことが可能になります。Saga パターンの核となるのは、各処理ステップに対応する 補償トランザクション(Compensating Transaction) の概念です。
補償トランザクションの基本概念
Saga パターンにおいて、補償トランザクション(Compensating Transaction) は失敗時のデータ整合性を保つための仕組みです。従来の ACID トランザクションでは、エラー発生時に自動的にロールバックが実行されますが、Saga では各ローカルトランザクションが既にコミットされているため、この自動ロールバック機能を利用できません。
補償トランザクションは、既に実行・コミットされたトランザクションの効果を論理的に取り消すための操作 です。重要な点は、物理的な状態の巻き戻しではなく、ビジネス的に同等の結果を実現することです。例えば、在庫を 1 個減らしたトランザクションの補償は、在庫を 1 個増やすトランザクションになります。
Saga が途中で失敗した場合、それまでに実行された全てのトランザクションに対応する補償トランザクションが、逆順で実行 されます。この仕組みにより、システム全体を論理的に初期状態に戻すことができます。
ACID から ACD への変化と分離性の欠如
従来の ACID トランザクションと比較して、Saga パターンは ACD 特性 のみを提供します。この変化の最も重要な点は、分離性(Isolation)の欠如 です。
Atomicity(原子性) は、Saga レベルで維持されます。Saga 全体が成功するか、補償トランザクションによって初期状態に戻されるかのいずれかです。ただし、従来のトランザクションと異なり、中間状態が他の Saga から見える可能性があります。
Consistency(一貫性) は、各サービス内でのローカルトランザクションによって保証されます。サービス間の参照整合性は、アプリケーションレベルで管理する必要があります。
Durability(耐久性) は、各ローカルトランザクションがコミットされた時点で保証されます。
分離性の欠如 が最も重要な変更点です。Saga の実行中は、中間状態が他の Saga やトランザクションから参照できる状態になります。例えば、在庫予約は完了しているが決済がまだ実行されていない状態で、他のユーザーがその在庫情報を参照できる可能性があります。
分離性の欠如と主要な異常
Saga パターンの分離性の欠如により、並行して実行される複数の Saga が互いに干渉し、データの不整合や予期しない動作を引き起こす可能性があります。主な異常として、更新消失(Lost Update)、ダーティリード(Dirty Read)、反復不能読み取り(Fuzzy Read) があります。
更新消失(Lost Update)
更新消失(Lost Update)は、一つの Saga による更新が別の Saga によって上書きされる現象です。
例えば、注文作成 Saga と注文キャンセル Saga が並行実行された場合、注文キャンセル Saga による更新が、注文作成 Saga の最終ステップによって上書きされ、キャンセルされるべき注文が確定状態になってしまう可能性があります。
ダーティリード(Dirty Read)
ダーティリード(Dirty Read)は、一つの Saga が別の Saga の未完了な中間状態を読み取る現象です。
例えば、顧客の信用限度額を管理するシステムにおいて、注文キャンセル Saga が途中で失敗し補償処理が実行されるにも関わらず、新規注文 Saga が一時的に増加した信用限度額を参照して処理を続行してしまう場合があります。
反復不能読み取り(Fuzzy Read)
反復不能読み取り(Fuzzy Read)は、同一 Saga 内で同じデータを複数回読み取った際に、他の Saga による更新により異なる値が返される現象です。
例えば、在庫補充 Saga が処理開始時に在庫数を 10 個と読み取り、補充量の計算中に注文処理 Saga が並行実行されて在庫を 5 個に減算したとします。補充 Saga が補充前の確認のため在庫数を再読み取りすると、最初の読み取り時とは異なる 5 個という値が返され、想定していた補充計算が不正確になってしまいます。
オンライン注文システムでの基本実装例
オンライン注文システムでの Saga 実装を具体的に見てみましょう。
以下は、注文作成から配送完了までのワークフローです。
この例では、6 つのステップから構成される Saga が実装されています。
- 注文管理サービスが注文を保留状態で作成します。
- 在庫管理サービスが在庫数を減算し、予約を完了します。
- 決済サービスが決済を実行します。
- 配送サービスが配送計画を作成します。
- メール送信サービスが確認メールを送信します。
- 注文管理サービスが注文状態を確定に変更します。
各ステップで失敗が発生した場合、それまでに実行されたステップの補償トランザクションが逆順で実行されます。例えば、ステップ 4 の配送手配で失敗が発生した場合、決済取消、在庫予約解除、注文キャンセルの順で補償処理が実行され、システムを初期状態に戻します。
従来のトランザクションとの根本的な違い
Saga パターンと従来の ACID トランザクションには、実装と運用において重要な違いがあります。
状態の可視性 において、従来のトランザクションでは中間状態が外部から見えませんが、Saga では各ステップの実行後に状態が確定し、他のプロセスから参照可能になります。
失敗時の対応 では、従来のトランザクションは自動的にロールバックされますが、Saga では明示的に補償トランザクションを設計・実装する必要があります。
パフォーマンス特性 として、従来のトランザクションはリソースロックによる待機時間が発生しますが、Saga は非同期実行により高いスループットを実現できます。
複雑性 の面では、従来のトランザクションは実装が簡単ですが、Saga は補償ロジックの設計やメッセージングの管理により実装複雑度が増加します。
これらの基本概念を理解した上で、次章では具体的な実装方式について詳しく解説します。
4. Saga パターンの実装方式
Saga パターンには、大きく分けて 2 つの実装方式があります。
Choreography(コレオグラフィ)方式 と Orchestration(オーケストレーション)方式 です。それぞれ異なるアプローチでサービス間の協調を実現し、特有の利点と課題を持っています。
Choreography 方式の仕組みとイベント駆動の実装
Choreography 方式は、分散型の意思決定とシーケンス制御 を特徴とする実装方式です。
中央制御コンポーネントを持たず、各サービスが独立してイベントに反応し、次のアクションを決定します。この方式では、サービス間の協調はイベントの発行と購読によって実現されます。
各サービスは、ローカルトランザクションの完了時にイベントを発行します。
他のサービスはこれらのイベントを購読し、自身の処理を実行する必要があるかを判断します。この仕組みにより、サービス間の直接的な依存関係を排除し、疎結合なアーキテクチャを実現できます。
オンライン注文システムでの Choreography 方式の実装では、注文管理サービスが注文作成イベントを発行すると、在庫管理サービスと決済サービスがそれぞれこのイベントを受信します。
在庫管理サービスは在庫予約処理を実行し、完了後に在庫予約完了イベントを発行します。決済サービスは注文作成イベントと在庫予約完了イベントの両方を受信してから決済処理を実行します。
各サービスが特定のイベントを購読し、必要な条件が満たされた時点で処理を実行する仕組みです。
決済サービスは複数のイベントを待機し、すべての前提条件が満たされてから決済を実行します。配送サービスは決済完了イベントを受信してから配送手配を開始します。
Choreography 方式の実装では、各サービスは以下のような構造を持ちます。
決済サービスの例では、注文情報を一時保存し、複数のイベントを受信して条件が整った時点で決済処理を実行します。冪等性の確保、状態遷移の正確性、異常シナリオでの動作を重点的に考慮する必要があります。
// 決済サービスの例
class PaymentService {
private pendingOrders: Map<string, OrderInfo> = new Map();
async handleOrderCreated(event: OrderCreatedEvent) {
// 注文情報を一時保存
this.pendingOrders.set(event.orderId, {
orderId: event.orderId,
amount: event.amount,
customerId: event.customerId,
hasInventoryReserved: false,
});
}
async handleInventoryReserved(event: InventoryReservedEvent) {
const orderInfo = this.pendingOrders.get(event.orderId);
if (orderInfo) {
orderInfo.hasInventoryReserved = true;
// 全ての前提条件が満たされた場合に決済実行
if (this.canProcessPayment(orderInfo)) {
await this.processPayment(orderInfo);
await this.publishPaymentCompletedEvent(orderInfo);
}
}
}
private canProcessPayment(orderInfo: OrderInfo): boolean {
return orderInfo.hasInventoryReserved;
}
}
Orchestration 方式の仕組みと中央制御の実装
Orchestration 方式は、中央集権的な制御 を特徴とする実装方式です。
Saga オーケストレーターと呼ばれる専用コンポーネントが、Saga 全体の実行を管理し、各サービスに対してコマンドを送信します。オーケストレーターは、どのサービスをいつ実行するかを決定し、応答を受けて次のステップを判断します。
オーケストレーターは、Saga の状態を管理し、ビジネスプロセス全体の流れを明示的に定義します。この方式では、各参加サービスはオーケストレーターからのコマンドに応答するだけで、Saga 全体の流れを把握する必要がありません。
オンライン注文システムでの Orchestration 方式の実装では、注文 Saga オーケストレーターがすべてのステップを制御します。
クライアントからの注文リクエストを受けると、オーケストレーターは順次各サービスにコマンドを送信し、応答を受けて次のステップを実行します。注文作成、在庫予約、決済処理、配送手配、メール送信、注文確定の各ステップを順番に実行し、各ステップの完了を待ってから次に進みます。
Orchestration 方式の実装では、オーケストレーターが状態マシンとして実装されることが一般的です。注文 Saga オーケストレーターの例では、各ステップを順次実行し、エラーが発生した場合は補償処理を実行します。Saga の状態を管理し、ビジネスプロセス全体の流れを明示的に定義することで、理解しやすく保守性の高い実装を実現できます。
// 注文Sagaオーケストレーターの例
class OrderSagaOrchestrator {
async executeOrderSaga(orderRequest: OrderRequest): Promise<OrderResult> {
const sagaState = new OrderSagaState(orderRequest);
try {
// ステップ1:注文作成
const orderResult = await this.orderService.createOrder(orderRequest);
sagaState.orderId = orderResult.orderId;
// ステップ2:在庫予約
await this.inventoryService.reserveInventory({
orderId: sagaState.orderId,
productId: orderRequest.productId,
quantity: orderRequest.quantity,
});
// ステップ3:決済処理
await this.paymentService.processPayment({
orderId: sagaState.orderId,
amount: orderRequest.amount,
customerId: orderRequest.customerId,
});
// ステップ4:配送手配
await this.shippingService.scheduleShipping({
orderId: sagaState.orderId,
address: orderRequest.shippingAddress,
});
// ステップ5:メール送信
await this.emailService.sendConfirmationEmail({
orderId: sagaState.orderId,
customerEmail: orderRequest.customerEmail,
});
// ステップ6:注文確定
await this.orderService.confirmOrder(sagaState.orderId);
return { success: true, orderId: sagaState.orderId };
} catch (error) {
await this.executeCompensation(sagaState, error);
throw error;
}
}
}
各方式のメリットとデメリットの詳細比較
両方式には、それぞれ異なる特性があり、プロジェクトの要件に応じて適切な選択が必要です。
特性 | Choreography 方式 | Orchestration 方式 |
---|---|---|
アーキテクチャ | 分散型、疎結合 | 中央集権型、明確な制御 |
複雑性 | サービス間の協調が複雑 | オーケストレーター内に集約 |
パフォーマンス | 並列処理により高スループット | 線形処理によりレスポンス時間長 |
可用性 | 単一障害点なし | オーケストレーターが単一障害点 |
開発・保守 | 全体の流れが見えにくい | ビジネスプロセスが明確 |
スケーラビリティ | 各サービス独立スケール | オーケストレーターがボトルネック |
Choreography 方式のメリット として、各サービスが独立して動作するため、システム全体の可用性が高くなります。また、イベント駆動により並列処理が可能で、高いパフォーマンスを実現できます。サービス間の結合度が低く、マイクロサービスアーキテクチャの原則に適合します。単一障害点が存在せず、特定のコンポーネントの障害が全体に影響を与えにくい構造です。
Choreography 方式のデメリット は、ビジネスプロセス全体の流れが分散しているため、デバッグや監視が困難になることです。また、サービス間の循環依存が発生しやすく、複雑なビジネスルールを実装する際の難易度が高くなります。全体の処理状況を把握するのが困難で、トラブル時の原因特定に時間がかかる場合があります。
Orchestration 方式のメリット は、ビジネスプロセスが一箇所に集約されているため、理解しやすく保守性が高いことです。デバッグや監視も容易で、複雑なビジネスロジックを実装しやすくなります。処理の流れが明確で、新しいメンバーの理解も早くなります。エラーハンドリングや補償処理の制御も集約されているため、管理しやすくなります。
Orchestration 方式のデメリット として、オーケストレーターが単一障害点となるリスクがあります。また、オーケストレーターに過度にビジネスロジックが集約されると、参加サービスが貧血症候群に陥る可能性があります。オーケストレーターがボトルネックになる可能性があり、スケーラビリティの面で課題となる場合があります。
実装方式の選択基準とガイドライン
適切な実装方式を選択するためには、以下の観点から評価することが重要です。
チーム構成と所有権 の観点では、単一チームが全体の Saga を管理できる場合は Orchestration 方式が適しています。複数チームが関与し、各サービスの独立性を重視する場合は Choreography 方式が有効です。チーム間の協調コストや責任分界点を明確にすることが重要です。
ビジネスプロセスの複雑さ も重要な判断基準です。シンプルなワークフローでは Choreography 方式でも十分ですが、複雑な条件分岐や例外処理が多い場合は Orchestration 方式の方が管理しやすくなります。ビジネスルールの変更頻度や複雑さを考慮して選択する必要があります。
パフォーマンス要件 において、高いスループットと低レイテンシが求められる場合は Choreography 方式が有利です。一方、処理の順序性や正確性が重要な場合は Orchestration 方式が適しています。システムの負荷特性と要求されるパフォーマンスレベルを評価することが必要です。
運用・監視要件 では、詳細な処理状況の把握が必要な場合や、ビジネスユーザーによる可視化が重要な場合は、Orchestration 方式の方が管理しやすくなります。運用チームの技術レベルや監視ツールの充実度も考慮すべき要因です。
ハイブリッドアプローチの考慮
実際のシステム開発では、単一の方式にこだわる必要はありません。システム全体では複数の Saga が存在し、それぞれに最適な実装方式を選択することが可能です。
例えば、オンライン注文システムにおいて、注文処理 Saga は複雑なビジネスルールを含むため Orchestration 方式で実装し、在庫補充 Saga はシンプルな処理フローのため Choreography 方式で実装するといったアプローチが考えられます。
また、単一の Saga 内でも部分的に異なる方式を採用することが可能です。全体的には Orchestration 方式で制御しながら、特定のステップでは並列処理のためにイベント駆動の仕組みを取り入れることもできます。
このハイブリッドアプローチにより、各部分の特性に応じた最適な実装を選択でき、システム全体のパフォーマンスと保守性のバランスを取ることができます。ただし、複数の方式を混在させる場合は、開発チームの理解度や運用の複雑性も考慮する必要があります。
次章では、Saga パターンの重要な要素である補償トランザクションの詳細設計と実装について詳しく解説します。
5. 補償トランザクションの詳細設計と実装
セマンティックロールバックの仕組み
セマンティックロールバックは、従来のデータベースロールバックとは根本的に異なる概念です。データベースロールバックは、トランザクションがコミットされる前に実行され、まるで何も起こらなかったかのように状態を戻します。一方、セマンティックロールバックは、新しいトランザクションを実行することで、過去のトランザクションの効果を相殺 します。
この違いにより、セマンティックロールバックでは次の特徴があります。
補償トランザクション自体も履歴として記録され、監査証跡が残ります。物理的には元の状態に戻らない場合があっても、ビジネス的には同等の結果を実現します。時系列の記録が保持されるため、何が起こったかを後から追跡できます。
// セマンティックロールバックの例
class InventoryCompensation {
async compensateInventoryReservation(
orderId: string,
productId: string,
quantity: number
) {
// 在庫予約の補償:予約された数量を在庫に戻す
await this.inventoryRepository.increaseStock(productId, quantity);
// 補償処理の記録を残す
await this.compensationLogRepository.create({
orderId,
action: "INVENTORY_RESERVATION_COMPENSATED",
productId,
quantity,
timestamp: new Date(),
});
}
}
セマンティックロールバックの実装では、在庫予約の補償として予約された数量を在庫に戻し、補償処理の記録も残す必要があります。この記録により、後からシステムの動作を追跡し、問題の原因を特定することが可能になります。
オンライン注文システムでの補償処理の具体例
オンライン注文システムにおける各ステップと対応する補償トランザクションを詳しく見てみましょう。
通常の処理フローでは、注文作成、在庫予約、決済処理、配送手配、メール送信、注文確定の順でステップが実行されます。
ステップ 4 で配送手配が失敗した場合の補償処理は逆順で実行されます。
決済取消では、実行された決済を取り消し、顧客のクレジットカードまたは決済アカウントに返金処理を実行します。在庫予約解除では、予約によって減少した在庫数を元に戻し、該当商品を再び購入可能な状態にします。注文キャンセルでは、注文の状態を「キャンセル」に変更し、注文データ自体は履歴として保持します。
各ステップの具体的な補償処理の実装では、冪等性の確保が最も重要です。既に補償済みかをチェックし、重複実行を防ぐ必要があります。返金処理実行後は、補償記録を保存して処理の追跡を可能にします。
補償処理の設計原則と注意点
効果的な補償トランザクションを設計するためには、いくつかの重要な原則があります。
冪等性の確保 が最も重要です。同じ補償トランザクションが複数回実行されても、結果が変わらないように設計する必要があります。ネットワーク障害や再試行により、補償処理が重複実行される可能性があるためです。
完全性の保証 も重要な原則です。補償トランザクションは、対応する通常のトランザクションの効果を完全に相殺できるように設計する必要があります。部分的な補償では、データの不整合が残る可能性があります。
信頼性の確保 として、補償トランザクション自体が失敗する可能性も考慮する必要があります。補償処理にも適切なエラーハンドリングと再試行機能を実装し、最終的には手動介入が可能な仕組みを用意することが重要です。
すべてのステップに補償が必要ではない ことも理解しておく必要があります。読み取り専用の操作や、後続のステップで必ず成功するステップには補償トランザクションは不要です。
補償不可能な操作への対処法
実際のシステムでは、技術的またはビジネス的に補償が困難な操作が存在します。メール送信が代表的な例です。一度送信されたメールを「未送信」状態に戻すことは物理的に不可能です。
このような場合の対処法として、以下のアプローチが有効です。
代替的な補償アクション を実装します。
メール送信の場合、キャンセル通知メールを送信することで、ユーザーに状況を伝えることができます。確認メールの送信は取り消せませんが、キャンセル通知を送信することで、顧客に対して適切な説明を提供できます。
ワークフローの再設計 により、補償困難な操作を後ろに移動させることも有効です。
例えば、メール送信を最後のステップにすることで、それ以前のステップで失敗した場合にメール送信自体が実行されないようにできます。
手動介入プロセス の用意も重要です。
自動的な補償が困難な場合に備えて、オペレーターが手動で対応できる仕組みとプロセスを整備しておく必要があります。
ワークフロー順序の最適化による補償処理の軽減
適切なワークフロー設計により、補償処理の複雑さを大幅に軽減できます。
この最適化を実現するために、Saga の構成要素を理解し、トランザクションを 3 つの種類に分類して考えることが重要です。
トランザクション種類の分類
補償可能トランザクション(Compensatable Transaction) は、後続のステップで失敗が発生した場合に、対応する補償トランザクションによって取り消すことができるトランザクションです。これらのトランザクションは、Saga の前半部分に配置され、ビジネスルールの検証や外部システムとの連携など、失敗の可能性があるステップの前に実行されます。
ピボットトランザクション(Pivot Transaction) は、Saga における 決定点(Go/No-Go Point) として機能する特別なトランザクションです。ピボットトランザクションが成功すると、Saga は最後まで実行されることが保証されます。逆に、ピボットトランザクションが失敗した場合は、それまでに実行された補償可能トランザクションの補償処理が実行されます。
再試行可能トランザクション(Retriable Transaction) は、ピボットトランザクション成功後に実行され、必ず最終的に成功することが保証されているトランザクション です。これらのトランザクションは失敗しても補償処理を行わず、代わりに成功するまで再試行を続けます。
オンライン注文システムでは、注文作成と在庫予約が補償可能トランザクション、決済処理がピボットトランザクション、配送手配とメール送信と注文確定が再試行可能トランザクションとして設計されます。
処理内容 | トランザクションの種類 |
---|---|
注文作成 | 補償可能トランザクション |
在庫予約 | 補償可能トランザクション |
決済処理 | ピボットトランザクション |
配送手配 | 再試行可能トランザクション |
メール送信 | 再試行可能トランザクション |
注文確定 | 再試行可能トランザクション |
トランザクション種類に基づく配置戦略
補償可能トランザクション は可能な限り前方に集約します。外部 API との連携やビジネスルール検証など、失敗リスクの高い処理を早い段階で実行することで、後続の処理が不要になる可能性を高められます。
ピボットトランザクション は、ビジネス的な決定点に配置します。このトランザクションが成功すると、以降の処理は必ず完了することが保証されるため、適切な位置の選択が重要です。
再試行可能トランザクション は後方に配置します。メール送信や外部システムへの通知など、補償が困難だが失敗時に再試行可能な操作を最後に実行することで、補償処理の複雑さを回避できます。
3 フェーズ構造による最適化
この配置戦略により、Saga を以下の 3 つのフェーズに構造化できます。
この図では、赤色が補償可能トランザクションを集約させたフェーズ 1(検証・確認)、黄色がピボットトランザクションであるフェーズ 2(決定)、緑色が再試行可能トランザクションを集約させたフェーズ 3(実行・確定)を表しています。
フェーズ 1(検証・確認) では、補償可能トランザクションを集約し、失敗時は即座に処理を終了できます。外部在庫確認などのリスクの高い処理をここで実行します。
フェーズ 2(決定) では、ピボットトランザクションである決済処理を実行します。この段階の成功により、以降の処理完了が保証されます。
フェーズ 3(実行・確定) では、再試行可能トランザクションを順次実行し、データの確定と関係者への通知を行います。
補償処理軽減の効果
この構造化により、早期失敗により後続の補償不要な処理の実行を回避でき、補償処理の実装・実行コストを大幅に削減できます。
また、決済完了後は補償処理ではなく再試行処理のみとなるため、システムの安定性が向上し、手動介入が必要な異常ケースも大幅に減少します。
この設計指針により、補償処理の複雑さを大幅に軽減しながら、ビジネス要件を満たす堅牢な Saga を構築できます。
次章では、Saga パターンにおける分離性問題への対策手法について詳しく解説します。
6. 分離性問題への対策手法
対策手法の詳細
分離性の欠如による異常を防ぐため、複数の対策手法が提案されています。これらの手法を適切に組み合わせることで、Saga パターンの課題を大幅に軽減できます。
セマンティックロック
セマンティックロックは、アプリケーションレベルでのロック機能です。オンライン注文システムでは、注文の状態フィールドを使ってセマンティックロックを実装できます。注文を保留状態で作成することで、ロック状態を示し、他の Saga による同時アクセスを制御します。
class SemanticLock {
async createOrder(orderData: OrderData): Promise<Order> {
// 注文を保留状態で作成(セマンティックロック)
const order = await this.orderRepository.create({
...orderData,
status: "APPROVAL_PENDING", // ロック状態を示す
lockedAt: new Date(),
lockedBy: "CREATE_ORDER_SAGA",
});
return order;
}
async handleConcurrentAccess(orderId: string): Promise<void> {
const order = await this.orderRepository.findById(orderId);
if (order.status.endsWith("_PENDING")) {
// ロック中の注文に対する処理
throw new OrderLockedException(
`Order ${orderId} is currently being processed by another saga`
);
}
}
}
セマンティックロックの実装では、注文を保留状態で作成し、ロック状態を明示的に記録します。ロック中の注文に対する処理では、適切なエラーメッセージを返すか、ロックが解除されるまで待機する仕組みを実装します。
セマンティックロックの利点は、ACID トランザクションの分離性を疑似的に再現できることです。同じリソースを更新する Saga が順序化され、プログラミングの複雑さが大幅に軽減されます。一方で、デッドロック検出アルゴリズムの実装やロック管理の複雑性という課題があります。
楽観的更新(Optimistic Locking)
楽観的更新(Optimistic Locking)では、データのバージョン情報を使用して競合状態を検出します。
class OptimisticLocking {
async updateOrderWithVersionCheck(
orderId: string,
updates: Partial<Order>
): Promise<Order> {
const currentOrder = await this.orderRepository.findById(orderId);
const updatedOrder = await this.orderRepository.updateWithVersion({
id: orderId,
version: currentOrder.version, // バージョンチェック
...updates,
version: currentOrder.version + 1,
});
if (!updatedOrder) {
// バージョン競合が発生
throw new OptimisticLockException(
`Order ${orderId} was modified by another transaction`
);
}
return updatedOrder;
}
}
更新前にデータを再度読み取り、バージョンが変更されていないことを確認してから更新を実行します。バージョン競合が検出された場合は、適切なエラーハンドリングを行います。
値の再読み取り(Re-read Value)
値の再読み取り(Re-read Value)では、更新前にデータを再度読み取って変更がないことを確認します。
class RereadValue {
async safeOrderApproval(orderId: string): Promise<void> {
// 初回読み取り
const initialOrder = await this.orderRepository.findById(orderId);
// Sagaの他のステップを実行...
await this.processOtherSteps(orderId);
// 承認前に再読み取り
const currentOrder = await this.orderRepository.findById(orderId);
if (
currentOrder.status !== initialOrder.status ||
currentOrder.version !== initialOrder.version
) {
// 状態が変更されている場合はSagaを中断
throw new OrderModifiedException(
`Order ${orderId} was modified during saga execution`
);
}
// 安全に承認処理を実行
await this.approveOrder(orderId);
}
}
初回読み取り時のデータと、承認前の再読み取り時のデータを比較し、状態が変更されている場合は Saga を中断します。この手法により、他の Saga による変更を検出し、安全に処理を継続できます。
可換更新(Commutative Updates) は、操作の実行順序に依存しない更新操作を設計する手法です。銀行口座の借方・貸方操作のように、順序に関係なく同じ結果が得られる操作では、更新消失の問題を根本的に解決できます。補償処理も同様に可換性を持つように設計することで、複雑な競合制御を回避できます。
悲観的ビュー(Pessimistic View) は、Saga のステップ順序を再編成してビジネスリスクを最小化する手法です。ダーティリードによる影響を受けやすい処理を後方に移動させることで、中間状態による不整合のリスクを軽減します。
並行実行時の競合状態への対処
複数の Saga が同時実行される環境では、適切な競合制御が不可欠です。
リソース順序付け により、デッドロック状態を回避できます。常に同じ順序でリソースにアクセスすることで、循環待機を防げます。例えば、複数の注文を同時処理する場合、注文 ID の昇順でリソースロックを取得するルールを設けることで、デッドロックを防止できます。
タイムアウト機能 の実装により、無限待機状態を防ぐことができます。一定時間内に処理が完了しない場合は、自動的に補償処理を開始します。タイムアウト値は、通常の処理時間とネットワーク遅延を考慮して適切に設定する必要があります。
監視とアラート機能 により、異常な競合状態を早期に検出し、オペレーターが手動で介入できる仕組みを整備することも重要です。競合発生頻度、処理時間の異常な延長、補償処理の頻繁な実行などを監視し、システムの健全性を継続的に評価します。
並行実行制御の実装では、リソースを順序付けてロック取得し、タイムアウト機能により無限待機を防止します。エラー発生時やタイムアウト時には適切な処理を実行し、リソースロックを確実に解放する必要があります。
バリューベースの動的選択
バリューベース(By Value) の対策手法は、各リクエストのビジネスリスクに応じて、動的に並行制御メカニズムを選択する戦略です。
低リスクなリクエストには Saga パターンを適用し、高いパフォーマンスと可用性を実現します。一方、高額な金融取引など高リスクなリクエストには、従来の分散トランザクションを使用して強い一貫性を保証します。
この手法により、ビジネスリスク、可用性、スケーラビリティの間で動的にトレードオフを調整できます。大部分の処理は高性能な Saga で実行し、重要な処理のみ厳密な一貫性制御を適用することで、システム全体の最適化を図れます。
バージョンファイルパターン
バージョンファイル(Version File) は、非可換操作を可換操作に変換する手法です。レコードに対する操作を記録し、それらを適切な順序で再実行することで、操作順序の問題を解決します。
例えば、注文作成 Saga と注文キャンセル Saga が並行実行される場合、会計サービスで操作の順序が逆転する可能性があります。バージョンファイルパターンでは、到着した操作を記録し、正しい順序で実行することで、順序の問題を解決します。
この実装では、操作の履歴を保持し、到着順序と実行順序を分離することで、並行実行による問題を回避できます。ただし、操作履歴の管理と順序制御の複雑性という課題があります。
これらの対策手法を適切に設計・実装することで、分離性の欠如による問題を軽減し、実用的な Saga システムを構築できます。
次章では、実装における具体的な考慮事項とベストプラクティスについて解説します。
7. 実装上の考慮事項とベストプラクティス
メッセージの冪等性とリライアビリティの確保
Saga パターンの実装において、メッセージの冪等性 は最も重要な要件の一つです。ネットワーク障害や再試行メカニズムにより同一メッセージが複数回配信される可能性があるため、各サービスは同一操作を複数回受信しても安全に処理できるよう設計する必要があります。
オンライン注文システムでは、決済サービスが同じ注文 ID に対する決済要求を複数回受信した場合の処理が重要です。冪等性を確保するため、既に処理済みかを確認し、処理済みの場合は既存の結果を返します。新規処理の場合は、処理開始の記録を残してから実際の処理を実行し、結果を保存します。
class PaymentService {
async processPayment(request: PaymentRequest): Promise<PaymentResult> {
// 冪等性チェック:既に処理済みかを確認
const existingPayment = await this.paymentRepository.findByOrderId(
request.orderId
);
if (existingPayment) {
return existingPayment.result; // 既存の結果を返す
}
// 処理開始の記録(処理中状態を保存)
await this.paymentRepository.createProcessingRecord(request.orderId);
try {
const result = await this.executePayment(request);
await this.paymentRepository.saveResult(request.orderId, result);
return result;
} catch (error) {
await this.paymentRepository.saveError(request.orderId, error);
throw error;
}
}
}
メッセージの信頼性確保 も同様に重要です。メッセージが確実に配信され、処理されることを保証するため、メッセージブローカーの機能を適切に設定し、配信確認とエラー処理メカニズムを実装する必要があります。
At-least-once 配信保証により、メッセージが少なくとも一度は配信されることを保証しますが、重複配信の可能性があるため冪等性の実装が必須です。Exactly-once 配信は理想的ですが、実装が複雑で性能への影響も大きいため、実用的ではない場合が多いです。
Transactional Outbox Pattern との連携
Saga の実装において、データベース更新とメッセージ送信を原子的に実行するため、Transactional Outbox Pattern の併用が推奨されます。
デュアルライト問題と Outbox テーブルの概念
従来のアプローチでは、データベース更新とメッセージ送信を別々に実行するため、一方が成功して他方が失敗する可能性があります(デュアルライト問題と呼ばれます)。
この問題を解決するため、Outbox パターンでは Outbox テーブル という特別なテーブルを使用します。
Outbox テーブル は、送信したいメッセージやイベントを一時的に保存するためのテーブルです。ビジネスデータの更新と Outbox テーブルへのメッセージ記録を同一トランザクション内で実行することで、データの整合性を保ちます。
Saga との連携実装
Saga との連携実装では、各ステップでデータベース更新と Outbox へのメッセージ記録を同一トランザクションで実行します。別プロセス(メッセージリレー)が Outbox からメッセージを読み取り、メッセージブローカーに送信することで、確実なメッセージ配信を実現します。
この手法により、Saga の各ステップが確実に次のステップをトリガーし、メッセージの損失を防ぐことができます。ただし、メッセージリレープロセスの管理や、Outbox テーブルのクリーンアップなど、追加の運用上の考慮が必要です。
相関 ID による処理の追跡
分散システムにおいて、単一のビジネス処理が複数のサービスにまたがる場合、相関 ID(Correlation ID) による追跡が不可欠です。相関 ID は、Saga 全体を通じて一意の識別子として使用され、ログ分析やデバッグ時に処理の流れを追跡するために使用されます。
相関 ID の実装では、すべてのログエントリ、メッセージ、外部 API 呼び出しに相関 ID を含め、処理の全体像を追跡できるようにします。これにより、問題発生時の原因特定や処理状況の把握が容易になります。
相関 ID は、Saga の開始時に生成され、すべてのサービス間でプロパゲートされます。各サービスは受信した相関 ID を保持し、ログ出力や下流サービスへの呼び出し時に引き継ぎます。この一貫した追跡により、分散した処理全体を統合的に監視できます。
状態管理とモニタリングの重要性
Saga の実行状態を適切に管理し、監視することは運用上極めて重要です。各 Saga インスタンスの現在の状態、実行済みステップ、残りステップを正確に把握する必要があります。
interface SagaState {
sagaId: string;
correlationId: string;
currentStep: number;
totalSteps: number;
status: "RUNNING" | "COMPLETED" | "COMPENSATING" | "FAILED";
executedSteps: Array<{
stepName: string;
executedAt: Date;
result: any;
}>;
compensationSteps?: Array<{
stepName: string;
executedAt: Date;
}>;
}
Saga の状態管理では、Saga ID、相関 ID、現在のステップ、総ステップ数、実行状態、実行済みステップの詳細、補償ステップの情報を包括的に管理します。この情報により、Saga の進行状況をリアルタイムで把握し、問題の早期発見が可能になります。
状態管理システムでは、Saga の進行状況をリアルタイムで追跡し、異常な状況(長時間実行中、補償処理の失敗など)を検出してアラートを発生させる機能が必要です。ダッシュボードにより、運用チームがシステム全体の状況を一目で把握できるようにします。
エラーハンドリングとタイムアウト処理
堅牢な Saga 実装には、包括的なエラーハンドリング戦略が必要です。一時的なエラー(ネットワーク障害など)と永続的なエラー(ビジネスルール違反など)を区別し、適切な対応を行う必要があります。
エラー処理のフローでは、まずエラーの種別を判定します。一時的なエラーの場合は再試行を実行し、再試行上限に達していなければ処理を再実行します。永続的なエラーや再試行上限到達の場合は、補償処理を開始します。
タイムアウト処理 も重要な要素です。各ステップに適切なタイムアウトを設定し、応答がない場合の処理を明確に定義する必要があります。オンライン注文システムでは、外部決済サービスとの通信に 30 秒のタイムアウトを設定し、超過した場合は補償処理を開始するといった設計が考えられます。
タイムアウト値の設定では、通常の処理時間、ネットワーク遅延、システム負荷を考慮して適切な値を決定します。短すぎると正常な処理でもタイムアウトが発生し、長すぎると障害の検出が遅れる可能性があります。
パフォーマンスとスケーラビリティの考慮点
Saga パターンの実装では、パフォーマンスとスケーラビリティの両面を考慮する必要があります。
メッセージ処理の並列化 により、スループットを向上させることができます。ただし、業務ルールによる順序制約がある場合は、適切な制御が必要です。並列処理可能なステップと順次処理が必要なステップを明確に区別し、最適な並列度を設定します。
状態ストレージの最適化 も重要です。Saga の状態情報を効率的に保存・検索できるよう、適切なデータベース設計とインデックス戦略を実装する必要があります。状態データの分散配置や、読み取り専用レプリカの活用により、パフォーマンスを向上させることができます。
負荷分散の考慮 として、複数の Saga オーケストレーターインスタンスを実行する場合の負荷分散メカニズムを設計し、特定のインスタンスに処理が集中しないよう配慮が必要です。水平スケーリングに対応した設計により、処理量の増加に柔軟に対応できます。
運用時の監視とトラブルシューティング
本番環境での Saga 運用には、包括的な監視とトラブルシューティング機能が不可欠です。
メトリクス収集 では、Saga 実行時間、成功率、失敗率、補償処理実行回数などの重要指標を継続的に監視します。これらの指標により、システムの健全性を定量的に評価できます。処理時間の分布や、エラー発生パターンの分析により、システムの改善点を特定できます。
ログ集約 により、分散した各サービスのログを相関 ID で関連付けて分析できるようにします。問題発生時の根本原因分析において、この統合されたログが重要な情報源となります。構造化ログの採用により、自動的な分析と異常検出を可能にします。
アラート機能 では、Saga 実行時間の異常な延長、高い失敗率、補償処理の頻繁な実行などを検出し、運用チームに即座に通知する仕組みを整備します。段階的なアラートレベルにより、問題の重要度に応じた適切な対応を可能にします。
ダッシュボード により、現在実行中の Saga 数、完了した Saga 数、失敗した Saga 数などをリアルタイムで可視化し、システムの全体状況を把握できるようにします。ビジネスメトリクスとの相関分析により、技術的な問題がビジネスに与える影響を評価できます。
手動介入機能 も重要な要素です。自動復旧が困難な状況において、オペレーターが安全に Saga を再開、中断、または手動で補償処理を実行できる機能を提供する必要があります。適切な権限制御と監査ログにより、手動操作の安全性を確保します。
これらの実装上の考慮事項を適切に設計・実装することで、堅牢で運用しやすい Saga システムを構築できます。次章では、これまで学んだ Saga パターンの重要な概念を総括し、実践適用への道筋を示します。
8. おわりに
本記事では、分散システムにおけるデータ整合性の課題を解決する Saga パターンについて、基本概念から実装方式、実践的な考慮事項まで体系的に解説してきました。
Saga パターンは、従来の 2 フェーズコミットの限界を克服し、現代のマイクロサービスアーキテクチャにおいて高可用性とスケーラビリティを両立させる重要な設計パターンです。
ACID の分離性を犠牲にする代わりに、補償トランザクションという概念を導入することで、分散環境でのデータ整合性を実用的なレベルで維持できます。
本記事を通じて、Choreography 方式と Orchestration 方式のそれぞれの特性を理解し、システム要件に応じた適切な選択を行うことの大切さが見えてきました。また、補償トランザクションの設計では、3 つのトランザクション種別(補償可能、ピボット、再試行可能)に基づくワークフロー最適化により、実装の複雑さを軽減できることがわかりました。分離性の欠如による問題は避けられませんが、セマンティックロックや楽観的更新などの対策手法を適切に組み合わせることで、実用的なシステム構築が可能になります。
Saga パターンは万能ではなく、従来のトランザクション管理と比較して実装と運用の複雑さが増加することも事実です。それでも、適切に設計・実装された Saga システムは、現代のスケーラブルなアプリケーションにとって重要な基盤技術となります。
本記事で解説した概念と手法が、読者の皆様の実際の開発現場において少しでもお役に立てれば幸いです。
9. 参考文献
Microservices Patterns
- Chapter 4. Managing transactions with sagas
Building Microservices, 2nd Edition
-
- Workflow
Design Patterns for Cloud Native Applications
-
- Connectivity and Composition Patterns
Discussion