マイクロサービスアーキテクチャの出現により、伝統的なACID保証が不可能なサービス境界を越えた分散トランザクションを管理するためのSagaパターンが必要となりました。歴史的に、テストは即時的一貫性を持つモノリシックなデータベースに依存していましたが、現代の多言語システムは非同期ワークフローや補償ロジックの検証が要求されます。コアの問題は、従来の統合テストが同期応答を前提としており、レースコンディション、ネットワーク分割、および一部のSaga参加者がコミットし他が失敗する際に発生する曖昧な状態を捉えられないことです。
このソリューションには、テストハーネスに統合されたカオスエンジニアリングアプローチが必要です。Testcontainersを使用して、隔離されたDockerネットワーク内でリアルなPostgreSQL、MongoDB、およびRedisインスタンスをオーケストレーションするフレームワークを設計します。サービス間のプログラム可能なTCPプロキシとしてToxiproxyを導入し、正確なSagaステップでのレイテンシ、帯域幅制約、そしてネットワーク分割を注入します。静的なスリープではなく、ポーリングベースの非同期アサーションのためにAwaitilityを採用し、正確な実行経路を再構成するために分散トレーシングのためのJaegerを統合します。補償の正確な一度だけの意味論を確認するためにUUIDベースの冪等性キー追跡を実装し、不変の保存を確認するためにすべての永続層にわたる状態をスナップショットするGlobalConsistencyValidatorを構築します。
コンテキスト: 多国籍なeコマースプラットフォームが、インベントリサービス(PostgreSQL)、決済サービス(トランザクションログ用のMongoDB)、および配送サービス(Elasticsearch)を介してイベント駆動型のSagaを通じて注文を処理しました。アーキテクチャは、Javaベースのマイクロサービス間での振舞を管理するためにApache Kafkaを使用しました。
問題の説明: ピークトラフィック中にネットワークの不安定さが原因で、決済処理が成功した一方で、在庫予約が失敗し、補償が引き起こされました。しかし、補償ロジックには、初回の返金リクエストがタイムアウトした場合に重複した返金リクエストが発行されるという重大なレースコンディションが含まれており、冪等性契約を違反していました。また、多言語のストア間での最終的一貫性の遅延が、即時の在庫回復を確認する既存のテストでの偽陽性を引き起こし、脆弱なCI/CDパイプラインと、顧客が利用できないアイテムに対して料金を請求されるという欠陥を招いていました。
アプローチ1: 固定遅延のあるUIベースのエンドツーエンドテスト
最初は、ユーザーのチェックアウトフローをシミュレーションするためにSelenium WebDriverを使用し、非同期処理を待つためにThread.sleep(5000)を挿入することを検討しました。
利点: 実装が簡単で、完全なユーザージャーニーをカバーし、サービスコードに変更を必要としません。
欠点: 非常に脆弱で、5秒は負荷がかかると不十分であり、アイドル時には過剰でした。ネットワーク障害を正確なSagaステージで注入できず、特定のレース条件を再現することが不可能でした。このアプローチは、サービス間のHTTP通信パターンやデータベース状態遷移への可視性を提供しませんでした。
アプローチ2: メモリ内データベースを使用したモック単体テスト 2番目のオプションでは、すべての外部サービス呼び出しをMockitoを使用してモックし、各サービスの単体テストにはH2メモリ内データベースを使用しました。 利点: 実行時間が10秒未満で、インフラ依存がなく、隔離された状態で決定的な結果を得られます。 欠点: 実際のシリアル化の問題や、PostgreSQLに存在するがH2には存在しないTCPソケットタイムアウトの挙動、またはデータベース固有のロックメカニズムを検出できませんでした。冪等性レース条件は、実際のネットワークパケットの挙動と接続プール枯渇が発生した場合にのみ現れ、モックでは再現できません。
アプローチ3: 実際のインフラを使ったオーケストレーションされたカオス(選定) JUnit 5とTestcontainersを使用して専用のテストハーネスを実装しました。各サービスは、Toxiproxyがそれらの間の全てのネットワークリンクを管理する隔離されたDockerコンテナで実行されました。APIエントリポイントにはRestAssuredを使用し、外部決済処理業者の冪等性の挙動をシミュレートするためにWireMockを使用しました。 利点: 特定のSagaステップでの正確な障害注入が可能(例: 決済コミット後に接続を切断するが、在庫チェック前)。Awaitilityを使うことで、固定された遅延なしで最終的一貫性を動的に待つことができました。Jaegerのトレースにより、補償経路の正確な実行パスを検証するための法廷分析が可能となりました。 欠点: 初期設定の複雑さとリソース要件が高く(ローカル実行に最低8GBのRAMが必要)、単体テストに比べて初期のブートストラップ時間が長くなりました。
結果: このフレームワークは、重複キーに対する適切なHTTP 409 Conflict処理が欠けている場合の補償再試行の冪等性バグを検出しました。返金リクエストを提出する前にRedisの冪等性キーを確認するロジックを修正した後、製品の重複請求はゼロに減少しました。テスト実行時間は8分(脆弱なUIテスト)から45秒(ターゲット統合テスト)に短縮され、失敗シナリオのカバレッジは300%向上しました。
ネットワーク障害が曖昧なリクエスト結果を引き起こすとき、どのようにして補償トランザクションの冪等性を確認しますか?
候補者は通常、最終的な口座残高のみを主張し、下流のシステムが正確に1つのリクエストを受信したかどうかを確認する重要な検証を見逃します。正しい実装には、カオス注入の前にUUID冪等性キーをキャプチャし、次にWireMockのverify(exactly(1), postRequestedFor())メソッドを使用して、正確に1つの一致するリクエストが決済ゲートウェイに到達したことを確認します。さらに、Saga Orchestratorの状態機械のログを確認して、遷移がCOMPENSATING -> COMPENSATEDの形で進行し、中間のFAILED状態が発生していないことを確認し、不必要なアラートをトリガーしないようにします。これは、リクエストバイトが送信された後、応答バイトが到着する前に接続を切断するためにTCPレベルのプロキシ制御が必要であり、冪等性処理をテストするための正確な曖昧なタイムアウト条件を作り出します。
異なる複製レイテンシを持つ異種データストア間での最終的一貫性を主張する際に、テストの脆弱性を防ぐための戦略は?
ほとんどの候補者は固定タイムアウトでポーリングを提案します。堅牢な解決策は、Awaitilityを使用して指数バックオフを行い、100msから始めて99パーセンタイルの生産レイテンシ(例: 3秒)に制限します。重要なのは、テストの開始前にPostgreSQL、MongoDB、そしてRedis全体で論理タイムスタンプをスナップショットするためにグローバルクロックまたはベクトルクロックメカニズムを実装することです。アサーションは、その後、読み取り操作がSaga開始時刻以上のタイムスタンプを持つデータを返すことを確認します。CQRSシナリオのために、データベースをポーリングするのではなく、テストに埋め込まれたDebeziumを使用してCDCイベントを購読し、待機時間を数秒からミリ秒に短縮し、テストのアサーションとデータ複製との間のレース条件を排除します。
いかにして、一部のSaga参加者がコミットし、他が保留の状態にある部分的な実行状態を、製品の可視性ツールにアクセスすることなく検出しますか?
候補者はしばしば、テストハーネスにアクセス可能なプロセス内SagaトラッキングやSaga監査ログの必要性を見逃します。このソリューションには、Envoyやカスタムプロキシを使用して、参加者サービスへのgRPCまたはHTTP呼び出しを傍受するテストコンテナ内にサイドカーパターンを注入する必要があります。テストハーネス内で各参加者の状態(PENDING、COMMITTED、ABORTED)を追跡するSaga状態マトリックスを維持します。Toxiproxyがパーティションを注入したときに、このマトリックスを照会して、コミットされた参加者が期待される障害前の状態と一致することを確認し、キャンセルされた参加者は副作用を示さないようにします。補償パスが補助状態の参加者に対してのみ実行されることを確認するために、JaegerスパンタグのJSONPathアサーションを使用し、実際に予約されていないトランザクションのためにリソースが解放されないようにします。