注文を確定すると 3 つのサービスが動きます。在庫サービスが在庫を引き当て、決済サービスがカードのオーソリを取り、配送サービスが出荷を作成します。モノリスなら、これは 1 つのデータベーストランザクションです。それぞれが独自のデータベースを持つサービスに分かれると、3 つすべてにまたがるトランザクションはなくなり、サービス間の 2 フェーズコミットも実際にはほとんど選択肢になりません。
その標準的な答えが Saga パターンです。この記事では、Saga の仕組み、オーケストレーションとコレオグラフィーの違い、補償とピボットステップの組み合わせ方、そして Saga が代わりに解決してはくれない分離性の問題を説明します。図はオーケストレーション方式の注文 Saga を示しています。
Saga とは
Saga はローカルトランザクションの連なりです。各ステップは自分のサービスのデータベースでコミットし、次のステップを起動します。あるステップが失敗すると、Saga は 補償トランザクション を実行し、すでにコミットされたステップの効果を取り消します。
この考え方はマイクロサービスより古く、Hector Garcia-Molina と Kenneth Salem が 1987 年に長時間のデータベーストランザクションに関する論文で Saga を説明しました。サービスの境界が同じ問題を再び生み出したため、このパターンが復活したのです。
次の 2 つの帰結が、ほかのすべてを左右します。
- 結果整合性。 最初のステップから最後のステップまでの間、システムはほかのリクエストから見える中間状態にあります。注文は PENDING、在庫は引き当て済み、カードはオーソリ済みですが、まだ売上確定はしていません。
- 補償はロールバックではない。 送ったメールは取り消せず、カードへの請求を黙って取り消すこともできません。補償は、オーソリの取り消しや返金のように、以前の処理を意味的に打ち消す新しい業務アクションです。
補償可能、ピボット、再試行可能なステップ
何かを描く前に、すべてのステップを分類します。Azure Architecture Center の Saga パターンのガイド は 3 種類を使っています。
| 種類 | 意味 | 例 |
|---|---|---|
| 補償可能 | 別のトランザクションで取り消せる | 在庫の引き当て、決済のオーソリ |
| ピボット | 引き返せない地点。成功したら Saga は最後まで進めなければならない | 出荷の作成 |
| 再試行可能 | ピボットの後に来て、いずれ必ず成功しなければならない。冪等 | 確認メールの送信 |
順序が重要です。補償可能なステップを先に、次にピボットを置き、その後ろには再試行可能なステップだけを置きます。ピボットが失敗したら補償し、成功したら、その後のステップはすべて成功するまで再試行します。ピボットの後に補償可能なステップがある Saga は、そのステップが失敗したときに一貫した状態にたどり着けません。
オーケストレーションとコレオグラフィー
ステップを調整する方法は 2 つあります。
オーケストレーション: Saga オーケストレーター(多くの場合、状態を永続化するステートマシン)が各サービスに何をすべきかを伝え、応答を待ちます。順序、再試行、タイムアウト、補償はオーケストレーターが受け持ちます。
コレオグラフィー: コーディネーターはいません。各サービスがイベントを購読して反応します。在庫サービスは OrderCreated を受けて在庫を引き当て、決済サービスは StockReserved を受けてオーソリを取る、という具合です。失敗もイベントであり、各サービスはどの補償を実行すべきかを知っています。
| オーケストレーション | コレオグラフィー | |
|---|---|---|
| フローがある場所 | 1 か所:オーケストレーター | 各サービスのイベントハンドラーに分散 |
| ステップの追加 | オーケストレーターを変更する | どのイベントを誰が購読するかを変える |
| 1 件の注文の状態を見る | Saga ログを照会する | イベントから再構成する |
| 結合 | サービスがオーケストレーターのコマンドを知る | サービスが互いのイベントを知る |
| 向いているケース | ステップが多い、分岐がある、厳密な順序 | 2〜3 ステップの単純なフロー |
コレオグラフィーは短いフローなら快適ですが、「注文 8812 はなぜ止まっているのか」と聞かれたときに苦労します。オーケストレーションなら、その問いに対して見るべき場所が 1 か所にまとまります。Temporal、AWS Step Functions、Camunda などのワークフローエンジンは永続的なオーケストレーションを提供するので、ステートマシンを自作する必要はありません。Chris Richardson の Saga パターンのページ には、両方のスタイルの具体例があります。
分離性:Saga があなたに任せる部分
データベーストランザクションは、中間状態をほかのトランザクションから隠します。Saga は隠しません。注文 Saga の実行中に、別のリクエストが引き当て済みの在庫を読んだり、2 つ目の Saga が同じレコードを変更したりできます。よくある対策は次のとおりです。
- セマンティックロック。 レコードを処理中としてマークし(PENDING の注文、RESERVED の在庫)、ほかの操作が待つか拒否すべきだとわかるようにします。
- 可換な更新。 どの順序で実行しても同じ結果になるように操作を設計します。たとえば、値を直接設定する代わりにカウントを増減させます。
- 書き込み前の再読み込み。 Saga が読んでから値が変わっていないかを確認し、変わっていたら失敗させるか最初からやり直します。
すべてのステップとすべての補償を、Saga ID とステップをキーにして冪等にします。メッセージは二重に届きますし、再試行された「オーソリの取り消し」が別の決済を取り消してはいけません。
タイムアウトと止まった Saga
すべてのステップに期限が必要です。決して応答しない決済サービスがあると、Saga は永遠に待ち続け、在庫は引き当てられたままになります。各ステップにタイムアウトを設定し、ピボットより前のタイムアウトは失敗(補償する)として、ピボットの後のタイムアウトは再試行の理由として扱い、想定より長く同じ状態にとどまる Saga にはアラートを出します。現在のステップごとに Saga を並べた小さなダッシュボードがあれば、サポートからの質問の大半はそれだけで答えられます。各 Saga が終了した理由も記録しておきましょう。
アーキテクチャ図
ダイアグラムの内容
- Order service
- Saga orchestrator
- Saga log
- Inventory service
- Payment service
- Shipping service
- All steps succeeded?
- Order APPROVED
- Compensate in reverse order
- Order REJECTED
- start
- persist each step
- 1. reserve stock
- 2. authorize payment
- 3. create shipment
- yes
- no
- void, release
図の読み方は次のとおりです。
- 左: 注文サービスが PENDING の状態で注文を作成し、Saga を開始します。
- 中央: オーケストレーターがステップを順に実行し、それぞれを Saga ログに永続化します。そのため、再起動しても止まったところから再開できます。
- 右: 3 つのサービスで、それぞれにステップ番号と種類のラベルが付いています。在庫と決済は補償可能で、配送がピボットです。
- 下: すべてのステップが成功すると注文は APPROVED になります。ピボットより前のステップが失敗すると、オーケストレーターは逆順に補償し(オーソリを取り消してから在庫を解放)、注文は REJECTED になります。
- Markdown の表 は各アクションとその補償を対応づけています。レビュアーが最初に確認すべきなのはここです。
Saga の図に描くべきこと
- ステップの順序と各ステップの種類。 補償可能、ピボット、再試行可能のどれかをボックスに書きます。
- すべての補償。 補償のないステップは、ピボットか、再試行可能か、バグのどれかです。
- Saga の状態を保存する場所。 途中でのクラッシュを乗り越えられない Saga は完成していません。
- 外から見える中間状態。 PENDING と RESERVED は設計の一部です。ほかの機能からも見えます。
Saga は トランザクショナルアウトボックスパターン と自然に組み合わさります。各ステップはデータベースを更新し、かつメッセージを送る必要があり、そのどちらも失ってはならないからです。これらのサービスの配置は マイクロサービスのアーキテクチャ図 で、オーソリと売上確定のステップの詳細は 決済ゲートウェイ連携の図 で扱っています。
Archboard で作る
- Google または GitHub でサインインし、図を作成します。
- C4 モデルパックから、注文サービス、Saga オーケストレーター、参加する 3 つのサービス用に コンテナ ノードを追加し、Saga ログ用に データベース を追加します。
- 各サービスの技術フィールドに、ステップ番号と種類(補償可能またはピボット)を書きます。
- 成功の経路と補償の経路のために、フローチャートパックから 判断 を 1 つ、処理 を 1 つ、開始 / 終了 ノードを 2 つ追加します。
- アクションと補償の表を書いた基本パックの Markdown ブロックを追加します。
この例で使うダイアグラムノードは 11 個で、Free プランの範囲に収まります。
よくある質問
Saga と分散トランザクションの違いは何ですか? 分散トランザクション(2 フェーズコミット)は、全員がコミットするまで複数のデータベースにまたがってリソースをロックします。Saga は各ステップをローカルでコミットし、後のステップが失敗したら補償によって前のステップを取り消します。分離性と引き換えに可用性を得るのです。
オーケストレーションとコレオグラフィーのどちらを使うべきですか? ステップが多い、分岐がある、厳密な順序が必要といったフローにはオーケストレーションです。各 Saga の状態が 1 か所にまとまるからです。2〜3 のサービスが互いのイベントに反応するだけの短いフローにはコレオグラフィーが向いています。
ピボットトランザクションとは何ですか? それ以降は Saga を補償できなくなるステップです。それより前のステップはすべて補償可能、それより後のステップはすべて成功するまで再試行可能でなければなりません。
補償が失敗したらどうすればいいですか? 再試行します。補償は冪等で、いずれ必ず成功しなければなりません。どうしても成功しない場合、Saga には手動で解決する経路とアラートが必要です。
Saga は分離性を提供してくれますか? いいえ。ほかのリクエストから中間状態が見えます。PENDING 状態のようなセマンティックロック、可換な更新、再読み込みを使って、自分の業務にとって問題になる異常を防ぎます。