アーキテクチャの段階的進化モデル
複雑な分散システムを一度に構築するのではなく、学習とリファクタリングを繰り返しながら、システムの結合度を慎重に下げていく、極めて現実的なアプローチです。
まさにアーキテクチャの「成熟度モデル」そのものです。
各フェーズは、特定の課題を解決し、次のより高度なステップに進むための安全な足がかりとなります。
フェーズ1:オーケストレーション(学習フェーズ)
アーキテクチャ:同期/非同期オーケストレーション
エピックサーガや、おとぎ話サーガ、パラレルサーガなどがこれに該当します。
目的
複雑で不確実なビジネスプロセスを、まずは単一の場所で明確に定義し、実行可能な形にすること。
解決する課題
プロセス全体の流れが不明確であるという 「知識の欠如」。
フェーズ2:ステートフル・コレオグラフィ(論理的分離の開始)
このころは、皆さんが理想とする、疎結合なマイクロサービスはまだ実現できてはいません。
どうしても、各サービスが他のサービスからもらうべき情報が決まり切っていないフェーズなので、スタンプ結合をせざるを得ないフェーズです。
目的
オーケストレーターという単一障害点(SPOF)を排除し、各サービスが自律的に動き始めるための第一歩を踏み出す。
解決する課題
オーケストレーターへの 「中央集権的な依存」。
このフェーズでの学習
各サービスは、他のサービスからどのような情報が本当に必要なのかを、コールバックを通じて実践的に学びます。
フェーズ3:ステートレス・コレオグラフィ(真の疎結合)
目的
フェーズ2で学んだ知識を基に、イベントの契約(コントラクト)をさらに洗練させ、実行時の依存(コールバック)を完全に排除する。
解決する課題
サービス間の 「実行時のデータ依存性」。
このフェーズでの状態
各サービスは、イベントを受け取るだけで自己完結して処理を完遂できる、真に自律したコンポーネントになります。
フェーズ4:インフラ分離とサービスメッシュ(物理的分離と統治)
目的
論理的に完全に分離されたサービス群を、物理的にも分離することで、障害影響範囲を限定し、究極の耐障害性を実現する。
同時に、サービスメッシュによって統一的なガバナンスを適用する。
解決する課題
共有インフラに起因する
運命共同体リスク(Noisy Neighbor問題など)
分散したことによる「ガバナンスの欠如」
ここまでのまとめ
この進化の道筋は、以下の点で非常に優れています。
リスク管理
各フェーズで解決すべき課題を一つに絞り、次のステップに進む前に安定性を確認することで、大規模な変更に伴うリスクを最小限に抑えます。
暗黙的に、単一責務をめっちゃ意識しています。
経験的設計
「最初に完璧な設計はできない」という前提に立ち、運用を通じて得られた学びを次のアーキテクチャに反映させていきます。
単なる技術の選択ではなく、ビジネスの成長と組織の学習に合わせてシステムを適応させていく、進化的アーキテクチャの思想を体現するうえで必須です。
非同期コレオグラフィにおけるエラーハンドリング
非同期コレオグラフィでは、エラーハンドリングもイベント駆動で行われます。
失敗したサービスが「失敗した」という事実をイベントとして発行し、他のサービスがそれを受けて、自律的に自身の補償処理を実行する必要があります。
1. 失敗イベントの発行
あるサービスが自身のトランザクションを実行しようとして失敗した場合(例: 在庫サービスが在庫不足で引当に失敗)、そのサービスは成功イベントの代わりに失敗イベントを発行します。
例:StockReservationFailed イベント
ペイロード
失敗の理由(在庫不足)、元のトランザクションを特定するためのSaga IDなどを含む。
2. 補償トランザクションの実行
Sagaの前段のステップを既に完了していた他のサービス(例: 決済サービス)は、この失敗イベントを購読しています。
StockReservationFailedイベントを受け取った決済サービスは、「このSagaは失敗した」と認識し、誰からの直接的な命令も受けることなく、自身の責務である補償処理(この場合は返金処理)を自律的に実行します。
なぜ自己完結できるのか?
各サービスが自身の状態(「どのSaga IDに対して、どの処理を完了させたか」というローカルトランザクション状態)を所有しているため、自己完結が可能になります。
決済サービスは、自身のデータベースに「Saga-123の決済を完了した」という記録を持っています。
StockReservationFailed(Saga-123)というイベントを受け取ると、その記録を照合し、
「Saga-123の決済を取り消すべきだ」と判断して、返金処理を実行できるのです。
これは、外部からのAPI呼び出しではなく、イベントをきっかけとした自律的な判断です。
このように、失敗もイベントとして通知することで、補償トランザクションの実行も疎結合なコレオグラフィのまま、完結させることができます。
単一トピックかそれとも分離するか
ドメインイベントを送るメッセージブローカーでは、論理スキーマで、正常系のものと補償処理系のものとで分離しておいた方が良いのでしょうか??
これについて考えていきましょう。
基本の考え方は、
モノリス ⇒ 単一トピック
モジュラーモノリス ⇒ 分離トピック
みたいなもんです。
パターンA:単一トピックで管理する(シンプルだが混在)
まず、分離しない場合のパターンです。
方法:
正常系のイベント(例: OrderPlaced, PaymentSucceeded)と、
補償系のイベント(例: OrderCancelled, PaymentRefunded)を、全て同じordersというトピックに発行します。
メリット
管理するトピックが少なく、構成が単純です。
デメリット -関心の混在-
補償処理にしか興味がないサービスも、正常系のイベントを全て受信して、自分でフィルタリングする必要があり、非効率です。
パターンB:トピックを分離する(推奨)
正常系と補償系でトピックを論理的に分離するパターンです。
方法
・order_eventsトピック:正常系のイベント(OrderPlacedなど)を発行します。
・order_compensation_eventsトピック:補償系のイベント(OrderCancelledなど)を発行します。
メリット
関心の分離
各サービスは、自分が関心のあるトピックだけを購読すればよくなります。
例えば、注文をキャンセルする責務を持つサービスはorder_compensation_eventsだけを購読すればよく、OrderPlacedイベントのノイズを受け取る必要がありません。
アクセス制御
トピックごとに、書き込み・読み取りの権限を細かく設定でき、セキュリティが向上します。
効率的な消費
消費者は不要なメッセージをフィルタリングする負荷がなくなります。
ここのまとめ
システムが成長し、参加するサービスが増えることを見越すのであれば、最初から正常系と補償系でトピックを分離しておくのが、クリーンでスケールしやすいアーキテクチャへの近道です。
ただし、
やりすぎは、YAGNI違反になる
ので、私のオススメは、
補償系が最初から超複雑系になることが明確なら、分離トピックで対応。
まだ未確定要素が高くて読めないのなら、最初は単一トピックで作っておき、後からでも分離トピックに切り替えられるようにしておく
のをお勧めします。
