モジュール 08ターム3 · 信頼性とスケール10 ポスター項目
非同期処理、イベント、リアルタイム
ログインすると、進捗を複数のデバイスで保存できます。ログインするまでは、このブラウザに保存されます。
なぜ重要なのか
メール送信、PDF 生成、LLM エージェントの呼び出し、webhook の処理。これらをユーザーのリクエスト内で実行すると、リクエストは遅くなり、サーバーレスの制限でタイムアウトし、クラッシュすれば作業が失われます。本格的なシステムが高速さを保ち、何も失わずにいられるのは非同期設計のおかげです。
学習目標
- 遅い処理や不安定な処理を、永続的なバックグラウンドジョブに移せる。
- キュー、pub/sub、イベント駆動設計を使い分けられる。
- webhook を安全に扱える(検証、重複排除、即時 ack、非同期処理)。
- saga と outbox パターンで複数ステップのワークフローを協調させられる。
- 適切なリアルタイム通信方式を選べる: WebSockets、SSE、ロングポーリング。
- 重複実行しても、実行が抜けても安全なスケジュールジョブを運用できる。
レッスン
8.1 キューとワーカー(15)
パターン: リクエストで検証し、意図を DB に書き込み、ジョブをキューに入れ、ジョブ ID 付きで 202 Accepted を返します。ワーカーが処理し、ステータスを更新し、通知します。
- 実際の配信は at-least-once(少なくとも 1 回)です。すべてのジョブハンドラーを idempotent に設計してください。
- 選択肢: マネージド(SQS、Cloud Tasks)、Redis ベース(BullMQ)、Postgres ベース(pg-boss、Graphile Worker)、永続ワークフローエンジン(Inngest、Trigger.dev、Temporal)。
- AI エージェントには永続ワークフローエンジンが最適なことが多いです。各ステップがチェックポイント化され、個別にリトライされます。
8.2 Pub/Sub とイベント駆動アーキテクチャ(16, 17)
- キュー: 1 つのメッセージを 1 つのコンシューマーが処理します(やるべきタスク)。
- Pub/Sub: 1 つのイベントを、互いに独立した多数のサブスクライバーが受け取ります(起きた事実:
order.paid)。 - イベント駆動: サービス同士が直接呼び出し合うのではなく、イベントに反応します。疎結合になる一方で追跡は難しくなり、イベントのスキーマと十分な可観測性が必要です。
- イベント名は過去形にし、イベントスキーマはバージョン管理し、重複排除のためにイベント ID を含めます。
8.3 分散トランザクション、outbox、saga(18, 19)
- 自社の DB、Stripe、メールプロバイダーにまたがる 1 つの ACID トランザクションは実現できません。
- Transactional outbox: 同じ DB トランザクション内で、ビジネス上の変更 と outbox 行を書き込みます。リレーが outbox 行を発行します。「保存されたが発行されなかった」「発行されたが保存されなかった」を防ぎます。
- Saga: 各ステップが 補償アクション を持つローカルステップの連なりです(在庫を引き当てる → カードに請求する → 出荷を作成する。出荷が失敗したら: 返金し、在庫を解放する)。オーケストレーション型(コーディネーターがいる)とコレオグラフィ型(イベントでつなぐ)があります。
- 2 フェーズコミットも存在しますが、現代の Web システムで適切な場面はまれです。
8.4 webhook(101)
受信側:
- 署名を検証 し(共有シークレットによる HMAC)、タイムスタンプも確認して、偽造と replay を防ぎます。
- プロバイダーのイベント ID で 重複排除 します(ユニーク制約)。
- 即座に ack し(数秒以内に 2xx)、その後バックグラウンドジョブで処理します。
- 順序の入れ替わった配信に対応します。判断に迷ったらプロバイダーから現在の状態を取得します。 送信側: ペイロードに署名し、バックオフ付きでリトライし、顧客向けに配信ログと replay を提供し、顧客が指定した URL に対する SSRF を防ぎます。
8.5 cron ジョブ(21)
- 実行は スキップされる、遅れる、2 回走る ことがあると想定します。ジョブは idempotent かつ再開可能にします。
- ロック(DB の advisory lock)で重複実行を防ぎます。
- 巨大な 1 つのトランザクションではなく、チェックポイント付きのバッチで処理します。
- 「デッドマンスイッチ」型のハートビートで監視します(ジョブが 実行されなかった らアラート)。
- スケジュールは UTC で保存し、他の市場のサマータイムに注意します。
8.6 リアルタイム通信方式(22, 23, 24)
| 方式 | 方向 | 使う場面 |
|---|---|---|
| SSE | サーバーからクライアント | LLM トークンのストリーミング、通知、進捗表示。シンプルで HTTP 上で動き、自動再接続がある |
| WebSockets | 双方向 | チャット、共同編集、ライブゲーム、音声 |
| ロングポーリング | 疑似的なプッシュ | ストリーミングがブロックされる環境でのフォールバック |
| 考慮事項: 接続を認証し、長時間の接続では再認可し、再接続と取りこぼしたメッセージに対応します(最後のイベント ID から再開)。サーバーレスプラットフォームでは接続時間に上限がある場合があります。規模が大きくなったらマネージドなリアルタイムサービス(Supabase Realtime、Pusher、Ably、Cloudflare Durable Objects)を使います。 |
Lab 8: 非同期にする
- 最も遅い同期処理(LLM 呼び出し、PDF、メール)を、ステータスのポーリングまたは SSE による進捗表示付きの永続ジョブに移します。
- 署名検証、重複排除、非同期処理を備えた webhook レシーバー(Stripe または Resend)を実装します。同じイベントを 10 回 replay し、効果が 1 回だけであることを証明します。
- 補償付きの 3 ステップの saga を実装し、ステップ 3 で障害を注入して rollback を証明します。
- 重複実行ロックとハートビートアラート付きの cron ジョブを追加します。
完了の定義
AI プロンプトパック
- 「リクエストハンドラー内で外部サービスを呼び出している、または 1 秒を超える可能性がある処理をすべて特定してください。どれをバックグラウンドジョブに移すべきか提案してください。」
- 「この webhook ハンドラーを実装してください: 定数時間比較で署名を検証し、古いタイムスタンプを拒否し、ユニーク制約でイベント ID の重複を排除し、処理をキューに入れ、200 を返すこと。」