/note/tech

イベント駆動設計を支える非同期処理について

要約:

■ 1. 非同期処理を採用する動機

  • 領域をまたぐ処理: 異なるビジネス領域(例: 注文、配達)間の連携において、サービス同士を疎結合にするために非同期処理を採用している。これにより、一部の領域の障害が他の領域に波及するカスケード障害を防いでいる。
  • 同期処理のミニマル化: クライアントアプリからのAPI呼び出しなど、高速なレスポンスが求められる処理において、同期処理を最小限に抑えている。これにより、APIのレイテンシを低く保ち、ユーザー体験を向上させている。

■ 2. 非同期処理の具体的な実装例

  • ピック・パックの例: ピッキングAPIではピックのみを同期的に行い、パック予定への追加は非同期に実行している。これにより、APIの応答速度を維持している。この設計には、「ピックができたら必ずパック予定に追加できる」といった前提条件の確認と、結果整合性が許容される業務手順の確立が必要である。
  • データ集計の例: 都度クエリによる集計が難しい場合、イベント駆動型の非同期処理でデータを事前に集計している。これにより、単一プロセス内での処理に起因する競合やサーバ停止時の処理漏れを防ぎ、確実に処理を実行できるようになった。

■ 3. 技術スタックとアーキテクチャ

  • 技術要素: Firestore、Eventarc、Cloud Runを組み合わせて非同期処理を実現している。
  • 処理の流れ:
    • (1) Firestoreにドキュメントを書き込む。このドキュメントはイミュータブル(不変)な非同期メッセージとして扱われ、イベント発生日時や一意の識別子を含む。
    • (2) EventarcがFirestoreの書き込みをトリガーとしてメッセージを検知し、Cloud Pub/Subにプッシュする。
    • (3) Cloud Pub/SubのSubscriptionが、メッセージをCloudEventsの形式でCloud RunにHTTPリクエストとして送信する。
    • (4) Cloud Run上で、メッセージを受信したイベントハンドラが後続の処理を実行する。
  • 実装の工夫:
    • サブスクライバー側: メッセージの重複処理を防ぐための排他制御や、メッセージの順序を正しく処理するためのユーティリティライブラリを用意している。
    • 参考パターン: アウトボックスパターンを参考にしている。FirestoreとEventarcの組み合わせが、データベースをポーリングするよりも運用が容易であることを評価している。

■ 4. 非同期処理導入のステップ

  • ステップ1: gRPCリクエストハンドラ内で、イベントハンドラをプログラム上で同期的に実行する。
  • ステップ2: gRPCリクエストハンドラ内で、イベントハンドラをプログラム上で非同期的に実行する。この段階で本番環境と同等のメッセージ流量を検証し、フィーチャーフラグを用いてステップ3への切り替え準備を行う。
  • ステップ3: gRPCリクエストハンドラ内ではイベントの永続化のみを行い、イベントハンドラの実行は別プロセス(Cloud Run)に完全に移管する。これにより、システム全体が非同期処理となる。