背景
ドメイン駆動設計の勉強会で、分散トランザクションの管理について、
オブザーバビリティの話題なども出て、議論が白熱したため、
ある程度の仮説をもってGeminiとやり取りした結果、
一番それっぽい結論にたどり着いたので、それをここに残す。
BASE特性の範囲を一意に示す仕組みは何があるか?
投げたスクリプト
マイクロサービスでは、1つの境界付けられたコンテキストから別のコンテキストにメッセージを送る際に、その間のメッセージのトレースはどのような仕組みで、結果整合のトランザクション境界だと保証するのですか?
この時点で持っていた仮説
マイクロサービスでは、通信を伴って、整合性をBASE特性で担保する。
ということは、オブザーバビリティの提供するトレースIDを追えば、
結果整合性の範囲を一位に特定できるはずだ。
返ってきた出力
マイクロサービスアーキテクチャでは、
1つの境界付けられたコンテキストから別のコンテキストにメッセージ(またはAPI呼び出し)を送る際に、
その間のメッセージのトレースと、それが結果整合性のトランザクション境界であることを保証する仕組みは、主に分散トレーシングとサーガパターン(特にオーケストレーション型)、
そしてデータコントラクトの組み合わせによって実現されます。
1. メッセージのトレースの仕組み:分散トレーシング
これは、複数のマイクロサービスにまたがる一連のリクエストの流れを追跡するための、
オブザーバビリティ(可観測性) の重要な要素です。
仕組み:
- トレースID (Trace ID):
ユーザーからの最初のリクエスト(または、ビジネスプロセスを開始する最初のイベント)がシステムに入った際に、一意のトレースIDが生成されます。
このトレースIDは、テレメトリーデータの一部であり、オブザーバビリティツールが提供してくれます。
-
スパンID (Span ID):
このリクエストが、あるサービスから別のサービスへメッセージを送信したり、
内部で処理を行ったりするたびに、その個々の操作に対応するスパン(Span)が生成されます。各スパンには一意のスパンIDが付与されます。 -
親子関係:
スパンは親子関係を持ち、親スパン(呼び出し元)と子スパン(呼び出し先)が紐付けられます。トレースIDは、すべてのスパンで共通です。 -
コンテキスト伝播:
各サービスが他のサービスにメッセージを送信する際
(HTTPヘッダー、メッセージキューのペイロードなど)、
トレースIDと現在のスパンID(次のスパンの親となるID)を含むトレースコンテキストを伝播させます。 -
データ収集:
各サービスは、自身の処理に関する情報を(実行時間、エラー、ログなど)
そのスパンに紐付けて収集し、分散トレーシングシステム(例: Jaeger, Zipkin, New Relic,Datadog)に送信します。
トレースの可視化:
分散トレーシングシステムは、収集されたスパン情報をトレースIDで関連付け、
視覚的なガントチャートのような形式で表示します。
これにより、リクエストがシステム内のどのサービスを、どの順序で、どれくらいの時間で通過したか、どこでエラーが発生したかなどを一目で把握できます。
境界付けられたコンテキストのトレース:
1つのコンテキストから別のコンテキストへのメッセージ送信は、
1つのスパンの終了と次のスパンの開始として記録され、トレースIDを通じて両者が紐付けられます。
これにより、異なるコンテキストをまたがるビジネスプロセス全体の流れが追跡可能になります。
2. 結果整合性のトランザクション境界の保証(メカニズム)
分散システムにおける結果整合性のトランザクション境界(一連の操作が最終的に整合すること)は、主にサーガパターンによって保証されます。
特に、境界付けられたコンテキストをまたぐ場合は、このパターンが適用されます。
● サーガパターン(特にオーケストレーション型):
○ オーケストレーター:
1つの境界付けられたコンテキスト内のオーケストレーター(プロセスマネージャー)が、
複数のコンテキストにまたがるビジネスプロセス全体を調整します。
このオーケストレーターは、プロセス全体の状態(どのステップが完了し、次は何を実行すべきか)を永続的に管理します。
○ メッセージングとローカルトランザクション:
オーケストレーターは、次のコンテキストにメッセージ(コマンド)を送信し、
そのコンテキストがローカルトランザクションを実行し、結果をイベントとしてオーケストレーターに返します。
この各ステップは独立したローカルトランザクションであり、それぞれがアトミックにコミットされます。
○ トレースIDの伝播:
ここで、オーケストレーターは各メッセージに共通のトレースIDを含めて伝播させます。
これにより、異なるコンテキストをまたがる各ローカルトランザクションが、
論理的に同じビジネスプロセスの一部であることが追跡可能になります。
○ 補償トランザクション:
最も重要な保証メカニズムです。
同時にこいつが、本来はビジネスプロセス上では、
連携があってほしくないような、コンテキスト同士の不要な結合を生み出しかねません。
もしサーガの途中でエラーが発生した場合、
オーケストレーターはトレースIDと保存している状態情報に基づいて、
それまでにコミットされたローカルトランザクションを「補償トランザクション」で取り消すように指示します。
これにより、一時的に不整合になったデータも、
最終的には整合性の取れた状態(元の状態に戻るか、別の定義された最終状態になる)に回復されます。
● イベントソーシングとの組み合わせ:
○ もし各境界付けられたコンテキストがイベントソーシングを採用している場合、
そのコンテキスト内で発生する全てのイベントが永続的に記録されます。
これにより、コンテキスト内部状態がどのように変化したかの完全な監査証跡が残り、
必要に応じて状態をリプレイして整合性を確認できます。
○ サービス間のメッセージングもイベントとして記録されることで、
分散プロセス全体の「真実の記録」が提供されます。
3. 保証を強化するその他の仕組み
● データコントラクト (Data Contract):
○ データプロダクトの境界:
各境界付けられたコンテキスト(データプロダクト)が公開するメッセージやAPIのスキーマ、セマンティクス、品質特性を明示的に定義した「契約」です。
○ 保証:
この契約により、メッセージの受信側は、送信側がどのような形式のデータを送ってくるかを事前に知り、それに合わせた処理を実装できます。
これにより、スキーマの不整合によるデータの破損を防ぎ、データの一貫性に関する期待値を明確にします。
データコントラクトの変更は、バージョン管理と通知プロセスを通じて管理され、デプロイ時の互換性を保証します。
● コンシューマ駆動契約テスト:
○ コンシューマーとプロデューサーの間の契約
メッセージの受信側(コンシューマー)が、メッセージの発行者(プロデューサー)に対し、
自分が期待するメッセージの形式や振る舞いを定義したテスト(契約)を作成し、
プロデューサー側でその契約が破られていないかを自動で検証します。
○ 保証:
これにより、プロデューサーがメッセージスキーマを変更する際に、
その変更が既存のコンシューマーに破壊的な影響を与えないか(契約違反がないか)を事前に、
自動的に確認できます。
もし契約違反があれば、そのメッセージが本番環境に到達前に問題特定し修正できます。
これにより、メッセージの一貫性と、サービス間の互換性が保証されます。
まとめ
マイクロサービスにおいて、1つの境界付けられたコンテキストから別のコンテキストへのメッセージの流れは、単なる通信ではなく、多くの場合、ビジネスプロセスの一部です。
● 分散トレーシング:
そのビジネスプロセスを構成するメッセージの流れを可視化し、追跡する仕組みです。
● サーガパターン
オーケストレーション型やコレオグラフィー型は、そのメッセージの流れによって、
複数のローカルトランザクションが実行され、全体として「全か無か」の原子性(アトミック性)と、結果的な整合性を保証するための設計パターンです。
ただし、アトミック性の分散トランザクションでは、
エピックサーガや伝言ゲームサーガのように同期通信を求められるため、
せっかくアプリでは疎結合になっているのに、インフラ部分で蜜結合状態を生んでしまいます。
これは、マイクロサービスの恩恵を激減させるため、個人的には推奨しません。
● データコントラクトとCDC
これらはメッセージの内容と振る舞いの「契約」を保証し、
異なるコンテキスト間でのデータの整合性と互換性を高めるための重要なツールとなります。
これらが組み合わさることで、マイクロサービスは高い疎結合と独立性を保ちながら、
分散環境における複雑なビジネスプロセスの信頼性とデータ整合性を実現しています。
ビジネスプロセスIDとトレースID、スパンIDの関係性
投げたスクリプト
トレースIDとスパンIDの責務の違いは何でしょうか?
結果整合性のトランザクション境界の一意な値はどのように実装するのですか?
この時点で持っていた仮説
各分散トランザクションを一意に示すものはトレースIDであり、
各サービスにいつ行きついたのか?などを示すものがスパンIDであるだろう。
返ってきた出力
はい、分散トレーシングにおける「トレースID」と「スパンID」は、
リクエストの追跡において異なる役割を果たす、密接に関連した概念です。
1. トレースID (Trace ID) と スパンID (Span ID) の違い
ユーザーリクエスト (Trace ID: T123)
└── サービスA (Span ID: S_A, Parent ID: none)
├── サービスB呼び出し (Child Span ID: S_B1, Parent ID: S_A)
│ └── データベース呼び出し (Child Span ID: S_DB1, Parent ID: S_B1)
│ └── (S_DB1 完了)
└── サービスC呼び出し (Child Span ID: S_C1, Parent ID: S_A)
├── サービスD呼び出し (Child Span ID: S_D1, Parent ID: S_C1)
│ └── (S_D1 完了)
└── (S_C1 完了)
└── (S_B1 完了)
└── (S_A 完了)
この例では、すべてのスパン (S_A, S_B1, S_DB1, S_C1, S_D1) は同じ トレースID: T123 を共有します。S_B1 の親は S_A、S_DB1 の親は S_B1 となり、ツリー構造を形成します。
2. 結果整合性のトランザクション境界の一意な値の実装方法
結果整合性のトランザクション境界(つまり、複数のマイクロサービスにまたがる一連のビジネスプロセス全体)を識別する一意な値は、上記の**「トレースID」と、ビジネスプロセス自体に紐づく「ビジネスプロセスID」**を組み合わせることで実装するのが一般的です。
サーガパターンを例に、具体的な実装方法を説明します。
a. ビジネスプロセスID (Correlation ID / Business Transaction ID)
役割:
特定のビジネス上のイベントやトランザクション(例:注文、支払い、ユーザー登録など)を識別するための、ビジネス的な一意なID。これは、アプリケーションコードが生成・管理するIDです。
実装:
①初期生成:
ビジネスプロセスが開始される最初のサービス(例:注文作成サービス)で、
新しい注文ID、支払いIDなどのビジネスIDを生成します。
②伝播:
このビジネスIDを、そのビジネスプロセスに関連する全てのサービス間メッセージ(HTTPヘッダー、メッセージキューのペイロードなど)に含めて伝播させます。
③ログへの記録:
各サービスが、このビジネスIDをログ出力に含めます。
用途:
ビジネス上の観点から、特定の注文が今どの状態にあるのか、
この支払いがどの注文に紐づいているのかなどを追跡するために使われます。
監査証跡やビジネス分析にも利用されます。
b. トレースID (Observability Trace ID)
役割:
オブザーバビリティの観点から、システム内部での技術的なリクエストの流れを追跡するためのIDです。
実装:
①フレームワーク/ライブラリによる自動生成と伝播:
OpenTelemetry、OpenTracingなどの標準仕様に準拠したクライアントライブラリやサービスメッシュのサイドカーが、自動的にトレースIDを生成し、サービス間の通信(HTTPヘッダー、メッセージキューのメタデータなど)を介して伝播させます。
②ログへの関連付け:
開発者は、ログ出力の際に、現在のコンテキストからトレースID(とスパンID)を取得してログメッセージに含めるように実装します。
③オブザーバビリティツールでの可視化:
収集されたトレースIDを持つログやメトリクスが、分散トレーシングツールで統合され、
ビジネスプロセスを構成する技術的なフローとして可視化されます。
c. 両者の連携による「結果整合性のトランザクション境界」の保証
結果整合性のトランザクション境界を保証するために、これらのIDは密接に連携します。
ビジネスプロセスと技術的実行の紐付け:
各サービスは、ローカルなACIDトランザクションを実行する際に、関連するビジネスプロセスIDとトレースIDの両方をログに記録します。
例えば、サーガオーケストレーターは、特定の注文ID(ビジネスプロセスID)のサーガを開始し、各参加サービスにメッセージを送信する際に、新しいトレースIDを生成して伝播させます。
問題発生時の分析:
サービスメッシュやオブザーバビリティツールで、トレースIDを辿ることで、
どのサービスで技術的なエラーや遅延が発生したかを特定できます。
その技術的な問題が、どのビジネスプロセスIDに影響を与えたのかを、ログに含まれるビジネスプロセスIDを通じて特定できます。
逆に、ビジネス部門から「あの注文が完了していない」という問い合わせがあった場合、その注文ID(ビジネスプロセスID)をキーにログを検索し、それに関連するトレースIDを特定することで、技術的な実行フローを追跡できます。
補償トランザクションのトリガー:
サーガオーケストレーター(またはコレオグラフィーにおけるイベントリスナー)は、
ビジネスプロセスIDと自身が管理するサーガの状態に基づいて、補償トランザクションを発動します。
この補償トランザクションのメッセージにも、同じビジネスプロセスIDと新しいトレースID(または元のトレースIDに紐づく新しいスパン)を含めて伝播させます。
これにより、
ビジネスプロセス全体が、技術的なエラーや、中間の一時的な不整合を経て、最終的にアトミックに(成功または補償によって)完結したという状態を、ビジネス的にも技術的にも追跡・保証できます。
要するに、トレースIDは「技術的な実行パス」の一意な識別子、
ビジネスプロセスIDは「ビジネス上のトランザクション」の一意な識別子であり、
両者を組み合わせることで、複雑な結果整合性トランザクションの境界をエンドツーエンドで追跡し、保証することが可能になります。
データコントラクトとサービスメッシュの関係性
投げたスクリプト
分散システムにおける、データコントラクトの変更は、サービスメッシュなどの仕組みによって、運用が楽になるのですか?
この時点で持っていた仮説
独立して運用されるサービス群のバージョン管理などを行いやすくするために、
サービスメッシュがある以上、当然データコントラクトの変更にだって、
行いやすくなるような仕組みを提供してくれていないとおかしい。
返ってきた出力
データコントラクトの変更は、分散システムにおいて非常に複雑な課題です。
サービスメッシュのような仕組みは、この運用を「楽にする」というよりも、
「安全に、かつ管理可能にする」ことで、結果的に効率的な運用に貢献します。
データコントラクトの変更が難しい理由
サービスメッシュがない分散システムでデータコントラクト(APIスキーマなど)を変更する際は、次のような困難が伴います。
影響範囲の特定が困難:
どのコンシューマー(データ利用者)がそのコントラクト(APIやデータプロダクト)を使っているのか、そして変更によってどのような影響を受けるのか(破壊的変更かどうか)を把握しにくいです。
デプロイ順序の依存関係:
破壊的変更の場合、プロデューサー(データ提供者)とコンシューマー間で、特定のデプロイ順序や同時デプロイが必要になることが多く、複雑な調整が求められます。
これは、マイクロサービスアーキテクチャで避けたい分散モノリスの原因にもなり得ます。
ダウンタイムのリスク:
不適切な変更が行われると、コンシューマー側のシステムが停止したり、データが壊れたりするリスクがあります。
手動での調整とコミュニケーション:
変更のたびに、開発者間の手動での調整やコミュニケーションが不可欠となり、ヒューマンエラーのリスクが高まります。
サービスメッシュが運用を安全に管理可能にするメカニズム
サービスメッシュはデータコントラクトそのものを管理するツールではありませんが、
データコントラクトの「実行時における強制と検証」および「変更の影響を管理する」ための強力なインフラ基盤を提供します。
①トラフィック管理による安全な移行:
サービスメッシュは、アプリケーションのトラフィックを傍受し、ルーティングを制御できます。これにより、カナリアリリースやA/Bテストデプロイメントといった戦略が可能になります。
具体的な効果:
データコントラクトに破壊的変更(Majorバージョンアップ)があった場合、新しいAPIバージョン(サービス)をデプロイし、まず少量のトラフィックを流して検証できます。問題がなければ徐々にトラフィックを増やし、古いAPIバージョンを使っているコンシューマーがまだいる間は、古いAPIバージョンも並行稼働させてサポート期間を設けることができます。これにより、コンシューマー側のデプロイタイミングや変更対応を待つことなく、プロデューサー側が新しいAPIを安全にリリースできます。 ダウンタイムを伴わない、安全な移行パスを提供します。
②きめ細かなアクセス制御とポリシー強制:
サービスメッシュのコントロールプレーンを通じて、サービス間の認証(mTLS)や認可ポリシーを適用できます。
具体的な効果:
「サービスXはサービスYの旧APIバージョンにのみアクセス可能」といったルールを設定できます。
これにより、誤って古いコンシューマーが新しいAPIバージョンにアクセスしてしまったり、逆に新しいコンシューマーが古いAPIバージョンに依存し続けたりするのを防ぐなど、
ポリシーレベルでAPIの利用を制御し、移行を強制できます。
③可観測性による影響分析とデバッグの迅速化:
サービスメッシュは、サービス間の通信に関する詳細なメトリクス、ログ、分散トレーシングデータを自動収集します。
具体的な効果:
影響の把握:
特定のAPIエンドポイントへのトラフィック量やエラー率を監視することで、新しいコントラクトのAPIがどの程度利用されているか、古いコントラクトの利用が減っているかなどをリアルタイムで可視化できます。
問題の特定:
変更後に問題が発生した場合、分散トレーシングデータ(トレースIDなど)を辿ることで、どのサービス間、どのAPI呼び出しでエラーが起きているかを迅速に特定し、デバッグを効率化できます。
④データコントラクト(スキーマレジストリ)との連携:
サービスメッシュ自体はデータコントラクトを管理しませんが、多くの場合、スキーマレジストリ(Avro Schema Registryなど)と連携します。
役割:
スキーマレジストリは、データコントラクトのスキーマ定義を集中管理し、互換性の検証や変更履歴の管理を行います。
サービスメッシュは、これらのスキーマレジストリの情報に基づき、トラフィックのルーティングやポリシー適用を行うことができます。
まとめ
サービスメッシュは、データコントラクトの「変更そのもの」を楽にするわけではありません。
コントラクトの変更内容を決定したり、その変更をコンシューマーに通知したりする作業は依然として必要です。
しかし、その変更を「安全にデプロイし、運用し、管理する」プロセスを劇的に改善します。
具体的には、デプロイ時のリスクとダウンタイムを最小限に抑え、
変更の影響範囲を正確に把握し、問題発生時のデバッグを迅速化し、
ポリシーを通じてAPIの利用とバージョン管理を強制・制御します。
これらの側面において、サービスメッシュはデータコントラクトの変更運用を
より堅牢で、予測可能で、管理可能なものにするための、非常に強力なインフラ基盤となります。